add force-directed graph

This commit is contained in:
Krist Wongsuphasawat 2019-01-29 11:21:12 -08:00 committed by Yongjie Zhao
parent 560d17fe64
commit 4175a3d54f
8 changed files with 364 additions and 0 deletions

View File

@ -0,0 +1,41 @@
{
"name": "@superset-ui/legacy-plugin-chart-force-directed",
"version": "0.0.0",
"description": "Superset Legacy Chart - Force-directed Graph",
"sideEffects": false,
"main": "lib/index.js",
"module": "esm/index.js",
"files": [
"esm",
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/apache-superset/superset-ui-legacy.git"
},
"keywords": [
"superset"
],
"author": "Superset",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/apache-superset/superset-ui-legacy/issues"
},
"homepage": "https://github.com/apache-superset/superset-ui-legacy#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@superset-ui/core": "^0.9.0",
"d3": "^3.5.17",
"prop-types": "^15.6.2"
},
"devDependencies": {
"@superset-ui/chart": "^0.9.0",
"@superset-ui/translation": "^0.9.1"
},
"peerDependencies": {
"@superset-ui/chart": "^0.9.0",
"@superset-ui/translation": "^0.9.1"
}
}

View File

@ -0,0 +1,37 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.directed_force path.link {
fill: none;
stroke: #000;
stroke-width: 1.5px;
}
.directed_force circle {
fill: #ccc;
stroke: #000;
stroke-width: 1.5px;
stroke-opacity: 1;
opacity: 0.75;
}
.directed_force text {
fill: #000;
font: 10px sans-serif;
pointer-events: none;
}

View File

@ -0,0 +1,195 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable no-magic-numbers, sort-keys, func-names */
/* eslint-disable no-param-reassign, babel/no-invalid-this */
import d3 from 'd3';
import PropTypes from 'prop-types';
import './ForceDirected.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 */
function ForceDirected(element, props) {
const { data, width, height, linkLength = 200, charge = -500 } = props;
const div = d3.select(element);
const links = data;
const nodes = {};
// Compute the distinct nodes from the links.
links.forEach(link => {
link.source =
nodes[link.source] ||
(nodes[link.source] = {
name: link.source,
});
link.target =
nodes[link.target] ||
(nodes[link.target] = {
name: link.target,
});
link.value = Number(link.value);
const targetName = link.target.name;
const sourceName = link.source.name;
if (nodes[targetName].total === undefined) {
nodes[targetName].total = link.value;
}
if (nodes[sourceName].total === undefined) {
nodes[sourceName].total = 0;
}
if (nodes[targetName].max === undefined) {
nodes[targetName].max = 0;
}
if (link.value > nodes[targetName].max) {
nodes[targetName].max = link.value;
}
if (nodes[targetName].min === undefined) {
nodes[targetName].min = 0;
}
if (link.value > nodes[targetName].min) {
nodes[targetName].min = link.value;
}
nodes[targetName].total += link.value;
});
/* eslint-disable no-use-before-define */
// add the curvy lines
function tick() {
path.attr('d', d => {
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
const dr = Math.sqrt(dx * dx + dy * dy);
return `M${d.source.x},${d.source.y}A${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`;
});
node.attr('transform', d => `translate(${d.x},${d.y})`);
}
/* eslint-enable no-use-before-define */
const force = d3.layout
.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(linkLength)
.charge(charge)
.on('tick', tick)
.start();
div.selectAll('*').remove();
const svg = div
.append('svg')
.attr('width', width)
.attr('height', height);
// build the arrow.
svg
.append('svg:defs')
.selectAll('marker')
.data(['end']) // Different link/path types can be defined here
.enter()
.append('svg:marker') // This section adds in the arrows
.attr('id', String)
.attr('viewBox', '0 -5 10 10')
.attr('refX', 15)
.attr('refY', -1.5)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5');
const edgeScale = d3.scale.linear().range([0.1, 0.5]);
// add the links and the arrows
const path = svg
.append('svg:g')
.selectAll('path')
.data(force.links())
.enter()
.append('svg:path')
.attr('class', 'link')
.style('opacity', d => edgeScale(d.value / d.target.max))
.attr('marker-end', 'url(#end)');
// define the nodes
const node = svg
.selectAll('.node')
.data(force.nodes())
.enter()
.append('g')
.attr('class', 'node')
.on('mouseenter', function() {
d3.select(this)
.select('circle')
.transition()
.style('stroke-width', 5);
d3.select(this)
.select('text')
.transition()
.style('font-size', 25);
})
.on('mouseleave', function() {
d3.select(this)
.select('circle')
.transition()
.style('stroke-width', 1.5);
d3.select(this)
.select('text')
.transition()
.style('font-size', 12);
})
.call(force.drag);
// add the nodes
const ext = d3.extent(d3.values(nodes), d => Math.sqrt(d.total));
const circleScale = d3.scale
.linear()
.domain(ext)
.range([3, 30]);
node.append('circle').attr('r', d => circleScale(Math.sqrt(d.total)));
// add the text
node
.append('text')
.attr('x', 6)
.attr('dy', '.35em')
.text(d => d.name);
}
ForceDirected.displayName = 'ForceDirected';
ForceDirected.propTypes = propTypes;
export default ForceDirected;

View File

@ -0,0 +1,22 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { reactify } from '@superset-ui/chart';
import Component from './ForceDirected';
export default reactify(Component);

View File

@ -0,0 +1,39 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
const metadata = new ChartMetadata({
credits: ['http://bl.ocks.org/d3noob/5141278'],
description: '',
name: t('Force-directed Graph'),
thumbnail,
});
export default class ForceDirectedChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('./ReactForceDirected.js'),
metadata,
transformProps,
});
}
}

View File

@ -0,0 +1,30 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export default function transformProps(chartProps) {
const { width, height, formData, payload } = chartProps;
const { charge, linkLength } = formData;
return {
charge,
data: payload.data,
height,
linkLength,
width,
};
}