diff --git a/build/app/assets/netcreate-config.js b/build/app/assets/netcreate-config.js deleted file mode 100644 index fd489140..00000000 --- a/build/app/assets/netcreate-config.js +++ /dev/null @@ -1,9 +0,0 @@ - -// this file generated by NC command -const NC_CONFIG = { - dataset: "CHAT", - port: "3000", - googlea: "0" -}; -if (typeof process === "object") module.exports = NC_CONFIG; -if (typeof window === "object") window.NC_CONFIG = NC_CONFIG; \ No newline at end of file diff --git a/build/app/view/netcreate/NetCreate.jsx b/build/app/view/netcreate/NetCreate.jsx index 9c9ac6e7..ea238e4d 100644 --- a/build/app/view/netcreate/NetCreate.jsx +++ b/build/app/view/netcreate/NetCreate.jsx @@ -44,10 +44,13 @@ const PR = PROMPTS.Pad('ACD'); /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const React = require('react'); const { Route } = require('react-router-dom'); +const ReactStrap = require('reactstrap'); +const { Button } = ReactStrap; const NetGraph = require('./components/NetGraph'); const Search = require('./components/Search'); const NodeSelector = require('./components/NodeSelector'); const InfoPanel = require('./components/InfoPanel'); +const FiltersPanel = require('./components/filter/FiltersPanel'); const NCLOGIC = require('./nc-logic'); // require to bootstrap data loading const FILTERLOGIC = require('./filter-logic'); // handles filtering functions @@ -62,7 +65,9 @@ const FILTERLOGIC = require('./filter-logic'); // handles filtering functions isConnected: true, isLoggedIn: false, requireLogin: this.AppState('TEMPLATE').requireLogin, - disconnectMsg: '' + disconnectMsg: '', + layoutNodesOpen: true, + layoutFiltersOpen: false }; this.OnDOMReady(()=>{ if (DBG) console.log(PR,'OnDOMReady'); @@ -85,8 +90,10 @@ const FILTERLOGIC = require('./filter-logic'); // handles filtering functions // so that we can show a message explaining the cause of disconnect. // this.setState({ isConnected: false }); }); + this.onStateChange_SESSION = this.onStateChange_SESSION.bind(this); this.onDisconnect = this.onDisconnect.bind(this); + this.onFilterBtnClick = this.onFilterBtnClick.bind(this); this.OnAppStateChange('SESSION', this.onStateChange_SESSION); @@ -124,15 +131,33 @@ const FILTERLOGIC = require('./filter-logic'); // handles filtering functions this.AppStateChangeOff('SESSION',this.onStateChange_SESSION); } + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + onFilterBtnClick(e) { + this.setState(state => { + return {layoutFiltersOpen: !state.layoutFiltersOpen} + }) + } + + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Define the component structure of the web application /*/ render() { - const { isLoggedIn, disconnectMsg } = this.state; - let hideGraph = false; - if (this.state.requireLogin && !isLoggedIn) hideGraph = true; + const { isLoggedIn, disconnectMsg, layoutNodesOpen, layoutFiltersOpen } = this.state; + + // show or hide graph + // Use 'visibiliity' css NOT React's 'hidden' so size is properly + // calculated on init + let hideGraph = 'visible'; + if (this.state.requireLogin && !isLoggedIn) hideGraph = 'hidden'; + return (
-
-   -   - +   +   +
diff --git a/build/app/view/netcreate/components/NodeSelector.jsx b/build/app/view/netcreate/components/NodeSelector.jsx index b4c849d5..3c26815f 100644 --- a/build/app/view/netcreate/components/NodeSelector.jsx +++ b/build/app/view/netcreate/components/NodeSelector.jsx @@ -924,7 +924,7 @@ class NodeSelector extends UNISYS.Component { } return (
- +
- - - -
) diff --git a/build/app/view/netcreate/components/NodeTable.jsx b/build/app/view/netcreate/components/NodeTable.jsx index be95f1c2..538f73f9 100644 --- a/build/app/view/netcreate/components/NodeTable.jsx +++ b/build/app/view/netcreate/components/NodeTable.jsx @@ -4,6 +4,8 @@ NodeTable is used to to display a table of nodes for review. + It displays D3DATA. + But also checks FILTEREDD3DATA to show highlight/filtered state ## TO USE @@ -33,7 +35,7 @@ const isLocalHost = (SETTINGS.EJSProp('client').ip === '127.0.0.1') || (locatio /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const React = require('react'); const ReactStrap = require('reactstrap'); -const { Button, Table } = ReactStrap; +const { Button } = ReactStrap; const MarkdownNote = require('./MarkdownNote'); const UNISYS = require('unisys/client'); var UDATA = null; @@ -43,80 +45,123 @@ var UDATA = null; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// export a class object for consumption by brunch/require class NodeTable extends UNISYS.Component { - constructor (props) { - super(props); + constructor(props) { + super(props); - this.state = { - nodePrompts: this.AppState('TEMPLATE').nodePrompts, - nodes: [], - edgeCounts: {}, // {nodeID:count,...} - isExpanded: true, - sortkey: 'label' - }; + this.state = { + nodePrompts: this.AppState('TEMPLATE').nodePrompts, + nodes: [], + filteredNodes: [], + isExpanded: true, + sortkey: 'label' + }; - this.handleDataUpdate = this.handleDataUpdate.bind(this); - this.OnTemplateUpdate = this.OnTemplateUpdate.bind(this); - this.onButtonClick = this.onButtonClick.bind(this); - this.onToggleExpanded = this.onToggleExpanded.bind(this); - this.setSortKey = this.setSortKey.bind(this); - this.sortSymbol = this.sortSymbol.bind(this); + this.displayUpdated = this.displayUpdated.bind(this); + this.updateNodeFilterState = this.updateNodeFilterState.bind(this); + this.handleDataUpdate = this.handleDataUpdate.bind(this); + this.handleFilterDataUpdate = this.handleFilterDataUpdate.bind(this); + this.OnTemplateUpdate = this.OnTemplateUpdate.bind(this); + this.onButtonClick = this.onButtonClick.bind(this); + this.onToggleExpanded = this.onToggleExpanded.bind(this); + this.setSortKey = this.setSortKey.bind(this); + this.sortSymbol = this.sortSymbol.bind(this); - this.sortDirection = -1; + this.sortDirection = 1; // alphabetical A-Z - /// Initialize UNISYS DATA LINK for REACT - UDATA = UNISYS.NewDataLink(this); + /// Initialize UNISYS DATA LINK for REACT + UDATA = UNISYS.NewDataLink(this); - // Always make sure class methods are bind()'d before using them - // as a handler, otherwise object context is lost - this.OnAppStateChange('D3DATA', this.handleDataUpdate); + // Always make sure class methods are bind()'d before using them + // as a handler, otherwise object context is lost + this.OnAppStateChange('D3DATA', this.handleDataUpdate); - // Handle Template updates - this.OnAppStateChange('TEMPLATE', this.OnTemplateUpdate); - } // constructor + // Track Filtered Data Updates too + this.OnAppStateChange('FILTEREDD3DATA', this.handleFilterDataUpdate); + + // Handle Template updates + this.OnAppStateChange('TEMPLATE', this.OnTemplateUpdate); + + } // constructor + +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ +/*/ componentDidMount () { + if (DBG) console.error('NodeTable.componentDidMount!'); + // Explicitly retrieve data because we may not have gotten a D3DATA + // update while we were hidden. + + // filtered data needs to be set before D3Data + const FILTEREDD3DATA = UDATA.AppState('FILTEREDD3DATA'); + this.setState({ filteredNodes: FILTEREDD3DATA.nodes }, + () => { + let D3DATA = this.AppState('D3DATA'); + this.handleDataUpdate(D3DATA); + } + ) + } + + componentWillUnmount() { + this.AppStateChangeOff('D3DATA', this.handleDataUpdate); + this.AppStateChangeOff('FILTEREDD3DATA', this.handleFilterDataUpdate); + this.AppStateChangeOff('TEMPLATE', this.OnTemplateUpdate); + } + + displayUpdated(nodeEdge) { + var d = new Date(nodeEdge.meta.revision > 0 ? nodeEdge.meta.updated : nodeEdge.meta.created); + + var year = String(d.getFullYear()); + var date = (d.getMonth()+1)+"/"+d.getDate()+"/"+ year.substr(2,4); + var time = d.toTimeString().substr(0,5); + var dateTime = date+' at '+time; + var titleString = "v" + nodeEdge.meta.revision; + if (nodeEdge._nlog) titleString += " by " + nodeEdge._nlog[nodeEdge._nlog.length-1]; + var tag = {dateTime} ; + + return tag; + + } + + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /// Set node filtered status based on current filteredNodes + updateNodeFilterState(nodes, filteredNodes) { + // set filter status + if (filteredNodes.length > 0) { + nodes = nodes.map(node => { + const filteredNode = filteredNodes.find(n => n.id === node.id); + node.isFiltered = !filteredNode; // not in filteredNode, so it's been removed + return node + }); + } + this.setState({ nodes }); + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Handle updated SELECTION /*/ handleDataUpdate(data) { - if(DBG) - console.log('handle data update') - - // 2020-09-09 Removing this check and relying on other NodeTable optimizations. BL - // if (data.bMarkedNode) - // { - // //data.bMarkedNode = false; - // // counting on the edge table going second, which is sloppy - // // but we are in a rush, so ... do it that way for now - // } - // else - // {} - + if (DBG) console.log('handle data update') if (data.nodes) { - // const edgeCounts = this.countEdges(data.edges); const nodes = this.sortTable(this.state.sortkey, data.nodes); - this.setState({ - nodes: nodes, - // edgeCounts: edgeCounts + const { filteredNodes } = this.state; + this.updateNodeFilterState(nodes, filteredNodes); + } + } + + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + handleFilterDataUpdate(data) { + if (data.nodes) { + const filteredNodes = data.nodes; + this.setState({ filteredNodes }, () => { + const nodes = this.sortTable(this.state.sortkey, this.state.nodes); + this.updateNodeFilterState(nodes, filteredNodes); }); } } -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -OnTemplateUpdate(data) { - this.setState({nodePrompts: data.nodePrompts}); -} -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/*/ Build table of counts -/*/ -// JD removed because "size" seemed to work just fine? (I added a degrees that is size - 1) -countEdges(edges) { - let edgeCounts = {}; // this.state.edgeCounts; - edges.forEach(edge => { - edgeCounts[edge.source.id] = edgeCounts[edge.source.id] !== undefined ? edgeCounts[edge.source.id] + 1 : 1; - edgeCounts[edge.target.id] = edgeCounts[edge.target.id] !== undefined ? edgeCounts[edge.target.id] + 1 : 1; - }); - return edgeCounts; -} + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + OnTemplateUpdate(data) { + this.setState({nodePrompts: data.nodePrompts}); + } /// UTILITIES ///////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -126,26 +171,27 @@ countEdges(edges) { return nodes.sort( (a,b) => { let akey = a.id, bkey = b.id; - if (akeybkey) return 1*this.sortDirection; + if (akeybkey) return 1*Number(this.sortDirection); return 0; }); } + return 0; } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ /*/ sortByEdgeCount(nodes) { if (nodes) { - // let edgeCounts = this.state.edgeCounts; return nodes.sort( (a, b) => { let akey = a.degrees || 0, bkey = b.degrees || 0; // sort descending - if (akey > bkey) return 1*this.sortDirection; - if (akey < bkey) return -1*this.sortDirection; + if (akey > bkey) return 1*Number(this.sortDirection); + if (akey < bkey) return -1*Number(this.sortDirection); return 0; }); } + return 0; } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ @@ -157,6 +203,7 @@ countEdges(edges) { return (akey.localeCompare(bkey)*this.sortDirection); }); } + return 0; } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ @@ -165,22 +212,23 @@ countEdges(edges) { return nodes.sort( (a,b) => { let akey = a.attributes[key], bkey = b.attributes[key]; - if (akeybkey) return 1*this.sortDirection; + if (akeybkey) return 1*Number(this.sortDirection); return 0; }); } + return 0; } - /// --- - sortByUpdated(nodes) - { +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ +/*/ sortByUpdated(nodes) { if (nodes) { return nodes.sort( (a,b) => { let akey = (a.meta.revision > 0 ? a.meta.updated : a.meta.created), bkey = (b.meta.revision > 0 ? b.meta.updated : b.meta.created); - if (akeybkey) return 1*this.sortDirection; + if (akeybkey) return 1*Number(this.sortDirection); return 0; }); } @@ -217,12 +265,9 @@ countEdges(edges) { } } - sortSymbol(key) - { - if(key != this.state.sortkey) // this is not the current sort, so don't show anything - return ""; - else - return this.sortDirection==-1?"▼":"▲"; // default to "decreasing" and flip if clicked again + sortSymbol(key) { + if (key !== this.state.sortkey) return ""; // this is not the current sort, so don't show anything + else return this.sortDirection === 1?"▼":"▲"; // default to "decreasing" and flip if clicked again } @@ -254,10 +299,8 @@ countEdges(edges) { /*/ /*/ setSortKey (key) { - if(key == this.state.sortkey) - this.sortDirection = (-1 * this.sortDirection);// if this was already the key, flip the direction - else - this.sortDirection = 1; + if (key === this.state.sortkey) this.sortDirection = (-1 * this.sortDirection);// if this was already the key, flip the direction + else this.sortDirection = 1; const nodes = this.sortTable(key, this.state.nodes); this.setState({ sortkey: key, nodes }); @@ -287,57 +330,74 @@ countEdges(edges) { /*/ /*/ render() { - if(DBG) - console.log('nodetablerender!') if (this.state.nodes === undefined) return ""; - let { nodePrompts } = this.state; - let { tableHeight } = this.props; - let styles = `thead, tbody { } - thead { position: relative; } - tbody { overflow: auto; } - ` + const { nodePrompts } = this.state; + const { tableHeight } = this.props; + const styles = `thead, tbody { font-size: 0.8em } + .table { + display: table; /* override bootstrap for fixed header */ + border-spacing: 0; + } + .table th { + position: -webkit-sticky; + position: sticky; + top: 0; + background-color: #eafcff; + border-top: none; + } + xtbody { overflow: auto; } + .btn-sm { font-size: 0.6rem; padding: 0.1rem 0.2rem } + ` return ( -
+
- - - - + - - - - {this.state.nodes.map( (node,i) => ( - + - +
); } -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/*/ -/*/ componentDidMount () { - if (DBG) console.error('NodeTable.componentDidMount!'); - // Explicitly retrieve data because we may not have gotten a D3DATA - // update while we were hidden. - let D3DATA = this.AppState('D3DATA'); - this.handleDataUpdate(D3DATA); - } - - componentWillUnmount() { - this.AppStateChangeOff('D3DATA', this.handleDataUpdate); - this.AppStateChangeOff('TEMPLATE', this.OnTemplateUpdate); - } - -displayUpdated(nodeEdge) - { - var d = new Date(nodeEdge.meta.revision > 0 ? nodeEdge.meta.updated : nodeEdge.meta.created); - - var year = "" + d.getFullYear(); - var date = (d.getMonth()+1)+"/"+d.getDate()+"/"+ year.substr(2,4); - var time = d.toTimeString().substr(0,5); - var dateTime = date+' at '+time; - var titleString = "v" + nodeEdge.meta.revision; - if(nodeEdge._nlog) - titleString += " by " + nodeEdge._nlog[nodeEdge._nlog.length-1]; - var tag = {dateTime} ; - - return tag; - - } } // class NodeTable diff --git a/build/app/view/netcreate/components/Search.jsx b/build/app/view/netcreate/components/Search.jsx index 55689054..aa4eba94 100644 --- a/build/app/view/netcreate/components/Search.jsx +++ b/build/app/view/netcreate/components/Search.jsx @@ -76,7 +76,7 @@ class Search extends UNISYS.Component { disabledValue={this.state.searchString} inactiveMode={'disabled'} /> - + ) diff --git a/build/app/view/netcreate/components/Vocabulary.jsx b/build/app/view/netcreate/components/Vocabulary.jsx index c2a1a2d4..2a18c1f2 100644 --- a/build/app/view/netcreate/components/Vocabulary.jsx +++ b/build/app/view/netcreate/components/Vocabulary.jsx @@ -23,102 +23,71 @@ const UNISYS = require('unisys/client'); /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// export a class object for consumption by brunch/require class Vocabulary extends UNISYS.Component { - constructor (props) { - super(props); - this.state = {isExpanded: true}; - - this.onToggleExpanded = this.onToggleExpanded.bind(this); - - } // constructor - - - -/// UI EVENT HANDLERS ///////////////////////////////////////////////////////// -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/*/ -/*/ onToggleExpanded (event) { - this.setState({ - isExpanded: !this.state.isExpanded - }) - } - - - -/// REACT LIFECYCLE METHODS /////////////////////////////////////////////////// -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/*/ This is not yet implemented as of React 16.2. It's implemented in 16.3. - getDerivedStateFromProps (props, state) { - console.error('getDerivedStateFromProps!!!'); - } -/*/ -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/*/ -/*/ render () { - let { tableHeight } = this.props; - - return ( -
- - -
- ); - } + constructor (props) { + super(props); + this.state = {isExpanded: true}; + } // constructor + + render () { + let { tableHeight } = this.props; + + return ( +
+ +
+
Network
+
This is a collection of nodes and the edges between them.
+ +
Graph
+
a graphic representation of a network and its components. Similar terms include: sociogram, visualization
+ +
Node
+
The thing or entity (shown as circles) that is connected through relationships. This could be individual people, groups of people, institutions (like churches, organizations, schools). One way of thinking about this is that nodes are nouns and edges are verbs - nodes are things that are connected through edges. Similar terms include: actor, vertex
+
    +
    +
    Ego
    +
    This refers to the node you are focused on at the moment and the connections that they have.
    +
    +
