Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 49 additions & 14 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@
label="Edge opacity"
hide-details
></v-slider>
<v-slider
v-model="nodeOpacity"
:min="0.01" :max="1.00" :step="0.01"
thumb-label
label="Node opacity"
hide-details
></v-slider>
<v-slider
v-model="nodeStrokeWidth"
:min="0" :max="1.00" :step="0.01"
thumb-label
label="Node stroke width"
hide-details
></v-slider>
<v-slider
v-model="size"
:min="0.01" :max="1" :step="0.01"
Expand All @@ -34,7 +48,6 @@
v-model="alpha"
:min="0.00" :max="1.00" :step="0.01"
thumb-label
:readonly="this.layoutRunning"
label="Energy"
hide-details
></v-slider>
Expand Down Expand Up @@ -110,6 +123,13 @@
label="Radial field"
hide-details
></v-combobox>
<v-slider
v-model="gravityStrength"
:min="0.00" :max="0.1" :step="0.001"
thumb-label
label="Gravity"
hide-details
></v-slider>
<v-btn block @click="download()">Download JSON</v-btn>
<a ref="downloadAnchor" style="display:none"></a>
</v-navigation-drawer>
Expand Down Expand Up @@ -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,
Expand All @@ -155,6 +176,7 @@ export default {
yStrength: 0,
radialField: null,
radialStrength: 0,
gravityStrength: 0,
nodeCount: 0,
edgeCount: 0,
};
Expand Down Expand Up @@ -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,
});
Expand All @@ -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));
Expand Down Expand Up @@ -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
Expand All @@ -332,6 +353,7 @@ export default {
'yStrength',
'radialField',
'radialStrength',
'gravityStrength',
].forEach(name => {
function sendToWorker(value) {
layoutWorker.postMessage({type: name, value});
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/scales.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
33 changes: 25 additions & 8 deletions src/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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));
Expand Down Expand Up @@ -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);
Expand All @@ -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') {
Expand Down Expand Up @@ -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}'`);
}
Expand Down