@@ -31,24 +40,29 @@ export default class GraphWorkspace extends React.PureComponent {
);
}
+ filterBlock = () => {
+ 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
+
+
+
+
+ id | value | parent | link | |
+ {_(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'}>
+ {node.id} |
+ {node.value} |
+ {node.parentLabel} |
+ {node.edgeType} |
+
+
+ |
+
+ ).value()}
+
+
+
+
+
+
+
+
+ );
+ }
+
+ 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
-
-
-
- id | value | parent | link | | {/*frq | sc | sprd | */}
- {_(this.props.selectedNodes).sortBy(node => node.value).map(node =>
-
- {node.id} |
- {node.value} |
- {node.parentLabel} |
- {node.edgeType} |
- {/*{node.frequency} |
- {node.score} |
- {node.spread} | */}
- |
-
- ).value()}
-
-
-
-
-
- );
- }
-
- 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;
+}