From 4f097f5dd059158b10d414f8eaaa5b21524ac8c4 Mon Sep 17 00:00:00 2001 From: derya Date: Wed, 29 Dec 2021 09:07:47 -0700 Subject: [PATCH 01/21] Slice network by time --- src/components/Controls.vue | 4 +- src/components/TimeSlicing.vue | 209 +++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/components/TimeSlicing.vue diff --git a/src/components/Controls.vue b/src/components/Controls.vue index 18e67a7..1db2f02 100644 --- a/src/components/Controls.vue +++ b/src/components/Controls.vue @@ -1,5 +1,6 @@ + + From 6756ffac20f27b01244bb16e8207fdff0514b56f Mon Sep 17 00:00:00 2001 From: derya Date: Wed, 29 Dec 2021 11:02:19 -0700 Subject: [PATCH 02/21] Display build edge menu --- src/components/Controls.vue | 3 + src/components/EdgeBuilder.vue | 140 +++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 src/components/EdgeBuilder.vue diff --git a/src/components/Controls.vue b/src/components/Controls.vue index 1db2f02..cf2faf4 100644 --- a/src/components/Controls.vue +++ b/src/components/Controls.vue @@ -1,6 +1,7 @@ + + + + From 0157eced1df18636b5205dafba1af39d57574834 Mon Sep 17 00:00:00 2001 From: derya Date: Thu, 30 Dec 2021 20:04:33 -0700 Subject: [PATCH 03/21] Add summary stat to filter view --- src/components/EdgeBuilder.vue | 6 +- src/components/EdgeBuilderChart.vue | 666 ++++++++++++++++++++++++++++ 2 files changed, 670 insertions(+), 2 deletions(-) create mode 100644 src/components/EdgeBuilderChart.vue diff --git a/src/components/EdgeBuilder.vue b/src/components/EdgeBuilder.vue index 764da04..22678d4 100644 --- a/src/components/EdgeBuilder.vue +++ b/src/components/EdgeBuilder.vue @@ -2,12 +2,14 @@ import store from '@/store'; import { internalFieldNames, Edge } from '@/types'; import DragTarget from '@/components/DragTarget.vue'; -import LegendChart from '@/components/LegendChart.vue'; import { computed, defineComponent, ref } from '@vue/composition-api'; +import EdgeBuilderChart from '@/components/EdgeBuilderChart.vue'; +import LegendChart from '@/components/LegendChart.vue'; export default defineComponent({ components: { DragTarget, + EdgeBuilderChart, LegendChart, }, @@ -95,7 +97,7 @@ export default defineComponent({ :type="'edge'" /> - +import store from '@/store'; +import { Node, Edge } from '@/types'; +import { + computed, defineComponent, onMounted, PropType, watchEffect, +} from '@vue/composition-api'; +import { + histogram, max, min, mean, median, quantile, +} from 'd3-array'; +import { axisBottom, axisLeft } from 'd3-axis'; +import { brushX, D3BrushEvent } from 'd3-brush'; +import { + ScaleBand, scaleBand, ScaleLinear, scaleLinear, +} from 'd3-scale'; +import { select, selectAll } from 'd3-selection'; + +export default defineComponent({ + name: 'EdgeBuilderChart', + + props: { + varName: { + type: String, + required: true, + }, + type: { + type: String as PropType<'node' | 'edge'>, + required: true, + }, + selected: { + type: Boolean, + required: true, + }, + brushable: { + type: Boolean, + default: false, + }, + mappedTo: { + type: String, + default: '', + }, + filter: { + type: String, + default: '', + }, + }, + + setup(props) { + const yAxisPadding = 30; + const svgHeight = props.brushable === true ? 200 : 50; + const histogramHeight = 75; + + const network = computed(() => store.state.network); + const columnTypes = computed(() => store.state.columnTypes); + const nestedVariables = computed(() => store.state.nestedVariables); + const nodeColorScale = computed(() => store.getters.nodeColorScale); + const nodeBarColorScale = computed(() => store.state.nodeBarColorScale); + const nodeGlyphColorScale = computed(() => store.state.nodeGlyphColorScale); + const edgeWidthScale = computed(() => store.getters.edgeWidthScale); + const edgeColorScale = computed(() => store.getters.edgeColorScale); + const attributeRanges = computed(() => store.state.attributeRanges); + + // TODO: https://github.com/multinet-app/multilink/issues/176 + // use table name for var selection + function isQuantitative(varName: string, type: 'node' | 'edge') { + if (columnTypes.value !== null && Object.keys(columnTypes.value).length > 0) { + 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; + } + + function dragStart(event: DragEvent) { + if (event.dataTransfer !== null && event.target !== null) { + event.dataTransfer.setData('attr_id', (event.target as Element).id); + } + } + + function unAssignVar(variable?: string) { + if (props.type === 'node') { + if (props.mappedTo === 'size') { + store.commit.setNodeSizeVariable(''); + } else if (props.mappedTo === 'color') { + store.commit.setNodeColorVariable(''); + } else if (props.mappedTo === 'bars') { + const newBarVars = nestedVariables.value.bar.filter( + (barVar) => barVar !== variable, + ); + + store.commit.setNestedVariables({ + bar: newBarVars, + glyph: nestedVariables.value.glyph, + }); + } else if (props.mappedTo === 'glyphs') { + const newGlyphVars = nestedVariables.value.glyph.filter( + (glyphVar) => glyphVar !== props.varName, + ); + + store.commit.setNestedVariables({ + bar: nestedVariables.value.bar, + glyph: newGlyphVars, + }); + } + } else if (props.type === 'edge') { + if (props.mappedTo === 'width') { + store.commit.setEdgeVariables({ + width: '', + color: store.state.edgeVariables.color, + }); + } else if (props.mappedTo === 'color') { + store.commit.setEdgeVariables({ + width: store.state.edgeVariables.width, + color: '', + }); + } + } + } + + onMounted(() => { + const variableSvg = select(`#${props.type}${props.varName}${props.mappedTo}`); + + let variableSvgWidth = (variableSvg + .node() as Element) + .getBoundingClientRect() + .width - yAxisPadding; + + variableSvgWidth = variableSvgWidth < 0 ? 256 : variableSvgWidth; + + let xScale: ScaleLinear | ScaleBand; + let yScale: ScaleLinear; + + if (network.value === null) { + return; + } + + // Process data for bars/histogram + if (props.mappedTo === 'width') { // edge width + yScale = scaleLinear() + .domain(edgeWidthScale.value.domain()) + .range([svgHeight, 10]); + + const minValue = edgeWidthScale.value.range()[0]; + const maxValue = edgeWidthScale.value.range()[1]; + const middleValue = (edgeWidthScale.value.range()[1] + edgeWidthScale.value.range()[0]) / 2; + + // Draw width lines + variableSvg + .append('rect') + .attr('height', maxValue) + .attr('width', variableSvgWidth) + .attr('x', yAxisPadding) + .attr('y', 0) + .attr('fill', '#888888'); + + variableSvg + .append('rect') + .attr('height', middleValue) + .attr('width', variableSvgWidth) + .attr('x', yAxisPadding) + .attr('y', svgHeight / 2) + .attr('fill', '#888888'); + + variableSvg + .append('rect') + .attr('height', minValue + 2) + .attr('width', variableSvgWidth) + .attr('x', yAxisPadding) + .attr('y', svgHeight - 1) + .attr('fill', '#888888'); + } else if (props.mappedTo === 'color') { // node color and edge color + if (isQuantitative(props.varName, props.type)) { + // Gradient + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let scale: any; + + if (props.type === 'node') { + xScale = scaleLinear() + .domain(nodeColorScale.value.domain() as number[]) + .range([yAxisPadding, variableSvgWidth]); + + scale = nodeColorScale.value; + } else { + xScale = scaleLinear() + .domain(edgeColorScale.value.domain() as number[]) + .range([yAxisPadding, variableSvgWidth]); + + scale = edgeColorScale.value; + } + + const minColor = scale(scale.domain()[0]); + const midColor = scale((scale.domain()[0] + scale.domain()[1]) / 2); + const maxColor = scale(scale.domain()[1]); + + const gradient = variableSvg + .append('defs') + .append('linearGradient') + .attr('id', 'grad'); + + gradient + .append('stop') + .attr('offset', '0%') + .attr('stop-color', minColor); + + gradient + .append('stop') + .attr('offset', '50%') + .attr('stop-color', midColor); + + gradient + .append('stop') + .attr('offset', '100%') + .attr('stop-color', maxColor); + + variableSvg + .append('rect') + .attr('height', 20) + .attr('width', (xScale.range()[1] || 0) - (xScale.range()[0] || 0)) + .attr('x', xScale.range()[0] || 0) + .attr('y', 20) + .attr('fill', 'url(#grad)') + .style('opacity', 0.7); + } else { + const currentData = props.type === 'node' ? network.value.nodes : network.value.edges; + + // Swatches + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const binLabels: string[] = [...new Set((currentData as any).map((d: Node | Edge) => d[props.varName]))]; + + xScale = scaleBand() + .domain(binLabels) + .range([yAxisPadding, variableSvgWidth]); + + // Draw swatches + const swatchWidth = (variableSvgWidth - yAxisPadding) / binLabels.length; + + variableSvg + .selectAll('rect') + .data(binLabels) + .enter() + .append('rect') + .attr('height', 15) + .attr('width', swatchWidth) + .attr('x', (d, i) => (swatchWidth * i) + yAxisPadding) + .attr('y', 25) + .attr('fill', (d) => nodeGlyphColorScale.value(d)) + .classed('swatch', true); + } + } else if (props.mappedTo === 'bars') { // nested bars + watchEffect(() => { + selectAll('.legend-bars').remove(); + + // Draw bars + nestedVariables.value.bar.forEach((barVar, index) => { + // Bar backgrounds + variableSvg + .append('rect') + .attr('fill', '#EEEEEE') + .attr('height', 50) + .attr('width', 20) + .attr('x', 50 * (index) + 25) + .attr('y', 10) + .classed('legend-bars', true); + + // Main bar + const barHeight = 10 + (Math.random() * 40); + variableSvg + .append('rect') + .attr('fill', nodeBarColorScale.value(barVar)) + .attr('height', barHeight) + .attr('width', 20) + .attr('x', 50 * (index) + 25) + .attr('y', 60 - barHeight) + .classed('legend-bars', true); + + // Label + variableSvg + .append('foreignObject') + .attr('height', 20) + .attr('width', 30) + .attr('x', 50 * (index) + 15) + .attr('y', 60) + .classed('legend-bars', true) + .append('xhtml:p') + .attr('title', barVar) + .text(barVar); + + // Axis + const barScale = scaleLinear() + .domain([attributeRanges.value[barVar].min, attributeRanges.value[barVar].max]) + .range([59, 10]); + + variableSvg + .append('g') + .classed('legend-bars', true) + .attr('transform', `translate(${50 * (index) + 23},0)`) + .call(axisLeft(barScale).ticks(4, 's')) + .call((g) => g.select('path').remove()); + }); + }); + } else if (isQuantitative(props.varName, props.type)) { // main numeric legend charts + // WIP + let currentData: number[] = []; + // current data is all the values for the edges + currentData = network.value.edges.map((d: Node | Edge) => parseFloat(d[props.varName])); + + xScale = scaleLinear() + .domain([Math.min(...currentData), Math.max(...currentData) + 1]) + .range([yAxisPadding, variableSvgWidth]); + + // Generate stats + const summaryStats = { + q1: 0, min: 0, median: 0, mean: 0, q3: 0, max: 0, + }; + + summaryStats.min = min(currentData) as number; + summaryStats.max = max(currentData) as number; + summaryStats.mean = mean(currentData) as number; + summaryStats.median = median(currentData) as number; + summaryStats.q3 = quantile(currentData, 0.75) as number; + summaryStats.q1 = quantile(currentData, 0.25) as number; + + // TODO Create density plot + const binGenerator = histogram() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .domain((xScale as any).domain()) // then the domain of the graphic + .thresholds(xScale.ticks(50)); // then the numbers of bins + + const bins = binGenerator(currentData); + + store.commit.addAttributeRange({ + attr: props.varName, + min: xScale.domain()[0] || 0, + max: xScale.domain()[1] || 0, + binLabels: xScale.domain().map((label) => label.toString()), + binValues: xScale.range(), + }); + + yScale = scaleLinear() + .domain([0, max(bins, (d) => d.length) || 0]) + .range([histogramHeight, 10]); + + variableSvg + .selectAll('rect') + .data(bins) + .enter() + .append('rect') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .attr('x', (d) => xScale(d.x0 as any) || 0) + .attr('y', (d) => yScale(d.length)) + .attr('height', (d) => histogramHeight - yScale(d.length)) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .attr('width', (d) => (xScale(d.x1 as any) || 0) - (xScale(d.x0 as any) || 0)) + .attr('fill', '#82B1FF'); + + const boxPlotHeight = 100; + const whiskersHeight = 20; + const boxPlotSVG = variableSvg.append('g').attr('transform', `translate(${0}, ${histogramHeight + 15})`).attr('id', 'boxPlot'); + + const whiskerLineGrps = boxPlotSVG + .selectAll('.whiskers') + .data([summaryStats.min, summaryStats.max]) + .enter().append('g') + .attr('class', 'whiskers') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .attr('transform', (d) => `translate(${xScale(d as any) || 0},${boxPlotHeight / 2 - whiskersHeight / 2})`); + + whiskerLineGrps.append('line') + .attr('x2', 0) + .attr('y2', () => whiskersHeight) + .attr('fill', 'none') + .attr('stroke', '#acacac') + .attr('stroke-width', 1); + + whiskerLineGrps.append('text') + .attr('x', 0) + .attr('y', whiskersHeight + 16) + .attr('text-anchor', (d, i) => (i === 0 ? 'start' : 'end')) + .text((d) => Math.floor(d)) + .attr('fill', '#acacac'); + + // Horizontal Line + boxPlotSVG.append('line') + .attr('x1', xScale(summaryStats.min)) + .attr('y1', boxPlotHeight / 2) + .attr('x2', xScale(summaryStats.max)) + .attr('y2', boxPlotHeight / 2) + .attr('fill', 'none') + .attr('stroke', '#acacac') + .attr('stroke-width', 1); + + // Rectangle + boxPlotSVG.append('g').append('rect') + .attr('width', xScale(summaryStats.q3) - xScale(summaryStats.q1)) + .attr('height', whiskersHeight) + .attr('x', xScale(summaryStats.q1)) + .attr('y', boxPlotHeight / 2 - whiskersHeight / 2) + .attr('fill', '#f9f9f9') + .attr('stroke', '#acacac') + .attr('stroke-width', 1); + + // QuartileLine Groups + const quartileLineGrps = boxPlotSVG.selectAll('.quartiles') + .data([summaryStats.q1, summaryStats.mean, summaryStats.q3]) + .enter().append('g') + .attr('class', 'quartiles') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .attr('transform', (d) => `translate(${xScale(d as any) || 0},${boxPlotHeight / 2 - whiskersHeight / 2})`); + + quartileLineGrps.append('line') + .attr('x2', 0) + .attr('y2', () => whiskersHeight) + .attr('fill', 'none') + .attr('stroke', '#acacac') + .attr('stroke-width', 1); + + quartileLineGrps.append('text') + .attr('x', 0) + .attr('y', whiskersHeight + 16) + .attr('text-anchor', 'middle') + .text((d) => Math.floor(d)) + .attr('fill', '#acacac'); + } else { // main categorical legend charts + let currentData: string[] = []; + if (props.type === 'node') { + currentData = network.value.nodes.map((d: Node | Edge) => d[props.varName]).sort(); + } else { + currentData = network.value.edges.map((d: Node | Edge) => d[props.varName]).sort(); + } + + const bins = new Map([...new Set(currentData)].map( + (x) => [x, currentData.filter((y) => y === x).length], + )); + + const binLabels: string[] = Array.from(bins.keys()); + const binValues: number[] = Array.from(bins.values()); + + store.commit.addAttributeRange({ + attr: props.varName, + min: parseFloat(min(binLabels) || '0'), + max: parseFloat(max(binLabels) || '0'), + binLabels, + binValues, + }); + + // Generate axis scales + yScale = scaleLinear() + .domain([min(binValues) || 0, max(binValues) || 0]) + .range([svgHeight, 0]); + + xScale = scaleBand() + .domain(binLabels) + .range([yAxisPadding, variableSvgWidth]); + + variableSvg + .selectAll('rect') + .data(currentData) + .enter() + .append('rect') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .attr('x', (d: string) => xScale(d as any) || 0) + .attr('y', (d: string) => yScale(bins.get(d) || 0)) + .attr('height', (d: string) => svgHeight - yScale(bins.get(d) || 0)) + .attr('width', xScale.bandwidth()) + .attr('fill', (d: string) => nodeGlyphColorScale.value(d)); + } + + // Add the axis scales onto the chart + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (xScale! !== undefined) { + if (props.brushable === false) { + variableSvg + .append('g') + .attr('transform', `translate(0, ${svgHeight})`) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .call((axisBottom as any)(xScale).ticks(4, 's')) + .call((g) => g.select('path').remove()); + } else { + variableSvg + .append('g') + .attr('transform', `translate(0, ${histogramHeight})`) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .call((axisBottom as any)(xScale).ticks(4, 's')) + .call((g) => g.select('path').remove()); + } + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (yScale! !== undefined) { + variableSvg + .append('g') + .attr('transform', `translate(${yAxisPadding},0)`) + .call(axisLeft(yScale).ticks(3, 's')) + .call((g) => g.select('path').remove()); + } + + // For the brushable charts for filtering add brushing + if (props.brushable) { + const brush = brushX() + .extent([[yAxisPadding, 0], [variableSvgWidth, svgHeight]]) + .on('end', (event: unknown) => { + const brushEvent = event as D3BrushEvent; + const extent = brushEvent.selection as [number, number]; + + if (extent === null) { + return; + } + + const currentAttributeRange = attributeRanges.value[props.varName]; + + if ( + (props.filter === 'glyphs' && props.type === 'node') + || (props.filter === 'color' && !isQuantitative(props.varName, props.type)) + ) { + const firstIndex = Math.floor(((extent[0] - yAxisPadding) / (variableSvgWidth - yAxisPadding)) * attributeRanges.value[props.varName].binLabels.length); + const secondIndex = Math.ceil(((extent[1] - yAxisPadding) / (variableSvgWidth - yAxisPadding)) * attributeRanges.value[props.varName].binLabels.length); + + store.commit.addAttributeRange({ + ...currentAttributeRange, + currentBinLabels: currentAttributeRange.binLabels.slice(firstIndex, secondIndex), + currentBinValues: currentAttributeRange.binValues.slice(firstIndex, secondIndex), + }); + } else if ( + (props.filter === 'size' && props.type === 'node') + || (props.filter === 'color' && isQuantitative(props.varName, props.type)) + || (props.filter === 'width' && props.type === 'edge') + ) { + const newMin = (((extent[0] - yAxisPadding) / (variableSvgWidth - yAxisPadding)) * (currentAttributeRange.max - currentAttributeRange.min)) + currentAttributeRange.min; + const newMax = (((extent[1] - yAxisPadding) / (variableSvgWidth - yAxisPadding)) * (currentAttributeRange.max - currentAttributeRange.min)) + currentAttributeRange.min; + + store.commit.addAttributeRange({ ...currentAttributeRange, currentMax: newMax, currentMin: newMin }); + } + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (variableSvg as any) + .call(brush) + // start with the whole network brushed + .call(brush.move, [yAxisPadding, variableSvgWidth]); + } + }); + + return { + svgHeight, + dragStart, + unAssignVar, + nestedVariables, + }; + }, +}); + + + + + From 7539e8bffe3afe5914c4610bfddefb575b727b02 Mon Sep 17 00:00:00 2001 From: derya Date: Tue, 4 Jan 2022 10:55:47 -0700 Subject: [PATCH 04/21] Add brush handles + label --- src/components/EdgeBuilderChart.vue | 334 +++++++++++----------------- 1 file changed, 134 insertions(+), 200 deletions(-) diff --git a/src/components/EdgeBuilderChart.vue b/src/components/EdgeBuilderChart.vue index a298b72..492ea8b 100644 --- a/src/components/EdgeBuilderChart.vue +++ b/src/components/EdgeBuilderChart.vue @@ -2,7 +2,7 @@ import store from '@/store'; import { Node, Edge } from '@/types'; import { - computed, defineComponent, onMounted, PropType, watchEffect, + computed, defineComponent, onMounted, PropType, } from '@vue/composition-api'; import { histogram, max, min, mean, median, quantile, @@ -47,16 +47,13 @@ export default defineComponent({ setup(props) { const yAxisPadding = 30; const svgHeight = props.brushable === true ? 200 : 50; - const histogramHeight = 75; + const chartHeight = 75; const network = computed(() => store.state.network); const columnTypes = computed(() => store.state.columnTypes); const nestedVariables = computed(() => store.state.nestedVariables); - const nodeColorScale = computed(() => store.getters.nodeColorScale); - const nodeBarColorScale = computed(() => store.state.nodeBarColorScale); const nodeGlyphColorScale = computed(() => store.state.nodeGlyphColorScale); const edgeWidthScale = computed(() => store.getters.edgeWidthScale); - const edgeColorScale = computed(() => store.getters.edgeColorScale); const attributeRanges = computed(() => store.state.attributeRanges); // TODO: https://github.com/multinet-app/multilink/issues/176 @@ -83,43 +80,12 @@ export default defineComponent({ } } - function unAssignVar(variable?: string) { - if (props.type === 'node') { - if (props.mappedTo === 'size') { - store.commit.setNodeSizeVariable(''); - } else if (props.mappedTo === 'color') { - store.commit.setNodeColorVariable(''); - } else if (props.mappedTo === 'bars') { - const newBarVars = nestedVariables.value.bar.filter( - (barVar) => barVar !== variable, - ); - - store.commit.setNestedVariables({ - bar: newBarVars, - glyph: nestedVariables.value.glyph, - }); - } else if (props.mappedTo === 'glyphs') { - const newGlyphVars = nestedVariables.value.glyph.filter( - (glyphVar) => glyphVar !== props.varName, - ); - - store.commit.setNestedVariables({ - bar: nestedVariables.value.bar, - glyph: newGlyphVars, - }); - } - } else if (props.type === 'edge') { - if (props.mappedTo === 'width') { - store.commit.setEdgeVariables({ - width: '', - color: store.state.edgeVariables.color, - }); - } else if (props.mappedTo === 'color') { - store.commit.setEdgeVariables({ - width: store.state.edgeVariables.width, - color: '', - }); - } + function unAssignVar() { + if (props.mappedTo === 'width') { + store.commit.setEdgeVariables({ + width: '', + color: store.state.edgeVariables.color, + }); } } @@ -141,93 +107,42 @@ export default defineComponent({ } // Process data for bars/histogram - if (props.mappedTo === 'width') { // edge width - yScale = scaleLinear() - .domain(edgeWidthScale.value.domain()) - .range([svgHeight, 10]); + if (props.mappedTo === 'width') { + if (isQuantitative(props.varName, props.type)) { + yScale = scaleLinear() + .domain(edgeWidthScale.value.domain()) + .range([svgHeight, 10]); - const minValue = edgeWidthScale.value.range()[0]; - const maxValue = edgeWidthScale.value.range()[1]; - const middleValue = (edgeWidthScale.value.range()[1] + edgeWidthScale.value.range()[0]) / 2; + const minValue = edgeWidthScale.value.range()[0]; + const maxValue = edgeWidthScale.value.range()[1]; + const middleValue = (edgeWidthScale.value.range()[1] + edgeWidthScale.value.range()[0]) / 2; - // Draw width lines - variableSvg - .append('rect') - .attr('height', maxValue) - .attr('width', variableSvgWidth) - .attr('x', yAxisPadding) - .attr('y', 0) - .attr('fill', '#888888'); - - variableSvg - .append('rect') - .attr('height', middleValue) - .attr('width', variableSvgWidth) - .attr('x', yAxisPadding) - .attr('y', svgHeight / 2) - .attr('fill', '#888888'); + // Draw width lines + variableSvg + .append('rect') + .attr('height', maxValue) + .attr('width', variableSvgWidth) + .attr('x', yAxisPadding) + .attr('y', 0) + .attr('fill', '#888888'); - variableSvg - .append('rect') - .attr('height', minValue + 2) - .attr('width', variableSvgWidth) - .attr('x', yAxisPadding) - .attr('y', svgHeight - 1) - .attr('fill', '#888888'); - } else if (props.mappedTo === 'color') { // node color and edge color - if (isQuantitative(props.varName, props.type)) { - // Gradient - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let scale: any; - - if (props.type === 'node') { - xScale = scaleLinear() - .domain(nodeColorScale.value.domain() as number[]) - .range([yAxisPadding, variableSvgWidth]); - - scale = nodeColorScale.value; - } else { - xScale = scaleLinear() - .domain(edgeColorScale.value.domain() as number[]) - .range([yAxisPadding, variableSvgWidth]); - - scale = edgeColorScale.value; - } - - const minColor = scale(scale.domain()[0]); - const midColor = scale((scale.domain()[0] + scale.domain()[1]) / 2); - const maxColor = scale(scale.domain()[1]); - - const gradient = variableSvg - .append('defs') - .append('linearGradient') - .attr('id', 'grad'); - - gradient - .append('stop') - .attr('offset', '0%') - .attr('stop-color', minColor); - - gradient - .append('stop') - .attr('offset', '50%') - .attr('stop-color', midColor); - - gradient - .append('stop') - .attr('offset', '100%') - .attr('stop-color', maxColor); + variableSvg + .append('rect') + .attr('height', middleValue) + .attr('width', variableSvgWidth) + .attr('x', yAxisPadding) + .attr('y', svgHeight / 2) + .attr('fill', '#888888'); variableSvg .append('rect') - .attr('height', 20) - .attr('width', (xScale.range()[1] || 0) - (xScale.range()[0] || 0)) - .attr('x', xScale.range()[0] || 0) - .attr('y', 20) - .attr('fill', 'url(#grad)') - .style('opacity', 0.7); + .attr('height', minValue + 2) + .attr('width', variableSvgWidth) + .attr('x', yAxisPadding) + .attr('y', svgHeight - 1) + .attr('fill', '#888888'); } else { - const currentData = props.type === 'node' ? network.value.nodes : network.value.edges; + const currentData = network.value.edges; // Swatches // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -252,58 +167,6 @@ export default defineComponent({ .attr('fill', (d) => nodeGlyphColorScale.value(d)) .classed('swatch', true); } - } else if (props.mappedTo === 'bars') { // nested bars - watchEffect(() => { - selectAll('.legend-bars').remove(); - - // Draw bars - nestedVariables.value.bar.forEach((barVar, index) => { - // Bar backgrounds - variableSvg - .append('rect') - .attr('fill', '#EEEEEE') - .attr('height', 50) - .attr('width', 20) - .attr('x', 50 * (index) + 25) - .attr('y', 10) - .classed('legend-bars', true); - - // Main bar - const barHeight = 10 + (Math.random() * 40); - variableSvg - .append('rect') - .attr('fill', nodeBarColorScale.value(barVar)) - .attr('height', barHeight) - .attr('width', 20) - .attr('x', 50 * (index) + 25) - .attr('y', 60 - barHeight) - .classed('legend-bars', true); - - // Label - variableSvg - .append('foreignObject') - .attr('height', 20) - .attr('width', 30) - .attr('x', 50 * (index) + 15) - .attr('y', 60) - .classed('legend-bars', true) - .append('xhtml:p') - .attr('title', barVar) - .text(barVar); - - // Axis - const barScale = scaleLinear() - .domain([attributeRanges.value[barVar].min, attributeRanges.value[barVar].max]) - .range([59, 10]); - - variableSvg - .append('g') - .classed('legend-bars', true) - .attr('transform', `translate(${50 * (index) + 23},0)`) - .call(axisLeft(barScale).ticks(4, 's')) - .call((g) => g.select('path').remove()); - }); - }); } else if (isQuantitative(props.varName, props.type)) { // main numeric legend charts // WIP let currentData: number[] = []; @@ -326,7 +189,8 @@ export default defineComponent({ summaryStats.q3 = quantile(currentData, 0.75) as number; summaryStats.q1 = quantile(currentData, 0.25) as number; - // TODO Create density plot + // TODO: https://github.com/multinet-app/multidynamic/issues/3 + // Create density plot const binGenerator = histogram() // eslint-disable-next-line @typescript-eslint/no-explicit-any .domain((xScale as any).domain()) // then the domain of the graphic @@ -344,7 +208,7 @@ export default defineComponent({ yScale = scaleLinear() .domain([0, max(bins, (d) => d.length) || 0]) - .range([histogramHeight, 10]); + .range([chartHeight, 10]); variableSvg .selectAll('rect') @@ -354,14 +218,14 @@ export default defineComponent({ // eslint-disable-next-line @typescript-eslint/no-explicit-any .attr('x', (d) => xScale(d.x0 as any) || 0) .attr('y', (d) => yScale(d.length)) - .attr('height', (d) => histogramHeight - yScale(d.length)) + .attr('height', (d) => chartHeight - yScale(d.length)) // eslint-disable-next-line @typescript-eslint/no-explicit-any .attr('width', (d) => (xScale(d.x1 as any) || 0) - (xScale(d.x0 as any) || 0)) .attr('fill', '#82B1FF'); const boxPlotHeight = 100; const whiskersHeight = 20; - const boxPlotSVG = variableSvg.append('g').attr('transform', `translate(${0}, ${histogramHeight + 15})`).attr('id', 'boxPlot'); + const boxPlotSVG = variableSvg.append('g').attr('transform', `translate(${0}, ${chartHeight + 15})`).attr('id', 'boxPlot'); const whiskerLineGrps = boxPlotSVG .selectAll('.whiskers') @@ -427,12 +291,10 @@ export default defineComponent({ .text((d) => Math.floor(d)) .attr('fill', '#acacac'); } else { // main categorical legend charts + // change so that you click the edge types let currentData: string[] = []; - if (props.type === 'node') { - currentData = network.value.nodes.map((d: Node | Edge) => d[props.varName]).sort(); - } else { - currentData = network.value.edges.map((d: Node | Edge) => d[props.varName]).sort(); - } + + currentData = network.value.edges.map((d: Edge) => d[props.varName]).sort(); const bins = new Map([...new Set(currentData)].map( (x) => [x, currentData.filter((y) => y === x).length], @@ -452,7 +314,7 @@ export default defineComponent({ // Generate axis scales yScale = scaleLinear() .domain([min(binValues) || 0, max(binValues) || 0]) - .range([svgHeight, 0]); + .range([chartHeight, 0]); xScale = scaleBand() .domain(binLabels) @@ -466,7 +328,7 @@ export default defineComponent({ // eslint-disable-next-line @typescript-eslint/no-explicit-any .attr('x', (d: string) => xScale(d as any) || 0) .attr('y', (d: string) => yScale(bins.get(d) || 0)) - .attr('height', (d: string) => svgHeight - yScale(bins.get(d) || 0)) + .attr('height', (d: string) => chartHeight - yScale(bins.get(d) || 0)) .attr('width', xScale.bandwidth()) .attr('fill', (d: string) => nodeGlyphColorScale.value(d)); } @@ -484,7 +346,7 @@ export default defineComponent({ } else { variableSvg .append('g') - .attr('transform', `translate(0, ${histogramHeight})`) + .attr('transform', `translate(0, ${chartHeight})`) // eslint-disable-next-line @typescript-eslint/no-explicit-any .call((axisBottom as any)(xScale).ticks(4, 's')) .call((g) => g.select('path').remove()); @@ -502,8 +364,77 @@ export default defineComponent({ // For the brushable charts for filtering add brushing if (props.brushable) { + // brush labels + const labelGroup = variableSvg.append('g'); + + const labelL = labelGroup.append('text') + .attr('id', 'labelleft') + .attr('x', 0) + .attr('y', svgHeight + 5) + .attr('text-anchor', 'middle') + .attr('fill', '#acacac'); + const labelR = labelGroup.append('text') + .attr('id', 'labelright') + .attr('x', 0) + .attr('y', svgHeight + 5) + .attr('text-anchor', 'middle') + .attr('fill', '#acacac'); + const labelB = labelGroup.append('text') + .attr('id', 'labelbottom') + .attr('x', 0) + .attr('y', svgHeight + 5) + .attr('text-anchor', 'middle') + .attr('fill', '#acacac'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-inner-declarations + function brushResizePath(d: any) { + const e = +(d.type === 'e'); + const x = e ? 1 : -1; + const y = svgHeight / 2; + return `M${0.5 * x},${y}A6,6 0 0 ${e} ${6.5 * x},${y + 6}V${2 * y - 6 + // eslint-disable-next-line no-useless-concat + }A6,6 0 0 ${e} ${0.5 * x},${2 * y}Z` + `M${2.5 * x},${y + 8}V${2 * y - 8 + }M${4.5 * x},${y + 8}V${2 * y - 8}`; + } + + // create brush const brush = brushX() - .extent([[yAxisPadding, 0], [variableSvgWidth, svgHeight]]) + .extent([[yAxisPadding, 0], [variableSvgWidth, svgHeight - 10]]) + .on('brush', (event: unknown) => { + const brushEvent = event as D3BrushEvent; + const extent = brushEvent.selection as [number, number]; + + if (extent === null) { + return; + } + + const currentAttributeRange = attributeRanges.value[props.varName]; + + if (isQuantitative(props.varName, props.type)) { + const newMin = (((extent[0] - yAxisPadding) / (variableSvgWidth - yAxisPadding)) * (currentAttributeRange.max - currentAttributeRange.min)) + currentAttributeRange.min; + const newMax = (((extent[1] - yAxisPadding) / (variableSvgWidth - yAxisPadding)) * (currentAttributeRange.max - currentAttributeRange.min)) + currentAttributeRange.min; + + // update and move labels + labelL.attr('x', extent[0]) + .text(newMin.toFixed(2)); + labelR.attr('x', extent[1]) + .text(newMax.toFixed(2)); + // move brush handles + const handle = selectAll('.handle--custom'); + handle.attr('display', null).attr('transform', (d, i) => `translate(${[extent[i], -svgHeight / 4]})`); + } else { + const firstIndex = Math.floor(((extent[0] - yAxisPadding) / (variableSvgWidth - yAxisPadding)) * attributeRanges.value[props.varName].binLabels.length); + const secondIndex = Math.ceil(((extent[1] - yAxisPadding) / (variableSvgWidth - yAxisPadding)) * attributeRanges.value[props.varName].binLabels.length); + const currentBinLabels = currentAttributeRange.binLabels.slice(firstIndex, secondIndex); + + // update and move labels + labelB.attr('x', variableSvgWidth / 2) + .text(`Selection: ${currentBinLabels.length}`); + // move brush handles + const handle = selectAll('.handle--custom'); + handle.attr('display', null).attr('transform', (d, i) => `translate(${[extent[i], -svgHeight / 4]})`); + } + }) .on('end', (event: unknown) => { const brushEvent = event as D3BrushEvent; const extent = brushEvent.selection as [number, number]; @@ -514,10 +445,7 @@ export default defineComponent({ const currentAttributeRange = attributeRanges.value[props.varName]; - if ( - (props.filter === 'glyphs' && props.type === 'node') - || (props.filter === 'color' && !isQuantitative(props.varName, props.type)) - ) { + if (!isQuantitative(props.varName, props.type)) { const firstIndex = Math.floor(((extent[0] - yAxisPadding) / (variableSvgWidth - yAxisPadding)) * attributeRanges.value[props.varName].binLabels.length); const secondIndex = Math.ceil(((extent[1] - yAxisPadding) / (variableSvgWidth - yAxisPadding)) * attributeRanges.value[props.varName].binLabels.length); @@ -526,11 +454,7 @@ export default defineComponent({ currentBinLabels: currentAttributeRange.binLabels.slice(firstIndex, secondIndex), currentBinValues: currentAttributeRange.binValues.slice(firstIndex, secondIndex), }); - } else if ( - (props.filter === 'size' && props.type === 'node') - || (props.filter === 'color' && isQuantitative(props.varName, props.type)) - || (props.filter === 'width' && props.type === 'edge') - ) { + } else if (isQuantitative(props.varName, props.type)) { const newMin = (((extent[0] - yAxisPadding) / (variableSvgWidth - yAxisPadding)) * (currentAttributeRange.max - currentAttributeRange.min)) + currentAttributeRange.min; const newMax = (((extent[1] - yAxisPadding) / (variableSvgWidth - yAxisPadding)) * (currentAttributeRange.max - currentAttributeRange.min)) + currentAttributeRange.min; @@ -539,10 +463,20 @@ export default defineComponent({ }); // eslint-disable-next-line @typescript-eslint/no-explicit-any - (variableSvg as any) - .call(brush) - // start with the whole network brushed - .call(brush.move, [yAxisPadding, variableSvgWidth]); + const gBrush = (variableSvg as any) + .call(brush); + + gBrush.selectAll('.handle--custom') + .data([{ type: 'w' }, { type: 'e' }]) + .enter().append('path') + .attr('class', 'handle--custom') + .attr('stroke', '#000') + .attr('fill', '#eee') + .attr('cursor', 'ew-resize') + .attr('d', brushResizePath); + + // start with the whole network brushed + gBrush.call(brush.move, [yAxisPadding, variableSvgWidth]); } }); From a689ff2f99dc63b0ff8b1a74b13498073c2f4a19 Mon Sep 17 00:00:00 2001 From: derya Date: Tue, 4 Jan 2022 12:39:28 -0700 Subject: [PATCH 05/21] Add export button --- src/components/TimeSlicing.vue | 52 ++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/components/TimeSlicing.vue b/src/components/TimeSlicing.vue index 55f4f75..aa384d5 100644 --- a/src/components/TimeSlicing.vue +++ b/src/components/TimeSlicing.vue @@ -73,6 +73,7 @@ export default defineComponent({ const selectedRange = ref(timeMax.value > 0 ? [timeMin.value, timeMax.value] : [0, 100]); function sliceNetwork() { + let networkToReturn: { network: { edges: Edge[]; nodes: Node[]}; slice: number; time: number[] }[] = []; if (network.value !== null) { const slicedNetwork: { network: { edges: Edge[]; nodes: Node[]}; slice: number; time: number[] }[] = []; const timeInterval = (selectedRange.value[1] - selectedRange.value[0]) / timeSliceNumber.value; @@ -96,7 +97,27 @@ export default defineComponent({ slicedNetwork[i].network.edges.push(edge); } }); + networkToReturn = slicedNetwork; + + return networkToReturn; } + return networkToReturn; + } + + function exportNetwork() { + if (network.value === null) { + return; + } + + const networkToExport = sliceNetwork(); + const a = document.createElement('a'); + a.href = URL.createObjectURL( + new Blob([JSON.stringify(networkToExport)], { + type: 'text/json', + }), + ); + a.download = `${store.state.networkName}_${timeSliceNumber.value}-slices.json`; + a.click(); } return { @@ -108,6 +129,7 @@ export default defineComponent({ timeSliceNumber, cleanedEdgeVariables, sliceNetwork, + exportNetwork, timeRange, timeMin, timeMax, @@ -194,14 +216,28 @@ export default defineComponent({ - - Generate Slices - + + + + Generate Slices + + + + + Export Network + + + From dd9e3b3d48966e0c2e33d61b6a29270d49d3f942 Mon Sep 17 00:00:00 2001 From: derya Date: Wed, 5 Jan 2022 08:18:34 -0700 Subject: [PATCH 06/21] Add slicedNetwork type --- src/components/TimeSlicing.vue | 12 ++++++++---- src/types.ts | 7 +++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/TimeSlicing.vue b/src/components/TimeSlicing.vue index aa384d5..5741779 100644 --- a/src/components/TimeSlicing.vue +++ b/src/components/TimeSlicing.vue @@ -1,6 +1,8 @@ + + + + diff --git a/src/store/index.ts b/src/store/index.ts index c5b5d68..61f9172 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -6,7 +6,7 @@ import { } from 'd3-force'; import { - Edge, Node, Network, SimulationEdge, State, EdgeStyleVariables, LoadError, NestedVariables, ProvenanceEventTypes, Dimensions, AttributeRange, + Edge, Node, Network, SimulationEdge, State, EdgeStyleVariables, LoadError, NestedVariables, ProvenanceEventTypes, Dimensions, AttributeRange, SlicedNetworks, } from '@/types'; import api from '@/api'; import { ColumnTypes, NetworkSpec, UserSpec } from 'multinet'; @@ -81,6 +81,7 @@ const { x: null, y: null, }, + slicedNetwork: [], } as State, getters: { @@ -286,6 +287,10 @@ const { state.attributeRanges = { ...state.attributeRanges, [attributeRange.attr]: attributeRange }; }, + setSlicedNetwork(state, slicedNetwork: SlicedNetworks[]) { + state.slicedNetwork = slicedNetwork; + }, + setProvenance(state, provenance: Provenance) { state.provenance = provenance; }, From d222855c2d18cb27fe0559504cf3b734f7e03980 Mon Sep 17 00:00:00 2001 From: derya Date: Thu, 6 Jan 2022 12:15:54 -0700 Subject: [PATCH 08/21] Change brush handle label --- src/components/EdgeBuilderChart.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/EdgeBuilderChart.vue b/src/components/EdgeBuilderChart.vue index 492ea8b..f8104bf 100644 --- a/src/components/EdgeBuilderChart.vue +++ b/src/components/EdgeBuilderChart.vue @@ -416,9 +416,9 @@ export default defineComponent({ // update and move labels labelL.attr('x', extent[0]) - .text(newMin.toFixed(2)); + .text(newMin.toFixed(0)); labelR.attr('x', extent[1]) - .text(newMax.toFixed(2)); + .text(newMax.toFixed(0)); // move brush handles const handle = selectAll('.handle--custom'); handle.attr('display', null).attr('transform', (d, i) => `translate(${[extent[i], -svgHeight / 4]})`); From b4eea69a4f89fc1f59449c09b5de06a4dbeeb136 Mon Sep 17 00:00:00 2001 From: derya Date: Thu, 6 Jan 2022 12:21:40 -0700 Subject: [PATCH 09/21] Update network --- src/components/TimeLine.vue | 2 ++ src/components/TimeSlicing.vue | 1 + 2 files changed, 3 insertions(+) diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index ae5878a..38cf575 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -51,6 +51,8 @@ export default defineComponent({ currentTime.value.current = selection; selectAll('.timelineRectClass').classed('selected', false); select(`#timeSlice_${selection}`).classed('selected', true); + + store.commit.setNetwork(slicedNetwork.value[selection].network); } return { diff --git a/src/components/TimeSlicing.vue b/src/components/TimeSlicing.vue index 5741779..19f7763 100644 --- a/src/components/TimeSlicing.vue +++ b/src/components/TimeSlicing.vue @@ -102,6 +102,7 @@ export default defineComponent({ networkToReturn = slicedNetwork; store.commit.setSlicedNetwork(networkToReturn); + store.commit.setNetwork(networkToReturn[0].network); return networkToReturn; } store.commit.setSlicedNetwork(networkToReturn); From 7f0c67c9959ad2db7ecba4410b60f1d094da6874 Mon Sep 17 00:00:00 2001 From: derya Date: Thu, 6 Jan 2022 12:28:28 -0700 Subject: [PATCH 10/21] Update time range scale --- src/components/TimeSlicing.vue | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/TimeSlicing.vue b/src/components/TimeSlicing.vue index 19f7763..5ee13bc 100644 --- a/src/components/TimeSlicing.vue +++ b/src/components/TimeSlicing.vue @@ -3,7 +3,9 @@ import store from '@/store'; import { internalFieldNames, Edge, SlicedNetworks, } from '@/types'; -import { computed, defineComponent, ref } from '@vue/composition-api'; +import { + computed, defineComponent, ref, watch, +} from '@vue/composition-api'; export default defineComponent({ setup() { @@ -70,9 +72,11 @@ export default defineComponent({ const timeMin = computed(() => timeRange.value[0]); const timeMax = computed(() => timeRange.value[1]); - // TODO: https://github.com/multinet-app/multidynamic/issues/2 - // Add watch effect for time max? - const selectedRange = ref(timeMax.value > 0 ? [timeMin.value, timeMax.value] : [0, 100]); + const selectedRange = ref([0, 0]); + + watch([timeMax], () => { + if (timeMax.value > 0) { selectedRange.value = [timeMin.value, timeMax.value]; } + }); function sliceNetwork() { let networkToReturn: SlicedNetworks[] = []; From cd80e670e3d1405100bfed3c4d863cfdf1046cba Mon Sep 17 00:00:00 2001 From: derya Date: Thu, 6 Jan 2022 14:01:36 -0700 Subject: [PATCH 11/21] Add copy of original network --- src/components/TimeLine.vue | 7 +++- src/components/TimeSlicing.vue | 65 ++++++++++++++++++++-------------- src/store/index.ts | 7 ++++ src/types.ts | 1 + 4 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index 38cf575..2058dc7 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -2,7 +2,7 @@ import { select, selectAll } from 'd3-selection'; import store from '@/store'; import { - computed, defineComponent, ref, Ref, + computed, defineComponent, ref, Ref, watch, } from '@vue/composition-api'; export default defineComponent({ @@ -55,6 +55,11 @@ export default defineComponent({ store.commit.setNetwork(slicedNetwork.value[selection].network); } + watch([slicedNetwork], () => { + selectAll('.timelineRectClass').classed('selected', false); + select('#timeSlice_0').classed('selected', true); + }); + return { svg, svgDimensions, diff --git a/src/components/TimeSlicing.vue b/src/components/TimeSlicing.vue index 5ee13bc..a83f485 100644 --- a/src/components/TimeSlicing.vue +++ b/src/components/TimeSlicing.vue @@ -18,6 +18,7 @@ export default defineComponent({ ]; const network = computed(() => store.state.network); + const originalNetwork = computed(() => store.state.networkOnLoad); const edgeVariables = computed(() => store.state.edgeVariables); const columnTypes = computed(() => store.state.columnTypes); const startTimeVar = ref(''); @@ -37,10 +38,10 @@ export default defineComponent({ } const cleanedEdgeVariables = computed(() => { - if (network.value !== null) { + if (originalNetwork.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))); + originalNetwork.value.edges.map((edge: Edge) => Object.keys(edge).forEach((key) => allVars.add(key))); internalFieldNames.forEach((field) => allVars.delete(field)); allVars.delete('source'); @@ -55,9 +56,9 @@ export default defineComponent({ // Compute the min and max times const timeRange = computed(() => { const range: number[] = [0, 0]; - if (startTimeVar.value !== null && endTimeVar.value !== null && network.value !== null) { + if (startTimeVar.value !== null && endTimeVar.value !== null && originalNetwork.value !== null) { // Loop through all edges, return min and max time values - network.value.edges.forEach((edge: Edge) => { + originalNetwork.value.edges.forEach((edge: Edge) => { if (edge[startTimeVar.value] < range[0]) { range[0] = edge[startTimeVar.value]; } @@ -80,36 +81,46 @@ export default defineComponent({ function sliceNetwork() { let networkToReturn: SlicedNetworks[] = []; - if (network.value !== null) { - const slicedNetwork: SlicedNetworks[] = []; - const timeInterval = (selectedRange.value[1] - selectedRange.value[0]) / timeSliceNumber.value; - - // Generate time chunks - // eslint-disable-next-line no-plusplus - for (let i = 0; i < timeSliceNumber.value; i++) { - const currentSlice: SlicedNetworks = { slice: i, time: [0, 0], network: { nodes: [], edges: [] } }; - currentSlice.time = [i * timeInterval, (i + 1) * timeInterval]; - currentSlice.network.nodes = network.value.nodes; - slicedNetwork.push(currentSlice); + + // Resets to original network view + if (originalNetwork.value !== null) { + if (timeSliceNumber.value === 1) { + store.commit.setSlicedNetwork(networkToReturn); + store.commit.setNetwork(originalNetwork.value); + return networkToReturn; } + if (originalNetwork.value !== null) { + const slicedNetwork: SlicedNetworks[] = []; + const timeInterval = (selectedRange.value[1] - selectedRange.value[0]) / timeSliceNumber.value; - // Generate sliced network - let i = 0; - network.value.edges.forEach((edge: Edge) => { - if (edge[startTimeVar.value] >= slicedNetwork[i].time[0] && edge[startTimeVar.value] < slicedNetwork[i].time[1]) { - slicedNetwork[i].network.edges.push(edge); - } else if (i < timeSliceNumber.value) { - i += 1; - slicedNetwork[i].network.edges.push(edge); + // Generate time chunks + // eslint-disable-next-line no-plusplus + for (let i = 0; i < timeSliceNumber.value; i++) { + const currentSlice: SlicedNetworks = { slice: i, time: [0, 0], network: { nodes: [], edges: [] } }; + currentSlice.time = [i * timeInterval, (i + 1) * timeInterval]; + currentSlice.network.nodes = originalNetwork.value.nodes; + slicedNetwork.push(currentSlice); } - }); - networkToReturn = slicedNetwork; + // Generate sliced network + let i = 0; + originalNetwork.value.edges.forEach((edge: Edge) => { + if (edge[startTimeVar.value] >= slicedNetwork[i].time[0] && edge[startTimeVar.value] < slicedNetwork[i].time[1]) { + slicedNetwork[i].network.edges.push(edge); + } else if (i < timeSliceNumber.value) { + i += 1; + slicedNetwork[i].network.edges.push(edge); + } + }); + networkToReturn = slicedNetwork; + + store.commit.setSlicedNetwork(networkToReturn); + store.commit.setNetwork(networkToReturn[0].network); + return networkToReturn; + } store.commit.setSlicedNetwork(networkToReturn); - store.commit.setNetwork(networkToReturn[0].network); return networkToReturn; } - store.commit.setSlicedNetwork(networkToReturn); return networkToReturn; } diff --git a/src/store/index.ts b/src/store/index.ts index 61f9172..a7e20e3 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -33,6 +33,7 @@ const { workspaceName: null, networkName: null, network: null, + networkOnLoad: null, columnTypes: null, selectedNodes: new Set(), loadError: { @@ -138,6 +139,10 @@ const { state.network = network; }, + setNetworkOnLoad(state, network: Network) { + state.networkOnLoad = network; + }, + setColumnTypes(state, columnTypes: ColumnTypes) { state.columnTypes = columnTypes; }, @@ -502,6 +507,8 @@ const { edges: edges.results as Edge[], }; commit.setNetwork(networkElements); + // Store origingal copy of network + commit.setNetworkOnLoad(networkElements); const networkTables = await api.networkTables(workspaceName, networkName); // Get the network metadata promises diff --git a/src/types.ts b/src/types.ts index 100eb1f..c08834a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -78,6 +78,7 @@ export interface SlicedNetworks { } export interface State { + networkOnLoad: Network | null; slicedNetwork: SlicedNetworks[]; workspaceName: string | null; networkName: string | null; From 3878903a4e21470e64fb49f209f8f46a74803278 Mon Sep 17 00:00:00 2001 From: derya Date: Thu, 6 Jan 2022 14:31:50 -0700 Subject: [PATCH 12/21] Fix sliceRules to check for type --- src/components/TimeSlicing.vue | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/TimeSlicing.vue b/src/components/TimeSlicing.vue index a83f485..e696bdb 100644 --- a/src/components/TimeSlicing.vue +++ b/src/components/TimeSlicing.vue @@ -10,12 +10,8 @@ import { export default defineComponent({ setup() { const showOptions = ref(false); - // TODO: https://github.com/multinet-app/multidynamic/issues/1 - // Fix slice rules to check for type and show hints - const sliceRules = [ - (value: number) => !!value || 'Required.', - (value: number) => (value && typeof value === 'number') || 'Please type a number', - ]; + // eslint-disable-next-line no-restricted-globals + const sliceRules = (value: string) => !isNaN(parseFloat(value)) || 'Please type a number'; const network = computed(() => store.state.network); const originalNetwork = computed(() => store.state.networkOnLoad); @@ -226,7 +222,7 @@ export default defineComponent({ Date: Thu, 6 Jan 2022 14:40:33 -0700 Subject: [PATCH 13/21] Add names to components --- src/components/TimeLine.vue | 2 ++ src/components/TimeSlicing.vue | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index 2058dc7..e1caf95 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -6,6 +6,8 @@ import { } from '@vue/composition-api'; export default defineComponent({ + name: 'TimeLine', + setup() { const slicedNetwork = computed(() => store.state.slicedNetwork); const svg: Ref = ref(null); diff --git a/src/components/TimeSlicing.vue b/src/components/TimeSlicing.vue index e696bdb..d565b96 100644 --- a/src/components/TimeSlicing.vue +++ b/src/components/TimeSlicing.vue @@ -8,6 +8,8 @@ import { } from '@vue/composition-api'; export default defineComponent({ + name: 'TimeSlicing', + setup() { const showOptions = ref(false); // eslint-disable-next-line no-restricted-globals From 9e85ede758de3097c018cdd5b44762461de42194 Mon Sep 17 00:00:00 2001 From: derya Date: Fri, 7 Jan 2022 21:49:08 -0700 Subject: [PATCH 14/21] Fix drag functionality --- src/components/DragTarget.vue | 13 +++++++++++++ src/components/EdgeBuilder.vue | 8 ++++---- src/components/EdgeBuilderChart.vue | 7 ++++--- src/components/LegendChart.vue | 8 ++++++++ src/store/index.ts | 1 + src/types.ts | 1 + 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/components/DragTarget.vue b/src/components/DragTarget.vue index cfeefbf..a6c7d19 100644 --- a/src/components/DragTarget.vue +++ b/src/components/DragTarget.vue @@ -49,12 +49,25 @@ export default defineComponent({ const updatedEdgeVars = { width: droppedVarName, color: edgeVariables.value.color, + time: edgeVariables.value.time, }; store.commit.setEdgeVariables(updatedEdgeVars); } else if (props.type === 'edge' && props.title === 'color') { const updatedEdgeVars = { width: edgeVariables.value.width, color: droppedVarName, + time: edgeVariables.value.time, + + }; + store.commit.setEdgeVariables(updatedEdgeVars); + } else if (props.type === 'edge' && props.title === 'time') { + // TODO: https://github.com/multinet-app/multidynamic/issues/6 + // Make more elegant edge filter + const updatedEdgeVars = { + width: droppedVarName, + color: edgeVariables.value.color, + time: droppedVarName, + }; store.commit.setEdgeVariables(updatedEdgeVars); } diff --git a/src/components/EdgeBuilder.vue b/src/components/EdgeBuilder.vue index 22678d4..970ae75 100644 --- a/src/components/EdgeBuilder.vue +++ b/src/components/EdgeBuilder.vue @@ -92,17 +92,17 @@ export default defineComponent({ >
diff --git a/src/components/EdgeBuilderChart.vue b/src/components/EdgeBuilderChart.vue index f8104bf..d7b3f60 100644 --- a/src/components/EdgeBuilderChart.vue +++ b/src/components/EdgeBuilderChart.vue @@ -81,10 +81,11 @@ export default defineComponent({ } function unAssignVar() { - if (props.mappedTo === 'width') { + if (props.mappedTo === 'time') { store.commit.setEdgeVariables({ - width: '', + width: store.state.edgeVariables.width, color: store.state.edgeVariables.color, + time: '', }); } } @@ -107,7 +108,7 @@ export default defineComponent({ } // Process data for bars/histogram - if (props.mappedTo === 'width') { + if (props.mappedTo === 'time') { if (isQuantitative(props.varName, props.type)) { yScale = scaleLinear() .domain(edgeWidthScale.value.domain()) diff --git a/src/components/LegendChart.vue b/src/components/LegendChart.vue index 4bdecab..c1dd2e9 100644 --- a/src/components/LegendChart.vue +++ b/src/components/LegendChart.vue @@ -111,11 +111,19 @@ export default defineComponent({ store.commit.setEdgeVariables({ width: '', color: store.state.edgeVariables.color, + time: store.state.edgeVariables.time, }); } else if (props.mappedTo === 'color') { store.commit.setEdgeVariables({ width: store.state.edgeVariables.width, color: '', + time: store.state.edgeVariables.time, + }); + } else if (props.mappedTo === 'time') { + store.commit.setEdgeVariables({ + width: store.state.edgeVariables.width, + color: store.state.edgeVariables.color, + time: '', }); } } diff --git a/src/store/index.ts b/src/store/index.ts index a7e20e3..3e1e54e 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -53,6 +53,7 @@ const { edgeVariables: { width: '', color: '', + time: '', }, nodeSizeVariable: '', nodeColorVariable: '', diff --git a/src/types.ts b/src/types.ts index c08834a..3ae47be 100644 --- a/src/types.ts +++ b/src/types.ts @@ -53,6 +53,7 @@ export interface NestedVariables { export interface EdgeStyleVariables { width: string; color: string; + time: string; } export interface AttributeRange { From a48cc48add04f2d36340c170749012e19e9afda0 Mon Sep 17 00:00:00 2001 From: derya Date: Wed, 12 Jan 2022 12:50:26 -0700 Subject: [PATCH 15/21] Change type name --- src/components/TimeSlicing.vue | 8 ++++---- src/store/index.ts | 4 ++-- src/types.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/TimeSlicing.vue b/src/components/TimeSlicing.vue index d565b96..5b73dec 100644 --- a/src/components/TimeSlicing.vue +++ b/src/components/TimeSlicing.vue @@ -1,7 +1,7 @@