diff --git a/src/App.vue b/src/App.vue
index 829b1c8..6d51322 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -16,6 +16,20 @@
label="Edge opacity"
hide-details
>
+
+
@@ -110,6 +123,13 @@
label="Radial field"
hide-details
>
+
Download JSON
@@ -139,11 +159,12 @@ export default {
fields: [],
showEdges: false,
edgeOpacity: 0.5,
+ nodeOpacity: 0.5,
+ nodeStrokeWidth: 1.0,
size: 0.5,
sizeField: 'degree',
layoutRunning: false,
alpha: 1.0,
- alphaFromWorker: false,
chargeStrength: 30,
theta: 1.5,
collideStrength: 0.7,
@@ -155,6 +176,7 @@ export default {
yStrength: 0,
radialField: null,
radialStrength: 0,
+ gravityStrength: 0,
nodeCount: 0,
edgeCount: 0,
};
@@ -240,7 +262,6 @@ export default {
graph.nodes.forEach((n, i) => nodeMap[n.id] = i);
lines = layer.createFeature('line').data(graph.edges.map(e => [nodeMap[e.source], nodeMap[e.target]])).style({
position: nodeid => graph.nodes[nodeid],
- width: 1,
strokeColor: 'black',
strokeOpacity: this.edgeOpacity,
});
@@ -259,13 +280,18 @@ export default {
}
});
+ // TODO: call geoUtils.convertColor
+ // TODO: look into https://opengeoscience.github.io/geojs/examples/animation/
+ // pointFeature.updateStyleFromArray(updateStyles, null, true);
+
points = layer.createFeature('point', {
primitiveShape: 'triangle',
style: {
strokeColor: 'black',
fillColor: nodeid => graph.nodes[nodeid].select ? ['yellow', 'red'][graph.nodes[nodeid].select - 1] : 'grey',
- fillOpacity: 0.5,
- strokeOpacity: 0.5,
+ fillOpacity: this.nodeOpacity,
+ strokeOpacity: this.nodeOpacity,
+ strokeWidth: this.nodeStrokeWidth,
},
position: nodeid => graph.nodes[nodeid]
}).data(Object.keys(graph.nodes));
@@ -312,11 +338,6 @@ export default {
points.position(nodeid => positions[nodeid]);
map.draw();
}
- else if (e.data.type === 'alpha') {
- this.alphaFromWorker = true;
- this.alpha = e.data.value;
- this.$nextTick(() => this.alphaFromWorker = false);
- }
}
// Add watchers which sync data to layout worker
@@ -332,6 +353,7 @@ export default {
'yStrength',
'radialField',
'radialStrength',
+ 'gravityStrength',
].forEach(name => {
function sendToWorker(value) {
layoutWorker.postMessage({type: name, value});
@@ -359,11 +381,24 @@ export default {
map.draw();
}
},
+ nodeOpacity(value) {
+ if (points) {
+ points.style('strokeOpacity', value);
+ points.style('fillOpacity', value);
+ points.modified();
+ map.draw();
+ }
+ },
+ nodeStrokeWidth(value) {
+ if (points) {
+ points.style('strokeWidth', value);
+ points.modified();
+ map.draw();
+ }
+ },
alpha: {
handler(value) {
- if (!this.alphaFromWorker) {
- layoutWorker.postMessage({type: 'alpha', value: value});
- }
+ layoutWorker.postMessage({type: 'alpha', value: value});
},
immediate: true,
},
@@ -397,7 +432,7 @@ export default {
reader.onload = function (evt) {
const extension = file.name.split('.').slice(-1)[0];
if (extension === 'json') {
- layoutWorker.postMessage({type: 'loadJSON', text: evt.target.result});
+ layoutWorker.postMessage({type: 'loadJSON', text: JSON.parse(evt.target.result)});
} else if (extension === 'csv') {
layoutWorker.postMessage({type: 'loadEdgeList', text: evt.target.result});
} else {
diff --git a/src/scales.js b/src/scales.js
index f01bd1e..9602f81 100644
--- a/src/scales.js
+++ b/src/scales.js
@@ -21,6 +21,6 @@ export function generateSizeScale(arr, field, size) {
if (field === 'None') {
return () => 250 * size;
}
- const sizeScale = generateScale(arr, field, {min: 3, max: 500*500, invalid: 2});
+ const sizeScale = generateScale(arr, field, {min: 10*10, max: 500*500, invalid: 2});
return d => Math.sqrt(sizeScale(d)) * size;
}
diff --git a/src/worker.js b/src/worker.js
index a19ef5e..b76ce7b 100644
--- a/src/worker.js
+++ b/src/worker.js
@@ -12,10 +12,14 @@ let linkStrengthFunctions = {
inverseMinDegree: link => linkStrength / Math.min(link.source.degree, link.target.degree),
inverseSumDegree: link => linkStrength / (link.source.degree + link.target.degree),
inverseSumSqrtDegree: link => linkStrength / (Math.sqrt(link.source.degree) + Math.sqrt(link.target.degree)),
+ constant: () => linkStrength,
+ radius: link => linkStrength*linkStrength / Math.min(collide.radius()(link.source), collide.radius()(link.target)),
};
let linkDistanceFunctions = {
sumSqrtDegree: link => (Math.sqrt(link.source.degree) + Math.sqrt(link.target.degree)) * size,
+ constant: () => size / 20,
+ radius: link => (2 - linkStrength) * (collide.radius()(link.source) + collide.radius()(link.target)),
};
let link = d3.forceLink().id(d => d.id).distance(linkDistanceFunctions.sumSqrtDegree).strength(linkStrengthFunctions.inverseMinDegree);
@@ -25,33 +29,38 @@ let center = d3.forceCenter();
let x = d3.forceX();
let y = d3.forceY();
let radial = d3.forceRadial();
+let gravity = d3.forceRadial().radius(0);
let simulation = d3.forceSimulation()
.alphaMin(0)
.alphaTarget(0)
+ .alphaDecay(0)
.stop();
loadGraph = function(graph) {
function tick() {
- postMessage({type: 'alpha', value: simulation.alpha()});
postMessage({type: 'positions', nodes: graph.nodes.map(n => ({x: n.x, y: n.y}))});
}
if (!graph.nodes) {
graph.nodes = d3.set([...graph.edges.map(d => d.source), ...graph.edges.map(d => d.target)]).values().map(d => ({
id: d,
- degree: 0,
- x: Math.random()*1000,
- y: Math.random()*1000,
}));
}
const nodeMap = {};
graph.nodes.forEach(d => {
+ d.degree = 0;
+ d.id = '' + d.id;
nodeMap[d.id] = d;
});
+ graph.edges.forEach(d => {
+ d.source = '' + d.source;
+ d.target = '' + d.target;
+ });
graph.edges = graph.edges.filter(e => nodeMap[e.source] && nodeMap[e.target]);
graph.edges.forEach(d => {
- nodeMap[d.source].degree += 1;
- nodeMap[d.target].degree += 1;
+ const weight = d.weight !== undefined ? +d.weight : 1
+ nodeMap[d.source].degree += weight;
+ nodeMap[d.target].degree += weight;
});
graph.nodes.sort((a, b) => d3.ascending(a.degree, b.degree));
@@ -88,7 +97,7 @@ onmessage = function(e) {
loadGraph({edges: d3.csvParse(e.data.text)});
}
else if (e.data.type === 'loadJSON') {
- loadGraph(JSON.parse(e.data.text));
+ loadGraph(e.data.text);
}
else if (e.data.type === 'theta') {
charge.theta(e.data.value);
@@ -98,16 +107,20 @@ onmessage = function(e) {
}
else if (e.data.type === 'size') {
size = e.data.value;
- link.strength(link.strength());
collide.radius(scales.generateSizeScale(simulation.nodes(), sizeField, size));
+ link.distance(link.distance());
+ link.strength(link.strength());
}
else if (e.data.type === 'sizeField') {
sizeField = e.data.value;
collide.radius(scales.generateSizeScale(simulation.nodes(), sizeField, size));
+ link.distance(link.distance());
+ link.strength(link.strength());
}
else if (e.data.type === 'linkStrength') {
simulation.force('link', e.data.value ? link : null);
linkStrength = e.data.value;
+ link.distance(link.distance());
link.strength(link.strength());
}
else if (e.data.type === 'chargeStrength') {
@@ -147,6 +160,10 @@ onmessage = function(e) {
simulation.nodes(), radialField, {area: 1000, min: 0.5, max: 1.5, invalid: 1.6},
));
}
+ else if (e.data.type === 'gravityStrength') {
+ simulation.force('gravity', e.data.value ? gravity : null);
+ gravity.strength(e.data.value);
+ }
else {
throw Error(`Unknown message type '${e.data.type}'`);
}