diff --git a/client/js/components/exploration/ExplorationNetwork.jsx b/client/js/components/exploration/ExplorationNetwork.jsx
index 1c36310a..f6aa5250 100644
--- a/client/js/components/exploration/ExplorationNetwork.jsx
+++ b/client/js/components/exploration/ExplorationNetwork.jsx
@@ -9,10 +9,11 @@ import {format} from 'd3-format';
import {range} from 'lodash';
import Select from 'react-select';
import {branch} from 'baobab-react/decorators';
+import {ExportButton} from '../misc/Button.jsx';
import {ClassificationSelector, ItemSelector} from '../misc/Selectors.jsx';
import Network from './viz/Network.jsx';
import VizLayout from '../misc/VizLayout.jsx';
-import {exportCSV} from '../../lib/exports';
+import {exportCSV, exportSVG} from '../../lib/exports';
import {
addNetwork,
checkDefaultState,
@@ -21,7 +22,7 @@ import {
selectLabelSizeRatio,
selectLabelThreshold,
selectNodeSize,
- updateSelector
+ updateSelector,
} from '../../actions/network';
import Icon from '../misc/Icon.jsx';
@@ -30,7 +31,7 @@ import specs from '../../../specs.json';
const defaultSelectors = require('../../../config/defaultVizSelectors.json');
const NUMBER_FIXED_FORMAT = format(',.2f'),
- NUMBER_FORMAT = format(',');
+ NUMBER_FORMAT = format(',');
export default class ExplorationGlobals extends Component {
render() {
@@ -51,15 +52,15 @@ export default class ExplorationGlobals extends Component {
selectLabelSizeRatio,
selectLabelThreshold,
updateSelector,
- checkDefaultState
+ checkDefaultState,
},
cursors: {
alert: ['ui', 'alert'],
classifications: ['data', 'classifications', 'flat'],
directions: ['data', 'directions'],
sourceTypes: ['data', 'sourceTypes'],
- state: ['explorationNetworkState']
- }
+ state: ['explorationNetworkState'],
+ },
})
class NetworkPanel extends Component {
constructor(props, context) {
@@ -73,11 +74,9 @@ class NetworkPanel extends Component {
// Check for initial values:
const state = this.props.state;
const initialState = defaultSelectors.network.initialValues;
- const hasInitialState = Object.keys(initialState)
- .some(key => key.split('.').reduce(
- (iter, step) => iter && iter[step],
- state
- ));
+ const hasInitialState = Object.keys(initialState).some(key =>
+ key.split('.').reduce((iter, step) => iter && iter[step], state),
+ );
if (!hasInitialState) {
this.props.actions.checkDefaultState(defaultSelectors.network.initialValues);
@@ -91,11 +90,22 @@ class NetworkPanel extends Component {
this.props.actions.checkDefaultState(defaultSelectors.network.defaultValues);
}
- export() {
+ exportCsv() {
const now = new Date();
exportCSV({
data: this.props.state.data,
- name: `TOFLIT18_Locations_${now.toLocaleString('se-SE').replace(' ', '_')}.csv`
+ name: `TOFLIT18_Locations_${now.toLocaleString('se-SE').replace(' ', '_')}.csv`,
+ });
+ }
+
+ exportGraph() {
+ const now = new Date();
+ const graphSvg = sigma.instances(0).toSVG({
+ labels: true,
+ });
+ exportSVG({
+ nodes: [this.legend, graphSvg],
+ name: `TOFLIT18_Locations_${now.toLocaleString('se-SE').replace(' ', '_')}.svg`,
});
}
@@ -113,34 +123,27 @@ class NetworkPanel extends Component {
actions,
classifications,
sourceTypes,
- state: {
- graph,
- classification,
- nodeSize,
- edgeSize,
- labelSizeRatio,
- labelThreshold,
- loading,
- selectors,
- groups
- }
+ state: {graph, classification, nodeSize, edgeSize, labelSizeRatio, labelThreshold, loading, selectors, groups},
} = this.props;
- const {
- selectedNode,
- fullscreen
- } = this.state;
+ const {selectedNode, fullscreen} = this.state;
const dateMin = selectors.dateMin;
- const dateMaxOptions = range(dateMin || specs.limits.minYear, specs.limits.maxYear).map(d => ({name: '' + d, id: '' + d}));
+ const dateMaxOptions = range(dateMin || specs.limits.minYear, specs.limits.maxYear).map(d => ({
+ name: '' + d,
+ id: '' + d,
+ }));
const dateMax = selectors.dateMax;
- const dateMinOptions = range(specs.limits.minYear, dateMax ? +dateMax + 1 : specs.limits.maxYear).map(d => ({name: '' + d, id: '' + d}));
+ const dateMinOptions = range(specs.limits.minYear, dateMax ? +dateMax + 1 : specs.limits.maxYear).map(d => ({
+ name: '' + d,
+ id: '' + d,
+ }));
const sourceTypesOptions = (sourceTypes || []).map(type => {
return {
name: type,
- value: type
+ value: type,
};
});
@@ -152,8 +155,8 @@ class NetworkPanel extends Component {
description="Choose a partner classification and display a graph showing relations between partners & directions."
leftPanelName="Filters"
rightPanelName="Caption"
- fullscreen={fullscreen} >
- { /* Top of the left panel */ }
+ fullscreen={fullscreen}>
+ {/* Top of the left panel */}
Partner classification
@@ -166,16 +169,24 @@ class NetworkPanel extends Component {
onChange={actions.selectClassification}
selected={classification}
onUpdate={actions.selectClassification}
- defaultValue={defaultSelectors.network.classification} />
+ defaultValue={defaultSelectors.network.classification}
+ />
- { /* Left panel */ }
+ {/* Left panel */}
- { /* Content panel */ }
+ {/* Content panel */}
this.networkComponent = ref}
+ ref={ref => (this.networkComponent = ref)}
graph={graph}
sizeKey={nodeSize}
directed={directed}
@@ -272,13 +300,16 @@ class NetworkPanel extends Component {
toggleFullscreen={this.toggleFullscreen}
alert={alert}
loading={loading}
- className="col-xs-12 col-sm-6 col-md-8" />
+ className="col-xs-12 col-sm-6 col-md-8"
+ />
- { /* Right panel */ }
+ {/* Right panel */}
{/* Left panel */}
@@ -233,7 +228,8 @@ class TermsPanel extends Component {
onChange={actions.update.bind(null, 'sourceType')}
selected={selectors.sourceType}
onUpdate={v => actions.update('sourceType', v)}
- defaultValue={defaultSelectors.terms['selectors.sourceType']} />
+ defaultValue={defaultSelectors.terms['selectors.sourceType']}
+ />
@@ -255,21 +251,19 @@ class TermsPanel extends Component {
onChange={actions.update.bind(null, 'childClassification')}
selected={selectors.childClassification}
onUpdate={v => actions.update('childClassification', v)}
- defaultValue={
- defaultSelectors.terms['selectors.childClassification']
- } />
+ defaultValue={defaultSelectors.terms['selectors.childClassification']}
+ />
actions.update('child', v)}
- defaultValue={defaultSelectors.terms['selectors.child']} />
+ defaultValue={defaultSelectors.terms['selectors.child']}
+ />
@@ -289,23 +283,19 @@ class TermsPanel extends Component {
onChange={actions.update.bind(null, 'partnerClassification')}
selected={selectors.partnerClassification}
onUpdate={v => actions.update('partnerClassification', v)}
- defaultValue={
- defaultSelectors.terms['selectors.partnerClassification']
- } />
+ defaultValue={defaultSelectors.terms['selectors.partnerClassification']}
+ />
actions.update('partner', v)}
- defaultValue={defaultSelectors.terms['selectors.partner']} />
+ defaultValue={defaultSelectors.terms['selectors.partner']}
+ />
@@ -325,30 +315,28 @@ class TermsPanel extends Component {
onChange={actions.update.bind(null, 'direction')}
selected={selectors.direction}
onUpdate={v => actions.update('direction', v)}
- defaultValue={defaultSelectors.terms['selectors.direction']} />
+ defaultValue={defaultSelectors.terms['selectors.direction']}
+ />
Kind
-
- Should we look at import, export, or total?
-
+ Should we look at import, export, or total?
actions.update('kind', v)}
- defaultValue={defaultSelectors.terms['selectors.kind']} />
+ defaultValue={defaultSelectors.terms['selectors.kind']}
+ />
Dates
-
- Choose one date or a range data
-
+
Choose one date or a range data
actions.update('dateMin', v)}
- defaultValue={defaultSelectors.terms['selectors.dateMin']} />
+ defaultValue={defaultSelectors.terms['selectors.dateMin']}
+ />
actions.update('dateMax', v)}
- defaultValue={defaultSelectors.terms['selectors.dateMax']} />
+ defaultValue={defaultSelectors.terms['selectors.dateMax']}
+ />
@@ -397,7 +387,8 @@ class TermsPanel extends Component {
toggleFullscreen={this.toggleFullscreen}
alert={alert}
loading={loading}
- className="col-xs-12 col-sm-6 col-md-8" />
+ className="col-xs-12 col-sm-6 col-md-8"
+ />
{/* Right panel */}
@@ -435,21 +427,26 @@ class TermsPanel extends Component {
options={[
{
value: 'flows',
- label: 'Nb of flows.'
+ label: 'Nb of flows.',
},
{
value: 'value',
- label: 'Value of flows.'
+ label: 'Value of flows.',
},
{
value: 'degree',
- label: 'Degree.'
- }
+ label: 'Degree.',
+ },
]}
value={nodeSize}
- onChange={({value}) => actions.selectNodeSize(value)} />
+ onChange={({value}) => actions.selectNodeSize(value)}
+ />
-
+
{
+ this.legend = el;
+ }}>
Color
Community Louvain
@@ -466,10 +463,11 @@ class TermsPanel extends Component {
searchable={false}
options={range(1, 10).map(num => ({
value: num + '',
- label: num + ''
+ label: num + '',
}))}
value={labelSizeRatio + ''}
- onChange={({value}) => actions.selectLabelSizeRatio(+value)} />
+ onChange={({value}) => actions.selectLabelSizeRatio(+value)}
+ />
Threshold
@@ -479,10 +477,11 @@ class TermsPanel extends Component {
searchable={false}
options={range(0, 20).map(num => ({
value: num + '',
- label: num + ''
+ label: num + '',
}))}
value={labelThreshold + ''}
- onChange={({value}) => actions.selectLabelThreshold(+value)} />
+ onChange={({value}) => actions.selectLabelThreshold(+value)}
+ />
@@ -497,12 +496,10 @@ class TermsPanel extends Component {
Flows: {NUMBER_FORMAT(selectedNode.flows)}
- Value:{' '}
- {NUMBER_FIXED_FORMAT(selectedNode.value)}
+ Value: {NUMBER_FIXED_FORMAT(selectedNode.value)}
- Degree:{' '}
- {NUMBER_FORMAT(selectedNode.degree)}
+ Degree: {NUMBER_FORMAT(selectedNode.degree)}
@@ -511,9 +508,22 @@ class TermsPanel extends Component {
)}
- this.export()}>
- Export
-
+ {
+ this.exportCsv();
+ },
+ },
+ {
+ label: 'Export SVG',
+ fn: () => {
+ this.exportGraph();
+ },
+ },
+ ]}
+ />
diff --git a/client/js/components/exploration/viz/Network.jsx b/client/js/components/exploration/viz/Network.jsx
index 8ce9e702..1285e330 100644
--- a/client/js/components/exploration/viz/Network.jsx
+++ b/client/js/components/exploration/viz/Network.jsx
@@ -26,14 +26,14 @@ const SIGMA_SETTINGS = {
defaultEdgeColor: '#D1D1D1',
maxArrowSize: 5,
minArrowSize: 3,
- sideMargin: 10
+ sideMargin: 10,
};
const LAYOUT_SETTINGS = {
strongGravityMode: true,
gravity: 0.05,
scalingRatio: 10,
- slowDown: 2
+ slowDown: 2,
};
const LAYOUT_TIMEOUT = 7000;
@@ -47,11 +47,11 @@ function focusNode(camera, node) {
{
x: node['read_cammain:x'],
y: node['read_cammain:y'],
- ratio: 0.075
+ ratio: 0.075,
},
{
- duration: 150
- }
+ duration: 150,
+ },
);
}
@@ -59,11 +59,7 @@ function focusNode(camera, node) {
* Helper function used to rescale the camera.
*/
function rescale(camera) {
- sigma.misc.animation.camera(
- camera,
- {x: 0, y: 0, angle: 0, ratio: 1},
- {duration: 150}
- );
+ sigma.misc.animation.camera(camera, {x: 0, y: 0, angle: 0, ratio: 1}, {duration: 150});
}
/**
@@ -74,14 +70,14 @@ export default class Network extends Component {
super(props, context);
this.sigma = new sigma({
- settings: SIGMA_SETTINGS
+ settings: SIGMA_SETTINGS,
});
this.sigma.addCamera('main');
this.layoutSettings = LAYOUT_SETTINGS;
this.state = {
layoutRunning: true,
- selectedNode: null
+ selectedNode: null,
};
this.layoutTimeout = null;
@@ -91,8 +87,7 @@ export default class Network extends Component {
if (!running) {
this.sigma.startForceAtlas2(this.layoutSettings);
- }
- else {
+ } else {
this.sigma.stopForceAtlas2();
clearTimeout(this.layoutTimeout);
this.layoutTimeout = null;
@@ -110,8 +105,7 @@ export default class Network extends Component {
this.focusNode = node => {
if (!node) {
rescale(this.sigma.cameras.main);
- }
- else {
+ } else {
focusNode(this.sigma.cameras.main, this.sigma.graph.nodes(node.id));
}
};
@@ -127,7 +121,7 @@ export default class Network extends Component {
componentDidMount() {
this.sigma.addRenderer({
camera: 'main',
- container: this.refs.mount
+ container: this.refs.mount,
});
this.sigma.bind('clickNode', e => {
@@ -158,12 +152,12 @@ export default class Network extends Component {
// Styling
const nodes = g.nodes(),
- N = nodes.length;
+ N = nodes.length;
nodes.forEach(function(node, i) {
node.size = node.occurrences || node.size;
- node.x = 100 * Math.cos(2 * i * Math.PI / N);
- node.y = 100 * Math.sin(2 * i * Math.PI / N);
+ node.x = 100 * Math.cos((2 * i * Math.PI) / N);
+ node.y = 100 * Math.sin((2 * i * Math.PI) / N);
});
this.sigma.startForceAtlas2(this.layoutSettings);
@@ -182,20 +176,15 @@ export default class Network extends Component {
return this.sigma.refresh();
}
- if (nextProps.colorKey)
- g.nodes().forEach(node => node.color = node[nextProps.colorKey]);
+ if (nextProps.colorKey) g.nodes().forEach(node => (node.color = node[nextProps.colorKey]));
- if (nextProps.sizeKey)
- g.nodes().forEach(node => node.size = node[nextProps.sizeKey]);
+ if (nextProps.sizeKey) g.nodes().forEach(node => (node.size = node[nextProps.sizeKey]));
- if (nextProps.edgeSizeKey)
- g.edges().forEach(edge => edge.size = edge[nextProps.edgeSizeKey]);
+ if (nextProps.edgeSizeKey) g.edges().forEach(edge => (edge.size = edge[nextProps.edgeSizeKey]));
- if (nextProps.labelThreshold)
- this.sigma.settings({labelThreshold: +nextProps.labelThreshold});
+ if (nextProps.labelThreshold) this.sigma.settings({labelThreshold: +nextProps.labelThreshold});
- if (nextProps.labelSizeRatio)
- this.sigma.settings({labelSizeRatio: +nextProps.labelSizeRatio});
+ if (nextProps.labelSizeRatio) this.sigma.settings({labelSizeRatio: +nextProps.labelSizeRatio});
this.sigma.refresh();
}
@@ -205,44 +194,28 @@ export default class Network extends Component {
this.sigma = null;
}
- downloadGraphAsSVG() {
- this.sigma.toSVG({
- download: true,
- filename: 'graph.svg',
- labels: true
- });
- }
-
render() {
- const {
- graph,
- alert,
- loading,
- className,
- } = this.props;
+ const {graph, alert, loading, className} = this.props;
const isGraphEmpty = graph && (!graph.nodes || !graph.nodes.length);
return (
-
+
{isGraphEmpty &&
}
- {
- (alert || loading) && (
-
- {alert &&
{alert}
}
- {
- loading && (
-
- Loading...
-
- )
- }
-
- )
- }
+ {(alert || loading) && (
+
+ {alert && (
+
+ {alert}
+
+ )}
+ {loading && (
+
+ Loading...
+
+ )}
+
+ )}
+ onChangeQuery={this.focusNode}
+ />
);
}
@@ -261,11 +235,7 @@ export default class Network extends Component {
*/
class Message extends Component {
render() {
- return (
-
- {this.props.text}
-
- );
+ return
{this.props.text}
;
}
}
@@ -277,7 +247,7 @@ class Controls extends Component {
super(props, context);
this.state = {
- value: null
+ value: null,
};
this.handleSelection = this.handleSelection.bind(this);
@@ -299,36 +269,22 @@ class Controls extends Component {
zoom() {
const camera = this.props.camera;
- sigma.misc.animation.camera(
- camera,
- {ratio: camera.ratio / 1.5},
- {duration: 150}
- );
+ sigma.misc.animation.camera(camera, {ratio: camera.ratio / 1.5}, {duration: 150});
}
unzoom() {
const camera = this.props.camera;
- sigma.misc.animation.camera(
- camera,
- {ratio: camera.ratio * 1.5},
- {duration: 150}
- );
+ sigma.misc.animation.camera(camera, {ratio: camera.ratio * 1.5}, {duration: 150});
}
handleSelection(value) {
- if (typeof this.props.onChangeQuery === 'function')
- this.props.onChangeQuery(value);
+ if (typeof this.props.onChangeQuery === 'function') this.props.onChangeQuery(value);
this.setState({value});
}
render() {
- const {
- nodes,
- toggleLayout,
- toggleFullScreen,
- layoutRunning,
- } = this.props;
+ const {nodes, toggleLayout, toggleFullScreen, layoutRunning} = this.props;
return (
@@ -342,36 +298,27 @@ class Controls extends Component {
labelKey="label"
placeholder="Search a node in the graph..."
onChange={this.handleSelection}
- value={this.state.value} />
+ value={this.state.value}
+ />
-
+
-
+
-
+
-
+
-
+
diff --git a/client/js/lib/exports.js b/client/js/lib/exports.js
index c7dfa710..1a1ca783 100644
--- a/client/js/lib/exports.js
+++ b/client/js/lib/exports.js
@@ -1,36 +1,36 @@
-import gexf from "gexf";
-import { sum } from "lodash";
-import d2i from "dom-to-image";
-import csvParse from "papaparse";
-import { saveAs } from "browser-filesaver";
+import gexf from 'gexf';
+import {sum} from 'lodash';
+import d2i from 'dom-to-image';
+import csvParse from 'papaparse';
+import {saveAs} from 'browser-filesaver';
-export function exportGEXF({ data, meta, model, name, params }) {
+export function exportGEXF({data, meta, model, name, params}) {
const gexfParams = {
meta,
model,
nodes: data.nodes,
edges: data.edges,
- version: "0.0.1",
+ version: '0.0.1',
...params,
};
const writer = gexf.create(gexfParams);
- const blob = new Blob([writer.serialize()], { type: "text/gexf+xml;charset=utf-8" });
+ const blob = new Blob([writer.serialize()], {type: 'text/gexf+xml;charset=utf-8'});
return saveAs(blob, name);
}
-export function exportCSV({ data, name }) {
+export function exportCSV({data, name}) {
const csv = csvParse.unparse(data);
- const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
+ const blob = new Blob([csv], {type: 'text/csv;charset=utf-8'});
return saveAs(blob, name);
}
-export function exportSVG({ nodes, name }) {
+export function exportSVG({nodes, name}) {
const domNodes = Array.isArray(nodes) ? nodes : [nodes];
const svgs = [];
- let todos = domNodes.length;
+ const todos = domNodes.length;
function finalize() {
const widths = [];
@@ -41,26 +41,39 @@ export function exportSVG({ nodes, name }) {
heights.push(+((svg.match(/ height="([^"]+)"/) || [])[1] || 0));
return svg
- .replace(/^]+>/, "")
- .replace(/<\/svg>$/, "")
+ .replace(/^]+>/, '')
+ .replace(/<\/svg>$/, '')
.replace(/ y="[^"]*"/, ` y="${sum(heights.slice(0, -1))}"`);
});
const finalSvg = [
``,
...contents,
- " ",
- ].join("");
- const blob = new Blob([finalSvg], { type: "text/svg;charset=utf-8" });
+ ' ',
+ ].join('');
+ const blob = new Blob([finalSvg], {type: 'text/svg;charset=utf-8'});
return saveAs(blob, name);
}
- domNodes.forEach((node, i) => {
- d2i.toSvg(node).then(dataUrl => {
- svgs[i] = dataUrl.replace(/^[^<]*, "<");
-
- if (!--todos) finalize();
- });
+ Promise.all(
+ domNodes.map((node, i) => {
+ return new Promise((resolve, reject) => {
+ if (typeof node === 'string') {
+ svgs[i] = node
+ .replace(/^<\?xml[^>]+>/, '')
+ .replace(/<\!DOCTYPE[^>]+>/, '')
+ .replace(/^\n\n/, '');
+ resolve();
+ } else {
+ d2i.toSvg(node).then(dataUrl => {
+ svgs[i] = dataUrl.replace(/^[^<]*, '<');
+ resolve();
+ });
+ }
+ });
+ }),
+ ).then(() => {
+ finalize();
});
}