+ +
Edge
+
The relationships between nodes you are considering (shown as lines). Relationships can take on many forms: nodes could be connected through somewhat intangible relationships, such as friendship or not liking one another. Edges can be based on interactions, such as talking to one another or being in conflict. They could also be defined by sharing resources, such as money or information. Similar terms include: line, tie, arc
+ +
    +
    Edge weight
    +
    Edges can have a value attached to them. So, for instance, an node could sent $10,000 to another actor. Or, they could share three interactions of the same type with one another. This value is referred to as a weight. Similar terms include: value
    + +
    Directed or undirected edges
    +
    Edges can either be directed or undirected. If a relationship is directed, it is being sent from (originating from) one node to another node. Node A may say they are friends with Node B, but Node B does not say Node A does this. Or Node A gives Node B something, such as resources, information, or an illness. However, in some cases, edges are defined as undirected. Two people who share a meal together or are married are both engaged share an undirected edge. Note: in some academic literatures, the term "edge" is reserved for an undirected relationship, while the term "arc" is used to refer to directed ties.
    +
+ +
Attributes
+
Characteristics of the nodes or edges. A node could be designated by gender, for instance or the amount of wealth they possess. They could also be characteristics you find from the network itself - such as how many ties an node has (degree centrality).
+ +
Centrality
+
This is a way of ranking the importance of individuals within a network. There are many different ways to measure importance, such as degree centrality, betweenness centrality, and eigenvector centrality.
+
    + +
    Degree Centrality
    +
    Degree centrality is a measure of how many connections a node has. An node with many ties that are being sent to them has a high in-degree centrality. In a friendship network, this can be easily recognized as popularity. Nodes sending many outgoing ties (high out-degree centrality) may be thought of as expansive in their relationship.
    + +
    Betweenness Centrality
    +
    Nodes with high betweenness centrality serve as connectors between other individuals who wouldn't otherwise be directly connected. They may not be connected to a large number of people (that would be high degree centrality), but they are unique in their connections. If an actor with high betweenness centrality was removed from a network, the network would be more fragmented and less connected. Often researchers are interested in finding actors with high betweenness centrality because they can control whatever flows in the networks. For instance, military analysts often look for nodes with high betweenness in a terrorist network.
    + +
    Eigenvector Centrality
    +
    Eigenvector centrality ranks actors based on their connection to other highly central nodes. So, a nodes importance as measured by eigenvector centrality are dependent on the other nodes with whom they share connections. Google's PageRank algorithm was a famous application of a version of this type of centrality, and allowed them to return highly relevant results in search for users.
    +
