src/vis/SentenTreeVis.js (202 lines of code) (raw):

import 'd3-transition'; import * as d3 from 'd3'; import { SvgChart, helper } from 'd3kit'; import { flatMap, keyBy } from 'lodash'; import Layout from './Layout.js'; import { diagonal } from './shapeUtil.js'; class SentenTreeVis extends SvgChart { static getDefaultOptions() { return helper.deepExtend(super.getDefaultOptions(), { initialWidth: 800, initialHeight: 200, margin: { left: 0, top: 0, bottom: 0, right: 0 }, fontSize: [10, 32], gapBetweenGraph: 10, }); } static getCustomEventNames() { return [ 'layoutStart', 'layoutTick', 'layoutEnd', 'nodeMouseenter', 'nodeMouseleave', 'nodeMousemove', 'nodeClick', 'linkMouseenter', 'linkMouseleave', 'linkMousemove', 'linkClick', ]; } constructor(element, options) { super(element, options); this.layers.create(['link', 'node']); this.fontSizeScale = d3.scaleSqrt().clamp(true); this.strokeSizeScale = d3.scaleSqrt() .domain([1, 100]) .range([1, 6]) .clamp(true); this.layouts = []; this.updatePosition = this.updatePosition.bind(this); this.visualize = this.visualize.bind(this); this.on('data', this.visualize); this.on('options', this.visualize); // this.on('resize', this.visualize); } fontSize(node) { return `${Math.round(this.fontSizeScale(node.data.freq))}px`; } renderNodes(graphs) { const sUpdate = this.layers.get('node').selectAll('g.graph') .data(graphs); sUpdate.exit().remove(); const sEnter = sUpdate.enter().append('g') .classed('graph', true); this.sNodeGraphs = sUpdate.merge(sEnter) .attr('transform', `translate(${this.getInnerWidth() / 2},${this.getInnerHeight() / 2})`); const sUpdateNode = sEnter.selectAll('g') .data(d => d.nodes, n => n.id); sUpdateNode.exit().remove(); sUpdateNode.enter().append('g') .classed('node', true) .on('click.event', this.dispatchAs('nodeClick')) .on('mouseenter.event', this.dispatchAs('nodeMouseenter')) .on('mousemove.event', this.dispatchAs('nodeMousemove')) .on('mouseleave.event', this.dispatchAs('nodeMouseleave')) .append('text') .attr('dy', '.28em') .text(d => d.data.entity) .style('cursor', 'pointer'); this.sNodes = this.layers.get('node').selectAll('g.node'); this.sNodes.select('text') .style('font-size', d => this.fontSize(d)) .style('text-anchor', 'middle'); } renderLinks(graphs) { const sUpdate = this.layers.get('link').selectAll('g.graph') .data(graphs); sUpdate.exit().remove(); const sEnter = sUpdate.enter().append('g') .classed('graph', true); this.sLinkGraphs = sUpdate.merge(sEnter) .attr('transform', `translate(${this.getInnerWidth() / 2},${this.getInnerHeight() / 2})`); const sUpdateLink = sEnter.selectAll('path.link') .data(d => d.links, l => l.getKey()); sUpdateLink.exit().remove(); sUpdateLink.enter().append('path') .classed('link', true) .on('click.event', this.dispatchAs('linkClick')) .on('mouseenter.event', this.dispatchAs('linkMouseenter')) .on('mousemove.event', this.dispatchAs('linkMousemove')) .on('mouseleave.event', this.dispatchAs('linkMouseleave')) .style('vector-effect', 'non-scaling-stroke') .style('opacity', 0.5) .style('stroke', '#222') .style('fill', 'none'); graphs.forEach(graph => { graph.links.forEach(link => { link.strokeWidth = Math.round(this.strokeSizeScale(link.freq / graph.minSupport)); }); }); this.sLinks = this.layers.get('link').selectAll('path.link') .style('stroke-width', d => `${d.strokeWidth}px`) .style('stroke', l => (l.isTheOnlyBridge() ? '#777' : '#FF9800')); } updatePosition() { let yPos = 0; let maxw = 0; const { margin, gapBetweenGraph } = this.options(); const { top, left, bottom, right } = margin; // Get bbox of <g> for each graph to compute total dimension // and stack each graph on top of each other this.sNodeGraphs.each(function fn(graph) { const bbox = this.getBBox(); const w = bbox.width; const h = bbox.height; maxw = Math.max(w, maxw); graph.x = -bbox.x; graph.y = -bbox.y + yPos; yPos += h + gapBetweenGraph; }); this.sNodeGraphs .attr('transform', graph => `translate(${graph.x},${graph.y})`); this.sLinkGraphs .attr('transform', graph => `translate(${graph.x},${graph.y})`); // Update component size to fit all content this.dimension([ maxw + left + right, Math.max(0, yPos - gapBetweenGraph) + top + bottom, ]); this.placeNodes(); this.placeLinks(); } placeNodes() { this.sNodes.attr('transform', d => `translate(${d.x}, ${d.y})`); } placeLinks() { // draw directed edges with proper padding from node centers const graphs = this.data(); graphs.forEach(graph => { graph.nodes.forEach(node => { node.updateAttachPoints(); }); }); this.sLinks .attr('d', link => { const points = [ link.source.rightEdge(), link.attachPoints.y1, link.target.leftEdge(), link.attachPoints.y2, ]; // const xGap = link.target.leftEdge() - link.source.rightEdge(); // if (xGap > 30) { // return line(...points); // } return diagonal(...points); }); } visualize() { if (!this.hasData()) return; const graphs = this.data(); if (graphs.length > 0) { const { fontSize } = this.options(); this.fontSizeScale .domain(graphs[0].globalFreqRange) .range(fontSize); } this.linkLookup = keyBy(flatMap(graphs, g => g.links), l => [l.source.gid, l.target.gid].join(',')); this.renderNodes(graphs); this.renderLinks(graphs); // Update node position for layout computation this.sNodes.each(function fn(node) { const bbox = this.getBBox(); node.width = bbox.width + 4; node.height = bbox.height + 4; }); // Update layout pool const len = Math.max(graphs.length, this.layouts.length); for (let i = 0; i < len; i++) { if (i >= this.layouts.length) { this.layouts.push(new Layout().on('tick', this.updatePosition)); } if (i >= graphs.length) { this.layouts[i] .stop() .destroy(); continue; } this.layouts[i] .stop() .setGraph(graphs[i]) .start(); } this.layouts = this.layouts.slice(0, graphs.length); this.updatePosition(); // const colaAdaptor = this.colaAdaptor; // this.sNodes.call(colaAdaptor.drag); // this.sLinks.call(colaAdaptor.drag); // this.colaAdaptor.on('tick', event => { // this.placeNodes(); // this.placeLinks(); // }); } highlightNeighbors(node) { this.sNodes.transition() .style('opacity', d => (d.gid === node.gid || this.linkLookup[[d.gid, node.gid].join(',')] || this.linkLookup[[node.gid, d.gid].join(',')] ) ? 1 : 0.3 ); this.sLinks.transition() .style('opacity', d => (d.source.gid === node.gid || d.target.gid === node.gid) ? 1 : 0.3 ); } clearHighlightNeighbors() { this.sNodes.style('opacity', 1); this.sLinks.style('opacity', 1); } } export default SentenTreeVis;