chart = {
const width = 1170
const height = width + 0
const links = dataFiltered.links.map(d => Object.create(d));
const nodes = dataFiltered.nodes.map(d => Object.create(d));
const colourMissingKanji = '#111111'
const colourRadicals = '#f7ede2'
const colBg = '#352d39'
const svg = d3
.create("svg")
.attr('width', width)
.attr('height', height)
.style('background', colBg)
.style('overflow', 'scroll')
const g = svg.append('g')
.attr('transform', `translate(${width/2}, ${height/2})`)
const defs = svg.append("defs")
defs.append("filter")
.attr("id", "blur")
.attr("width", "200%")
.attr("height", "200%")
.attr("x", "-60%")
.attr("y", "-60%")
.attr("color-interpolation-filters","sRGB")
.append("feGaussianBlur")
.attr("in","SourceGraphic")
.attr("stdDeviation","3")
defs.append("filter")
.attr("id", "noise")
.append("feTurbulence")
.attr('type', 'fractalNoise')
.attr('baseFrequency', '1.29')
.attr('numOctaves', '2')
.attr('result', 'noisy')
.append("feColorMatrix")
.attr('type', 'saturate')
.attr('values', 0)
.append("feBlend")
.attr('in', 'SourceGraphic')
.attr('in2', 'noisy')
.attr('mode', 'multiply')
//Filter for the outside glow
const filter = defs.append("filter")
.attr("id","glow");
filter.append("feGaussianBlur")
.attr("stdDeviation","3.5")
.attr("result","coloredBlur");
const feMerge = filter.append("feMerge");
feMerge.append("feMergeNode")
.attr("in","coloredBlur");
feMerge.append("feMergeNode")
.attr("in","SourceGraphic")
const bg = g.append('rect')
.attr('x', -width/2)
.attr('y', -height/2)
.attr('width', width)
.attr('height', height)
.style('fill', colBg)
.style("filter", "url(#noise)")
.style('opacity', 0.2)
///////////////////////////////////////////////////////////////////
/////////// List of kanji category counts for radical /////////////
///////////////////////////////////////////////////////////////////
const drawListOfKanjiCategoriesPerRadical = (radical) => {
const radicalData = getCountOfKanjiCatPerRadical(radical)
const gCategories = svg.selectAll('.g-categories')
.data([null])
.join('g')
.classed('g-categories', true)
.attr('transform', `translate(${50}, ${150})`)
.style('fill', '#fff')
const counts = gCategories.selectAll('.count-for-radical')
.data(_.sortBy(radicalData, d => d.count).reverse())
.join('text')
.classed('count-for-radical', true)
.attr('transform', (d, i) => `translate(${0}, ${i * 20})`)
.text(d => d.count)
.style('font-weight', 'bold')
.style('text-anchor', 'end')
.style('opacity', 0)
.transition().duration(700).delay((d, i) => i * 50)
.style('opacity', 1)
const categories = gCategories.selectAll('.category-for-radical')
.data(_.sortBy(radicalData, d => d.count).reverse())
.join('text')
.classed('category-for-radical', true)
.attr('transform', (d, i) => `translate(${15}, ${i * 20})`)
.text(d => d.category)
.style('font-weight', '200')
.style('opacity', 0)
.transition().duration(700).delay((d, i) => i * 50)
.style('opacity', 1)
}
//drawListOfKanjiCategoriesPerRadical('木-radical') // example usage
////////////////////////////////
/////////// Scales /////////////
////////////////////////////////
const subcategories = _.uniqBy(nodes, d => d.subcategory).map(d => d.subcategory).filter(d => d !== 0)
// For positioning vertically (not used)
const subcategoryScale = d3.scaleBand()
.domain(subcategories)
.range([0, height])
// For radial positioning
const subcategoryRadialScale = d3.scaleBand()
.domain(subcategories)
.range([0, 2*Math.PI])
// Colour scales for kanji by level (grade)
const colourByGradeScale = d3.scaleOrdinal()
.domain(['1', '2', '3', '4', '5', '6', 'S'])
.range(['#cce3de', '#a4c3b2', '#6b9080', '#d1b3c4', '#b392ac', '#735d78', '#fe6d73'])
/////////////////////////////////////////////////
///////////// Simulation Definition /////////////
/////////////////////////////////////////////////
const radiusGroups = 500 // Radius for the kanji around the radicals
const radiusCollide = d => d.type === 'radical'
? 12 + 12 * (linksCopy.filter(l => l.source === d.id).length * 0.06)
: 14
const collideForce = d3.forceCollide().radius(radiusCollide).iterations(2).strength(1)
const radialForce = d3.forceRadial(0).strength(0.4)
// Non-radial (not used)
const xForce = d3.forceX(d => d.type === 'kanji' ? 0.8 * width : 0.1 * width).strength(1.2)
const yForce = d3.forceY(d => d.type === 'kanji' ? subcategoryScale(d.subcategory) : height/2).strength(2)
// Radial
const xForceRadial = d3.forceX(d => d.type === 'kanji' && d.subcategory !== 0
? d3.pointRadial(subcategoryRadialScale(d.subcategory), radiusGroups)[0]
: 0
).strength(1)
const yForceRadial = d3.forceY(d => d.type === 'kanji' && d.subcategory !== 0
? d3.pointRadial(subcategoryRadialScale(d.subcategory), radiusGroups)[1]
: 0
).strength(1)
const simulation = d3
.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("collide", collideForce)
//.force("radial", radialForce)
.force("x", xForceRadial)
.force("y", yForceRadial)
/////////////////////////////////////////////////
//////////////////// Links //////////////////////
/////////////////////////////////////////////////
const link = g
.append("g")
.style("stroke", "#b1a7a6")
.style("stroke-opacity", 0.2)
.selectAll("line")
.data(links)
.join("line")
.style("stroke-width", 0.3)
/////////////////////////////////////////////////
//////////////////// Nodes //////////////////////
/////////////////////////////////////////////////
const node = g
.append("g")
.attr("stroke", "#fff")
const nodesG = node
.selectAll('.node-g')
.data(nodes)
.join('g')
.classed('node-g', true)
.style('cursor', 'pointer')
const nodeCircle = nodesG
//.selectAll("circle")
.selectAll('rect')
.data(d => [d])
.join('rect')
.attr('x', -12)
.attr('y', -12)
.attr('width', 24)
.attr('height', 24)
.attr('transform', d => d.type === 'radical'
? `scale(${1 + linksCopy.filter(l => l.source === d.id).length * 0.06})`
: `scale(1)`)
// .join("circle")
// .attr("r", 13)
.style('stroke', '#fff')
.style('stroke-width', 1)
.style("fill", d => d.type === 'kanji'
? _.find(kanjiLevelsClean, e => e.kanji === d.kanji)
? colourByGradeScale( _.find(kanjiLevelsClean, e => e.kanji === d.kanji).Grade )
: colourMissingKanji
: colourRadicals)
// .style('opacity', d => {
// const kanjiExists = _.find(kanjiLevelsClean, e => e.kanji === d.kanji)
// if (kanjiExists) {
// if (kanjiExists.Grade === grade) {
// return 1
// } else {
// return 0.4
// }
// } else {
// return 0
// }
// })
//.style("filter", "url(#glow)")
// .style('mix-blend-mode', 'lighten')
const nodeText = nodesG.selectAll("text")
.data(d => [d])
.join("text")
.text(d => d.kanji)
.classed('kanji-text', true)
.attr('text-anchor', 'middle')
.attr('dy', '0.35em')
.style('font-size', '15px')
//.style('font-weight', 'lighter')
.style('stroke', 'none')
.style('fill', d => d.type === 'radical' ? colBg : 'white')
.attr('transform', d => d.type === 'radical'
? `scale(${1 + linksCopy.filter(l => l.source === d.id).length * 0.06})`
: `scale(1)`)
//////////////////////////////////////////////////
///////////////// Text Labels ////////////////////
//////////////////////////////////////////////////
const subcategoryLabels = g.selectAll('.subcategory-label-g')
.data(subcategories)
.join('g')
.classed('subcategory-label-g', true)
.attr("transform", d => `
rotate(${((subcategoryRadialScale(d)) * 180 / Math.PI - 90)})
translate(${radiusGroups},0)
`)
// subcategoryLabels.append('text')
// .classed('label', true)
// .text(d => d)
// //.style('text-decoration', 'underline')
// .style('font-family', 'sans-serif')
// .style('fill', 'white')
// //.attr("transform", "rotate(90)")
// //.attr("transform", d => (subcategoryRadialScale(d)) % (2 * Math.PI) < (2/3)*Math.PI ? "rotate(90)" : "rotate(-90)")
// .attr("transform", d => (subcategoryRadialScale(d) + Math.PI / 2) % (2 * Math.PI) < Math.PI
// ? `rotate(90) translate(${-subcategoryRadialScale.bandwidth()/2}, 0)`
// : `rotate(-90) translate(${-subcategoryRadialScale.bandwidth()/2},0)`
// )
/////////////////////////////////////////////////
//////////////////// Events //////////////////////
/////////////////////////////////////////////////
nodesG
.on("mouseenter", (evt, d) => {
// All the target nodes for selected node
const targetNodesIds = linksCopy.filter(l => l.target === d.id).map(l => l.source)
const sourceNodesIds = linksCopy.filter(l => l.source === d.id).map(l => l.target)
const nodesToHighlightIds = [...targetNodesIds, ...sourceNodesIds]
console.log('nodesToHighlightIds', nodesToHighlightIds)
// Highlight the selected links
// link
// .transition().duration(300)
// .attr("display", "none")
// .filter(l => l.source.id === d.id || l.target.id === d.id)
// .attr("display", "block");
link
.transition().duration(100)
.style('stroke-opacity', l => l.source.id === d.id || l.target.id === d.id ? 1 : 0)
.style("stroke-width", l => l.source.id === d.id || l.target.id === d.id ? 2 : 0.3)
// Highlight the connected nodes
nodesG
.transition().duration(100)
.style('opacity', n => n.id === d.id || nodesToHighlightIds.includes(n.id) ? 1 : 0.1)
// Get the count of kanji per category to display if we've selected a radical
if (d.type === 'radical') {
drawListOfKanjiCategoriesPerRadical(d.id)
}
})
.on("mouseleave", evt => {
// Restore original graph
link
.transition().duration(100)
//.attr("display", "block")
.style('stroke-opacity', 0.3)
.style("stroke-width", 0.2)
nodesG
.transition().duration(100)
.style('opacity', 1)
// Remove any kanji per category text
d3.selectAll('.g-categories').remove()
});
/////////////////////////////////////////////////
///////////// Simulation Activation /////////////
/////////////////////////////////////////////////
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
nodesG
.attr('transform', d => `translate(${d.x}, ${d.y})`)
.attr("stroke", d => (d.fx ? "#333" : "#fff"));
});
//invalidation.then(() => simulation.stop());
return svg.node();
}