+ +
Communities
+
A community in a network is a way of thinking about grouping, often by finding densely connected sets of nodes. A community within a network that is tightly connected to one another but not to an outside group might be seen as a faction, such as rival political groups. In this case, nodes with high betweenness centrality in a network with multiple factions might be some of the only points of contact between rival groups - a potentially powerful but also difficult position to be in.
+
+
+ ); + } } // class Vocabulary diff --git a/build/app/view/netcreate/components/d3-simplenetgraph.js b/build/app/view/netcreate/components/d3-simplenetgraph.js index 3d387f23..c4efdb18 100644 --- a/build/app/view/netcreate/components/d3-simplenetgraph.js +++ b/build/app/view/netcreate/components/d3-simplenetgraph.js @@ -2,6 +2,8 @@ D3 Simple NetGraph + This uses D3 Version 4.0. + This is designed to work with the NetGraph React component. NetGraph calls SetData whenever it receives an updated data object. @@ -165,10 +167,20 @@ class D3NetGraph { this._Dragged = this._Dragged.bind(this); this._Dragended = this._Dragended.bind(this); + // V1.4 CHANGE + // Ignore D3DATA Updates!!! Only listen to FILTEREDD3DATA Updates + // // watch for updates to the D3DATA data object - UDATA.OnAppStateChange('D3DATA',(data)=>{ + // UDATA.OnAppStateChange('D3DATA',(data)=>{ + // // expect { nodes, edges } for this namespace + // if (DBG) console.error(PR,'got state D3DATA',data); + // this._SetData(data); + // }); + + // Special handler for the remove filter + UDATA.OnAppStateChange('FILTEREDD3DATA',(data)=>{ // expect { nodes, edges } for this namespace - if (DBG) console.log(PR,'got state D3DATA',data); + if (DBG) console.log(PR,'got state FILTEREDD3DATA',data); this._SetData(data); }); @@ -182,6 +194,7 @@ class D3NetGraph { UDATA.HandleMessage('ZOOM_RESET', (data) => { if (DBG) console.log(PR, 'ZOOM_RESET got state D3DATA', data); + // NOTE: The transition/duration call means _HandleZoom will be called multiple times this.d3svg.transition() .duration(200) .call(this.zoom.scaleTo, 1); @@ -197,6 +210,13 @@ class D3NetGraph { this._Transition(0.8); }); + // Pan to 0,0 and zoom scale to 1 + // (Currently not used) + UDATA.HandleMessage('ZOOM_PAN_RESET', (data) => { + if (DBG) console.log(PR, 'ZOOM_PAN_RESET got state D3DATA', data); + const transform = d3.zoomIdentity.translate(0, 0).scale(1); + this.d3svg.call(this.zoom.transform, transform); + }); UDATA.HandleMessage('GROUP_PROPS', (data) => { console.log('GROUP_PROPS got ... '); @@ -331,7 +351,7 @@ class D3NetGraph { return COLORMAP[d.attributes["Node_Type"]]; }) .style("opacity", d => { - return d.isFilteredOut ? d.filteredTransparency : 1.0 + return d.filteredTransparency }); // enter node: also append 'text' element @@ -343,7 +363,7 @@ class D3NetGraph { .attr("dy", "0.35em") // ".15em") .text((d) => { return d.label }) .style("opacity", d => { - return d.isFilteredOut ? d.filteredTransparency : 1.0 + return d.filteredTransparency }); // enter node: also append a 'title' tag @@ -403,26 +423,28 @@ class D3NetGraph { if (d.selected || d.strokeColor) return '5px'; return undefined // don't set stroke width }) -// this "r" is necessary to resize after a link is added .attr("fill", (d) => { // REVIEW: Using label match. Should we use id instead? return COLORMAP[d.attributes["Node_Type"]]; }) .attr("r", (d) => { - let radius = this.data.edges.reduce((acc,ed)=>{ - return (ed.source.id===d.id || ed.target.id===d.id) ? acc+1 : acc - },1); - - d.weight = radius - d.size = radius // save the calculated size - d.degrees = radius - 1 - return this.defaultSize + (this.defaultSize * d.weight / 2) + // this "r" is necessary to resize after a link is added + let radius = this.data.edges.reduce((acc,ed)=>{ + return (ed.source.id===d.id || ed.target.id===d.id) ? acc+1 : acc + },1); + + d.weight = radius + d.size = radius // save the calculated size + // radius is calculated by counting the number of edges attached + // (+ 1 for a minimum radius), so we hack degrees by using radius-1 + d.degrees = radius - 1 + return this.defaultSize + (this.defaultSize * d.weight / 2) }) .transition() .duration(500) .style("opacity", d => { // console.log(d); - return d.isFilteredOut ? d.filteredTransparency : 1.0 + return d.filteredTransparency }); // UPDATE text in each node for all nodes @@ -443,7 +465,7 @@ class D3NetGraph { .transition() .duration(500) .style("opacity", d => { - return d.isFilteredOut ? d.filteredTransparency : 1.0 + return d.filteredTransparency }); nodeElements.merge(nodeElements) @@ -471,7 +493,7 @@ class D3NetGraph { // this.edgeClickFn( d ) // }) .style("opacity", d => { - return d.isFilteredOut ? d.filteredTransparency : 1.0 + return d.filteredTransparency }); // .merge() updates the visuals whenever the data is updated. @@ -482,7 +504,7 @@ class D3NetGraph { .transition() .duration(500) .style("opacity", d => { - return d.isFilteredOut ? d.filteredTransparency : 1.0 + return d.filteredTransparency }); linkElements.exit().remove() diff --git a/build/app/view/netcreate/components/filter/FilterEnums.js b/build/app/view/netcreate/components/filter/FilterEnums.js index 5dff9f90..2c4e0b39 100644 --- a/build/app/view/netcreate/components/filter/FilterEnums.js +++ b/build/app/view/netcreate/components/filter/FilterEnums.js @@ -1,5 +1,10 @@ const FILTER = {}; +// Determines whether filter action is to highlight/fade or remove (filter) nodes and edges +FILTER.ACTION = {}; +FILTER.ACTION.HIGHLIGHT = 'highlight'; +FILTER.ACTION.FILTER = 'filter'; + // Types of filters definable in template files. FILTER.TYPES = {}; FILTER.TYPES.STRING = 'string'; diff --git a/build/app/view/netcreate/components/filter/FilterGroup.jsx b/build/app/view/netcreate/components/filter/FilterGroup.jsx index 488ffbee..83e685f1 100644 --- a/build/app/view/netcreate/components/filter/FilterGroup.jsx +++ b/build/app/view/netcreate/components/filter/FilterGroup.jsx @@ -8,7 +8,7 @@ const ReactStrap = require('reactstrap'); const { Input, Label } = ReactStrap; export default function FilterGroup({ - group, label, filters, transparency + group, label, filters, filterAction, transparency }) { return (
@@ -17,13 +17,13 @@ export default function FilterGroup({ switch (filter.type) { case FILTER.TYPES.STRING: case FILTER.TYPES.NODE: - return + return break; case FILTER.TYPES.NUMBER: - return + return break; case FILTER.TYPES.SELECT: - return + return break; default: console.error(`FilterGroup: Filter Type not found ${filter.type} for filter`, filter); @@ -31,8 +31,8 @@ export default function FilterGroup({ } return ''; })} -
+
); } diff --git a/build/app/view/netcreate/components/filter/FilterGroupProperties.jsx b/build/app/view/netcreate/components/filter/FilterGroupProperties.jsx index 8b7f45c0..ba15dab4 100644 --- a/build/app/view/netcreate/components/filter/FilterGroupProperties.jsx +++ b/build/app/view/netcreate/components/filter/FilterGroupProperties.jsx @@ -20,6 +20,7 @@ class FilterGroupProperties extends React.Component { super(); this.OnChangeValue = this.OnChangeValue.bind(this); this.TriggerChangeHandler = this.TriggerChangeHandler.bind(this); + this.OnSubmit = this.OnSubmit.bind(this); this.state = { group: group, @@ -55,22 +56,27 @@ class FilterGroupProperties extends React.Component { } - render (){ - const { group, transparency } = this.props; + OnSubmit(e) { + // Prevent "ENTER" from triggering form submission! + e.preventDefault(); + e.stopPropagation(); + } - return( -
-
Settings for {group}
-
- - - -
-
+ render () { + const { group, transparency } = this.props; + return ( +
+
+
+ + + +
+
); } diff --git a/build/app/view/netcreate/components/filter/FiltersPanel.jsx b/build/app/view/netcreate/components/filter/FiltersPanel.jsx index 1b67aaa6..79d67944 100644 --- a/build/app/view/netcreate/components/filter/FiltersPanel.jsx +++ b/build/app/view/netcreate/components/filter/FiltersPanel.jsx @@ -20,7 +20,7 @@ import React from 'react'; import StringFilter from './StringFilter'; const ReactStrap = require('reactstrap'); -const { Button, Input, Label } = ReactStrap; +const { Button, ButtonGroup, Input, Label } = ReactStrap; const UNISYS = require('unisys/client'); var UDATA = null; @@ -32,55 +32,102 @@ class FiltersPanel extends UNISYS.Component { this.UpdateFilterDefs = this.UpdateFilterDefs.bind(this); this.OnClearBtnClick = this.OnClearBtnClick.bind(this); + this.SelectFilterAction = this.SelectFilterAction.bind(this); - /// Initialize UNISYS DATA LINK for REACT + /// Initialize UNISYS DATA LINK for REACT UDATA = UNISYS.NewDataLink(this); // Load Templates // The intial `OnAppStateChange("FDATA")` event when the template is // first loaded is called well before FiltersPanel is // even constructed. So we need to explicitly load it here. - let FDATA = UDATA.AppState("FDATA"); - this.state = FDATA; - + const FDATA = UDATA.AppState("FDATA"); + this.state = { + nodes: FDATA.nodes, + edges: FDATA.edges, + filterAction: FILTER.ACTION.HIGHLIGHT + }; UDATA.OnAppStateChange("FDATA", this.UpdateFilterDefs); } // constructor + componentWillUnmount() { + // console.error('TBD: gracefully unsubscribe!') + UDATA.AppStateChangeOff("FDATA", this.UpdateFilterDefs); + } UpdateFilterDefs(data) { - this.setState(data); + this.setState(state => { + return { + nodes: data.nodes, + edges: data.edges, + filterAction: data.filterAction || state.filterAction + } + }); } OnClearBtnClick() { UDATA.LocalCall('FILTER_CLEAR'); } - componentWillUnmount() { - // console.error('TBD: gracefully unsubscribe!') + SelectFilterAction(filterAction) { + this.setState({ filterAction }); + UDATA.LocalCall('FILTERS_UPDATE', { filterAction }); } render() { - const { tableHeight } = this.props; + const { filterAction } = this.state; const defs = [this.state.nodes, this.state.edges]; return (
-
+ + + + + +
+
{defs.map(def => )}
- +
) diff --git a/build/app/view/netcreate/components/filter/NumberFilter.js b/build/app/view/netcreate/components/filter/NumberFilter.js index 01823f86..467cba1a 100644 --- a/build/app/view/netcreate/components/filter/NumberFilter.js +++ b/build/app/view/netcreate/components/filter/NumberFilter.js @@ -72,6 +72,7 @@ class NumberFilter extends React.Component { this.OnChangeOperator = this.OnChangeOperator.bind(this); this.OnChangeValue = this.OnChangeValue.bind(this); this.TriggerChangeHandler = this.TriggerChangeHandler.bind(this); + this.OnSubmit = this.OnSubmit.bind(this); this.state = { operator: FILTER.OPERATORS.NO_OP, // Used locally to define result @@ -95,6 +96,7 @@ class NumberFilter extends React.Component { } TriggerChangeHandler() { + const { filterAction } = this.props; const { id, type, key, keylabel } = this.props.filter; const filter = { id, @@ -104,29 +106,43 @@ class NumberFilter extends React.Component { operator: this.state.operator, value: this.state.value }; - UDATA.LocalCall('FILTER_DEFINE', { + if (UDATA) UDATA.LocalCall('FILTER_DEFINE', { group: this.props.group, - filter + filter, + filterAction }); // set a SINGLE filter } + OnSubmit(e) { + // Prevent "ENTER" from triggering form submission! + e.preventDefault(); + e.stopPropagation(); + } + render() { + const { filterAction } = this.props; const { id, key, keylabel, operator, value } = this.props.filter; return ( -
- + + {/* FormGroup needs to unset flexFlow or fields will overflow + https://getbootstrap.com/docs/4.5/utilities/flex/ + */} + {OPERATORS.map(op => )} + style={{maxWidth:'12em', height:'1.5em', padding: '0'}} + onChange={this.OnChangeValue} bsSize="sm" + disabled={operator === FILTER.OPERATORS.NO_OP.key}/> ); diff --git a/build/app/view/netcreate/components/filter/SelectFilter.js b/build/app/view/netcreate/components/filter/SelectFilter.js index e969c6c1..008b3ffc 100644 --- a/build/app/view/netcreate/components/filter/SelectFilter.js +++ b/build/app/view/netcreate/components/filter/SelectFilter.js @@ -69,6 +69,7 @@ class SelectFilter extends React.Component { this.OnChangeOperator = this.OnChangeOperator.bind(this); this.OnChangeValue = this.OnChangeValue.bind(this); this.TriggerChangeHandler = this.TriggerChangeHandler.bind(this); + this.OnSubmit = this.OnSubmit.bind(this); this.state = { operator: FILTER.OPERATORS.NO_OP, // Used locally to define result @@ -92,6 +93,7 @@ class SelectFilter extends React.Component { } TriggerChangeHandler() { + const { filterAction } = this.props; const { id, type, key, keylabel, options } = this.props.filter; const filter = { id, @@ -102,12 +104,19 @@ class SelectFilter extends React.Component { value: this.state.value, options }; - UDATA.LocalCall('FILTER_DEFINE', { + if (UDATA) UDATA.LocalCall('FILTER_DEFINE', { group: this.props.group, - filter + filter, + filterAction }); // set a SINGLE filter } + OnSubmit(e) { + // Prevent "ENTER" from triggering form submission! + e.preventDefault(); + e.stopPropagation(); + } + componentDidMount() { // Autoselect the first item this.setState({ @@ -116,10 +125,14 @@ class SelectFilter extends React.Component { } render() { + const { filterAction } = this.props; const { id, key, keylabel, operator, value, options } = this.props.filter; return ( -
- + + {/* FormGroup needs to unset flexFlow or fields will overflow + https://getbootstrap.com/docs/4.5/utilities/flex/ + */} + diff --git a/build/app/view/netcreate/components/filter/StringFilter.jsx b/build/app/view/netcreate/components/filter/StringFilter.jsx index 1322272e..f4f3060d 100644 --- a/build/app/view/netcreate/components/filter/StringFilter.jsx +++ b/build/app/view/netcreate/components/filter/StringFilter.jsx @@ -64,6 +64,7 @@ class StringFilter extends React.Component { this.OnChangeOperator = this.OnChangeOperator.bind(this); this.OnChangeValue = this.OnChangeValue.bind(this); this.TriggerChangeHandler = this.TriggerChangeHandler.bind(this); + this.OnSubmit = this.OnSubmit.bind(this); this.state = { operator: FILTER.OPERATORS.NO_OP, // Used locally to define result @@ -87,6 +88,7 @@ class StringFilter extends React.Component { } TriggerChangeHandler() { + const { filterAction } = this.props; const { id, type, key, keylabel } = this.props.filter; const filter = { id, @@ -96,17 +98,28 @@ class StringFilter extends React.Component { operator: this.state.operator, value: this.state.value }; - UDATA.LocalCall('FILTER_DEFINE', { + if (UDATA) UDATA.LocalCall('FILTER_DEFINE', { group: this.props.group, - filter + filter, + filterAction }); // set a SINGLE filter } + OnSubmit(e) { + // Prevent "ENTER" from triggering form submission! + e.preventDefault(); + e.stopPropagation(); + } + render() { + const { filterAction } = this.props; const { id, key, keylabel, operator, value } = this.props.filter; return ( -
- + + {/* FormGroup needs to unset flexFlow or fields will overflow + https://getbootstrap.com/docs/4.5/utilities/flex/ + */} + ); diff --git a/build/app/view/netcreate/export-logic.js b/build/app/view/netcreate/export-logic.js new file mode 100644 index 00000000..12a6efb0 --- /dev/null +++ b/build/app/view/netcreate/export-logic.js @@ -0,0 +1,270 @@ +/*//////////////////////////////// ABOUT \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*\ + + * Export Logic + +\*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/ + +const DBG = false; + +/// LIBRARIES ///////////////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +const UNISYS = require("unisys/client"); + +/// INITIALIZE MODULE ///////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +var MOD = UNISYS.NewModule(module.id); +var UDATA = UNISYS.NewDataLink(MOD); + +/// CONSTANTS ///////////////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/// Define Node KEYS to export +/// Subkeys are mapped with a `:`, e.g. attributes:Node_Type +/// REVIEW: Should this be defined in the Template? +const NODEKEYS = [ + 'id', + 'label', + { 'attributes': ['Node_Type', 'Extra Info', 'Notes'] }, + 'degrees', + { 'meta': ['created', 'updated']} +]; +/// Export Labels -- Exported data uses these labels as headers +/// During the export, we do a simple key lookup to match the label +/// e.g. 'id' becomes 'ID' in the exported file +const NODEKEY_LABELS = { + 'id': 'ID', + 'label': 'Label', + 'attributes:Node_Type': 'Attributes:Node_Type', + 'attributes:Extra Info': 'Attributes:Extra Info', + 'attributes:Nodes': 'Attributes:Notes', + 'degrees': 'Degrees', + 'meta:created': 'Meta:Created', + 'meta:updated': 'Meta:Updated' +} +const EDGEKEYS = [ + 'id', + 'source', + 'target', + { 'attributes': ['Relationship', 'Info', 'Citations', 'Category', 'Notes'] }, + { 'meta': ['created', 'updated']} +]; + +/// UTILITIES ///////////////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +function m_formatDate(date) { + // wrap in quotes because time includes a comma + if (date) return `"${new Date(date).toUTCString()}"`; + return ''; +} + +function m_encode(data) { + // double quotes need to be escaped + return String(data).replace(/"/g, '""'); +} + +// Converts nested key definitions into a flat array, e.g. +// from ['id', { attributes: ['type', 'info'] } ] +// into ['id', 'attributes:type', 'attributes:info'] +function m_flattenKeys(keys, prefix) { + if (!Array.isArray(keys)) { + // Recurse + const pre = Object.keys(keys)[0]; + return m_flattenKeys(keys[pre], pre); + } else { + const flattenedKeys = keys.map(k => { + if (typeof k !== 'string') return m_flattenKeys(k); + if (prefix) return `${prefix}:${k}`; + else return k; + }); + return flattenedKeys.flat(); + } +} + +/// IMPORT / EXPORT METHODS /////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ Returns an array of export values for a given node record + e.g. [1,'Tacitus','Person',...] +/*/ +function m_getNodeValues(node, keys) { + const RESULT = []; + keys.forEach(key => { + // If the key is an object, recurse + // eslint-disable-next-line prefer-reflect + if (Object.prototype.toString.call(key) === '[object Object]') { + const subKeys = Object.keys(key); // can have multiple subKeys + subKeys.forEach(k => { + RESULT.push( m_getNodeValues(node[k], key[k]) ); + }); + } + // Special Data Handling + // -- DATE + if (['created', 'updated'].includes(key)) { + RESULT.push(m_formatDate(node[key])); + return; + } + // Else, normal processing + if (node.hasOwnProperty(key)) RESULT.push(`"${m_encode(node[key])}"`); // enclose in quotes to support commas + }) + return RESULT; +} + +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ Returns an array of node records + e.g. [[], [], ...] +/*/ +function m_GenerateNodesArray(nodes, nodekeys) { + /// Define Node KEYS + const nodesArr = []; + nodes.forEach(n => nodesArr.push(m_getNodeValues(n, nodekeys))); + return nodesArr; +} + +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ Returns an array of values for a given node record + e.g. [1,'Tacitus','Person',...] +/*/ +function m_getEdgeValues(edge, keys) { + const RESULT = []; + keys.forEach(key => { + // If the key is an object, recurse + // eslint-disable-next-line prefer-reflect + if (Object.prototype.toString.call(key) === '[object Object]') { + const subKeys = Object.keys(key); // can have multiple subKeys + subKeys.forEach(k => { + RESULT.push( m_getNodeValues(edge[k], key[k]) ); + }); + } + // Special Data Handling + // -- SOURCE / TARGET + if (['source', 'target'].includes(key)) { + RESULT.push(edge[key].id); + return; + } + // -- DATE + if (['created', 'updated'].includes(key)) { + RESULT.push(m_formatDate(edge[key])); + return; + } + // Else, normal processing + if (edge.hasOwnProperty(key)) RESULT.push(`"${edge[key]}"`); // enclose in quotes to support commas + }) + return RESULT; +} + +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ Returns an array of node records + e.g. [ [], [], ...] +/*/ +function m_GenerateEdgesArray(edges, edgekeys) { + /// Define Edge KEYS + const edgeArr = []; + edges.forEach(e => edgeArr.push(m_getEdgeValues(e, edgekeys))); + return edgeArr; +} + +/////////////////////////////////////////////////////////////////////////////// +/// MODULE EXPORT METHODS ///////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/// EXPORT NODES ////////////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/// Exports FILTERED data, not the full data set. +MOD.ExportNodes = () => { + const DATA = UDATA.AppState('FILTEREDD3DATA'); + const { nodes } = DATA; + let EXPORT = ''; + + /// 1. Export Nodes + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /// Define Node KEYS to export + const nodesArr = m_GenerateNodesArray(nodes, NODEKEYS); + + /// 2. Expand to CSV + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /// 3.1. NODES + /// 3.1.1. Create headers + const nodeHeadersArr = NODEKEYS.map(key => { + // eslint-disable-next-line prefer-reflect + if (Object.prototype.toString.call(key) === '[object Object]') { + const subKeys = Object.keys(key); // can have multiple subKeys + const internalkeys = subKeys.map(sk => key[sk].map(k => `${sk}:${k}`)).flat(); + return internalkeys.map(k => NODEKEY_LABELS[k]); + } else { + return NODEKEY_LABELS[key]; + } + }); + const nodeHeaders = nodeHeadersArr.flat(); + nodesArr.unshift(nodeHeaders); // add headers + /// 3.1.2. Expand Nodes to CSV + const commaDelimitedNodes = nodesArr.map(n => n.join(',')); + EXPORT += commaDelimitedNodes.join('\n') + + /// 3. Save to File + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // const encodedURI = encodeURI(EXPORT); + const link = document.createElement('a'); + const blob = new Blob(["\ufeff", EXPORT]); + const url = URL.createObjectURL(blob); + link.href = url; + const DATASET = window.NC_CONFIG.dataset || "netcreate"; + link.download = `${DATASET}_nodes.csv`; + // link.setAttribute('href', encodedURI); + // link.setAttribute('download', 'netcreate_export.csv'); + document.body.appendChild(link); // Required for FF + link.click(); + document.body.removeChild(link); +} +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/// EXPORT EDGES ////////////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/// Exports FILTERED data, not the full data set. +MOD.ExportEdges = () => { + const DATA = UDATA.AppState('FILTEREDD3DATA'); + const { edges } = DATA; + let EXPORT = ''; + + /// 1. Export Edges + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /// Define Edge KEYS + const edgesArr = m_GenerateEdgesArray(edges, EDGEKEYS); + + /// 3. Expand to CSV + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /// 3.1. EDGES + /// 3.1.1. Create headers + const edgeHeadersArr = EDGEKEYS.map(key => { + // eslint-disable-next-line prefer-reflect + if (Object.prototype.toString.call(key) === '[object Object]') { + const subKeys = Object.keys(key); // can have multiple subKeys + return subKeys.map(sk => key[sk].map(k => `${sk}:${k}`)).flat(); + } else { + return key; + } + }); + const edgeHeaders = edgeHeadersArr.flat(); + edgesArr.unshift(edgeHeaders); // add headers + /// 3.2.2 Expand Edges to CSV + const commaDelimitedEdges = edgesArr.map(e => e.join(',')); + EXPORT += commaDelimitedEdges.join('\n'); + + /// 4. Save to File + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // const encodedURI = encodeURI(EXPORT); + const link = document.createElement('a'); + const blob = new Blob(["\ufeff", EXPORT]); + const url = URL.createObjectURL(blob); + link.href = url; + const DATASET = window.NC_CONFIG.dataset || "netcreate"; + link.download = `${DATASET}_edges.csv`; + // link.setAttribute('href', encodedURI); + // link.setAttribute('download', 'netcreate_export.csv'); + document.body.appendChild(link); // Required for FF + link.click(); + document.body.removeChild(link); +} +/// EXPORT CLASS DEFINITION /////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +module.exports = MOD; diff --git a/build/app/view/netcreate/filter-logic.js b/build/app/view/netcreate/filter-logic.js index 046bf190..c951a9c3 100644 --- a/build/app/view/netcreate/filter-logic.js +++ b/build/app/view/netcreate/filter-logic.js @@ -40,6 +40,22 @@ FEATURES + * See Whimiscal [diagram](https://whimsical.com/d3-data-flow-B2tTGnQYPSNviUhsPL64Dz) + + * filterAction: "Highlight" vs "Filter" + -- Version 1.4 introduces two different types of filtering: + "Highlight" highlights the matching nodes/edges and fades the others + "Filter" shows matching nodes/edges and removes the non-matching + nodes/edges from the display without affecting the underlying data. + + * With Version 1.4, the only data that is graphed is FILTEREDD3DATA. + -- d3-simplenetgraph no longer plots on D3DATA changes. + -- Instead, it plots the new FILTEREDD3DATA state. Whenever D3DATA changes, + FILTERDD3DATA is udpated. + -- This way there is only one source of truth: all draw updates + are routed through filter-logic. + -- If filters have not been defined, we just pass the raw D3DATA + * Filters can be stacked. You can define two "Label" filters, for example. The only reason you can't do it right now is because the filter template @@ -113,8 +129,7 @@ MOD.Hook("INITIALIZE", () => { UDATA.OnAppStateChange("FDATA", data => { if (DBG) console.log(PR + 'OnAppStateChange: FDATA', data); // The filter defs have been updated, so apply the filters. - m_FiltersApply(); - m_UpdateFilterSummary(); + m_UpdateFilters(); }); /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -131,6 +146,22 @@ MOD.Hook("INITIALIZE", () => { m_ClearFilters(); }); + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /*/ FILTERS_UPDATE is called by FiltersPanel switches between filters and highlights + /*/ + UDATA.HandleMessage("FILTERS_UPDATE", data => { + const FDATA = UDATA.AppState("FDATA"); + FDATA.filterAction = data.filterAction; + UDATA.SetAppState("FDATA", FDATA); + }); + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /*/ // Listen for D3DATA updates so we know to trigger change? + /*/ + UDATA.OnAppStateChange('D3DATA',(data)=>{ + m_UpdateFilters(); + }); + + }); // end UNISYS_INIT @@ -234,12 +265,12 @@ function m_ImportPrompts(prompts) { */ function m_FilterDefine(data) { const FDATA = UDATA.AppState("FDATA"); + FDATA.filterAction = data.filterAction || FDATA.filterAction; // if 'transparency' then filterAction is not passed, so default to existing if (data.group === "nodes") { if (data.type === "transparency") { FDATA.nodes.transparency = data.transparency; - } else{ let nodeFilters = FDATA.nodes.filters; @@ -271,9 +302,17 @@ function m_FilterDefine(data) { * @param {Object} data A UDATA pkt {defs} */ function m_FiltersApply() { + const FILTEREDD3DATA = UDATA.AppState("D3DATA"); const FDATA = UDATA.AppState("FDATA"); - m_FiltersApplyToNodes(FDATA.nodes.filters, FDATA.nodes.transparency); - m_FiltersApplyToEdges(FDATA.edges.filters, FDATA.edges.transparency); + + // skip if FDATA has not been defined yet + if (Object.keys(FDATA).length < 1) return; + + m_FiltersApplyToNodes(FDATA, FILTEREDD3DATA); + m_FiltersApplyToEdges(FDATA, FILTEREDD3DATA); + // Update FILTEREDD3DATA + UDATA.SetAppState("FILTEREDD3DATA", FILTEREDD3DATA); + } function m_ClearFilters() { @@ -285,16 +324,28 @@ function m_ClearFilters() { function m_UpdateFilterSummary() { const FDATA = UDATA.AppState("FDATA"); + // skip if FDATA has not been defined yet + if (Object.keys(FDATA).length < 1) return; + const nodeFilters = FDATA.nodes.filters; const edgeFilters = FDATA.edges.filters; + const typeSummary = FDATA.filterAction === FILTER.ACTION.HIGHLIGHT + ? 'HIGHLIGHTING ' : 'FILTERING '; + const nodeSummary = m_FiltersToString(FDATA.nodes.filters); + const edgeSummary = m_FiltersToString(FDATA.edges.filters); let summary = ''; - summary += m_FiltersToString(FDATA.nodes.filters); - summary += m_FiltersToString(FDATA.edges.filters); + if (nodeSummary || edgeSummary) summary = + `${typeSummary} ${nodeSummary ? 'NODES: ' : ''}${nodeSummary} ${edgeSummary ? 'EDGES: ' : ''}${edgeSummary}`; UDATA.LocalCall('FILTER_SUMMARY_UPDATE', { filtersSummary: summary }); } +function m_UpdateFilters() { + m_FiltersApply(); + m_UpdateFilterSummary(); +} + function m_FiltersToString(filters) { let summary = '' filters.forEach(filter => { @@ -310,6 +361,7 @@ function m_FiltersToString(filters) { function m_OperatorToString(operator) { return FILTER.OPERATORS[operator].label; } + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ UTILITY FUNCTIONS /*/ @@ -368,39 +420,57 @@ function m_MatchNumber(operator, filterVal, objVal) { /** * Side effect: - * D3DATA.nodes are updated with `isFilteredOut` flags. + * FILTEREDD3DATA.nodes are updated with `isFilteredOut` flags. * * @param {Array} filters */ -function m_FiltersApplyToNodes(filters, transparency) { - const D3DATA = UDATA.AppState("D3DATA"); - D3DATA.nodes.forEach(node => { - m_FiltersApplyToNode(node, filters, transparency); +function m_FiltersApplyToNodes(FDATA, FILTEREDD3DATA) { + const { filterAction } = FDATA; + const { filters, transparency } = FDATA.nodes; + FILTEREDD3DATA.nodes = FILTEREDD3DATA.nodes.filter(node => { + return m_NodeIsFiltered(node, filters, transparency, filterAction); }); - UDATA.SetAppState("D3DATA", D3DATA); } -function m_FiltersApplyToNode(node, filters, transparency) { - let all_no_op = true; - let matched = true; +function m_NodeIsFiltered(node, filters, transparency, filterAction) { + // let all_no_op = true; + let keepNode = true; + + // 1. Look for matches // implicit AND. ALL filters must return true. filters.forEach(filter => { if (filter.operator === FILTER.OPERATORS.NO_OP.key) return; // skip no_op - all_no_op = false; + // all_no_op = false; if (!m_IsNodeMatchedByFilter(node, filter)) { - matched = false; + keepNode = false; } }); - if (all_no_op) { - // no filters defined, undo isFilteredOut - node.isFilteredOut = false; - } else { - // node is filtered out if it fails any filter tests - node.isFilteredOut = !matched; - - node.filteredTransparency = transparency; // set the transparency value ... right now it is inefficient to set this at the node / edge level, but that's more flexible + // 2. Decide based on filterAction + if (filterAction === FILTER.ACTION.FILTER) { + // not using highlight, so restore transparency + node.filteredTransparency = 1.0; // opaque, not tranparent + if (keepNode) return true; + return false; // remove from array + } else { + // FILTER.ACTION.HIGHLIGHT + if (!keepNode) { + node.filteredTransparency = transparency; // set the transparency value ... right now it is inefficient to set this at the node / edge level, but that's more flexible + } else { + node.filteredTransparency = 1.0; // opaque + } + return true; // don't filter out } + + // all_no_op + // This is currently redundant because matchesFilter will always + // be true if there are no filters. If matchesFilter is true, + // then the node will not be removed/faded. + // + // if (all_no_op) { + // // all filters are "no_op", so no filters defined, don't filter anything + // node.filteredTransparency = 1.0; // opaque, not tranparent + // } } function m_IsNodeMatchedByFilter(node, filter) { @@ -431,7 +501,6 @@ function m_IsNodeMatchedByFilter(node, filter) { break; default: // Else assume it's a number - console.log('NUMBER', filter, node); return m_MatchNumber(filter.operator, filter.value, nodeValue) break; } @@ -442,42 +511,83 @@ function m_IsNodeMatchedByFilter(node, filter) { /*/ EDGE FILTERS /*/ -function m_FiltersApplyToEdges(filters, transparency) { - const D3DATA = UDATA.AppState("D3DATA"); - D3DATA.edges.forEach(edge => { - m_FiltersApplyToEdge(edge, filters, transparency); +function m_FiltersApplyToEdges(FDATA, FILTEREDD3DATA) { + const { filterAction } = FDATA; + const { filters, transparency } = FDATA.edges; + FILTEREDD3DATA.edges = FILTEREDD3DATA.edges.filter(edge => { + return m_EdgeIsFiltered(edge, filters, transparency, filterAction, FILTEREDD3DATA); }); - UDATA.SetAppState("D3DATA", D3DATA); } -function m_FiltersApplyToEdge(edge, filters, transparency) { - // regardless of filter definition, - // always hide edge if it's attached to a filtered node - if (edge.source.isFilteredOut || edge.target.isFilteredOut) { - edge.isFilteredOut = true; // no filters, revert +/*/ Side effect: Sets `isFiltered` +/*/ +function m_EdgeIsFiltered(edge, filters, transparency, filterAction, FILTEREDD3DATA) { + // let all_no_op = true; // all filters are no_op + let keepEdge = true; + const source = FILTEREDD3DATA.nodes.find(e => { + // on init, edge.source is just an id. only with d3 processing does it + // get transformed into a node object. so we have to check the type. + const sourceId = (typeof edge.source === 'number') ? edge.source : edge.source.id; + return e.id === sourceId; + }); + const target = FILTEREDD3DATA.nodes.find(e => { + // on init, edge.target is just an id. only with d3 processing does it + // get transformed into a node object. so we have to check the type. + const targetId = (typeof edge.target === 'number') ? edge.target : edge.target.id; + return e.id === targetId; + }); + // 1. if source or target is filtered, then we are filtered too + if (source === undefined || target === undefined || + source.filteredTransparency < 1.0 || + target.filteredTransparency < 1.0) { + // regardless of filter definition... + // ...if filterAction is FILTER + // always hide edge if it's attached to a filtered node + if (filterAction === FILTER.ACTION.FILTER) return false; + // ...else if filterAction is HIGHLIGHT + // don't filter, just fade edge.filteredTransparency = transparency; // set the transparency value ... right now it is inefficient to set this at the node / edge level, but that's more flexible - return; + return true; } - let all_no_op = true; - let matched = true; + // 2. otherwise, look for matches // implicit AND. ALL filters must return true. + // edge is filtered out if it fails ANY filter tests filters.forEach(filter => { if (filter.operator === FILTER.OPERATORS.NO_OP.key) return; // skip no_op - all_no_op = false; + // Found a filter! Apply it! + // all_no_op = false; if (!m_IsEdgeMatchedByFilter(edge, filter)) { - matched = false; + keepEdge = false; } }); - if (all_no_op) { - // no filters defined, undo isFilteredOut - edge.isFilteredOut = false; - } else { - // edge is filtered out if it fails ANY filter tests - edge.isFilteredOut = !matched; - edge.filteredTransparency = transparency;; // set the transparency value ... right now it is inefficient to set this at the node / edge level, but that's more flexible + // 3. Decide how to filter based on filterAction + if (filterAction === FILTER.ACTION.FILTER) { + // not using highlight, so restore transparency + edge.filteredTransparency = 1.0; // opaque + if (keepEdge) return true; // keep in array + return false; // remove from array + } else { + // FILTER.ACTION.HIGHLIGHT, so don't filter + if (!keepEdge) { + edge.filteredTransparency = transparency; // set the transparency value ... right now it is inefficient to set this at the node / edge level, but that's more flexible + } else { + edge.filteredTransparency = 1.0; // opaque + } + return true; // always keep in array } + + // all_no_op + // This is currently redundant because matchesFilter will always + // be true if there are no filters. If matchesFilter is true, + // then the node will not be removed/faded. + // + // if (all_no_op) { + // // no filters defined, undo isFilteredOut + // edge.filteredTransparency = 1.0; + // } else { + // } } function m_IsEdgeMatchedByFilter(edge, filter) { diff --git a/build/app/view/netcreate/nc-logic.js b/build/app/view/netcreate/nc-logic.js index e351436b..4767859a 100644 --- a/build/app/view/netcreate/nc-logic.js +++ b/build/app/view/netcreate/nc-logic.js @@ -43,6 +43,7 @@ const SETTINGS = require("settings"); const UNISYS = require("unisys/client"); const JSCLI = require("system/util/jscli"); const D3 = require("d3"); +const EXPORT = require("./export-logic"); /// INITIALIZE MODULE ///////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -221,6 +222,7 @@ MOD.Hook("LOADASSETS", () => { /*/ CONFIGURE fires after LOADASSETS, so this is a good place to put TEMPLATE validation. /*/ +// eslint-disable-next-line complexity MOD.Hook("CONFIGURE", () => { // Process Node, NodeColorMap and Edge options @@ -674,6 +676,16 @@ MOD.Hook("INITIALIZE", () => { m_HandleAutoCompleteSelect(data); }); + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - inside hook + /*/ + /*/ + UDATA.HandleMessage("EXPORT_NODES", data => { + EXPORT.ExportNodes(); + }); + UDATA.HandleMessage("EXPORT_EDGES", data => { + EXPORT.ExportEdges(); + }); + }); // end UNISYS_INIT