Factor the tooltip out of line-chart and into a primitive

This commit is contained in:
Michael Lange 2021-03-08 10:31:48 -08:00
parent 391aef9c4d
commit 498d9d2b36
4 changed files with 93 additions and 43 deletions

View File

@ -0,0 +1,7 @@
<div class="chart-tooltip {{if @active "active" "inactive"}}" style={{@style}} ...attributes>
<ol>
{{#each @data as |props|}}
{{yield props.series props.datum (inc props.index)}}
{{/each}}
</ol>
</div>

View File

@ -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,
};
}

View File

@ -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}}
<div class="chart-tooltip is-snappy {{if this.isActive "active" "inactive"}}" style={{this.tooltipStyle}}>
<p>
<span class="label">
<span class="color-swatch {{this.chartClass}}" />
{{this.activeDatumLabel}}
</span>
<span class="value">{{this.activeDatumValue}}</span>
</p>
</div>
{{#unless @dataProp}}
<div class="chart-tooltip is-snappy {{if this.isActive "active" "inactive"}}" style={{this.tooltipStyle}}>
<p>
<span class="label">
<span class="color-swatch {{this.chartClass}}" />
{{this.activeDatumLabel}}
</span>
<span class="value">{{this.activeDatumValue}}</span>
</p>
</div>
{{/unless}}
</div>

View File

@ -370,6 +370,14 @@ export let MultiLine = () => ({
<c.Area @data={{series.data}} @colorScale="reds" @index={{idx}} />
{{/each}}
</:svg>
<:after as |c|>
<c.Tooltip class="is-snappy" as |series datum index|>
<li>
<span class="label"><span class="color-swatch swatch-reds swatch-reds-{{index}}" />{{series.name}}</span>
<span class="value">{{datum.y}}</span>
</li>
</c.Tooltip>
</:after>
</LineChart>
<p>{{this.activeAnnotation.info}}</p>
{{/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 },
],
},
]),
},
});