Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draw intermediary nodes matrix #302

Merged
merged 23 commits into from
Jul 15, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions src/components/ConnectivityQuery.vue
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ export default {
if (aqlResults.paths.length !== 0) {
// some data manipulation to show only start + end nodes
const newNetwork: Network = { nodes: [], edges: [] };
const nodesSet = new Set();
const endsNodesSet = new Set();
const middleNodesSet = new Set();
const middleNodesList: Node[] = [];

aqlResults.paths.forEach((path: { edges: Edge[]; vertices: Node[] }, val: number) => {
const newPath: Edge = {
Expand All @@ -170,13 +172,15 @@ export default {
for (let i = 0; i < selectedHops.value + 1; i += 1) {
if (i === 0) {
newPath._from = path.vertices[i]._id;
if (!nodesSet.has(path.vertices[i]._id)) { newNetwork.nodes.push(path.vertices[i]); }
nodesSet.add(path.vertices[i]._id);
}
if (i === (selectedHops.value)) {
if (!endsNodesSet.has(path.vertices[i]._id)) { newNetwork.nodes.push(path.vertices[i]); }
endsNodesSet.add(path.vertices[i]._id);
} else if (i > 0 && i < selectedHops.value) {
if (!middleNodesSet.has(path.vertices[i]._id)) { middleNodesList.push(path.vertices[i]); }
middleNodesSet.add(path.vertices[i]._id);
} else {
newPath._to = path.vertices[i]._id;
if (!nodesSet.has(path.vertices[i]._id)) { newNetwork.nodes.push(path.vertices[i]); }
nodesSet.add(path.vertices[i]._id);
if (!endsNodesSet.has(path.vertices[i]._id)) { newNetwork.nodes.push(path.vertices[i]); }
endsNodesSet.add(path.vertices[i]._id);
}
}

Expand All @@ -186,6 +190,12 @@ export default {
newNetwork.edges.push(newPath);
});

// Update state for use in intermediate node vis
store.commit.setConnectivityMatrixPaths({ nodes: middleNodesList, paths: aqlResults.paths });

// Update state for showing intermediate node vis
if (selectedHops.value > 1) store.commit.toggleShowIntNodeVis(true);

// Update state with new network
store.dispatch.updateEnableAggregation(false);
store.dispatch.updateNetwork({ network: newNetwork });
Expand Down
237 changes: 237 additions & 0 deletions src/components/IntermediaryNodes.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
<script lang="ts">
import {
computed, onMounted, watch,
} from '@vue/composition-api';
import {
scaleLinear,
} from 'd3-scale';
import { select } from 'd3-selection';
import store from '@/store';
import { ConnectivityCell } from '@/types';

export default {
name: 'IntermediaryNodes',

setup() {
const network = computed(() => store.state.network);
const connectivityPaths = computed(() => store.state.connectivityMatrixPaths);
const matrix: ConnectivityCell[][] = [];
const cellSize = computed(() => store.state.cellSize);
const pathLength = computed(() => connectivityPaths.value.paths[0].vertices.length);
const edgeLength = computed(() => connectivityPaths.value.paths[0].edges.length);

const margin = {
top: 79,
right: 50,
bottom: 0,
left: 40,
};
const matrixWidth = computed(() => (connectivityPaths.value.nodes.length > 0 ? connectivityPaths.value.paths[0].edges.length * cellSize.value + margin.left + margin.right : 0));
const matrixHeight = computed(() => (connectivityPaths.value.nodes.length > 0 ? connectivityPaths.value.nodes.length * cellSize.value + margin.top + margin.bottom : 0));

const intNodeWidth = computed(() => (store.state.connectivityMatrixPaths.nodes.length > 0
? store.state.connectivityMatrixPaths.nodes.length * cellSize.value + margin.left + margin.right
: 0));
const sortOrder = computed(() => store.state.connectivityMatrixPaths.nodes.map((node) => node._key).sort());
const yScale = scaleLinear().domain([0, sortOrder.value.length]).range([0, sortOrder.value.length * cellSize.value]);
const xScale = scaleLinear().domain([0, (edgeLength.value - 1)]).range([0, (connectivityPaths.value.paths[0].edges.length - 1) * cellSize.value]);

const opacity = scaleLinear().domain([0, 0]).range([0, 1]).clamp(true);

function processData() {
if (network.value !== null && connectivityPaths.value.nodes.length > 0) {
const hops = edgeLength.value - 1;

// Set up matrix intermediate nodes x # of hops
sortOrder.value.forEach((rowNode, i) => {
matrix[i] = [...Array(hops).keys()].slice(0).map((j) => ({
cellName: `${rowNode}`,
nodePosition: j + 1,
startingNode: '',
endingNode: '',
x: j,
y: i,
z: 0,
paths: [],
}));
});

// Set up number of connections based on paths
// Record associated paths
connectivityPaths.value.paths.forEach((path, i) => {
matrix.forEach((matrixRow) => {
[...Array(hops).keys()].slice(0).forEach((j) => {
if (path.vertices[j + 1]._key === matrixRow[j].cellName) {
// eslint-disable-next-line no-param-reassign
matrixRow[j].z += 1;
matrixRow[j].paths.push(i);
}
});
});
});
}

// Update opacity
const allPaths = matrix.map((row) => row.map((cell) => cell.z)).flat();
const maxPath = allPaths.reduce((a, b) => Math.max(a, b));
opacity.domain([
0,
maxPath,
]);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function makeRow(this: any, rowData: ConnectivityCell[]) {
const cell = select(this)
.selectAll('rect')
.data(rowData)
.enter()
.append('rect')
.attr('x', (d) => yScale(d.x))
.attr('y', 1)
.attr('width', cellSize.value - 2)
.attr('height', cellSize.value - 2)
.style('fill-opacity', (d) => opacity(d.z))
.style('fill', 'blue');

cell.append('title').text((d) => `${d.cellName} in ${d.z} paths`);
}

function buildIntView() {
if (matrix.length > 0) {
const svg = select('#intNode').append('g').attr('transform', `translate(${margin.left},${margin.top})`);
const svgWidth: number = parseFloat(select('#intNode').attr('width'));
const svgHeight: number = parseFloat(select('#intNode').attr('height'));
const rowLabelWidth = 20;

// Draw path symbols
const circles = svg.selectAll('g.circles')
.data([...Array(pathLength.value).keys()])
.enter()
.append('g')
.attr('transform', `translate(${svgWidth / 7}, ${(margin.top - svgHeight) / 4})`);

circles.append('circle')
.attr('class', 'circleIcons')
.attr('cx', (_, i) => xScale(i))
.attr('cy', 0)
.attr('r', cellSize.value / 2)
.attr('fill', (_, i) => (i !== 0 && i !== (pathLength.value - 1) ? 'lightgrey' : 'none'));

circles.append('text')
.attr('x', (_, i) => xScale(i))
.attr('y', cellSize.value / 2 - 3)
.attr('text-anchor', 'middle')
.attr('font-size', `${cellSize.value - 2}px`)
.text((_, i) => i + 1);

// Draw gridlines
const gridLines = svg.append('g')
.append('g')
.attr('class', 'gridLines');

const horizontalLines = gridLines.selectAll('line')
.data(matrix)
.enter();

const verticalLines = gridLines.selectAll('line')
.data([...Array(edgeLength.value).keys()])
.enter();

// vertical grid lines
verticalLines
.append('line')
.attr('x1', -yScale.range()[1])
.attr('y1', 0)
.attr('y2', yScale.range()[1])
.attr('x1', (_, i) => xScale(i))
.attr('x2', (_, i) => xScale(i))
.attr('transform', `translate(${svgWidth / 5 - 1},0)`);

// horizontal grid lines
horizontalLines
.append('line')
.attr('x1', 0)
.attr('x2', xScale.range()[1] - 1)
.attr('y1', (_, i) => yScale(i))
.attr('y2', (_, i) => yScale(i))
.attr('transform', `translate(${svgWidth / 5},0)`);

// horizontal grid line edges
gridLines
.append('line')
.attr('x1', 0)
.attr('x2', xScale.range()[1])
.attr('y1', yScale.range()[1])
.attr('y2', yScale.range()[1])
.attr('transform', `translate(${svgWidth / 5},0)`);

// Draw rows
svg
.selectAll('g.row')
.data(matrix)
.enter()
.append('g')
.attr('class', 'row')
.attr('transform', (_, i) => `translate(${svgWidth / 5},${yScale(i)})`)
.each(makeRow)
.append('text')
.attr('class', 'rowLabels')
.attr('y', cellSize.value / 2 + 5)
.attr('x', -(svgWidth / 5 + rowLabelWidth))
.text((_, i) => sortOrder.value[i]);
}
}

onMounted(() => {
processData();
buildIntView();
});

watch(connectivityPaths, () => {
processData();
buildIntView();
});

return {
intNodeWidth,
matrixWidth,
matrixHeight,
};
},
};
</script>

<template>
<div
id="intNodeDiv"
:style="`width: ${intNodeWidth}px;`"
>
<svg
id="intNode"
:width="matrixWidth"
:height="matrixHeight"
:viewbox="`0 0 ${matrixWidth} ${matrixHeight}`"
/>
</div>
</template>

<style scoped>
svg >>> .gridLines {
pointer-events: none;
stroke: #BBBBBB;
}

svg >>> .circleIcons {
stroke: black;
stroke-width: 1;
}

svg >>> .rowLabels {
max-width: 20px;
text-overflow: ellipsis;
overflow: hidden;
font-size: 12pt;
z-index: 100;
}
</style>
6 changes: 5 additions & 1 deletion src/components/LineUp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ export default {
const lineupWidth = computed(() => {
const controlsElement = select<Element, Element>('.app-sidebar').node();
const matrixElement = select<Element, Element>('#matrix').node();
const intermediaryElement = select<Element, Element>('#intNodeDiv').node();

if (controlsElement !== null && matrixElement !== null) {
const availableSpace = context.root.$vuetify.breakpoint.width - controlsElement.clientWidth - matrixElement.clientWidth - 12; // 12 from the svg container padding
let availableSpace = context.root.$vuetify.breakpoint.width - controlsElement.clientWidth - matrixElement.clientWidth - 12; // 12 from the svg container padding
if (intermediaryElement !== null) {
availableSpace -= intermediaryElement.clientWidth;
}
return availableSpace < 330 ? 330 : availableSpace;
}

Expand Down
7 changes: 7 additions & 0 deletions src/components/MultiMatrix.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { select, selectAll } from 'd3-selection';
import { transition } from 'd3-transition';
import store from '@/store';
import LineUp from '@/components/LineUp.vue';
import IntermediaryNodes from '@/components/IntermediaryNodes.vue';

import 'science';
import 'reorder.js';
Expand All @@ -26,6 +27,7 @@ declare const reorder: any;
export default Vue.extend({
components: {
LineUp,
IntermediaryNodes,
},

data(): {
Expand Down Expand Up @@ -169,6 +171,10 @@ export default Vue.extend({
parentColorScale() {
return store.getters.parentColorScale;
},

showIntNodeVis() {
return store.state.showIntNodeVis;
},
},

watch: {
Expand Down Expand Up @@ -957,6 +963,7 @@ export default Vue.extend({
:viewbox="`0 0 ${matrixWidth} ${matrixHeight}`"
/>
</div>
<intermediary-nodes v-if="finishedMounting && showIntNodeVis" />
<line-up v-if="finishedMounting" />
</v-container>

Expand Down
12 changes: 12 additions & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from 'multinet';
import {
ArangoAttributes,
ArangoPath,
Cell,
Edge, LoadError, Network, Node, ProvenanceEventTypes, State,
} from '@/types';
Expand Down Expand Up @@ -59,6 +60,8 @@ const {
showProvenanceVis: false,
nodeAttributes: {},
edgeAttributes: {},
showIntNodeVis: false,
connectivityMatrixPaths: { nodes: [], paths: [] },
} as State,

getters: {
Expand Down Expand Up @@ -257,6 +260,15 @@ const {
state.nodeAttributes = payload.nodeAttributes;
state.edgeAttributes = payload.edgeAttributes;
},

toggleShowIntNodeVis(state, showIntNodeVis: boolean) {
state.showIntNodeVis = showIntNodeVis;
},

setConnectivityMatrixPaths(state, payload: { nodes: Node[]; paths: ArangoPath[]}) {
state.connectivityMatrixPaths.nodes = payload.nodes;
state.connectivityMatrixPaths.paths = payload.paths;
},
},

actions: {
Expand Down
Loading