diff --git a/src/App.vue b/src/App.vue
index 1b556bd9..141684ee 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -31,7 +31,7 @@ store.fetchNetwork(
-
+
diff --git a/src/components/AlertBanner.vue b/src/components/AlertBanner.vue
index 5f612962..25873509 100644
--- a/src/components/AlertBanner.vue
+++ b/src/components/AlertBanner.vue
@@ -27,7 +27,7 @@ watchEffect(async () => {
const buttonHref = ref(loadError.value.href);
const buttonText = ref('');
watchEffect(async () => {
- if (workspace.value !== null && network.value !== null) {
+ if (workspace.value !== null) {
buttonHref.value = `./?workspace=${workspace.value}&network=${network.value}`;
buttonText.value = 'Go To Network';
} else if (loadError.value.message === 'There was a network issue when getting data') {
diff --git a/src/components/ContextMenu.vue b/src/components/ContextMenu.vue
index 9605efd3..dfdd39e9 100644
--- a/src/components/ContextMenu.vue
+++ b/src/components/ContextMenu.vue
@@ -10,24 +10,20 @@ const {
} = storeToRefs(store);
function pinSelectedNodes() {
- if (network.value !== null) {
- network.value.nodes
- .filter((node) => selectedNodes.value.includes(node._id))
- .forEach((node) => {
- node.fx = node.x;
- node.fy = node.y;
- });
- }
+ network.value.nodes
+ .filter((node) => selectedNodes.value.includes(node._id))
+ .forEach((node) => {
+ node.fx = node.x;
+ node.fy = node.y;
+ });
}
function unPinSelectedNodes() {
- if (network.value !== null) {
- network.value.nodes
- .filter((node) => selectedNodes.value.includes(node._id))
- .forEach((node) => {
- delete node.fx;
- delete node.fy;
- });
- }
+ network.value.nodes
+ .filter((node) => selectedNodes.value.includes(node._id))
+ .forEach((node) => {
+ delete node.fx;
+ delete node.fy;
+ });
}
diff --git a/src/components/ControlPanel.vue b/src/components/ControlPanel.vue
index 9bba90da..78834183 100644
--- a/src/components/ControlPanel.vue
+++ b/src/components/ControlPanel.vue
@@ -31,20 +31,17 @@ const searchErrors = ref([]);
const showMenu = ref(false);
const multiVariableList = computed(() => {
- if (network.value !== null) {
- // Loop through all nodes, flatten the 2d array, and turn it into a set
- const allVars: Set = new Set();
- network.value.nodes.forEach((node) => Object.keys(node).forEach((key) => allVars.add(key)));
+ // Loop through all nodes, flatten the 2d array, and turn it into a set
+ const allVars: Set = new Set();
+ network.value.nodes.forEach((node) => Object.keys(node).forEach((key) => allVars.add(key)));
- internalFieldNames.forEach((field) => allVars.delete(field));
- allVars.delete('vx');
- allVars.delete('vy');
- allVars.delete('x');
- allVars.delete('y');
- allVars.delete('index');
- return allVars;
- }
- return new Set();
+ internalFieldNames.forEach((field) => allVars.delete(field));
+ allVars.delete('vx');
+ allVars.delete('vy');
+ allVars.delete('x');
+ allVars.delete('y');
+ allVars.delete('index');
+ return allVars;
});
const markerSize = computed({
@@ -52,21 +49,17 @@ const markerSize = computed({
return store.markerSize || 0;
},
set(value: number) {
- store.setMarkerSize({ markerSize: value, updateProv: false });
+ store.setMarkerSize(value, false);
},
});
const autocompleteItems = computed(() => {
- if (network.value !== null && labelVariable.value !== undefined) {
+ if (labelVariable.value !== undefined) {
return network.value.nodes.map((node) => (node[labelVariable.value || '']));
}
return [];
});
function exportNetwork() {
- if (network.value === null) {
- return;
- }
-
const networkToExport = {
nodes: network.value.nodes.map((node) => {
const newNode = { ...node };
@@ -94,26 +87,24 @@ function exportNetwork() {
function search() {
searchErrors.value = [];
- if (network.value !== null) {
- const nodeIDsToSelect = network.value.nodes
- .filter((node) => (labelVariable.value !== undefined ? node[labelVariable.value] === searchTerm.value : false))
- .map((node) => node._id);
+ const nodeIDsToSelect = network.value.nodes
+ .filter((node) => (labelVariable.value !== undefined ? node[labelVariable.value] === searchTerm.value : false))
+ .map((node) => node._id);
- if (nodeIDsToSelect.length > 0) {
- selectedNodes.value.push(...nodeIDsToSelect);
- } else {
- searchErrors.value.push('Enter a valid node to search');
- }
+ if (nodeIDsToSelect.length > 0) {
+ selectedNodes.value.push(...nodeIDsToSelect);
+ } else {
+ searchErrors.value.push('Enter a valid node to search');
}
}
function updateSliderProv(value: number, type: 'markerSize' | 'fontSize' | 'edgeLength') {
if (type === 'markerSize') {
- store.setMarkerSize({ markerSize: value, updateProv: true });
+ store.setMarkerSize(value, true);
} else if (type === 'fontSize') {
fontSize.value = value;
} else if (type === 'edgeLength') {
- store.setEdgeLength({ edgeLength: value, updateProv: true });
+ store.setEdgeLength(value, true);
}
}
diff --git a/src/components/LegendChart.vue b/src/components/LegendChart.vue
index b3aa0e8b..b1d93cbb 100644
--- a/src/components/LegendChart.vue
+++ b/src/components/LegendChart.vue
@@ -55,15 +55,9 @@ function isQuantitative(varName: string, type: 'node' | 'edge') {
return columnTypes.value[varName] === 'number';
}
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- let nodesOrEdges: any[];
-
- if (network.value !== null) {
- nodesOrEdges = type === 'node' ? network.value.nodes : network.value.edges;
- const uniqueValues = [...new Set(nodesOrEdges.map((element) => parseFloat(element[varName])))];
- return uniqueValues.length > 5;
- }
- return false;
+ const nodesOrEdges = type === 'node' ? network.value.nodes : network.value.edges;
+ const uniqueValues = [...new Set(nodesOrEdges.map((element) => parseFloat(element[varName])))];
+ return uniqueValues.length > 5;
}
function dragStart(event: DragEvent) {
@@ -129,10 +123,6 @@ onMounted(() => {
let xScale: ScaleLinear | ScaleBand;
let yScale: ScaleLinear;
- if (network.value === null) {
- return;
- }
-
// Process data for bars/histogram
if (props.mappedTo === 'size' && nodeSizeScale.value !== null) { // node size
xScale = scaleLinear()
diff --git a/src/components/LegendPanel.vue b/src/components/LegendPanel.vue
index 0eff6a87..155f45b8 100644
--- a/src/components/LegendPanel.vue
+++ b/src/components/LegendPanel.vue
@@ -33,36 +33,30 @@ function cleanVariableList(list: Set): Set {
}
const cleanedNodeVariables = computed(() => {
- if (network.value !== null) {
- // Loop through all nodes, flatten the 2d array, and turn it into a set
- const allVars: Set = new Set();
- network.value.nodes.forEach((node: Node) => Object.keys(node).forEach((key) => allVars.add(key)));
-
- internalFieldNames.forEach((field) => allVars.delete(field));
- allVars.delete('vx');
- allVars.delete('vy');
- allVars.delete('x');
- allVars.delete('y');
- allVars.delete('index');
- return cleanVariableList(allVars);
- }
- return new Set();
+ // Loop through all nodes, flatten the 2d array, and turn it into a set
+ const allVars: Set = new Set();
+ network.value.nodes.forEach((node: Node) => Object.keys(node).forEach((key) => allVars.add(key)));
+
+ internalFieldNames.forEach((field) => allVars.delete(field));
+ allVars.delete('vx');
+ allVars.delete('vy');
+ allVars.delete('x');
+ allVars.delete('y');
+ allVars.delete('index');
+ return cleanVariableList(allVars);
});
const cleanedEdgeVariables = computed(() => {
- if (network.value !== null) {
- // Loop through all edges, flatten the 2d array, and turn it into a set
- const allVars: Set = new Set();
- network.value.edges.map((edge: Edge) => Object.keys(edge).forEach((key) => allVars.add(key)));
-
- internalFieldNames.forEach((field) => allVars.delete(field));
- allVars.delete('source');
- allVars.delete('target');
- allVars.delete('index');
-
- return cleanVariableList(allVars);
- }
- return new Set();
+ // Loop through all edges, flatten the 2d array, and turn it into a set
+ const allVars: Set = new Set();
+ network.value.edges.map((edge: Edge) => Object.keys(edge).forEach((key) => allVars.add(key)));
+
+ internalFieldNames.forEach((field) => allVars.delete(field));
+ allVars.delete('source');
+ allVars.delete('target');
+ allVars.delete('index');
+
+ return cleanVariableList(allVars);
});
const attributeLayout = ref(false);
diff --git a/src/components/MultiLink.vue b/src/components/MultiLink.vue
index 87abd881..52c31ef9 100644
--- a/src/components/MultiLink.vue
+++ b/src/components/MultiLink.vue
@@ -170,16 +170,14 @@ function dragNode(node: Node, event: MouseEvent) {
const dx = eventX - nodeX;
const dy = eventY - nodeY;
- if (network.value !== null) {
- network.value.nodes
- .filter((innerNode) => selectedNodes.value.includes(innerNode._id) && innerNode._id !== node._id)
- .forEach((innerNode) => {
- innerNode.x = (innerNode.x || 0) + dx;
- innerNode.y = (innerNode.y || 0) + dy;
- innerNode.fx = (innerNode.fx || innerNode.x || 0) + dx;
- innerNode.fy = (innerNode.fy || innerNode.y || 0) + dy;
- });
- }
+ network.value.nodes
+ .filter((innerNode) => selectedNodes.value.includes(innerNode._id) && innerNode._id !== node._id)
+ .forEach((innerNode) => {
+ innerNode.x = (innerNode.x || 0) + dx;
+ innerNode.y = (innerNode.y || 0) + dy;
+ innerNode.fx = (innerNode.fx || innerNode.x || 0) + dx;
+ innerNode.fy = (innerNode.fy || innerNode.y || 0) + dy;
+ });
}
node.x = eventX;
@@ -240,33 +238,30 @@ function hideTooltip() {
}
function arcPath(edge: Edge): string {
- if (network.value !== null) {
- const fromNode = network.value.nodes.find((node) => node._id === edge._from);
- const toNode = network.value.nodes.find((node) => node._id === edge._to);
+ const fromNode = network.value.nodes.find((node) => node._id === edge._from);
+ const toNode = network.value.nodes.find((node) => node._id === edge._to);
- if (fromNode === undefined || toNode === undefined) {
- throw new Error('Couldn\'t find the source or target for a edge, didn\'t draw arc.');
- }
+ if (fromNode === undefined || toNode === undefined) {
+ throw new Error('Couldn\'t find the source or target for a edge, didn\'t draw arc.');
+ }
- if (fromNode.x === undefined || fromNode.y === undefined || toNode.x === undefined || toNode.y === undefined) {
- throw new Error('_from or _to node didn\'t have an x or a y position.');
- }
+ if (fromNode.x === undefined || fromNode.y === undefined || toNode.x === undefined || toNode.y === undefined) {
+ throw new Error('_from or _to node didn\'t have an x or a y position.');
+ }
- const x1 = fromNode.x + calculateNodeSize(fromNode) / 2;
- const y1 = fromNode.y + calculateNodeSize(fromNode) / 2;
- const x2 = toNode.x + calculateNodeSize(toNode) / 2;
- const y2 = toNode.y + calculateNodeSize(toNode) / 2;
+ const x1 = fromNode.x + calculateNodeSize(fromNode) / 2;
+ const y1 = fromNode.y + calculateNodeSize(fromNode) / 2;
+ const x2 = toNode.x + calculateNodeSize(toNode) / 2;
+ const y2 = toNode.y + calculateNodeSize(toNode) / 2;
- const dx = x2 - x1;
- const dy = y2 - y1;
- const dr = Math.sqrt(dx * dx + dy * dy);
- const sweep = 1;
- const xRotation = 0;
- const largeArc = 0;
+ const dx = x2 - x1;
+ const dy = y2 - y1;
+ const dr = Math.sqrt(dx * dx + dy * dy);
+ const sweep = 1;
+ const xRotation = 0;
+ const largeArc = 0;
- return (`M ${x1}, ${y1} A ${dr}, ${dr} ${xRotation}, ${largeArc}, ${sweep} ${x2},${y2}`);
- }
- return '';
+ return (`M ${x1}, ${y1} A ${dr}, ${dr} ${xRotation}, ${largeArc}, ${sweep} ${x2},${y2}`);
}
function isSelected(nodeID: string): boolean {
@@ -274,20 +269,17 @@ function isSelected(nodeID: string): boolean {
}
const oneHop = computed(() => {
- if (network.value !== null) {
- const inNodes = network.value.edges.map((edge) => (selectedNodes.value.includes(edge._to) ? edge._from : null));
- const outNodes = network.value.edges.map((edge) => (selectedNodes.value.includes(edge._from) ? edge._to : null));
+ const inNodes = network.value.edges.map((edge) => (selectedNodes.value.includes(edge._to) ? edge._from : null));
+ const outNodes = network.value.edges.map((edge) => (selectedNodes.value.includes(edge._from) ? edge._to : null));
- const oneHopNodeIDs: Set = new Set([...outNodes, ...inNodes]);
+ const oneHopNodeIDs: Set = new Set([...outNodes, ...inNodes]);
- // Remove null if it exists
- if (oneHopNodeIDs.has(null)) {
- oneHopNodeIDs.delete(null);
- }
-
- return oneHopNodeIDs;
+ // Remove null if it exists
+ if (oneHopNodeIDs.has(null)) {
+ oneHopNodeIDs.delete(null);
}
- return new Set();
+
+ return oneHopNodeIDs;
});
function nodeGroupClass(node: Node): string {
if (selectedNodes.value.length > 0) {
@@ -358,10 +350,10 @@ function edgeStyle(edge: Edge): string {
);
return `
- stroke: ${useCalculatedColorValue ? edgeColorScale.value(calculatedColorValue) : '#888888'};
- stroke-width: ${(edgeWidth > 20 || edgeWidth < 1) ? 0 : edgeWidth}px;
- opacity: 0.7;
- `;
+ stroke: ${useCalculatedColorValue ? edgeColorScale.value(calculatedColorValue) : '#888888'};
+ stroke-width: ${(edgeWidth > 20 || edgeWidth < 1) ? 0 : edgeWidth}px;
+ opacity: 0.7;
+ `;
}
function glyphFill(node: Node, glyphVar: string) {
@@ -440,15 +432,13 @@ function rectSelectDrag(event: MouseEvent) {
// Find which nodes are in the box
let nodesInRect: Node[] = [];
- if (network.value !== null) {
- nodesInRect = network.value.nodes.filter((node) => {
- const nodeSize = calculateNodeSize(node) / 2;
- return (node.x || 0) + nodeSize > boxX1
- && (node.x || 0) + nodeSize < boxX2
- && (node.y || 0) + nodeSize > boxY1
- && (node.y || 0) + nodeSize < boxY2;
- });
- }
+ nodesInRect = network.value.nodes.filter((node) => {
+ const nodeSize = calculateNodeSize(node) / 2;
+ return (node.x || 0) + nodeSize > boxX1
+ && (node.x || 0) + nodeSize < boxX2
+ && (node.y || 0) + nodeSize > boxY1
+ && (node.y || 0) + nodeSize < boxY2;
+ });
// Select the nodes inside the box if there are any
nodesInRect.forEach((node) => selectedNodes.value.push(node._id));
@@ -497,23 +487,16 @@ function generateNodePositions(nodes: Node[]) {
}
});
}
-if (network.value !== null) {
- generateNodePositions(network.value.nodes);
-}
+generateNodePositions(network.value.nodes);
-const simulationEdges = computed(() => {
- if (network.value !== null) {
- return network.value.edges.map((edge: Edge) => {
- const newEdge: SimulationEdge = {
- ...structuredClone(edge),
- source: edge._from,
- target: edge._to,
- };
- return newEdge;
- });
- }
- return null;
-});
+const simulationEdges = computed(() => network.value.edges.map((edge: Edge) => {
+ const newEdge: SimulationEdge = {
+ ...structuredClone(edge),
+ source: edge._from,
+ target: edge._to,
+ };
+ return newEdge;
+}));
watch(attributeRanges, () => {
if (simulationEdges.value !== null && layoutVars.value.x === null && layoutVars.value.y === null) {
const simEdges = simulationEdges.value.filter((edge: Edge) => {
@@ -579,7 +562,7 @@ function makePositionScale(axis: 'x' | 'y', type: ColumnType, range: AttributeRa
let maxValue = range.max - 1; // subtract 1, because of the + 1 on the legend chart scale
// Check IQR for outliers
- if (network.value !== null && varName !== null) {
+ if (varName !== null) {
const values = network.value.nodes.map((node) => node[varName]).sort((a, b) => a - b);
let q1;
@@ -632,12 +615,12 @@ function makePositionScale(axis: 'x' | 'y', type: ColumnType, range: AttributeRa
if (varName !== null) {
// Set node size smaller
- store.setMarkerSize({ markerSize: 10, updateProv: true });
+ store.setMarkerSize(10, true);
// Clear the label variable
labelVariable.value = undefined;
- if (network.value !== null && columnTypes.value !== null) {
+ if (columnTypes.value !== null) {
const otherAxisPadding = axis === 'x' ? 80 : 60;
if (type === 'number') {
@@ -801,7 +784,7 @@ const minimumY = svgEdgePadding;
const maximumX = svgDimensions.value.width - svgEdgePadding;
const maximumY = svgDimensions.value.height - svgEdgePadding;
onMounted(() => {
- if (network.value !== null && simulationEdges.value !== null) {
+ if (simulationEdges.value !== null) {
// Make the simulation
simulation.value = forceSimulation(network.value.nodes)
.on('tick', () => {
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 6fa30831..524c75f2 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,7 +1,7 @@
// Get the url querystring variables
export function getUrlVars() {
const url = new URL(window.location.href);
- const vars: { [key: string]: string } = {};
+ const vars: { [key: string]: string | undefined } = {};
url.searchParams.forEach((value: string, key: string) => {
vars[key] = value;
diff --git a/src/store/index.ts b/src/store/index.ts
index e1b9366a..c6581595 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,407 +1,450 @@
import { defineStore } from 'pinia';
-import { forceCollide } from 'd3-force';
-import { ColumnTypes, NetworkSpec } from 'multinet';
+import { forceCollide, Simulation } from 'd3-force';
+import { ColumnTypes, NetworkSpec, UserSpec } from 'multinet';
import { scaleLinear, scaleOrdinal, scaleSequential } from 'd3-scale';
import { interpolateBlues, interpolateReds, schemeCategory10 } from 'd3-scale-chromatic';
-import { initProvenance } from '@visdesignlab/trrack';
+import { initProvenance, Provenance } from '@visdesignlab/trrack';
import api from '@/api';
import {
- Edge, Node, State, NestedVariables, ProvenanceEventTypes, AttributeRange,
+ Edge, Node, State, NestedVariables, ProvenanceEventTypes, AttributeRanges, LoadError, Network, SimulationEdge, AttributeRange,
} from '@/types';
import { undoRedoKeyHandler, updateProvenanceState } from '@/lib/provenanceUtils';
import { isInternalField } from '@/lib/typeUtils';
import { applyForceToSimulation } from '@/lib/d3ForceUtils';
import oauthClient from '@/oauth';
-
-export const useStore = defineStore('store', {
- state: (): State => ({
- workspaceName: null,
- networkName: null,
- network: null,
- columnTypes: null,
- selectedNodes: [],
- loadError: {
- message: '',
- href: '',
- },
- simulation: null,
- displayCharts: false,
- markerSize: 50,
- fontSize: 12,
- labelVariable: undefined,
- selectNeighbors: true,
- nestedVariables: {
- bar: [],
- glyph: [],
- },
- edgeVariables: {
- width: '',
- color: '',
- },
- nodeSizeVariable: '',
- nodeColorVariable: '',
- attributeRanges: {},
- nodeBarColorScale: scaleOrdinal(schemeCategory10),
- nodeGlyphColorScale: scaleOrdinal(schemeCategory10),
- provenance: null,
- directionalEdges: false,
- controlsWidth: 256,
- simulationRunning: false,
- showProvenanceVis: false,
- rightClickMenu: {
- show: false,
- top: 0,
- left: 0,
- },
- userInfo: null,
- edgeLength: 10,
- svgDimensions: {
- height: 0,
- width: 0,
- },
- layoutVars: {
- x: null,
- y: null,
- },
- }),
-
- getters: {
- nodeColorScale(state) {
- if (state.columnTypes !== null && Object.keys(state.columnTypes).length > 0 && state.columnTypes[state.nodeColorVariable] === 'number') {
- const minValue = state.attributeRanges[state.nodeColorVariable].currentMin || state.attributeRanges[state.nodeColorVariable].min;
- const maxValue = state.attributeRanges[state.nodeColorVariable].currentMax || state.attributeRanges[state.nodeColorVariable].max;
-
- return scaleSequential(interpolateBlues)
- .domain([minValue, maxValue]);
- }
-
- return state.nodeGlyphColorScale;
- },
-
- edgeColorScale(state) {
- if (state.columnTypes !== null && Object.keys(state.columnTypes).length > 0 && state.columnTypes[state.edgeVariables.color] === 'number') {
- const minValue = state.attributeRanges[state.edgeVariables.color].currentMin || state.attributeRanges[state.edgeVariables.color].min;
- const maxValue = state.attributeRanges[state.edgeVariables.color].currentMax || state.attributeRanges[state.edgeVariables.color].max;
-
- return scaleSequential(interpolateReds)
- .domain([minValue, maxValue]);
- }
-
- return state.nodeGlyphColorScale;
- },
-
- nodeSizeScale(state) {
- if (state.columnTypes !== null && Object.keys(state.columnTypes).length > 0 && state.columnTypes[state.nodeSizeVariable]) {
- const minValue = state.attributeRanges[state.nodeSizeVariable].currentMin || state.attributeRanges[state.nodeSizeVariable].min;
- const maxValue = state.attributeRanges[state.nodeSizeVariable].currentMax || state.attributeRanges[state.nodeSizeVariable].max;
-
- return scaleLinear()
- .domain([minValue, maxValue])
- .range([10, 40]);
- }
- return scaleLinear();
- },
-
- edgeWidthScale(state) {
- if (state.columnTypes !== null && Object.keys(state.columnTypes).length > 0 && state.columnTypes[state.edgeVariables.width] === 'number') {
- const minValue = state.attributeRanges[state.edgeVariables.width].currentMin || state.attributeRanges[state.edgeVariables.width].min;
- const maxValue = state.attributeRanges[state.edgeVariables.width].currentMax || state.attributeRanges[state.edgeVariables.width].max;
-
- return scaleLinear().domain([minValue, maxValue]).range([1, 20]);
- }
- return scaleLinear();
- },
- },
-
- actions: {
- async fetchNetwork(workspaceName: string, networkName: string) {
- this.workspaceName = workspaceName;
- this.networkName = networkName;
-
- let network: NetworkSpec | undefined;
-
- // Get all table names
- try {
- network = await api.network(workspaceName, networkName);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- } catch (error: any) {
- if (error.status === 404) {
- if (workspaceName === undefined || networkName === undefined) {
- this.loadError = {
- message: 'Workspace and/or network were not defined in the url',
- href: 'https://multinet.app',
- };
- } else {
- this.loadError = {
- message: error.statusText,
- href: 'https://multinet.app',
- };
- }
- } else if (error.status === 401) {
- this.loadError = {
- message: 'You are not authorized to view this workspace',
- href: 'https://multinet.app',
- };
- } else {
- this.loadError = {
- message: 'An unexpected error ocurred',
- href: 'https://multinet.app',
- };
- }
- } finally {
- if (this.loadError.message === '' && typeof network === 'undefined') {
- // Catches CORS errors, issues when DB/API are down, etc.
- this.loadError = {
- message: 'There was a network issue when getting data',
- href: `./?workspace=${workspaceName}&network=${networkName}`,
- };
- }
- }
-
- if (network === undefined) {
- return;
- }
-
- // Check network size
- if (network.node_count > 300) {
- this.loadError = {
- message: 'The network you are loading is too large',
- href: 'https://multinet.app',
- };
- }
-
- if (this.loadError.message !== '') {
- return;
- }
-
- // Generate all node table promises
- const nodes = await api.nodes(workspaceName, networkName, { offset: 0, limit: 300 });
-
- // Generate and resolve edge table promise and extract rows
- const edges = await api.edges(workspaceName, networkName, { offset: 0, limit: 1000 });
-
- // Build the network object and set it as the network in the store
- const networkElements = {
- nodes: nodes.results as Node[],
- edges: edges.results as Edge[],
- };
- this.network = networkElements;
-
- const networkTables = await api.networkTables(workspaceName, networkName);
- // Get the network metadata promises
- const metadataPromises: Promise[] = [];
- networkTables.forEach((table) => {
- metadataPromises.push(api.columnTypes(workspaceName, table.name));
- });
-
- // Resolve network metadata promises
- const resolvedMetadataPromises = await Promise.all(metadataPromises);
-
- // Combine all network metadata
- const columnTypes: ColumnTypes = {};
- resolvedMetadataPromises.forEach((types) => {
- Object.assign(columnTypes, types);
- });
-
- this.columnTypes = columnTypes;
-
- // Guess the best label variable and set it
+import { computed, ref } from 'vue';
+
+export const useStore = defineStore('store', () => {
+ const workspaceName = ref('');
+ const networkName = ref('');
+ const network = ref({ nodes: [], edges: [] });
+ const columnTypes = ref(null);
+ const selectedNodes = ref([]);
+ const loadError = ref({
+ message: '',
+ href: '',
+ });
+ const simulation = ref | null>(null);
+ const displayCharts = ref(false);
+ const markerSize = ref(50);
+ const fontSize = ref(12);
+ const labelVariable = ref(undefined);
+ const selectNeighbors = ref(true);
+ const nestedVariables = ref({
+ bar: [],
+ glyph: [],
+ });
+ const edgeVariables = ref({
+ width: '',
+ color: '',
+ });
+ const nodeSizeVariable = ref('');
+ const nodeColorVariable = ref('');
+ const attributeRanges = ref({});
+ const nodeBarColorScale = ref(scaleOrdinal(schemeCategory10));
+ const nodeGlyphColorScale = ref(scaleOrdinal(schemeCategory10));
+ const provenance = ref | null>(null);
+ const directionalEdges = ref(false);
+ const controlsWidth = ref(256);
+ const simulationRunning = ref(false);
+ const showProvenanceVis = ref(false);
+ const rightClickMenu = ref({
+ show: false,
+ top: 0,
+ left: 0,
+ });
+ const userInfo = ref(null);
+ const edgeLength = ref(10);
+ const svgDimensions = ref({
+ height: 0,
+ width: 0,
+ });
+ const layoutVars = ref<{ x: string | null; y: string | null }>({
+ x: null,
+ y: null,
+ });
+
+ const nodeColorScale = computed(() => {
+ if (columnTypes.value !== null && Object.keys(columnTypes.value).length > 0 && columnTypes.value[nodeColorVariable.value] === 'number') {
+ const minValue = attributeRanges.value[nodeColorVariable.value].currentMin || attributeRanges.value[nodeColorVariable.value].min;
+ const maxValue = attributeRanges.value[nodeColorVariable.value].currentMax || attributeRanges.value[nodeColorVariable.value].max;
+
+ return scaleSequential(interpolateBlues)
+ .domain([minValue, maxValue]);
+ }
+
+ return nodeGlyphColorScale;
+ });
+
+ const edgeColorScale = computed(() => {
+ if (columnTypes.value !== null && Object.keys(columnTypes.value).length > 0 && columnTypes.value[edgeVariables.value.color] === 'number') {
+ const minValue = attributeRanges.value[edgeVariables.value.color].currentMin || attributeRanges.value[edgeVariables.value.color].min;
+ const maxValue = attributeRanges.value[edgeVariables.value.color].currentMax || attributeRanges.value[edgeVariables.value.color].max;
+
+ return scaleSequential(interpolateReds)
+ .domain([minValue, maxValue]);
+ }
+
+ return scaleSequential(interpolateReds);
+ });
+
+ const nodeSizeScale = computed(() => {
+ if (columnTypes.value !== null && Object.keys(columnTypes.value).length > 0 && columnTypes.value[nodeSizeVariable.value]) {
+ const minValue = attributeRanges.value[nodeSizeVariable.value].currentMin || attributeRanges.value[nodeSizeVariable.value].min;
+ const maxValue = attributeRanges.value[nodeSizeVariable.value].currentMax || attributeRanges.value[nodeSizeVariable.value].max;
+
+ return scaleLinear()
+ .domain([minValue, maxValue])
+ .range([10, 40]);
+ }
+ return scaleLinear();
+ });
+
+ const edgeWidthScale = computed(() => {
+ if (columnTypes.value !== null && Object.keys(columnTypes.value).length > 0 && columnTypes.value[edgeVariables.value.width] === 'number') {
+ const minValue = attributeRanges.value[edgeVariables.value.width].currentMin || attributeRanges.value[edgeVariables.value.width].min;
+ const maxValue = attributeRanges.value[edgeVariables.value.width].currentMax || attributeRanges.value[edgeVariables.value.width].max;
+
+ return scaleLinear().domain([minValue, maxValue]).range([1, 20]);
+ }
+ return scaleLinear();
+ });
+
+ function guessLabel() {
+ if (columnTypes.value !== null) {
+ // Guess the best label variable and set it
const allVars: Set = new Set();
- networkElements.nodes.map((node: Node) => Object.keys(node).forEach((key) => allVars.add(key)));
-
- this.guessLabel();
- },
-
- async fetchUserInfo() {
- const info = await api.userInfo();
- this.userInfo = info;
- },
-
- async logout() {
- // Perform the server logout.
- oauthClient.logout();
- this.userInfo = null;
- },
-
- releaseNodes() {
- if (this.network !== null) {
- this.network.nodes.forEach((n: Node) => {
- n.fx = null;
- n.fy = null;
- });
- this.startSimulation();
- }
- },
-
- startSimulation() {
- if (this.simulation !== null) {
- this.simulation.alpha(0.2);
- this.simulation.restart();
- this.simulationRunning = true;
- }
- },
-
- stopSimulation() {
- if (this.simulation !== null) {
- this.simulation.stop();
- this.simulationRunning = false;
- }
- },
-
- setMarkerSize(payload: { markerSize: number; updateProv: boolean }) {
- const { markerSize, updateProv } = payload;
- this.markerSize = markerSize;
-
- // Apply force to simulation and restart it
- applyForceToSimulation(
- this.simulation,
- 'collision',
- forceCollide((markerSize / 2) * 1.5),
- );
-
- if (this.provenance !== null && updateProv) {
- updateProvenanceState(this.$state, 'Set Marker Size');
- }
- },
-
- setNestedVariables(nestedVariables: NestedVariables) {
- const newNestedVars = {
- ...nestedVariables,
- bar: [...new Set(nestedVariables.bar)],
- glyph: [...new Set(nestedVariables.glyph)],
+ network.value.nodes.forEach((node: Node) => Object.keys(node).forEach((key) => allVars.add(key)));
+
+ // Remove _key from the search
+ allVars.delete('_key');
+ const bestLabelVar = [...allVars]
+ .find((colName) => !isInternalField(colName) && columnTypes.value?.[colName] === 'label');
+
+ // Use the label variable we found or _key if we didn't find one
+ labelVariable.value = bestLabelVar || '_key';
+ }
+ }
+
+ async function fetchNetwork(workspaceNameInput: string | undefined, networkNameInput: string | undefined) {
+ if (workspaceNameInput === undefined || networkNameInput === undefined) {
+ loadError.value = {
+ message: 'Workspace and/or network were not defined in the url',
+ href: 'https://multinet.app',
};
-
- // Allow only 2 variables for the glyphs
- newNestedVars.glyph.length = Math.min(2, newNestedVars.glyph.length);
-
- this.nestedVariables = newNestedVars;
- },
-
- addAttributeRange(attributeRange: AttributeRange) {
- this.attributeRanges = { ...this.attributeRanges, [attributeRange.attr]: attributeRange };
- },
-
- setEdgeLength(payload: { edgeLength: number; updateProv: boolean }) {
- const { edgeLength, updateProv } = payload;
- this.edgeLength = edgeLength;
-
- // Apply force to simulation and restart it
- applyForceToSimulation(
- this.simulation,
- 'edge',
- undefined,
- edgeLength * 10,
- );
- this.startSimulation();
-
- if (this.provenance !== null && updateProv) {
- updateProvenanceState(this.$state, 'Set Edge Length');
- }
- },
-
- goToProvenanceNode(node: string) {
- if (this.provenance !== null) {
- this.provenance.goToNode(node);
+ return;
+ }
+
+ workspaceName.value = workspaceNameInput;
+ networkName.value = networkNameInput;
+
+ let networkRequest: NetworkSpec | undefined;
+
+ // Get all table names
+ try {
+ networkRequest = await api.network(workspaceName.value, networkName.value);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } catch (error: any) {
+ if (error.status === 404) {
+ loadError.value = {
+ message: error.statusText,
+ href: 'https://multinet.app',
+ };
+ } else if (error.status === 401) {
+ loadError.value = {
+ message: 'You are not authorized to view this workspace',
+ href: 'https://multinet.app',
+ };
+ } else {
+ loadError.value = {
+ message: 'An unexpected error ocurred',
+ href: 'https://multinet.app',
+ };
}
- },
-
- createProvenance() {
- const storeState = this.$state;
-
- const stateForProv = JSON.parse(JSON.stringify(this));
- stateForProv.selectedNodes = [];
-
- this.provenance = initProvenance(
- stateForProv,
- { loadFromUrl: false },
- );
-
- // Add a global observer to watch the state and update the tracked elements in the store
- // enables undo/redo + navigating around provenance graph
- this.provenance.addGlobalObserver(
- () => {
- const provenanceState = this.provenance.state;
-
- const { selectedNodes } = provenanceState;
-
- // If the sets are not equal (happens when provenance is updated through provenance vis),
- // update the store's selectedNodes to match the provenance state
- if (selectedNodes.sort().toString() !== storeState.selectedNodes.sort().toString()) {
- storeState.selectedNodes = selectedNodes;
- }
-
- // Iterate through vars with primitive data types
- [
- 'displayCharts',
- 'markerSize',
- 'fontSize',
- 'labelVariable',
- 'nodeSizeVariable',
- 'nodeColorVariable',
- 'selectNeighbors',
- 'directionalEdges',
- 'edgeLength',
- ].forEach((primitiveVariable) => {
- // If not modified, don't update
- if (provenanceState[primitiveVariable] === storeState[primitiveVariable]) {
- return;
- }
-
- if (primitiveVariable === 'markerSize') {
- this.setMarkerSize({ markerSize: provenanceState[primitiveVariable], updateProv: false });
- } else if (primitiveVariable === 'edgeLength') {
- this.setEdgeLength({ edgeLength: provenanceState[primitiveVariable], updateProv: false });
- } else if (storeState[primitiveVariable] !== provenanceState[primitiveVariable]) {
- storeState[primitiveVariable] = provenanceState[primitiveVariable];
- }
- });
- },
- );
-
- storeState.provenance.done();
-
- // Add keydown listener for undo/redo
- document.addEventListener('keydown', (event) => undoRedoKeyHandler(event, storeState));
- },
-
- guessLabel() {
- if (this.network !== null && this.columnTypes !== null) {
- // Guess the best label variable and set it
- const allVars: Set = new Set();
- this.network.nodes.forEach((node: Node) => Object.keys(node).forEach((key) => allVars.add(key)));
-
- // Remove _key from the search
- allVars.delete('_key');
- const bestLabelVar = [...allVars]
- .find((colName) => !isInternalField(colName) && this.columnTypes?.[colName] === 'label');
-
- // Use the label variable we found or _key if we didn't find one
- this.labelVariable = bestLabelVar || '_key';
+ return;
+ } finally {
+ if (loadError.value.message === '' && typeof networkRequest === 'undefined') {
+ // Catches CORS errors, issues when DB/API are down, etc.
+ loadError.value = {
+ message: 'There was a network issue when getting data',
+ href: `./?workspace=${workspaceName}&network=${networkName}`,
+ };
}
- },
+ }
- applyVariableLayout(payload: { varName: string | null; axis: 'x' | 'y'}) {
- const {
- varName, axis,
- } = payload;
- const otherAxis = axis === 'x' ? 'y' : 'x';
+ if (network === undefined) {
+ return;
+ }
- const updatedLayoutVars = { [axis]: varName, [otherAxis]: this.layoutVars[otherAxis] } as {
- x: string | null;
- y: string | null;
+ // Check network size
+ if (networkRequest.node_count > 300) {
+ loadError.value = {
+ message: 'The network you are loading is too large',
+ href: 'https://multinet.app',
};
- this.layoutVars = updatedLayoutVars;
-
- // Reapply the layout if there is still a variable
- if (varName === null && this.layoutVars[otherAxis] !== null) {
- // Set marker size to 11 to trigger re-render (will get reset to 10 in dispatch again)
- this.markerSize = 11;
-
- this.applyVariableLayout({ varName: this.layoutVars[otherAxis], axis: otherAxis });
- } else if (varName === null && this.layoutVars[otherAxis] === null) {
- // If both null, release
- this.releaseNodes();
- }
- },
- },
+ }
+
+ if (loadError.value.message !== '') {
+ return;
+ }
+
+ // Generate all node table promises
+ const nodes = await api.nodes(workspaceName.value, networkName.value, { offset: 0, limit: 300 });
+
+ // Generate and resolve edge table promise and extract rows
+ const edges = await api.edges(workspaceName.value, networkName.value, { offset: 0, limit: 1000 });
+
+ // Build the network object and set it as the network in the store
+ const networkElements = {
+ nodes: nodes.results as Node[],
+ edges: edges.results as Edge[],
+ };
+ network.value = networkElements;
+
+ const networkTables = await api.networkTables(workspaceName.value, networkName.value);
+ // Get the network metadata promises
+ const metadataPromises: Promise[] = [];
+ networkTables.forEach((table) => {
+ metadataPromises.push(api.columnTypes(workspaceName.value, table.name));
+ });
+
+ // Resolve network metadata promises
+ const resolvedMetadataPromises = await Promise.all(metadataPromises);
+
+ // Combine all network metadata
+ const columnTypesFromRequests: ColumnTypes = {};
+ resolvedMetadataPromises.forEach((types) => {
+ Object.assign(columnTypesFromRequests, types);
+ });
+
+ columnTypes.value = columnTypesFromRequests;
+
+ // Guess the best label variable and set it
+ const allVars: Set = new Set();
+ networkElements.nodes.map((node: Node) => Object.keys(node).forEach((key) => allVars.add(key)));
+
+ guessLabel();
+ }
+
+ async function fetchUserInfo() {
+ const info = await api.userInfo();
+ userInfo.value = info;
+ }
+
+ async function logout() {
+ // Perform the server logout.
+ oauthClient.logout();
+ userInfo.value = null;
+ }
+
+ function startSimulation() {
+ if (simulation.value !== null) {
+ simulation.value.alpha(0.2);
+ simulation.value.restart();
+ simulationRunning.value = true;
+ }
+ }
+
+ function stopSimulation() {
+ if (simulation.value !== null) {
+ simulation.value.stop();
+ simulationRunning.value = false;
+ }
+ }
+
+ function releaseNodes() {
+ network.value.nodes.forEach((n: Node) => {
+ n.fx = null;
+ n.fy = null;
+ });
+ startSimulation();
+ }
+
+ function setMarkerSize(markerSizeInput: number, updateProv: boolean) {
+ markerSize.value = markerSizeInput;
+
+ // Apply force to simulation and restart it
+ applyForceToSimulation(
+ simulation.value,
+ 'collision',
+ forceCollide((markerSize.value / 2) * 1.5),
+ );
+
+ if (provenance.value !== null && updateProv) {
+ // updateProvenanceState(this.$state, 'Set Marker Size');
+ }
+ }
+
+ function setNestedVariables(nestedVariablesInput: NestedVariables) {
+ const newNestedVars = {
+ ...nestedVariables.value,
+ bar: [...new Set(nestedVariablesInput.bar)],
+ glyph: [...new Set(nestedVariablesInput.glyph)],
+ };
+
+ // Allow only 2 variables for the glyphs
+ newNestedVars.glyph.length = Math.min(2, newNestedVars.glyph.length);
+
+ nestedVariables.value = newNestedVars;
+ }
+
+ function addAttributeRange(attributeRange: AttributeRange) {
+ attributeRanges.value = { ...attributeRanges.value, [attributeRange.attr]: attributeRange };
+ }
+
+ function setEdgeLength(edgeLengthInput: number, updateProv: boolean) {
+ edgeLength.value = edgeLengthInput;
+
+ // Apply force to simulation and restart it
+ applyForceToSimulation(
+ simulation.value,
+ 'edge',
+ undefined,
+ edgeLength.value * 10,
+ );
+ startSimulation();
+
+ if (provenance.value !== null && updateProv) {
+ // updateProvenanceState(this.$state, 'Set Edge Length');
+ }
+ }
+
+ function goToProvenanceNode(node: string) {
+ if (provenance.value !== null) {
+ provenance.value.goToNode(node);
+ }
+ }
+
+ function createProvenance() {
+ // const storeState = this.$state;
+
+ // const stateForProv = JSON.parse(JSON.stringify(this));
+ // stateForProv.selectedNodes = [];
+
+ // provenance.value = initProvenance(
+ // stateForProv,
+ // { loadFromUrl: false },
+ // );
+
+ // // Add a global observer to watch the state and update the tracked elements in the store
+ // // enables undo/redo + navigating around provenance graph
+ // provenance.value.addGlobalObserver(
+ // () => {
+ // const provenanceState = provenance.value.state;
+
+ // const { selectedNodes } = provenanceState;
+
+ // // If the sets are not equal (happens when provenance is updated through provenance vis),
+ // // update the store's selectedNodes to match the provenance state
+ // if (selectedNodes.sort().toString() !== storeState.selectedNodes.sort().toString()) {
+ // storeState.selectedNodes = selectedNodes;
+ // }
+
+ // // Iterate through vars with primitive data types
+ // [
+ // 'displayCharts',
+ // 'markerSize',
+ // 'fontSize',
+ // 'labelVariable',
+ // 'nodeSizeVariable',
+ // 'nodeColorVariable',
+ // 'selectNeighbors',
+ // 'directionalEdges',
+ // 'edgeLength',
+ // ].forEach((primitiveVariable) => {
+ // // If not modified, don't update
+ // if (provenanceState[primitiveVariable] === storeState[primitiveVariable]) {
+ // return;
+ // }
+
+ // if (primitiveVariable === 'markerSize') {
+ // setMarkerSize(provenanceState[primitiveVariable], false);
+ // } else if (primitiveVariable === 'edgeLength') {
+ // setEdgeLength({ edgeLength: provenanceState[primitiveVariable], updateProv: false });
+ // } else if (storeState[primitiveVariable] !== provenanceState[primitiveVariable]) {
+ // storeState[primitiveVariable] = provenanceState[primitiveVariable];
+ // }
+ // });
+ // },
+ // );
+
+ // storeState.provenance.done();
+
+ // // Add keydown listener for undo/redo
+ // document.addEventListener('keydown', (event) => undoRedoKeyHandler(event, storeState));
+ }
+
+ function applyVariableLayout(payload: { varName: string | null; axis: 'x' | 'y'}) {
+ const {
+ varName, axis,
+ } = payload;
+ const otherAxis = axis === 'x' ? 'y' : 'x';
+
+ const updatedLayoutVars = { [axis]: varName, [otherAxis]: layoutVars.value[otherAxis] } as {
+ x: string | null;
+ y: string | null;
+ };
+ layoutVars.value = updatedLayoutVars;
+
+ // Reapply the layout if there is still a variable
+ if (varName === null && layoutVars.value[otherAxis] !== null) {
+ // Set marker size to 11 to trigger re-render (will get reset to 10 in dispatch again)
+ markerSize.value = 11;
+
+ applyVariableLayout({ varName: layoutVars.value[otherAxis], axis: otherAxis });
+ } else if (varName === null && layoutVars.value[otherAxis] === null) {
+ // If both null, release
+ releaseNodes();
+ }
+ }
+
+ return {
+ workspaceName,
+ networkName,
+ network,
+ columnTypes,
+ selectedNodes,
+ loadError,
+ simulation,
+ displayCharts,
+ markerSize,
+ fontSize,
+ labelVariable,
+ selectNeighbors,
+ nestedVariables,
+ edgeVariables,
+ nodeSizeVariable,
+ nodeColorVariable,
+ attributeRanges,
+ nodeBarColorScale,
+ nodeGlyphColorScale,
+ provenance,
+ directionalEdges,
+ controlsWidth,
+ simulationRunning,
+ showProvenanceVis,
+ rightClickMenu,
+ userInfo,
+ edgeLength,
+ svgDimensions,
+ layoutVars,
+ nodeColorScale,
+ edgeColorScale,
+ nodeSizeScale,
+ edgeWidthScale,
+ fetchNetwork,
+ fetchUserInfo,
+ logout,
+ startSimulation,
+ stopSimulation,
+ releaseNodes,
+ setMarkerSize,
+ setNestedVariables,
+ addAttributeRange,
+ setEdgeLength,
+ goToProvenanceNode,
+ createProvenance,
+ guessLabel,
+ applyVariableLayout,
+ };
});