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 */}

Filters

e.preventDefault()}>
- - Type of sources the data comes from. + + + Type of sources the data comes from.{' '} + + + + actions.updateSelector('sourceType', val)} selected={selectors.sourceType} onUpdate={val => actions.updateSelector('sourceType', val)} - defaultValue={defaultSelectors.network['selectors.sourceType']} /> + defaultValue={defaultSelectors.network['selectors.sourceType']} + />
- - The type of product being shipped. + + + The type of product being shipped.{' '} + + + + actions.updateSelector('productClassification', val)} selected={selectors.productClassification} onUpdate={val => actions.updateSelector('productClassification', val)} - defaultValue={defaultSelectors.network['selectors.productClassification']} /> + defaultValue={defaultSelectors.network['selectors.productClassification']} + /> actions.updateSelector('product', val)} selected={selectors.product} onUpdate={val => actions.updateSelector('product', val)} - defaultValue={defaultSelectors.network['selectors.product']} /> + defaultValue={defaultSelectors.network['selectors.product']} + />
- + Should we look at import, export, or total? actions.updateSelector('kind', val)} selected={selectors.kind} onUpdate={val => actions.updateSelector('kind', val)} - defaultValue={defaultSelectors.network['selectors.kind']} /> + defaultValue={defaultSelectors.network['selectors.kind']} + />
- + Choose one date or a range data
@@ -233,7 +259,8 @@ class NetworkPanel extends Component { onChange={val => actions.updateSelector('dateMin', val)} selected={selectors.dateMin} onUpdate={val => actions.updateSelector('dateMin', val)} - defaultValue={defaultSelectors.network['selectors.dateMin']} /> + defaultValue={defaultSelectors.network['selectors.dateMin']} + />
actions.updateSelector('dateMax', val)} selected={selectors.dateMax} onUpdate={val => actions.updateSelector('dateMax', val)} - defaultValue={defaultSelectors.network['selectors.dateMax']} /> + defaultValue={defaultSelectors.network['selectors.dateMax']} + />
@@ -252,16 +280,16 @@ class NetworkPanel extends Component { className="btn btn-default" data-loading={loading} disabled={!classification} - onClick={actions.addNetwork} > + onClick={actions.addNetwork}> Update
- { /* 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 */}
e.preventDefault()}>
- + Thickness actions.selectNodeSize(value)} /> + onChange={({value}) => actions.selectNodeSize(value)} + />
-
+
{ + this.legend = el; + }}>
  • @@ -334,7 +376,9 @@ class NetworkPanel extends Component {
- +
Size @@ -348,7 +392,8 @@ class NetworkPanel extends Component { label: num + '', }))} value={labelSizeRatio + ''} - onChange={({value}) => actions.selectLabelSizeRatio(+value)} /> + onChange={({value}) => actions.selectLabelSizeRatio(+value)} + />
Threshold @@ -362,30 +407,50 @@ class NetworkPanel extends Component { label: num + '', }))} value={labelThreshold + ''} - onChange={({value}) => actions.selectLabelThreshold(+value)} /> + onChange={({value}) => actions.selectLabelThreshold(+value)} + />
- { - selectedNode ? -
-
    -
  • {selectedNode.label}
  • -
  • Flows: {NUMBER_FORMAT(selectedNode.flows)}
  • -
  • Value: {NUMBER_FIXED_FORMAT(selectedNode.value)}
  • -
  • Degree: {NUMBER_FORMAT(selectedNode.degree)}
  • -
-
: - undefined - } + {selectedNode ? ( +
+
    +
  • + {selectedNode.label} +
  • +
  • + Flows: {NUMBER_FORMAT(selectedNode.flows)} +
  • +
  • + Value: {NUMBER_FIXED_FORMAT(selectedNode.value)} +
  • +
  • + Degree: {NUMBER_FORMAT(selectedNode.degree)} +
  • +
+
+ ) : ( + undefined + )}
- + { + this.exportCsv(); + }, + }, + { + label: 'Export SVG', + fn: () => { + this.exportGraph(); + }, + }, + ]} + />
diff --git a/client/js/components/exploration/ExplorationTerms.jsx b/client/js/components/exploration/ExplorationTerms.jsx index f96c293a..6fde3cad 100644 --- a/client/js/components/exploration/ExplorationTerms.jsx +++ b/client/js/components/exploration/ExplorationTerms.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 { selectTerms, selectNodeSize, @@ -22,7 +23,7 @@ import { updateSelector as update, addChart, checkDefaultState, - checkGroups + checkGroups, } from '../../actions/terms'; import Icon from '../misc/Icon.jsx'; const defaultSelectors = require('../../../config/defaultVizSelectors.json'); @@ -80,7 +81,7 @@ export default class ExplorationGlobalsTerms extends Component { update, addChart, checkDefaultState, - checkGroups + checkGroups, }, cursors: { alert: ['ui', 'alert'], @@ -88,8 +89,8 @@ export default class ExplorationGlobalsTerms extends Component { classificationIndex: ['data', 'classifications', 'index'], directions: ['data', 'directions'], sourceTypes: ['data', 'sourceTypes'], - state: ['explorationTermsState'] - } + state: ['explorationTermsState'], + }, }) class TermsPanel extends Component { constructor(props, context) { @@ -104,13 +105,11 @@ class TermsPanel extends Component { const state = this.props.state; const initialState = defaultSelectors.terms.initialValues; const hasInitialState = Object.keys(initialState).some(key => - key.split('.').reduce((iter, step) => iter && iter[step], state) + key.split('.').reduce((iter, step) => iter && iter[step], state), ); if (!hasInitialState) { - this.props.actions.checkDefaultState( - defaultSelectors.terms.initialValues - ); + this.props.actions.checkDefaultState(defaultSelectors.terms.initialValues); } this.props.actions.checkGroups(this.props.actions.addChart); @@ -121,13 +120,22 @@ class TermsPanel extends Component { this.props.actions.checkDefaultState(defaultSelectors.terms.defaultValues); } - export() { + exportCsv() { const now = new Date(); exportCSV({ data: this.props.state.data, - name: `TOFLIT18_Product_terms_${now - .toLocaleString('se-SE') - .replace(' ', '_')}.csv` + name: `TOFLIT18_Product_terms_${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_Product_terms_${now.toLocaleString('se-SE').replace(' ', '_')}..svg`, }); } @@ -147,17 +155,7 @@ class TermsPanel extends Component { classificationIndex, directions, 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; @@ -165,29 +163,25 @@ class TermsPanel extends Component { const sourceTypesOptions = (sourceTypes || []).map(type => { return { name: type, - value: type + value: type, }; }); 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, + })); let childClassifications = []; - if (classification) - childClassifications = getChildClassifications( - classificationIndex, - classification - ); + if (classification) childClassifications = getChildClassifications(classificationIndex, classification); return ( + defaultValue={defaultSelectors.terms.classification} + />
{/* 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']} + />
- - 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']} + />
- - 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 */}
e.preventDefault()}> @@ -413,15 +404,16 @@ 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={edgeSize} - onChange={({value}) => actions.selectEdgeSize(value)} /> + onChange={({value}) => actions.selectEdgeSize(value)} + />
-
+
{ + this.legend = el; + }}> 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.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(/^[^<]* { + 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(/^[^<]* { + finalize(); }); }