mirror of
https://gitee.com/antv/g6.git
synced 2024-12-04 20:59:15 +08:00
add: simplify case demo. feat: clustering in fruchterman layout
This commit is contained in:
commit
75869536dc
1
demos/assets/data/filtered-trade.json
Normal file
1
demos/assets/data/filtered-trade.json
Normal file
File diff suppressed because one or more lines are too long
354
demos/simplify.html
Normal file
354
demos/simplify.html
Normal file
@ -0,0 +1,354 @@
|
||||
<!DOCTYPE html></script>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<style>
|
||||
body{
|
||||
background: rgb(255, 255, 255);
|
||||
}
|
||||
.g6-tooltip {
|
||||
border: 1px solid #e2e2e2;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #545454;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
padding: 10px 8px;
|
||||
box-shadow: rgb(174, 174, 174) 0px 0px 10px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script src="./assets/d3-4.13.0.min.js"></script>
|
||||
<script src="../build/fruchterman.js"></script>
|
||||
<script src="../build/bundling.js"></script>
|
||||
|
||||
<script>
|
||||
const colors = [ '#f5222d', '#faad14',
|
||||
'#a0d911', '#13c2c2', '#1890ff', '#b37feb', '#eb2f96' ];
|
||||
const beginColor = '#ff4d4f';
|
||||
const endColor = '#5b8c00';
|
||||
const testData = {
|
||||
nodes: [
|
||||
{
|
||||
id: '0',
|
||||
region: '1'
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
region: '2'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
region: '1'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
region: '2'
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: '0',
|
||||
target: '1',
|
||||
},
|
||||
{
|
||||
source: '1',
|
||||
target: '2',
|
||||
},
|
||||
{
|
||||
source: '2',
|
||||
target: '3',
|
||||
},
|
||||
{
|
||||
source: '3',
|
||||
target: '0',
|
||||
},
|
||||
]
|
||||
}
|
||||
d3.json("./assets/data/filtered-trade.json", function(data) {
|
||||
const nodes = data.nodes;
|
||||
const edges = data.edges;
|
||||
|
||||
const nodeMap = new Map();
|
||||
const clusterMap = new Map();
|
||||
let cidx = 0;
|
||||
nodes.forEach(n => {
|
||||
nodeMap.set(n.id, n);
|
||||
let region = n.region.split(" ");
|
||||
if (n.region === 'East Asia') region = n.region;
|
||||
else region = region[region.length - 1];
|
||||
|
||||
if (clusterMap.get(region) === undefined) {
|
||||
clusterMap.set(region, cidx);
|
||||
cidx++;
|
||||
}
|
||||
const clusterId = clusterMap.get(region);
|
||||
const color = colors[clusterId % colors.length];
|
||||
n.style = {
|
||||
color,
|
||||
fill: color
|
||||
};
|
||||
n.cluster = clusterId;
|
||||
n.importValue = 0;
|
||||
n.exportValue = 0;
|
||||
});
|
||||
// map the value of
|
||||
edges.forEach(e => {
|
||||
if (e.value === '') e.value = 0;
|
||||
const v = parseFloat(e.value);
|
||||
nodeMap.get(e.source).exportValue += v;
|
||||
nodeMap.get(e.target).targetValue += v;
|
||||
});
|
||||
mapValueToProp(nodes, 'exportValue', 'size', [2, 12]);
|
||||
// mapValueToProp(edges, 'value', 'style.lineWidth', [0.2, 1]);
|
||||
|
||||
// const fruchtermanLayout = new Fruchterman({
|
||||
// maxIteration: 8000,
|
||||
// center: [500, 300],
|
||||
// gravity: 10,
|
||||
// clustering: true
|
||||
// });
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 1000,
|
||||
height: 700,
|
||||
// plugins: [ fruchtermanLayout],
|
||||
layout: {
|
||||
type: 'fruchterman',
|
||||
maxIteration: 8000,
|
||||
center: [500, 300],
|
||||
gravity: 10,
|
||||
clustering: true
|
||||
},
|
||||
fitView: true,
|
||||
linkCenter: true,
|
||||
defaultNode: {
|
||||
shape: 'circle',
|
||||
size: 5
|
||||
},
|
||||
defaultEdge: {
|
||||
shape: 'quadratic'
|
||||
},
|
||||
nodeStyle: {
|
||||
default: {
|
||||
lineWidth: 0.6
|
||||
},
|
||||
},
|
||||
edgeStyle: {
|
||||
default: {
|
||||
lineWidth: 0.4,
|
||||
strokeOpacity: 0.2,
|
||||
stroke: 'l(0) 0:' + beginColor + ' 1:' + endColor
|
||||
}
|
||||
},
|
||||
modes: {
|
||||
default: [ 'drag-node', 'drag-canvas', {
|
||||
type: 'tooltip',
|
||||
formatText(model) {
|
||||
let name = '';
|
||||
let countries = '';
|
||||
if (model.name) name = model.name + '<br>';
|
||||
if (model.countries) countries = '<br>Number of Countries: ' + model.countries;
|
||||
const text = name
|
||||
+ 'Export Value: ' + model.exportValue + "(1000USD)"
|
||||
+ '<br>Region: ' + model.region
|
||||
+ '<br>Cluster: ' + model.cluster
|
||||
+ countries;
|
||||
return text;
|
||||
},
|
||||
shouldUpdate: e => {
|
||||
return true;
|
||||
}
|
||||
}]
|
||||
},
|
||||
});
|
||||
// fruchtermanLayout.layout(data);
|
||||
edges.forEach(e => {
|
||||
if (nodeMap.get(e.source).x < nodeMap.get(e.target).x) {
|
||||
if (e.style === undefined) e.style = {};
|
||||
e.style.stroke = 'l(0) 0:' + endColor + ' 1:' + beginColor;
|
||||
}
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
graph.on('node:click', e => {
|
||||
const targetItem = e.item;
|
||||
const model = targetItem.getModel();
|
||||
const graphEdges = graph.getEdges();
|
||||
const graphNodes = graph.getNodes();
|
||||
// click on the cluster node
|
||||
if (model.id.substr(0, 7) === 'cluster') {
|
||||
graphNodes.forEach(gn => {
|
||||
const gnModel = gn.getModel();
|
||||
// show the common nodes
|
||||
if (gnModel.cluster === model.cluster && gnModel.id.substr(0, 7) != 'cluster') {
|
||||
gn.show();
|
||||
}
|
||||
// remove the cluster nodes
|
||||
if (gnModel.id === model.id) graph.removeItem(gn);
|
||||
});
|
||||
|
||||
graphEdges.forEach(ge => {
|
||||
const sourceModel = ge.get('sourceNode').getModel();
|
||||
const targetModel = ge.get('targetNode').getModel();
|
||||
// show the common edges
|
||||
if ((sourceModel.cluster === model.cluster && sourceModel.id.substr(0, 7) !== 'cluster')
|
||||
|| (targetModel.cluster === model.cluster && targetModel.id.substr(0, 7) !== 'cluster')) {
|
||||
ge.show();
|
||||
// add the edges to other cluster nodes
|
||||
if (!ge.get('sourceNode').get('visible') && sourceModel.cluster !== model.cluster) {
|
||||
let c1 = beginColor, c2 = endColor;
|
||||
if (model.x < targetModel.x) {
|
||||
c1 = endColor;
|
||||
c2 = beginColor;
|
||||
}
|
||||
graph.addItem('edge', {
|
||||
source: 'cluster' + sourceModel.cluster,
|
||||
target: targetModel.id,
|
||||
id: 'cluster-edge-' + ge.id,
|
||||
style: {
|
||||
stroke: 'l(0) 0:' + c1 + ' 1:' + c2
|
||||
}
|
||||
});
|
||||
} else if (ge.get('targetNode').get('visible') && targetModel.cluster !== model.cluster) {
|
||||
let c1 = beginColor, c2 = endColor;
|
||||
if (sourceModel.x < model.x) {
|
||||
c1 = endColor;
|
||||
c2 = beginColor;
|
||||
}
|
||||
graph.addItem('edge', {
|
||||
source: sourceModel.id,
|
||||
target: 'cluster' + targetModel.id,
|
||||
id: 'cluster-edge-' + ge.id,
|
||||
style: {
|
||||
stroke: 'l(0) 0:' + c1 + ' 1:' + c2
|
||||
}
|
||||
});
|
||||
}
|
||||
// hide the edges to the common nodes in other clusters
|
||||
if (!ge.get('sourceNode').get('visible') || !ge.get('targetNode').get('visible')) {
|
||||
ge.hide();
|
||||
}
|
||||
}
|
||||
// remove the cluster edges
|
||||
if (sourceModel.id === model.id || targetModel.id === model.id) {
|
||||
graph.removeItem(ge);
|
||||
}
|
||||
});
|
||||
} else { // click on the common node, cllapse them
|
||||
// calculate the cluster center
|
||||
const center = {x: 0, y: 0, count: 0, exportValue: 0};
|
||||
nodes.forEach(n => {
|
||||
if (n.cluster === model.cluster) {
|
||||
center.x += n.x;
|
||||
center.y += n.y;
|
||||
center.count++;
|
||||
center.exportValue += n.exportValue;
|
||||
}
|
||||
});
|
||||
center.x /= center.count;
|
||||
center.y /= center.count;
|
||||
// add cluster node on the center
|
||||
const size = center.count * 1;
|
||||
const clusterNodeId = 'cluster' + model.cluster;
|
||||
const color = colors[ model.cluster % colors.length ];
|
||||
const regionStrs = model.region.split(' ');
|
||||
let region = regionStrs[regionStrs.length - 1];
|
||||
if (model.region === 'East Asia') region = model.region;
|
||||
let labelPosition = 'center';
|
||||
if (region.length > size) labelPosition = 'left';
|
||||
const clusterNode = graph.addItem('node', {
|
||||
x: center.x,
|
||||
y: center.y,
|
||||
id: clusterNodeId,
|
||||
cluster: model.cluster,
|
||||
region: region,
|
||||
countries: center.count,
|
||||
exportValue: center.exportValue,
|
||||
style: {
|
||||
color,
|
||||
fill: color
|
||||
},
|
||||
size,
|
||||
label: region,
|
||||
labelCfg: {
|
||||
style: { fontSize: 8.5 },
|
||||
position: labelPosition
|
||||
}
|
||||
});
|
||||
|
||||
// add edges about the cluster
|
||||
graphEdges.forEach(ge => {
|
||||
const sourceModel = ge.get('sourceNode').getModel();
|
||||
const targetModel = ge.get('targetNode').getModel();
|
||||
if (!ge.get('sourceNode').get('visible') || !ge.get('targetNode').get('visible')) return;
|
||||
if (sourceModel.cluster === model.cluster && targetModel.cluster !== model.cluster) {
|
||||
let c1 = beginColor, c2 = endColor;
|
||||
if (center.x < targetModel.x) {
|
||||
c1 = endColor;
|
||||
c2 = beginColor;
|
||||
}
|
||||
graph.addItem('edge', {
|
||||
source: clusterNodeId,
|
||||
target: targetModel.id,
|
||||
id: 'cluster-edge-' + ge.id,
|
||||
style: {
|
||||
stroke: 'l(0) 0:' + c1 + ' 1:' + c2
|
||||
}
|
||||
});
|
||||
} else if (targetModel.cluster === model.cluster && sourceModel.cluster !== model.cluster) {
|
||||
let c1 = beginColor, c2 = endColor;
|
||||
if (sourceModel.x < center.x) {
|
||||
c1 = endColor;
|
||||
c2 = beginColor;
|
||||
}
|
||||
graph.addItem('edge', {
|
||||
source: sourceModel.id,
|
||||
target: clusterNodeId,
|
||||
id: 'cluster-edge-' + ge.id,
|
||||
style: {
|
||||
stroke: 'l(0) 0:' + c1 + ' 1:' + c2
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// hide the common nodes in the cluster
|
||||
graphNodes.forEach(gn => {
|
||||
if (gn.getModel().cluster === model.cluster
|
||||
&& gn.getModel().id.substr(0, 7) !== 'cluster') gn.hide();
|
||||
});
|
||||
// hide the common edges about cluster
|
||||
graphEdges.forEach(ge => {
|
||||
if (!ge.get('sourceNode').get('visible') || !ge.get('targetNode').get('visible')) ge.hide();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
function mapValueToProp(items, valueName, propName, range){
|
||||
const valueRange = [9999999999, -9999999999];
|
||||
items.forEach(n => {
|
||||
if (n[valueName] > valueRange[1]) valueRange[1] = n[valueName];
|
||||
if (n[valueName] < valueRange[0]) valueRange[0] = n[valueName];
|
||||
});
|
||||
const valueLength = valueRange[1] - valueRange[0];
|
||||
const rLength = range[1] - range[0];
|
||||
const propNameStrs = propName.split('.');
|
||||
if (propNameStrs[0] === 'style' && propNameStrs.length > 1) {
|
||||
items.forEach(n => {
|
||||
if (n.style === undefined) n.style = {};
|
||||
n.style[propNameStrs[1]] = rLength * (n[valueName] - valueRange[0]) / valueLength + range[0];
|
||||
});
|
||||
} else {
|
||||
items.forEach(n => {
|
||||
n[propNameStrs[0]] = rLength * (n[valueName] - valueRange[0]) / valueLength + range[0];
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -10,6 +10,8 @@ class Fruchterman extends Base {
|
||||
center: [ 0, 0 ], // 布局中心
|
||||
gravity: 10, // 重力大小,影响图的紧凑程度
|
||||
speed: 1, // 速度
|
||||
clustering: false, // 是否产生聚类力
|
||||
clusterGravity: 10, // 是否产生聚类力
|
||||
onLayoutEnd() {}, // 布局完成回调
|
||||
onTick() {} // 每一迭代布局回调
|
||||
};
|
||||
@ -82,12 +84,45 @@ class Fruchterman extends Base {
|
||||
const k = Math.sqrt(width * height / (nodes.length + 1));
|
||||
const gravity = self.get('gravity');
|
||||
const speed = self.get('speed');
|
||||
const clustering = self.get('clustering');
|
||||
const clusters = [];
|
||||
if (clustering) {
|
||||
nodes.forEach(n => {
|
||||
if (clusters[n.cluster] === undefined) {
|
||||
clusters[n.cluster] = {
|
||||
cx: 0,
|
||||
cy: 0,
|
||||
count: 0
|
||||
};
|
||||
}
|
||||
clusters[n.cluster].cx += n.x;
|
||||
clusters[n.cluster].cy += n.y;
|
||||
clusters[n.cluster].count++;
|
||||
});
|
||||
clusters.forEach(c => {
|
||||
c.cx /= c.count;
|
||||
c.cy /= c.count;
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < maxIteration; i++) {
|
||||
const disp = [];
|
||||
nodes.forEach((n, i) => {
|
||||
disp[i] = { x: 0, y: 0 };
|
||||
});
|
||||
self.getDisp(nodes, edges, nodeMap, nodeIndexMap, disp, k);
|
||||
let lastIters = false;
|
||||
if (maxIteration - i < 10) lastIters = true;
|
||||
self.getDisp(nodes, edges, nodeMap, nodeIndexMap, disp, k, lastIters);
|
||||
|
||||
// gravity for clusters
|
||||
if (clustering) {
|
||||
const clusterGravity = self.get('clusterGravity') || gravity;
|
||||
nodes.forEach((n, i) => {
|
||||
const distLength = Math.sqrt((n.x - clusters[n.cluster].cx) * (n.x - clusters[n.cluster].cx) + (n.y - clusters[n.cluster].cy) * (n.y - clusters[n.cluster].cy));
|
||||
const gravityForce = 0.1 * k * clusterGravity * distLength;
|
||||
disp[i].x -= gravityForce * (n.x - clusters[n.cluster].cx) / distLength;
|
||||
disp[i].y -= gravityForce * (n.y - clusters[n.cluster].cy) / distLength;
|
||||
});
|
||||
}
|
||||
|
||||
// gravity
|
||||
nodes.forEach((n, i) => {
|
||||
@ -96,7 +131,7 @@ class Fruchterman extends Base {
|
||||
disp[i].x -= gravityForce * n.x / distLength;
|
||||
disp[i].y -= gravityForce * n.y / distLength;
|
||||
});
|
||||
// speed
|
||||
// speed
|
||||
nodes.forEach((n, i) => {
|
||||
disp[i].dx *= speed / SPEED_DIVISOR;
|
||||
disp[i].dy *= speed / SPEED_DIVISOR;
|
||||
@ -113,12 +148,15 @@ class Fruchterman extends Base {
|
||||
});
|
||||
}
|
||||
}
|
||||
getDisp(nodes, edges, nodeMap, nodeIndexMap, disp, k) {
|
||||
getDisp(nodes, edges, nodeMap, nodeIndexMap, disp, k, lastIters = false) {
|
||||
const self = this;
|
||||
self.calRepulsive(nodes, disp, k);
|
||||
self.calRepulsive(nodes, disp, k, lastIters);
|
||||
self.calAttractive(edges, nodeMap, nodeIndexMap, disp, k);
|
||||
}
|
||||
calRepulsive(nodes, disp, k) {
|
||||
calRepulsive(nodes, disp, k, lastIters = false) {
|
||||
const self = this;
|
||||
let repStrength = k;
|
||||
const clustering = self.get('clustering');
|
||||
nodes.forEach((v, i) => {
|
||||
disp[i] = { x: 0, y: 0 };
|
||||
nodes.forEach((u, j) => {
|
||||
@ -127,7 +165,8 @@ class Fruchterman extends Base {
|
||||
const vecy = v.y - u.y;
|
||||
let vecLengthSqr = vecx * vecx + vecy * vecy;
|
||||
if (vecLengthSqr === 0) vecLengthSqr = 1;
|
||||
const common = (k * k) / vecLengthSqr;
|
||||
if (clustering && lastIters && u.cluster !== v.cluster) repStrength *= 2;
|
||||
const common = (repStrength * repStrength) / vecLengthSqr;
|
||||
disp[i].x += vecx * common;
|
||||
disp[i].y += vecy * common;
|
||||
});
|
||||
|
@ -15,10 +15,12 @@ Layout.registerLayout('fruchterman', {
|
||||
layoutType: 'fruchterman',
|
||||
getDefaultCfg() {
|
||||
return {
|
||||
maxIteration: 1000, // 停止迭代的最大迭代数
|
||||
center: [ 0, 0 ], // 布局中心
|
||||
maxIteration: 8000, // 停止迭代的最大迭代数
|
||||
gravity: 10, // 重力大小,影响图的紧凑程度
|
||||
speed: 1 // 速度
|
||||
speed: 1, // 速度
|
||||
clustering: false, // 是否产生聚类力
|
||||
clusterGravity: 10 // 是否产生聚类力
|
||||
};
|
||||
},
|
||||
/**
|
||||
@ -72,6 +74,26 @@ Layout.registerLayout('fruchterman', {
|
||||
const k = Math.sqrt(width * height / (nodes.length + 1));
|
||||
const gravity = self.gravity;
|
||||
const speed = self.speed;
|
||||
const clustering = self.clustering;
|
||||
const clusters = [];
|
||||
if (clustering) {
|
||||
nodes.forEach(n => {
|
||||
if (clusters[n.cluster] === undefined) {
|
||||
clusters[n.cluster] = {
|
||||
cx: 0,
|
||||
cy: 0,
|
||||
count: 0
|
||||
};
|
||||
}
|
||||
clusters[n.cluster].cx += n.x;
|
||||
clusters[n.cluster].cy += n.y;
|
||||
clusters[n.cluster].count++;
|
||||
});
|
||||
clusters.forEach(c => {
|
||||
c.cx /= c.count;
|
||||
c.cy /= c.count;
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < maxIteration; i++) {
|
||||
const disp = [];
|
||||
nodes.forEach((n, i) => {
|
||||
@ -79,6 +101,17 @@ Layout.registerLayout('fruchterman', {
|
||||
});
|
||||
self.getDisp(nodes, edges, nodeMap, nodeIndexMap, disp, k);
|
||||
|
||||
// gravity for clusters
|
||||
if (clustering) {
|
||||
const clusterGravity = self.clusterGravity || gravity;
|
||||
nodes.forEach((n, i) => {
|
||||
const distLength = Math.sqrt((n.x - clusters[n.cluster].cx) * (n.x - clusters[n.cluster].cx) + (n.y - clusters[n.cluster].cy) * (n.y - clusters[n.cluster].cy));
|
||||
const gravityForce = 0.1 * k * clusterGravity * distLength;
|
||||
disp[i].x -= gravityForce * (n.x - clusters[n.cluster].cx) / distLength;
|
||||
disp[i].y -= gravityForce * (n.y - clusters[n.cluster].cy) / distLength;
|
||||
});
|
||||
}
|
||||
|
||||
// gravity
|
||||
nodes.forEach((n, i) => {
|
||||
const distLength = Math.sqrt(disp[i].x * disp[i].x + disp[i].y * disp[i].y);
|
||||
@ -86,7 +119,7 @@ Layout.registerLayout('fruchterman', {
|
||||
disp[i].x -= gravityForce * n.x / distLength;
|
||||
disp[i].y -= gravityForce * n.y / distLength;
|
||||
});
|
||||
// speed
|
||||
// speed
|
||||
nodes.forEach((n, i) => {
|
||||
disp[i].dx *= speed / SPEED_DIVISOR;
|
||||
disp[i].dy *= speed / SPEED_DIVISOR;
|
||||
|
Loading…
Reference in New Issue
Block a user