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; + }