From 57da27c148cfcbbef6785fa578d59a2b6b8b67c8 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 8 Mar 2021 10:31:48 -0800 Subject: [PATCH] Factor the tooltip out of line-chart and into a primitive --- .../components/chart-primitives/tooltip.hbs | 7 ++ ui/app/components/line-chart.js | 73 +++++++++++++------ ui/app/templates/components/line-chart.hbs | 24 +++--- ui/stories/charts/line-chart.stories.js | 32 +++++--- 4 files changed, 93 insertions(+), 43 deletions(-) create mode 100644 ui/app/components/chart-primitives/tooltip.hbs diff --git a/ui/app/components/chart-primitives/tooltip.hbs b/ui/app/components/chart-primitives/tooltip.hbs new file mode 100644 index 000000000..172735192 --- /dev/null +++ b/ui/app/components/chart-primitives/tooltip.hbs @@ -0,0 +1,7 @@ +
+
    + {{#each @data as |props|}} + {{yield props.series props.datum (inc props.index)}} + {{/each}} +
+
diff --git a/ui/app/components/line-chart.js b/ui/app/components/line-chart.js index bd58d5a1c..21271bc7e 100644 --- a/ui/app/components/line-chart.js +++ b/ui/app/components/line-chart.js @@ -68,6 +68,7 @@ export default class LineChart extends Component { @tracked height = 0; @tracked isActive = false; @tracked activeDatum = null; + @tracked activeData = []; @tracked tooltipPosition = null; @tracked element = null; @tracked ready = false; @@ -251,37 +252,65 @@ export default class LineChart extends Component { canvas.on('mouseleave', () => { run.schedule('afterRender', this, () => (this.isActive = false)); this.activeDatum = null; + this.activeData = []; }); } updateActiveDatum(mouseX) { - const { xScale, xProp, yScale, yProp, data } = this; + if (!this.data || !this.data.length) return; - if (!data || !data.length) return; + const { xScale, xProp, yScale, yProp } = this; + let { dataProp, data } = this.args; - // Map the mouse coordinate to the index in the data array - const bisector = d3Array.bisector(d => d[xProp]).left; - const x = xScale.invert(mouseX); - const index = bisector(data, x, 1); - - // The data point on either side of the cursor - const dLeft = data[index - 1]; - const dRight = data[index]; - - let datum; - - // If there is only one point, it's the activeDatum - if (dLeft && !dRight) { - datum = dLeft; - } else { - // Pick the closer point - datum = x - dLeft[xProp] > dRight[xProp] - x ? dRight : dLeft; + if (!dataProp) { + dataProp = 'data'; + data = [{ data: this.data }]; } - this.activeDatum = datum; + // Map screen coordinates to data domain + const bisector = d3Array.bisector(d => d[xProp]).left; + const x = xScale.invert(mouseX); + + // Find the closest datum to the cursor for each series + const activeData = data.map((series, seriesIndex) => { + const dataset = series[dataProp]; + const index = bisector(dataset, x, 1); + + // The data point on either side of the cursor + const dLeft = dataset[index - 1]; + const dRight = dataset[index]; + + let datum; + + // If there is only one point, it's the activeDatum + if (dLeft && !dRight) { + datum = dLeft; + } else { + // Pick the closer point + datum = x - dLeft[xProp] > dRight[xProp] - x ? dRight : dLeft; + } + + // TODO: Preformat numbers + return { series, datum, index: seriesIndex }; + }); + + // Of the selected data, determine which is closest + const closestDatum = activeData.sort( + (a, b) => Math.abs(a.datum[xProp] - x) - Math.abs(b.datum[xProp] - x) + )[0]; + + // If any other selected data are beyond a distance threshold, drop them from the list + // xScale is used here to measure distance in screen-space rather than data-space. + const dist = Math.abs(xScale(closestDatum.datum[xProp]) - mouseX); + const filteredData = activeData.filter( + d => Math.abs(xScale(d.datum[xProp]) - mouseX) < dist + 10 + ); + + this.activeData = filteredData; + this.activeDatum = closestDatum.datum; this.tooltipPosition = { - left: xScale(datum[xProp]), - top: yScale(datum[yProp]) - 10, + left: xScale(this.activeDatum[xProp]), + top: yScale(this.activeDatum[yProp]) - 10, }; } diff --git a/ui/app/templates/components/line-chart.hbs b/ui/app/templates/components/line-chart.hbs index 1ec523bbf..37d8619f0 100644 --- a/ui/app/templates/components/line-chart.hbs +++ b/ui/app/templates/components/line-chart.hbs @@ -45,15 +45,21 @@ prop=this.yProp left=this.canvasDimensions.left width=this.canvasDimensions.width) + Tooltip=(component "chart-primitives/tooltip" + active=this.activeData.length + style=this.tooltipStyle + data=this.activeData) ) to="after"}} {{/if}} -
-

- - - {{this.activeDatumLabel}} - - {{this.activeDatumValue}} -

-
+ {{#unless @dataProp}} +
+

+ + + {{this.activeDatumLabel}} + + {{this.activeDatumValue}} +

+
+ {{/unless}} diff --git a/ui/stories/charts/line-chart.stories.js b/ui/stories/charts/line-chart.stories.js index fa2c95aaa..32e6a1bef 100644 --- a/ui/stories/charts/line-chart.stories.js +++ b/ui/stories/charts/line-chart.stories.js @@ -370,6 +370,14 @@ export let MultiLine = () => ({ {{/each}} + <:after as |c|> + +
  • + {{series.name}} + {{datum.y}} +
  • +
    +

    {{this.activeAnnotation.info}}

    {{/if}} @@ -379,18 +387,6 @@ export let MultiLine = () => ({ data: DelayedArray.create([ { name: 'Series 1', - data: [ - { x: 1, y: 5 }, - { x: 2, y: 1 }, - { x: 3, y: 2 }, - { x: 4, y: 2 }, - { x: 5, y: 9 }, - { x: 6, y: 3 }, - { x: 7, y: 4 }, - ], - }, - { - name: 'Series 2', data: [ { x: 3, y: 7 }, { x: 4, y: 5 }, @@ -401,6 +397,18 @@ export let MultiLine = () => ({ { x: 9, y: 6 }, ], }, + { + name: 'Series 2', + data: [ + { x: 1, y: 5 }, + { x: 2, y: 1 }, + { x: 3, y: 2 }, + { x: 4, y: 2 }, + { x: 5, y: 9 }, + { x: 6, y: 3 }, + { x: 7, y: 4 }, + ], + }, ]), }, });