Refactor force-directed graph (#5691)

* Refactor and add props to Force Directed Graph

* update label and enable renderTrigger
This commit is contained in:
Krist Wongsuphasawat 2018-08-21 20:48:58 -07:00 committed by Maxime Beauchemin
parent ebe585df3d
commit 3929f0f79d
3 changed files with 91 additions and 61 deletions

View File

@ -838,6 +838,7 @@ export const controls = {
link_length: { link_length: {
type: 'SelectControl', type: 'SelectControl',
renderTrigger: true,
freeForm: true, freeForm: true,
label: t('Link Length'), label: t('Link Length'),
default: '200', default: '200',
@ -847,6 +848,7 @@ export const controls = {
charge: { charge: {
type: 'SelectControl', type: 'SelectControl',
renderTrigger: true,
freeForm: true, freeForm: true,
label: t('Charge'), label: t('Charge'),
default: '-500', default: '-500',

View File

@ -1398,7 +1398,7 @@ export const visTypes = {
}, },
directed_force: { directed_force: {
label: t('Directed Force Layout'), label: t('Force-directed Graph'),
controlPanelSections: [ controlPanelSections: [
{ {
label: t('Query'), label: t('Query'),

View File

@ -1,18 +1,34 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import d3 from 'd3'; import d3 from 'd3';
import PropTypes from 'prop-types';
import './directed_force.css';
require('./directed_force.css'); const propTypes = {
data: PropTypes.arrayOf(PropTypes.shape({
source: PropTypes.string,
target: PropTypes.string,
value: PropTypes.number,
})),
width: PropTypes.number,
height: PropTypes.number,
linkLength: PropTypes.number,
charge: PropTypes.number,
};
/* Modified from http://bl.ocks.org/d3noob/5141278 */ /* Modified from http://bl.ocks.org/d3noob/5141278 */
const directedForceVis = function (slice, json) { function ForceDirectedGraph(element, props) {
const div = d3.select(slice.selector); PropTypes.checkPropTypes(propTypes, props, 'prop', 'ForceDirectedGraph');
const width = slice.width();
const height = slice.height();
const fd = slice.formData;
const linkLength = fd.link_length || 200;
const charge = fd.charge || -500;
const links = json.data; const {
data,
width,
height,
linkLength = 200,
charge = -500,
} = props;
const div = d3.select(element);
const links = data;
const nodes = {}; const nodes = {};
// Compute the distinct nodes from the links. // Compute the distinct nodes from the links.
links.forEach(function (link) { links.forEach(function (link) {
@ -73,73 +89,73 @@ const directedForceVis = function (slice, json) {
/* eslint-enable no-use-before-define */ /* eslint-enable no-use-before-define */
const force = d3.layout.force() const force = d3.layout.force()
.nodes(d3.values(nodes)) .nodes(d3.values(nodes))
.links(links) .links(links)
.size([width, height]) .size([width, height])
.linkDistance(linkLength) .linkDistance(linkLength)
.charge(charge) .charge(charge)
.on('tick', tick) .on('tick', tick)
.start(); .start();
div.selectAll('*').remove(); div.selectAll('*').remove();
const svg = div.append('svg') const svg = div.append('svg')
.attr('width', width) .attr('width', width)
.attr('height', height); .attr('height', height);
// build the arrow. // build the arrow.
svg.append('svg:defs').selectAll('marker') svg.append('svg:defs').selectAll('marker')
.data(['end']) // Different link/path types can be defined here .data(['end']) // Different link/path types can be defined here
.enter() .enter()
.append('svg:marker') // This section adds in the arrows .append('svg:marker') // This section adds in the arrows
.attr('id', String) .attr('id', String)
.attr('viewBox', '0 -5 10 10') .attr('viewBox', '0 -5 10 10')
.attr('refX', 15) .attr('refX', 15)
.attr('refY', -1.5) .attr('refY', -1.5)
.attr('markerWidth', 6) .attr('markerWidth', 6)
.attr('markerHeight', 6) .attr('markerHeight', 6)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('svg:path') .append('svg:path')
.attr('d', 'M0,-5L10,0L0,5'); .attr('d', 'M0,-5L10,0L0,5');
const edgeScale = d3.scale.linear() const edgeScale = d3.scale.linear()
.range([0.1, 0.5]); .range([0.1, 0.5]);
// add the links and the arrows // add the links and the arrows
const path = svg.append('svg:g').selectAll('path') const path = svg.append('svg:g').selectAll('path')
.data(force.links()) .data(force.links())
.enter() .enter()
.append('svg:path') .append('svg:path')
.attr('class', 'link') .attr('class', 'link')
.style('opacity', function (d) { .style('opacity', function (d) {
return edgeScale(d.value / d.target.max); return edgeScale(d.value / d.target.max);
}) })
.attr('marker-end', 'url(#end)'); .attr('marker-end', 'url(#end)');
// define the nodes // define the nodes
const node = svg.selectAll('.node') const node = svg.selectAll('.node')
.data(force.nodes()) .data(force.nodes())
.enter() .enter()
.append('g') .append('g')
.attr('class', 'node') .attr('class', 'node')
.on('mouseenter', function () { .on('mouseenter', function () {
d3.select(this) d3.select(this)
.select('circle') .select('circle')
.transition() .transition()
.style('stroke-width', 5); .style('stroke-width', 5);
d3.select(this) d3.select(this)
.select('text') .select('text')
.transition() .transition()
.style('font-size', 25); .style('font-size', 25);
}) })
.on('mouseleave', function () { .on('mouseleave', function () {
d3.select(this) d3.select(this)
.select('circle') .select('circle')
.transition() .transition()
.style('stroke-width', 1.5); .style('stroke-width', 1.5);
d3.select(this) d3.select(this)
.select('text') .select('text')
.transition() .transition()
.style('font-size', 12); .style('font-size', 12);
}) })
.call(force.drag); .call(force.drag);
@ -148,21 +164,33 @@ const directedForceVis = function (slice, json) {
return Math.sqrt(d.total); return Math.sqrt(d.total);
}); });
const circleScale = d3.scale.linear() const circleScale = d3.scale.linear()
.domain(ext) .domain(ext)
.range([3, 30]); .range([3, 30]);
node.append('circle') node.append('circle')
.attr('r', function (d) { .attr('r', function (d) {
return circleScale(Math.sqrt(d.total)); return circleScale(Math.sqrt(d.total));
}); });
// add the text // add the text
node.append('text') node.append('text')
.attr('x', 6) .attr('x', 6)
.attr('dy', '.35em') .attr('dy', '.35em')
.text(function (d) { .text(d => d.name);
return d.name; }
});
};
module.exports = directedForceVis; function adaptor(slice, payload) {
const { selector, formData } = slice;
const { link_length: linkLength, charge } = formData;
const element = document.querySelector(selector);
return ForceDirectedGraph(element, {
data: payload.data,
width: slice.width(),
height: slice.height(),
linkLength,
charge,
});
}
export default adaptor;