From 2207e1fd7f8acaf0326ac2321f37f0d02b6ee351 Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Tue, 28 Nov 2023 07:23:08 -1000 Subject: [PATCH 01/13] Initial pass at adding a toggle to show mode changes as a line chart --- src/components/timeline/LayerLine.svelte | 2 +- src/components/timeline/LayerXRange.svelte | 225 ++++++++++++------ src/components/timeline/Row.svelte | 2 + src/components/timeline/RowYAxes.svelte | 156 +++++++----- .../form/TimelineEditorLayerSettings.svelte | 11 + src/types/timeline.ts | 1 + src/utilities/generic.ts | 17 +- src/utilities/timeline.ts | 34 ++- 8 files changed, 312 insertions(+), 136 deletions(-) diff --git a/src/components/timeline/LayerLine.svelte b/src/components/timeline/LayerLine.svelte index 0e05edb2c3..f095e1cac1 100644 --- a/src/components/timeline/LayerLine.svelte +++ b/src/components/timeline/LayerLine.svelte @@ -83,7 +83,7 @@ const [yAxis] = yAxes.filter(axis => yAxisId === axis.id); const domain = yAxis?.scaleDomain || []; - yScale = getYScale(domain, drawHeight); + const yScale = getYScale(domain, drawHeight) as ScaleLinear; ctx.lineWidth = lineWidth; ctx.strokeStyle = lineColor; diff --git a/src/components/timeline/LayerXRange.svelte b/src/components/timeline/LayerXRange.svelte index 9f966f5aec..58dba346fc 100644 --- a/src/components/timeline/LayerXRange.svelte +++ b/src/components/timeline/LayerXRange.svelte @@ -2,7 +2,7 @@ From 4cb930fd90712d744580f373b19e800b56034568 Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Tue, 12 Dec 2023 09:12:03 -1000 Subject: [PATCH 08/13] Added totalWidth sum back and fixed more issues with latest merge --- src/components/timeline/LayerLine.svelte | 24 ++++++++++++++++-------- src/components/timeline/RowYAxes.svelte | 3 ++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/timeline/LayerLine.svelte b/src/components/timeline/LayerLine.svelte index 6a49e7af6b..dd407fd110 100644 --- a/src/components/timeline/LayerLine.svelte +++ b/src/components/timeline/LayerLine.svelte @@ -2,7 +2,7 @@ From 783df6aeadc2139f7a591f5366accff4f3529bcb Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Tue, 19 Dec 2023 07:16:26 -1000 Subject: [PATCH 09/13] Fixed an issue where multiple axis were not being shown --- src/components/timeline/RowYAxes.svelte | 156 +++++++++++++----------- 1 file changed, 84 insertions(+), 72 deletions(-) diff --git a/src/components/timeline/RowYAxes.svelte b/src/components/timeline/RowYAxes.svelte index 45eda5d4f9..7eaa4d82b5 100644 --- a/src/components/timeline/RowYAxes.svelte +++ b/src/components/timeline/RowYAxes.svelte @@ -18,6 +18,7 @@ const dispatch = createEventDispatcher(); let g: SVGGElement; + let xRangeAxisDrawn: boolean; $: if (drawHeight && g && yAxes && resourcesByViewLayerId && layers) { draw(); @@ -33,16 +34,18 @@ let marginWidth = 0; const axisClass = 'y-axis'; gSelection.selectAll(`.${axisClass}`).remove(); + // TODO: Use this flag to only draw the xRange axis once, can be removed when the TODO below is resolved. + xRangeAxisDrawn = false; for (let i = 0; i < yAxes.length; ++i) { const axis = yAxes[i]; const xRangeLayers = layers.filter(layer => layer.yAxisId === axis.id && layer.chartType === 'x-range'); - const axisG = gSelection.append('g').attr('class', axisClass); - axisG.selectAll('*').remove(); - if (xRangeLayers.length === 1) { + if (xRangeLayers.length === 1 && !xRangeAxisDrawn) { const layer = xRangeLayers[0] as XRangeLayer; const resources = resourcesByViewLayerId[layer.id]; + const xRangeAxisG = gSelection.append('g').attr('class', axisClass); + xRangeAxisG.selectAll('*').remove(); /** * TODO: This is a temporary solution to showing state mode changes as a line chart. @@ -69,81 +72,82 @@ const axisMargin = 2; const startPosition = -(totalWidth + axisMargin * i); marginWidth += i > 0 ? axisMargin : 0; - axisG.attr('transform', `translate(${startPosition}, 0)`); - axisG.style('color', axis.color); - axisG.call(axisLeft); - axisG.call(g => g.select('.domain').remove()); - } - } else { - // Get color for axis by examining associated layers. If more than one layer is associated, - // use the default axis color, otherwise use the color from the layer. - // TODO we don't expose y-axis color and this refactor would elimate need to store it in view. - // That is unless we want to allow user override of this behavior? - let color = axis.color; - const yAxisLayers = layers.filter(layer => layer.yAxisId === axis.id && layer.chartType === 'line'); - if (yAxisLayers.length === 1) { - color = (yAxisLayers[0] as LineLayer).lineColor; - } - - // TODO deprecate these view properties? - // const labelColor = axis.label?.color || 'black'; - // const labelFontFace = axis.label?.fontFace || 'sans-serif'; - // const labelFontSize = axis.label?.fontSize || 12; - // const labelText = axis.label.text; - const tickCount = axis.tickCount || 1; - if ( - tickCount > 0 && - axis.scaleDomain && - axis.scaleDomain.length === 2 && - typeof axis.scaleDomain[0] === 'number' && - typeof axis.scaleDomain[1] === 'number' - ) { - const domain = axis.scaleDomain; - const scale = getYScale(domain, drawHeight); - const axisLeft = d3AxisLeft(scale) - .tickSizeInner(0) - .tickSizeOuter(0) - .ticks(tickCount) - .tickFormat(n => { - // Format -1 to 1 as normal numbers instead of m (milli) which d3 - // does out of the box to align with various standards but which can be - // commonly confused for M (million). - const number = n as number; - if (number > -1 && number < 1) { - return d3Format('.2r')(n); - } - return d3Format('~s')(n); - }) - .tickPadding(2); + xRangeAxisG.attr('transform', `translate(${startPosition}, 0)`); + xRangeAxisG.style('color', axis.color); + xRangeAxisG.call(axisLeft); + xRangeAxisG.call(g => g.select('.domain').remove()); - const axisMargin = 2; - const startPosition = -(totalWidth + axisMargin * i); - marginWidth += i > 0 ? axisMargin : 0; - axisG.attr('transform', `translate(${startPosition}, 0)`); - axisG.style('color', color); - if (domain.length === 2 && domain[0] !== null && domain[1] !== null) { - axisG.call(axisLeft); - axisG.call(g => g.select('.domain').remove()); - } + totalWidth += getBoundingClientRectWidth(xRangeAxisG.node()); + xRangeAxisDrawn = true; } + } + + const axisG = gSelection.append('g').attr('class', axisClass); + axisG.selectAll('*').remove(); - // Draw separator - axisG - .append('line') - .attr('x1', 2) - .attr('y1', 0) - .attr('x2', 2) - .attr('y2', drawHeight) - .style('stroke', '#EBECEC') - .style('stroke-width', 2); + // Get color for axis by examining associated layers. If more than one layer is associated, + // use the default axis color, otherwise use the color from the layer. + // TODO we don't expose y-axis color and this refactor would elimate need to store it in view. + // That is unless we want to allow user override of this behavior? + let color = axis.color; + const yAxisLayers = layers.filter(layer => layer.yAxisId === axis.id && layer.chartType === 'line'); + if (yAxisLayers.length === 1) { + color = (yAxisLayers[0] as LineLayer).lineColor; } - const axisGElement: SVGGElement | null = axisG.node(); - if (axisGElement !== null) { - // TODO might be able to save minor perf by getting bounding rect of entire - // container instead of each individual axis? - totalWidth += axisGElement.getBoundingClientRect().width; + // TODO deprecate these view properties? + // const labelColor = axis.label?.color || 'black'; + // const labelFontFace = axis.label?.fontFace || 'sans-serif'; + // const labelFontSize = axis.label?.fontSize || 12; + // const labelText = axis.label.text; + const tickCount = axis.tickCount || 1; + if ( + tickCount > 0 && + axis.scaleDomain && + axis.scaleDomain.length === 2 && + typeof axis.scaleDomain[0] === 'number' && + typeof axis.scaleDomain[1] === 'number' + ) { + const domain = axis.scaleDomain; + const scale = getYScale(domain, drawHeight); + const axisLeft = d3AxisLeft(scale) + .tickSizeInner(0) + .tickSizeOuter(0) + .ticks(tickCount) + .tickFormat(n => { + // Format -1 to 1 as normal numbers instead of m (milli) which d3 + // does out of the box to align with various standards but which can be + // commonly confused for M (million). + const number = n as number; + if (number > -1 && number < 1) { + return d3Format('.2r')(n); + } + return d3Format('~s')(n); + }) + .tickPadding(2); + + const axisMargin = 2; + const startPosition = -(totalWidth + axisMargin * i); + marginWidth += i > 0 ? axisMargin : 0; + axisG.attr('transform', `translate(${startPosition}, 0)`); + axisG.style('color', color); + if (domain.length === 2 && domain[0] !== null && domain[1] !== null) { + axisG.call(axisLeft); + axisG.call(g => g.select('.domain').remove()); + } } + + // Draw separator + axisG + .append('line') + .attr('x1', 2) + .attr('y1', 0) + .attr('x2', 2) + .attr('y2', drawHeight) + .style('stroke', '#EBECEC') + .style('stroke-width', 2); + + totalWidth += getBoundingClientRectWidth(axisG.node()); } totalWidth += marginWidth; @@ -151,6 +155,14 @@ dispatch('updateYAxesWidth', totalWidth); } } + + function getBoundingClientRectWidth(axisG: SVGGElement | null): number { + if (axisG !== null) { + return axisG.getBoundingClientRect().width + 4; + } + + return 0; + } From bfe37dba9038f19a19fe3f19af93c0284f715d02 Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Tue, 19 Dec 2023 07:21:57 -1000 Subject: [PATCH 10/13] Messed up the rebase... --- src/components/timeline/LayerXRange.svelte | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/components/timeline/LayerXRange.svelte b/src/components/timeline/LayerXRange.svelte index f263b1cdcc..9e1c2e9d38 100644 --- a/src/components/timeline/LayerXRange.svelte +++ b/src/components/timeline/LayerXRange.svelte @@ -143,7 +143,7 @@ if (textWidth < xWidth) { ctx.fillText(labelText, xStart + xWidth / 2 - textWidth / 2, drawHeight / 2 + textHeight / 2, textWidth); } else { - const extraLabelPadding = 10; + const extraLabelPadding = 8; let newLabelText = labelText; let newTextWidth = textWidth; @@ -154,7 +154,6 @@ newTextWidth = textMeasurement.textWidth; } -<<<<<<< HEAD // Only draw if text will be visible if (newTextWidth > 0) { ctx.fillText( @@ -164,14 +163,6 @@ newTextWidth, ); } -======= - ctx.fillText( - `${newLabelText}...`, - xStart + xWidth / 2 - newTextWidth / 2, - drawHeight / 2 + textHeight / 2, - newTextWidth, - ); ->>>>>>> ebee5695 (Moved state mode changes out of LayerXRange and into LayerLine) } } } From 2c570d4878d3aee7f6f9dc715ac47ccfa6643625 Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Tue, 19 Dec 2023 10:38:38 -1000 Subject: [PATCH 11/13] Fixed another issue when drawing multiple xRange layers inside of a single row --- src/components/timeline/RowYAxes.svelte | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/timeline/RowYAxes.svelte b/src/components/timeline/RowYAxes.svelte index 7eaa4d82b5..5763c9afb4 100644 --- a/src/components/timeline/RowYAxes.svelte +++ b/src/components/timeline/RowYAxes.svelte @@ -18,7 +18,6 @@ const dispatch = createEventDispatcher(); let g: SVGGElement; - let xRangeAxisDrawn: boolean; $: if (drawHeight && g && yAxes && resourcesByViewLayerId && layers) { draw(); @@ -34,25 +33,21 @@ let marginWidth = 0; const axisClass = 'y-axis'; gSelection.selectAll(`.${axisClass}`).remove(); - // TODO: Use this flag to only draw the xRange axis once, can be removed when the TODO below is resolved. - xRangeAxisDrawn = false; for (let i = 0; i < yAxes.length; ++i) { const axis = yAxes[i]; const xRangeLayers = layers.filter(layer => layer.yAxisId === axis.id && layer.chartType === 'x-range'); + const xRangeAxisG = gSelection.append('g').attr('class', axisClass); + xRangeAxisG.selectAll('*').remove(); - if (xRangeLayers.length === 1 && !xRangeAxisDrawn) { - const layer = xRangeLayers[0] as XRangeLayer; + for (const layer of xRangeLayers) { const resources = resourcesByViewLayerId[layer.id]; - const xRangeAxisG = gSelection.append('g').attr('class', axisClass); - xRangeAxisG.selectAll('*').remove(); - /** * TODO: This is a temporary solution to showing state mode changes as a line chart. * The correct way to do this would be generating a Y axes when the user toggles the line chart, * but for now we're just setting the Y axes dynamically based on the data. */ - if (layer.showAsLinePlot && resources && resources.length > 0) { + if ((layer as XRangeLayer).showAsLinePlot && resources && resources.length > 0) { let domain: string[] = []; // Get all the unique ordinal values of the chart. @@ -78,7 +73,6 @@ xRangeAxisG.call(g => g.select('.domain').remove()); totalWidth += getBoundingClientRectWidth(xRangeAxisG.node()); - xRangeAxisDrawn = true; } } From 51506436d522161844f82d6e4505d192dc293935 Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Tue, 19 Dec 2023 13:50:12 -1000 Subject: [PATCH 12/13] Removed link between xrange layer and axis --- src/components/timeline/RowYAxes.svelte | 104 +++++++++++++----------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/src/components/timeline/RowYAxes.svelte b/src/components/timeline/RowYAxes.svelte index 5763c9afb4..ac8bb5446e 100644 --- a/src/components/timeline/RowYAxes.svelte +++ b/src/components/timeline/RowYAxes.svelte @@ -3,7 +3,7 @@