From f89a5a0c6ed2fcec42ed9ec2cad99f65585db3c0 Mon Sep 17 00:00:00 2001 From: Olga Belyaeva Date: Tue, 24 Jan 2017 18:09:09 +0300 Subject: [PATCH 1/2] Add filter --- src/Filter.jsx | 161 +++++++++++++++++++---------- src/GraphViz.jsx | 155 +++++++++++++++------------- src/GraphWorkspace.jsx | 55 ++++++---- src/NodesTable.jsx | 206 ++++++++++++++++++++++++++++++++++++++ src/SelectedTable.jsx | 49 --------- src/scss/filter.scss | 61 +++++++++++ src/scss/graph.scss | 19 ++++ src/scss/nodes-table.scss | 63 ++++++++++++ src/styles.scss | 33 +++--- 9 files changed, 592 insertions(+), 210 deletions(-) create mode 100644 src/NodesTable.jsx delete mode 100644 src/SelectedTable.jsx create mode 100644 src/scss/filter.scss create mode 100644 src/scss/graph.scss create mode 100644 src/scss/nodes-table.scss diff --git a/src/Filter.jsx b/src/Filter.jsx index 3e46b9f..b13fd83 100644 --- a/src/Filter.jsx +++ b/src/Filter.jsx @@ -1,70 +1,129 @@ -import React from 'react' -import GraphWorkspace from './GraphWorkspace' +import React from 'react'; +import { computeMinMax } from './utils' + +import './scss/filter.scss'; export default class Filter extends React.PureComponent { propTypes: { graph: React.PropTypes.any.isRequired, - onResult: React.PropTypes.func.isRequired - } - state = { - minFrequency: 0, - minScore: 0, - minSpread: 0 + onChange: React.PropTypes.func.isRequired, }; + constructor(props) { + super(props); + this.state = { + value: '', + frequency: '0', + spread: '0', + score: '0', + }; + } + componentWillReceiveProps(nextProps) { - if (nextProps.graph != this.props.graph) { - this.setState({ - minFrequency: 0, - minScore: 0, - minSpread: 0 - }); - this.props.onResult(nextProps.graph); + if (!_.isEqual(nextProps.graph, this.props.graph)) { + this.clearFilter(); } } - updateFilter = () => { - if (this.props.graph.nodes) { - const removeNodes = _(this.props.graph.nodes).filter(node => { - if (node.entities) { - const entity = node.entities[0]; - return !(entity.frequency > this.state.minFrequency && entity.spread > this.state.minSpread && (!entity.score || entity.score > this.state.minScore)); - } else return false; - }).map(node => node.id).value(); - const filteredGraph = { - nodes: _(this.props.graph.nodes).filter(node => !_.includes(removeNodes, node.id)).value(), - edges: _(this.props.graph.edges).filter(edge => !(_.includes(removeNodes, edge.origin) || _.includes(removeNodes, edge.destination))).value() - }; - this.props.onResult(filteredGraph); - } else { - this.props.onResult(this.emptyGraph()); + frequencyFn = node => { + return node && node.entities && node.entities[0].frequency ? node.entities[0].frequency : undefined; + }; + + spreadFn = node => { + return node && node.entities && node.entities[0].spread ? node.entities[0].spread : undefined; + }; + + scoreFn = node => { + return node && node.entities && node.entities[0].score ? node.entities[0].score : undefined; + }; + + getRange = paramFn => { + return computeMinMax(this.props.graph.nodes, paramFn); + }; + + onChange = () => { + const {value, frequency, spread, score} = this.state; + + if (frequency.length && spread.length && score.length) { + this.props.onChange({ + value, + frequency: parseInt(frequency), + spread: parseInt(spread), + score: parseInt(score), + }); } }; - emptyGraph() { - return { nodes: [], edges: []}; - } + clearFilter = () => { + this.setState({ + value: '', + frequency: '0', + spread: '0', + score: '0', + }, this.onChange); + }; render() { - return ( -
-
- - {this.setState({minFrequency: parseFloat(e.target.value)}); }} /> + const {value, frequency, spread, score} = this.state; + const frequencyMax = this.getRange(this.frequencyFn).mx; + const spreadMax = this.getRange(this.spreadFn).mx; + const scoreMax = this.getRange(this.scoreFn).mx; + + return +
+
+ + this.setState({value: e.target.value}, this.onChange)}/> +
+
+ + this.setState({frequency: e.target.value}, this.onChange)}/> + this.setState({frequency: e.target.value}, this.onChange)}/>
-
- - {this.setState({minScore: parseFloat(e.target.value)}); }} /> +
+ + this.setState({spread: e.target.value}, this.onChange)}/> + this.setState({spread: e.target.value}, this.onChange)}/>
-
- - {this.setState({minSpread: parseFloat(e.target.value)}); }} /> +
+ + this.setState({score: e.target.value}, this.onChange)}/> + this.setState({score: e.target.value}, this.onChange)}/>
- - - ); +
+ + ; } -} \ No newline at end of file +} diff --git a/src/GraphViz.jsx b/src/GraphViz.jsx index cd29f75..96f1cea 100644 --- a/src/GraphViz.jsx +++ b/src/GraphViz.jsx @@ -7,18 +7,20 @@ import * as tooltipInfo from "./tooltipInfo"; import * as plugins from 'imports-loader?sigma=linkurious,this=>window!linkurious/dist/plugins' +import './scss/graph.scss'; +const MODES = ['all', 'filtered', 'selected']; export default class GraphViz extends React.PureComponent { propTypes: { graph: React.PropTypes.any.isRequired, - onSelected: React.PropTypes.func.isRequired + onSelected: React.PropTypes.func.isRequired, + filter: React.PropTypes.any.isRequired, + selectedNodes: React.PropTypes.array.isRequired, }; state = { sizeParam: 'frequency', - frequencyFilter: 0, - spreadFilter: 0, - scoreFilter: 0 + mode: 'filtered', }; registerSigmaElement(element) { @@ -26,6 +28,18 @@ export default class GraphViz extends React.PureComponent { this.sigmaNode = element; } + shouldComponentUpdate(nextProps, nextState) { + const {graph, selectedNodes} = this.props; + const {mode} = this.state; + + return !( + mode === nextState.mode && + (mode === 'all' || mode === 'selected') && + _.isEqual(graph, nextProps.graph) && + _.isEqual(selectedNodes, nextProps.selectedNodes) + ); + }; + componentDidMount() { this.sigma = this.createSigmaInstance(); this.initGraph(); @@ -48,15 +62,13 @@ export default class GraphViz extends React.PureComponent { this.sigma.refresh(); this.resetLayout(); this.initGraph(); + this.update(); } if (prevState.sizeParam != this.state.sizeParam) this.updateSizes(); - if (prevProps.selectedNodes != this.props.selectedNodes) { - this.updateSelection(); - } - if (prevState.frequencyFilter != this.state.frequencyFilter || - prevState.spreadFilter != this.state.spreadFilter || - prevState.scoreFilter != this.state.scoreFilter) { - this.updateFilter(); + + if (!_.isEqual(prevProps.selectedNodes, this.props.selectedNodes) || + !_.isEqual(prevProps.filter, this.props.filter)) { + this.update(); } } @@ -88,18 +100,34 @@ export default class GraphViz extends React.PureComponent { this.filter = sigmajs.sigma.plugins.filter(sigma); }; + update = () => { + this.updateFilter(); + this.updateSelection(); + }; + updateFilter = () => { - this.filter.undo().nodesBy((node) => { - return (this.frequencyFn(node) ? this.frequencyFn(node) > this.state.frequencyFilter : true) && - (this.spreadFn(node) ? this.spreadFn(node) > this.state.spreadFilter : true) && - (this.scoreFn(node) ? this.scoreFn(node) > this.state.scoreFilter : true); - } - ).apply(); + const {filter, selectedNodes} = this.props; + const {value, frequency, spread, score} = filter; + const {mode} = this.state; + + if (mode === 'all') { + this.filter.undo().apply(); + } else if (mode === 'filtered') { + this.filter.undo().nodesBy((node) => { + return (node.label.indexOf(value) === 0) && + (this.frequencyFn(node) ? this.frequencyFn(node) > frequency : true) && + (this.spreadFn(node) ? this.spreadFn(node) > spread : true) && + (this.scoreFn(node) ? this.scoreFn(node) > score : true); + } + ).apply(); + } else if (mode === 'selected') { + this.filter.undo().nodesBy(node => selectedNodes.indexOf(node.id) !== -1).apply(); + } }; updateSelection = () => { this.activeState.dropNodes(); - this.activeState.addNodes(_.map(this.props.selectedNodes, node => node.nodeId)); + this.activeState.addNodes(this.props.selectedNodes); this.sigma.refresh(); }; @@ -112,14 +140,6 @@ export default class GraphViz extends React.PureComponent { sigma.bind('clickNode', this.onClick); } - transformNode(node) { - const newNode = node.data ? node.data : {id: node.id, value: node.label}; - newNode.nodeId = node.id; - newNode.parentLabel = node.parentLabel; - newNode.edgeType = node.edgeType; - return newNode; - } - onClick = (event) => { //If selecting with shift key, select all descendants let affectedNodes = []; @@ -129,12 +149,10 @@ export default class GraphViz extends React.PureComponent { affectedNodes = [event.data.node.id]; } - //transform nodeId list to nodes - const eventNodes = _.chain(affectedNodes).map(nodeId => this.sigma.graph.nodes(nodeId)).map(this.transformNode).value(); - if (!_.find(this.props.selectedNodes, (node) => node.nodeId === event.data.node.id)) { - this.props.onSelectionAdd(eventNodes); + if (this.props.selectedNodes.indexOf(event.data.node.id) === -1) { + this.props.onSelectionAdd(affectedNodes); } else { - this.props.onSelectionRemove(eventNodes); + this.props.onSelectionRemove(affectedNodes); } }; @@ -285,53 +303,46 @@ export default class GraphViz extends React.PureComponent { } render() { + const {mode} = this.state; + return ( -
-
this.registerSigmaElement(element)}/> - {this.renderSizeMenu()} - {this.renderFilterPanel()} - {/*
-
-
spacebar + click Multi-select
-
spacebar + s Lasso tool
-
spacebar + a Select/deselect all
-
spacebar + u Deselect all
-
spacebar + Del Drop selected
-
spacebar + e Select neighbors
-
spacebar + i Select isolated
-
spacebar + l Select leaf
+
+
+

Graph visualization

+
+ {MODES.map((item, index) => { + const className = mode === item ? "btn btn-success btn-sm" : "btn btn-secondary btn-sm"; + return ( + + ); + })}
-
*/} +
+
+
this.registerSigmaElement(element)}/> + {this.renderSizeMenu()} + {/*
+
+
spacebar + click Multi-select
+
spacebar + s Lasso tool
+
spacebar + a Select/deselect all
+
spacebar + u Deselect all
+
spacebar + Del Drop selected
+
spacebar + e Select neighbors
+
spacebar + i Select isolated
+
spacebar + l Select leaf
+
+
*/} +
); } - renderFilterPanel = () => ( -
-
-
Filter
-
- - this.setState({frequencyFilter: e.target.value})}/> - {this.state.frequencyFilter} -
-
- - this.setState({spreadFilter: e.target.value})}/> - {this.state.spreadFilter} -
-
- - this.setState({scoreFilter: e.target.value})}/> - {this.state.scoreFilter} -
-
-
- ); - renderSizeMenu = () => (
diff --git a/src/GraphWorkspace.jsx b/src/GraphWorkspace.jsx index d71e45a..cade4bd 100644 --- a/src/GraphWorkspace.jsx +++ b/src/GraphWorkspace.jsx @@ -1,6 +1,6 @@ import React from 'react' import GraphViz from './GraphViz' -import SelectedTable from './SelectedTable' +import NodesTable from './NodesTable' import Filter from './Filter' export default class GraphWorkspace extends React.PureComponent { @@ -9,6 +9,12 @@ export default class GraphWorkspace extends React.PureComponent { }; state = { selectedNodes: [], + filter: { + value: '', + frequency: 0, + spread: 0, + score: 0, + }, }; componentDidUpdate(prevProps, prevState) { @@ -20,9 +26,12 @@ export default class GraphWorkspace extends React.PureComponent { render() { return ( (this.props.graph && this.props.graph.nodes.length) > 0 ? -
- {this.selectedBlock()} - {this.graphBlock()} +
+ {this.filterBlock()} +
+ {this.nodesBlock()} + {this.graphBlock()} +
:
@@ -31,24 +40,29 @@ export default class GraphWorkspace extends React.PureComponent { ); } + filterBlock = () => { + return
+ +
+ }; - - selectedBlock = () => { - return
-
-

Selected Nodes

- -
+ nodesBlock = () => { + return
+
; }; graphBlock = () => { return
-
-

Graph visualization

- -
+
}; @@ -69,14 +83,17 @@ export default class GraphWorkspace extends React.PureComponent { }; onSelectionAdd = (nodes) => { - var newSelected = _.unionBy(this.state.selectedNodes, nodes, (node)=>node.nodeId); - this.setState({selectedNodes : newSelected}); + const newSelected = _.unionBy(this.state.selectedNodes, nodes); + this.setState({selectedNodes: newSelected}); }; onSelectionRemove = (nodes) => { - var newSelected = _.differenceBy(this.state.selectedNodes, nodes, node=>node.nodeId); + const newSelected = _.differenceBy(this.state.selectedNodes, nodes); this.setState({selectedNodes: newSelected}); }; + onFilterChange = filter => { + this.setState({filter}); + }; } \ No newline at end of file diff --git a/src/NodesTable.jsx b/src/NodesTable.jsx new file mode 100644 index 0000000..2798a4d --- /dev/null +++ b/src/NodesTable.jsx @@ -0,0 +1,206 @@ +import React from 'react' +import * as fileSaver from 'file-saver' + +import './scss/nodes-table.scss'; + +const MODES = ['all', 'filtered', 'selected']; + +export default class NodesTable extends React.Component { + propTypes: { + graph: React.PropTypes.any.isRequired, + filter: React.PropTypes.any.isRequired, + selectedNodes: React.PropTypes.array.isRequired, + onSelectionAdd: React.PropTypes.func.isRequired, + onSelectionRemove: React.PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + mode: 'filtered', + }; + } + + shouldComponentUpdate(nextProps, nextState) { + const {graph, selectedNodes} = this.props; + const {mode} = this.state; + + return !( + mode === nextState.mode && + (mode === 'all' || mode === 'selected') && + _.isEqual(graph, nextProps.graph) && + _.isEqual(selectedNodes, nextProps.selectedNodes) + ); + }; + + frequencyFn = node => { + return node.entities && node.entities[0].frequency ? node.entities[0].frequency : undefined; + }; + + spreadFn = node => { + return node.entities && node.entities[0].spread ? node.entities[0].spread : undefined; + }; + + scoreFn = node => { + return node.entities && node.entities[0].score ? node.entities[0].score : undefined; + }; + + filterNode = node => { + const {value, frequency, spread, score} = this.props.filter; + + return (node.label.indexOf(value) === 0) && + (this.frequencyFn(node) ? this.frequencyFn(node) > frequency : true) && + (this.spreadFn(node) ? this.spreadFn(node) > spread : true) && + (this.scoreFn(node) ? this.scoreFn(node) > score : true); + }; + + getNodeInfo = node => { + const {graph} = this.props; + + const edge = graph.edges.filter(e => e.destination === node.id)[0]; + + const parentLabel = edge + ? graph.nodes.filter(node => node.id === edge.origin)[0].label + : ''; + + const edgeType = edge ? edge.type : ''; + + return { + id: node.id, + value: node.label, + parentLabel, + edgeType, + } + }; + + onClickNode = id => { + if (this.props.selectedNodes.indexOf(id) === -1) { + this.props.onSelectionAdd([id]); + } else { + this.props.onSelectionRemove([id]); + } + }; + + computeDescendants(graph, currentDescendants, allDescendants = []) { + if (!allDescendants.length) allDescendants = currentDescendants; + + const nextDescendants = graph.edges.filter(edge => { + return currentDescendants.indexOf(edge.origin) !== -1; + }).map(edge => edge.destination); + + allDescendants = _.concat(allDescendants, nextDescendants); + + if (!nextDescendants.length) return allDescendants; + return this.computeDescendants(graph, nextDescendants, allDescendants); + } + + onSelectAllDescendants = id => { + const nodes = this.computeDescendants(this.props.graph, [id]); + + if (this.props.selectedNodes.indexOf(id) === -1) { + this.props.onSelectionAdd(nodes); + } else { + this.props.onSelectionRemove(nodes); + } + }; + + getNodes = () => { + const {graph, selectedNodes} = this.props; + const {mode} = this.state; + + if (mode === 'all') { + return graph.nodes; + } else if (mode === 'filtered') { + return graph.nodes.filter(this.filterNode); + } else if (mode === 'selected') { + return graph.nodes.filter(node => { + return selectedNodes.indexOf(node.id) !== -1; + }); + } else { + return []; + } + }; + + render() { + const {graph, selectedNodes} = this.props; + const {mode} = this.state; + const nodes = this.getNodes(); + return ( +
+
+

Nodes

+
+ {MODES.map((item, index) => { + const className = mode === item ? "btn btn-success btn-sm" : "btn btn-secondary btn-sm"; + return ( + + ); + })} +
+
+
+
+ Total: {graph.nodes.length} nodes; + Shown: {nodes.length} nodes +
+
+ + + + {_(nodes).sortBy(node => node.label).map(this.getNodeInfo).map(node => + this.onClickNode(node.id)} + className={selectedNodes.indexOf(node.id) !== -1 ? 'nodes-table__row active' : 'nodes-table__row'}> + + + + + + + ).value()} + +
idvalueparentlink 
{node.id}{node.value}{node.parentLabel}{node.edgeType} + +
+
+
+ +
+
+
+ ); + } + + exportCsv = () => { + const nodes = _(this.getNodes()).sortBy(node => node.label) + .filter(this.filterNode) + .map(this.getNodeInfo); + const csv = this.formatHeader() + "\n" + _(nodes).map(this.formatRow).reduce((accu, row) => accu + "\n" + row); + const blob = new Blob([csv], {type: 'text/csv;charset=utf-8'}); + fileSaver.saveAs(blob, "iKnowNodes.csv") + }; + + formatHeader() { + return 'id,value,parent,edgeType' + } + + formatRow(row) { + return row.id + ',"' + row.value + '","' + row.parentLabel + '","' + row.edgeType + '"'; + } +} \ No newline at end of file diff --git a/src/SelectedTable.jsx b/src/SelectedTable.jsx deleted file mode 100644 index 38e7862..0000000 --- a/src/SelectedTable.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react' -import * as fileSaver from 'file-saver' -require('./styles.scss') -export default class SelectedTable extends React.Component { - propTypes: { - selectedNodes: React.PropTypes.array.isRequired - }; - render() { - return ( -
-
Count: {this.props.selectedNodes.length} nodes
-
- - - {/**/} - {_(this.props.selectedNodes).sortBy(node => node.value).map(node => - - - - - - {/* - - */} - - - ).value()} - -
idvalueparentlink frqscsprd
{node.id}{node.value}{node.parentLabel}{node.edgeType}{node.frequency}{node.score}{node.spread}
-
- -
- ); - } - - exportCsv = () => { - const csv = this.formatHeader() + "\n" + _(this.props.selectedNodes).map(this.formatRow).reduce((accu, row)=>accu + "\n" + row); - var blob = new Blob([csv], {type: 'text/csv;charset=utf-8'}); - fileSaver.saveAs(blob, "iKnowSelectedNodes.csv") - } - - formatHeader() { - return 'id,value,parent,edgeType' - } - - formatRow(row) { - return row.id + ',"' + row.value + '","' + row.parentLabel + '","' + row.edgeType + '"'; - } -} \ No newline at end of file diff --git a/src/scss/filter.scss b/src/scss/filter.scss new file mode 100644 index 0000000..b848bdb --- /dev/null +++ b/src/scss/filter.scss @@ -0,0 +1,61 @@ +.filter { + background: #f7f7f7; + display: flex; + flex-direction: column; + padding: 13px 20px; + + &__content { + display: flex; + flex: 1 1 100%; + flex-direction: column; + margin: 0 -10px; + } + + &__col { + display: flex; + align-items: center; + flex: 1 1 100%; + padding: 0 10px; + } + + &__label { + flex: 0 0 70px; + margin-right: 10px; + } + + &__value { + flex: 0 0 55px; + padding-left: 5px; + padding-right: 5px; + margin-left: 10px; + width: 55px; + } + + &__control { + flex: 1 1 100%; + } + + &__btn { + align-self: flex-end; + flex: 0 0 auto; + margin: 20px 0 0; + } + + @media (min-width: 768px) { + align-items: center; + flex-direction: row; + + &__content { + flex-direction: row; + } + + &__label { + flex: 0 0 auto; + } + + &__btn { + align-self: center; + margin: 0 0 0 20px; + } + } +} diff --git a/src/scss/graph.scss b/src/scss/graph.scss new file mode 100644 index 0000000..2fb50da --- /dev/null +++ b/src/scss/graph.scss @@ -0,0 +1,19 @@ +.graph-block { + flex: 1 1 100%; + display: flex; + flex-direction: column; + + &__header { + display: flex; + flex: 0 0 auto; + } + + &__caption { + flex: 1 1 100%; + margin: 0; + } + + &__modes { + flex: 0 0 auto; + } +} \ No newline at end of file diff --git a/src/scss/nodes-table.scss b/src/scss/nodes-table.scss new file mode 100644 index 0000000..e9b46ee --- /dev/null +++ b/src/scss/nodes-table.scss @@ -0,0 +1,63 @@ +.nodes-table { + flex: 1 1 100%; + display: flex; + flex-direction: column; + + &__header { + display: flex; + align-items: center; + flex: 0 0 auto; + margin: 0 0 10px; + } + + &__caption { + flex: 1 1 100%; + margin: 0; + } + + &__modes { + flex: 0 0 auto; + } + + &__body { + display: flex; + flex: 1 1 100%; + flex-direction: column; + } + + &__row { + cursor: pointer; + + &:hover { + background: rgba(0, 0, 0, 0.075); + } + + &.active { + background: #ec5148; + color: #fff; + } + } + + &__select-btn { + background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iMCAwIDMxNC4wMTQgMzE0LjAxNSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMzE0LjAxNCAzMTQuMDE1OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnPgoJPGcgaWQ9Il94MzRfMjkuX05ldHdvcmsiPgoJCTxnPgoJCQk8cGF0aCBkPSJNMjgyLjYxMiwyMjIuNTU3di00OS44NDljMC0xNy4zNDItMTQuMDU4LTMxLjQwMi0zMS4zOTgtMzEuNDAyaC03OC41MDVWOTEuNDY0ICAgICBjMTguMjg2LTYuNDc0LDMxLjQwMi0yMy44NjYsMzEuNDAyLTQ0LjM2OGMwLTI2LjAwOC0yMS4xLTQ3LjA5Ni00Ny4xMDQtNDcuMDk2Yy0yNi4wMDgsMC00Ny4xMDIsMjEuMDg3LTQ3LjEwMiw0Ny4wOTYgICAgIGMwLDIwLjUwMiwxMy4xMTcsMzcuODk0LDMxLjQsNDQuMzY4djQ5Ljg0Mkg2Mi44MDNjLTE3LjM0LDAtMzEuNCwxNC4wNi0zMS40LDMxLjQwMnY0OS44NDlDMTMuMTE3LDIyOS4wMTcsMCwyNDYuNDEzLDAsMjY2LjkxMSAgICAgYzAsMjYuMDA0LDIxLjA5Myw0Ny4xMDQsNDcuMTAxLDQ3LjEwNHM0Ny4xMDMtMjEuMSw0Ny4xMDMtNDcuMTA0YzAtMjAuNDk4LTEzLjExOC0zNy44OTUtMzEuNDAyLTQ0LjM1NHYtNDkuODQ5aDc4LjUwM3Y0OS44NDkgICAgIGMtMTguMjg0LDYuNDYtMzEuNCwyMy44NTYtMzEuNCw0NC4zNTRjMCwyNi4wMDQsMjEuMDkzLDQ3LjEwNCw0Ny4xMDIsNDcuMTA0YzI2LjAwNCwwLDQ3LjEwNC0yMS4xLDQ3LjEwNC00Ny4xMDQgICAgIGMwLTIwLjQ5OC0xMy4xMTYtMzcuODk1LTMxLjQwMi00NC4zNTR2LTQ5Ljg0OWg3OC41MDV2NDkuODQ5Yy0xOC4yODUsNi40Ni0zMS40MDEsMjMuODU2LTMxLjQwMSw0NC4zNTQgICAgIGMwLDI2LjAwNCwyMS4wOTUsNDcuMTA0LDQ3LjA5OSw0Ny4xMDRjMjYuMDA5LDAsNDcuMTA0LTIxLjEsNDcuMTA0LTQ3LjEwNEMzMTQuMDE0LDI0Ni40MTMsMzAwLjg5OCwyMjkuMDE3LDI4Mi42MTIsMjIyLjU1N3ogICAgICBNNDcuMTAyLDI4Mi42MTJjLTguNjY2LDAtMTUuNjk5LTcuMDM3LTE1LjY5OS0xNS43MDFjMC04LjY3Nyw3LjAzMy0xNS42OTcsMTUuNjk5LTE1LjY5N2M4LjY2OCwwLDE1LjcwMSw3LjAyMSwxNS43MDEsMTUuNjk3ICAgICBDNjIuODAzLDI3NS41NzUsNTUuNzcsMjgyLjYxMiw0Ny4xMDIsMjgyLjYxMnogTTE1Ny4wMDcsMjgyLjYxMmMtOC42NjYsMC0xNS43MDEtNy4wMzctMTUuNzAxLTE1LjcwMSAgICAgYzAtOC42NzcsNy4wMzUtMTUuNjk3LDE1LjcwMS0xNS42OTdjOC42NjQsMCwxNS43MDEsNy4wMjEsMTUuNzAxLDE1LjY5N0MxNzIuNzA4LDI3NS41NzUsMTY1LjY3MSwyODIuNjEyLDE1Ny4wMDcsMjgyLjYxMnogICAgICBNMTU3LjAwNyw2Mi44MDNjLTguNjY2LDAtMTUuNzAxLTcuMDMzLTE1LjcwMS0xNS43MDdjMC04LjY3Niw3LjAzNS0xNS42OTMsMTUuNzAxLTE1LjY5M2M4LjY2NCwwLDE1LjcwMSw3LjAyNSwxNS43MDEsMTUuNjkzICAgICBDMTcyLjcwOCw1NS43NjIsMTY1LjY3MSw2Mi44MDMsMTU3LjAwNyw2Mi44MDN6IE0yNjYuOTExLDI4Mi42MTJjLTguNjYsMC0xNS42OTctNy4wMzctMTUuNjk3LTE1LjcwMSAgICAgYzAtOC42NzcsNy4wMzctMTUuNjk3LDE1LjY5Ny0xNS42OTdjOC42NjQsMCwxNS43MDEsNy4wMjEsMTUuNzAxLDE1LjY5N0MyODIuNjEyLDI3NS41NzUsMjc1LjU3NSwyODIuNjEyLDI2Ni45MTEsMjgyLjYxMnoiIGZpbGw9IiMwMDAwMDAiLz4KCQk8L2c+Cgk8L2c+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==); + background-position: center; + background-repeat: no-repeat; + height: 24px; + width: 24px; + } + + &__count { + font-size: 14px; + margin: 0 0 10px; + } + + &__footer { + flex: 0 0 auto; + margin: 20px 0 0; + } + + &__export-btn { + display: block; + width: 100%; + } +} \ No newline at end of file diff --git a/src/styles.scss b/src/styles.scss index 059c833..9fca244 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -27,7 +27,7 @@ flex-wrap: nowrap; } -.selected-nodes-panel { +.nodes-table-container { flex: 0 0 400px; margin-right: 20px; width: 400px; @@ -35,33 +35,16 @@ flex-direction: column; } -.selected-nodes-block { - flex: 1 1 100%; - display: flex; - flex-direction: column; -} - .flex-column { display: flex; flex-direction: column; } -.export-css { - flex: 0 0 auto; - margin: 20px 0 0; -} - .graph-panel { flex: 1 1 100%; display: flex; } -.graph-block { - flex: 1 1 100%; - display: flex; - flex-direction: column; -} - .graph-view { display: flex; flex: 1 1 100%; @@ -69,4 +52,16 @@ .graph-container { width: 100%; -} \ No newline at end of file +} + +.main-container { + display: flex; + flex: 1 1 100%; + flex-direction: column; + flex-wrap: nowrap; +} + +.filter-container { + flex: 0 0 auto; + margin-bottom: 20px; +} From 00b644780841badc075d702566e6d86ed638281c Mon Sep 17 00:00:00 2001 From: Olga Belyaeva Date: Thu, 26 Jan 2017 10:48:24 +0300 Subject: [PATCH 2/2] Add filter debounce --- src/Filter.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Filter.jsx b/src/Filter.jsx index b13fd83..4267047 100644 --- a/src/Filter.jsx +++ b/src/Filter.jsx @@ -17,6 +17,7 @@ export default class Filter extends React.PureComponent { spread: '0', score: '0', }; + this.onChangeDebounced = _.debounce(this.onChange, 200); } componentWillReceiveProps(nextProps) { @@ -84,7 +85,7 @@ export default class Filter extends React.PureComponent { className="filter__control" value={frequency.length ? parseInt(frequency) : 0} max={frequencyMax} - onChange={e => this.setState({frequency: e.target.value}, this.onChange)}/> + onChange={e => this.setState({frequency: e.target.value}, () => this.onChangeDebounced())}/> this.setState({spread: e.target.value}, this.onChange)}/> + onChange={e => this.setState({spread: e.target.value}, () => this.onChangeDebounced())}/> this.setState({score: e.target.value}, this.onChange)}/> + onChange={e => this.setState({score: e.target.value}, () => this.onChangeDebounced())}/>