Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(scatter): multi-tooltip doesn't show on hover for overlapping data points #652

Merged
merged 18 commits into from
Jun 12, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 5 additions & 20 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,23 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline

## [0.32.1](https://github.com/carbon-design-system/carbon-charts/compare/v0.32.0...v0.32.1) (2020-06-03)


### Bug Fixes

* fix missing title option in charts interface ([b74d658](https://github.com/carbon-design-system/carbon-charts/commit/b74d6582277632c8a2a1f460f3ce73aadd40e500))




- fix missing title option in charts interface ([b74d658](https://github.com/carbon-design-system/carbon-charts/commit/b74d6582277632c8a2a1f460f3ce73aadd40e500))

# [0.32.0](https://github.com/carbon-design-system/carbon-charts/compare/v0.30.24...v0.32.0) (2020-05-29)


### Bug Fixes

* **core:** threshold - support non JS-date values ([a132497](https://github.com/carbon-design-system/carbon-charts/commit/a1324972fa5266151b490cb7eeed92a92da5b2c4))
* **svelte:** copy svelte source to dist/src ([492a504](https://github.com/carbon-design-system/carbon-charts/commit/492a50470d2b64793bd2c67c4115bb2732bc44f7))




- **core:** threshold - support non JS-date values ([a132497](https://github.com/carbon-design-system/carbon-charts/commit/a1324972fa5266151b490cb7eeed92a92da5b2c4))
- **svelte:** copy svelte source to dist/src ([492a504](https://github.com/carbon-design-system/carbon-charts/commit/492a50470d2b64793bd2c67c4115bb2732bc44f7))

# [0.31.0](https://github.com/carbon-design-system/carbon-charts/compare/v0.30.24...v0.31.0) (2020-05-29)


### Bug Fixes

* **core:** threshold - support non JS-date values ([a132497](https://github.com/carbon-design-system/carbon-charts/commit/a1324972fa5266151b490cb7eeed92a92da5b2c4))
* **svelte:** copy svelte source to dist/src ([492a504](https://github.com/carbon-design-system/carbon-charts/commit/492a50470d2b64793bd2c67c4115bb2732bc44f7))




- **core:** threshold - support non JS-date values ([a132497](https://github.com/carbon-design-system/carbon-charts/commit/a1324972fa5266151b490cb7eeed92a92da5b2c4))
- **svelte:** copy svelte source to dist/src ([492a504](https://github.com/carbon-design-system/carbon-charts/commit/492a50470d2b64793bd2c67c4115bb2732bc44f7))

## [0.30.24](https://github.com/carbon-design-system/carbon-charts/compare/v0.30.23...v0.30.24) (2020-05-15)

Expand Down
66 changes: 33 additions & 33 deletions packages/core/demo/data/area.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
export const areaTimeSeriesData = [
{ group: "Dataset 1", date: new Date(2019, 0, 1), value: 0 },
{ group: "Dataset 1", date: new Date(2019, 0, 6), value: 57312 },
{ group: "Dataset 1", date: new Date(2019, 0, 8), value: 21432 },
{ group: "Dataset 1", date: new Date(2019, 0, 15), value: 70323 },
{ group: "Dataset 1", date: new Date(2019, 0, 19), value: 21300 },
{ group: "Dataset 2", date: new Date(2019, 0, 1), value: 50000 },
{ group: "Dataset 2", date: new Date(2019, 0, 5), value: 15000 },
{ group: "Dataset 2", date: new Date(2019, 0, 8), value: 20000 },
{ group: "Dataset 2", date: new Date(2019, 0, 13), value: 39213 },
{ group: "Dataset 2", date: new Date(2019, 0, 19), value: 61213 },
{ group: "Dataset 3", date: new Date(2019, 0, 2), value: 10 },
{ group: "Dataset 3", date: new Date(2019, 0, 6), value: 37312 },
{ group: "Dataset 3", date: new Date(2019, 0, 8), value: 51432 },
{ group: "Dataset 3", date: new Date(2019, 0, 13), value: 40323 },
{ group: "Dataset 3", date: new Date(2019, 0, 19), value: 31300 }
{ group: "Dataset 1", date: new Date(2019, 0, 1), value: 0 },
{ group: "Dataset 1", date: new Date(2019, 0, 6), value: 57312 },
{ group: "Dataset 1", date: new Date(2019, 0, 8), value: 21432 },
{ group: "Dataset 1", date: new Date(2019, 0, 15), value: 70323 },
{ group: "Dataset 1", date: new Date(2019, 0, 19), value: 21300 },
{ group: "Dataset 2", date: new Date(2019, 0, 1), value: 50000 },
{ group: "Dataset 2", date: new Date(2019, 0, 5), value: 15000 },
{ group: "Dataset 2", date: new Date(2019, 0, 8), value: 20000 },
{ group: "Dataset 2", date: new Date(2019, 0, 13), value: 39213 },
{ group: "Dataset 2", date: new Date(2019, 0, 19), value: 61213 },
{ group: "Dataset 3", date: new Date(2019, 0, 2), value: 10 },
{ group: "Dataset 3", date: new Date(2019, 0, 6), value: 37312 },
{ group: "Dataset 3", date: new Date(2019, 0, 8), value: 51432 },
{ group: "Dataset 3", date: new Date(2019, 0, 13), value: 40323 },
{ group: "Dataset 3", date: new Date(2019, 0, 19), value: 31300 },
];

export const areaTimeSeriesOptions = {
Expand All @@ -22,27 +22,27 @@ export const areaTimeSeriesOptions = {
bottom: {
title: "2019 Annual Sales Figures",
mapsTo: "date",
scaleType: "time"
scaleType: "time",
},
left: {
mapsTo: "value",
title: "Conversion rate",
scaleType: "linear"
}
}
scaleType: "linear",
},
},
};

export const areaTimeSeriesCurvedData = [
{ group: "Dataset 1", date: new Date(2019, 0, 1), value: 0 },
{ group: "Dataset 1", date: new Date(2019, 0, 6), value: -37312 },
{ group: "Dataset 1", date: new Date(2019, 0, 8), value: -22392 },
{ group: "Dataset 1", date: new Date(2019, 0, 15), value: -52576 },
{ group: "Dataset 1", date: new Date(2019, 0, 19), value: 20135 },
{ group: "Dataset 2", date: new Date(2019, 0, 1), value: 47263 },
{ group: "Dataset 2", date: new Date(2019, 0, 5), value: 14178 },
{ group: "Dataset 2", date: new Date(2019, 0, 8), value: 23094 },
{ group: "Dataset 2", date: new Date(2019, 0, 13), value: 45281 },
{ group: "Dataset 2", date: new Date(2019, 0, 19), value: -63954 }
{ group: "Dataset 1", date: new Date(2019, 0, 1), value: 0 },
{ group: "Dataset 1", date: new Date(2019, 0, 6), value: -37312 },
{ group: "Dataset 1", date: new Date(2019, 0, 8), value: -22392 },
{ group: "Dataset 1", date: new Date(2019, 0, 15), value: -52576 },
{ group: "Dataset 1", date: new Date(2019, 0, 19), value: 20135 },
{ group: "Dataset 2", date: new Date(2019, 0, 1), value: 47263 },
{ group: "Dataset 2", date: new Date(2019, 0, 5), value: 14178 },
{ group: "Dataset 2", date: new Date(2019, 0, 8), value: 23094 },
{ group: "Dataset 2", date: new Date(2019, 0, 13), value: 45281 },
{ group: "Dataset 2", date: new Date(2019, 0, 19), value: -63954 },
];

export const areaTimeSeriesCurvedOptions = {
Expand All @@ -51,12 +51,12 @@ export const areaTimeSeriesCurvedOptions = {
bottom: {
title: "2019 Annual Sales Figures",
mapsTo: "date",
scaleType: "time"
scaleType: "time",
},
left: {
mapsTo: "value",
scaleType: "linear"
}
scaleType: "linear",
},
},
curve: "curveNatural"
curve: "curveNatural",
};
2 changes: 1 addition & 1 deletion packages/core/demo/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ export const storybookDemoGroups = Tools.clone(allDemoGroups);
// in the demo page we want to show only demos with isDemoExample = true
export const demoGroups = Tools.clone(allDemoGroups)
.map((demoGroup) => {
demoGroup.demos = demoGroup.demos.filter(demo => demo.isDemoExample);
demoGroup.demos = demoGroup.demos.filter((demo) => demo.isDemoExample);
return demoGroup;
})
.filter((demoGroup) => demoGroup.demos.length); // remove demoGroup if it's children are all with isDemoExample = false
4 changes: 2 additions & 2 deletions packages/core/demo/data/line.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { timeFormat } from "d3";

export const lineData = [
{ group: "Dataset 1", key: "Qty", value: 32100 },
{ group: "Dataset 1", key: "Qty", value: 34200 },
{ group: "Dataset 1", key: "More", value: 23500 },
{ group: "Dataset 1", key: "Sold", value: 53100 },
{ group: "Dataset 1", key: "Restocking", value: 42300 },
Expand Down Expand Up @@ -44,7 +44,7 @@ export const lineOptions = {
};

export const lineTimeSeriesData = [
{ group: "Dataset 1", date: new Date(2019, 0, 1), value: 10000 },
{ group: "Dataset 1", date: new Date(2019, 0, 1), value: 50000 },
{ group: "Dataset 1", date: new Date(2019, 0, 5), value: 65000 },
{ group: "Dataset 1", date: new Date(2019, 0, 8), value: null },
{ group: "Dataset 1", date: new Date(2019, 0, 13), value: 49213 },
Expand Down
3 changes: 2 additions & 1 deletion packages/core/demo/data/scatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const doubleLinearScatterData = [
{ group: "Dataset 1", employees: 3000, sales: 25100 },
{ group: "Dataset 1", employees: 8000, sales: 12100 },
{ group: "Dataset 1", employees: 4000, sales: 53100 },
{ group: "Dataset 2", employees: 5000, sales: 32100 },
{ group: "Dataset 2", employees: 2000, sales: 34100 },
{ group: "Dataset 2", employees: 4000, sales: 23100 },
{ group: "Dataset 2", employees: 7000, sales: 14100 },
Expand All @@ -28,7 +29,7 @@ export const doubleLinearScatterOptions = {
};

export const scatterDiscreteData = [
{ group: "Dataset 1", key: "Qty", value: 32100 },
{ group: "Dataset 1", key: "Qty", value: 34200 },
{ group: "Dataset 1", key: "More", value: 23500 },
{ group: "Dataset 1", key: "Sold", value: 53100 },
{ group: "Dataset 1", key: "Restocking", value: 42300 },
Expand Down
39 changes: 21 additions & 18 deletions packages/core/src/components/graphs/area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export class Area extends Component {
type = "area";

init() {

const eventsFragment = this.services.events;

// Highlight correct area on legend item hovers
Expand All @@ -31,48 +30,52 @@ export class Area extends Component {
const { cartesianScales } = this.services;

const orientation = cartesianScales.getOrientation();
const areaGenerator = area()
.curve(this.services.curves.getD3Curve());
const areaGenerator = area().curve(this.services.curves.getD3Curve());

if (orientation === CartesianOrientations.VERTICAL) {
areaGenerator.x((d, i) => cartesianScales.getDomainValue(d, i))
areaGenerator
.x((d, i) => cartesianScales.getDomainValue(d, i))
.y0(cartesianScales.getRangeValue(0))
.y1((d, i) => cartesianScales.getRangeValue(d, i));
} else {
areaGenerator.x0(cartesianScales.getRangeValue(0))
areaGenerator
.x0(cartesianScales.getRangeValue(0))
.x1((d, i) => cartesianScales.getRangeValue(d, i))
.y((d, i) => cartesianScales.getDomainValue(d, i));
}

// Update the bound data on area groups
const groupedData = this.model.getGroupedData();
const areas = svg.selectAll("path.area")
.data(groupedData, group => group.name);
const areas = svg
.selectAll("path.area")
.data(groupedData, (group) => group.name);

// Remove elements that need to be exited
// We need exit at the top here to make sure that
// Data filters are processed before entering new elements
// Or updating existing ones
areas.exit()
.attr("opacity", 0)
.remove();
areas.exit().attr("opacity", 0).remove();

const self = this;

// Enter paths that need to be introduced
const enteringAreas = areas.enter()
.append("path")
.attr("opacity", 0);
const enteringAreas = areas.enter().append("path").attr("opacity", 0);

// Apply styles and datum
enteringAreas.merge(areas)
.attr("fill", group => {
return this.model.getFillColor(group.name)
enteringAreas
.merge(areas)
.attr("fill", (group) => {
return this.model.getFillColor(group.name);
})
.transition(this.services.transitions.getTransition("area-update-enter", animate))
.transition(
this.services.transitions.getTransition(
"area-update-enter",
animate
)
)
.attr("opacity", Configuration.area.opacity.selected)
.attr("class", "area")
.attr("d", group => {
.attr("d", (group) => {
const { data } = group;
return areaGenerator(data);
});
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/graphs/line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class Line extends Component {
// Apply styles and datum
enteringLines
.merge(lines)
.attr("stroke", group => {
.attr("stroke", (group) => {
return this.model.getStrokeColor(group.name);
})
// a11y
Expand Down
110 changes: 103 additions & 7 deletions packages/core/src/components/graphs/scatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@ import { TooltipTypes, Roles, Events } from "../../interfaces";
import { Tools } from "../../tools";

// D3 Imports
import { select, Selection, event as d3Event } from "d3-selection";
import { mouse, select, Selection, event as d3Event } from "d3-selection";

const THRESHOLD = 5;

// Check if x and y are inside threshold area extents
function pointIsWithinThreshold(dx: number, dy: number, x: number, y: number) {
return (
dx > x - THRESHOLD &&
dx < x + THRESHOLD &&
dy > y - THRESHOLD &&
dy < y + THRESHOLD
);
}

export class Scatter extends Component {
type = "scatter";
Expand Down Expand Up @@ -251,6 +263,96 @@ export class Scatter extends Component {
)
);

// Find all the data points that are placed within the threshold along x and y axis.
const [x, y] = mouse(self.parent.node());
const displayData = self.model.getDisplayData();
const scaledData: {
domainValue: number;
rangeValue: number;
originalData: any;
}[] = displayData.map((d) => ({
domainValue: self.services.cartesianScales.getDomainValue(
d
),
rangeValue: self.services.cartesianScales.getRangeValue(d),
originalData: d,
}));

const dataPointsWithinThreshold: {
domainValue: number;
rangeValue: number;
originalData: any;
}[] = scaledData
.filter((d) =>
pointIsWithinThreshold(
d.domainValue,
d.rangeValue,
x,
y
)
)
.reduce((accum, currentValue) => {
if (accum.length === 0) {
accum.push(currentValue);
return accum;
}

// Store the first element of the accumulator array to compare it with current element being processed.
const xAccumValue = accum[0].domainValue;
const yAccumValue = accum[0].rangeValue;
const xDistanceToCurrentValue = Math.abs(
x - currentValue.domainValue
);
const yDistanceToCurrentValue = Math.abs(
y - currentValue.rangeValue
);
const xDistanceToAccumValue = Math.abs(x - xAccumValue);
const yDistanceToAccumValue = Math.abs(y - yAccumValue);

if (
xDistanceToCurrentValue > xDistanceToAccumValue ||
yDistanceToCurrentValue > yDistanceToAccumValue
) {
// If distance with current value is bigger than already existing value in the accumulator,
// skip current iteration.
return accum;
} else if (
xDistanceToCurrentValue < xDistanceToAccumValue ||
yDistanceToCurrentValue < yDistanceToAccumValue
) {
// CurrentValue data point is closer to mouse inside the threshold area, so reinstantiate array.
accum = [currentValue];
} else {
// CurrentValue is equal to already stored values,
// which means there's another match on the same coordinate.
accum.push(currentValue);
}

return accum;
}, []);

if (dataPointsWithinThreshold.length > 0) {
const rangeIdentifier = self.services.cartesianScales.getRangeIdentifier();
const tooltipData = dataPointsWithinThreshold
.map((d) => d.originalData)
.filter((d) => {
const value = d[rangeIdentifier];
return value !== null && value !== undefined;
});
// Show tooltip
self.services.events.dispatchEvent(Events.Tooltip.SHOW, {
hoveredElement,
multidata: tooltipData,
type: TooltipTypes.DATAPOINT,
});
} else {
// Show tooltip
self.services.events.dispatchEvent(Events.Tooltip.SHOW, {
hoveredElement,
type: TooltipTypes.DATAPOINT,
});
}

navjeets marked this conversation as resolved.
Show resolved Hide resolved
const eventNameToDispatch =
d3Event.type === "mouseover"
? Events.Scatter.SCATTER_MOUSEOVER
Expand All @@ -260,12 +362,6 @@ export class Scatter extends Component {
element: hoveredElement,
datum,
});

// Show tooltip
self.services.events.dispatchEvent(Events.Tooltip.SHOW, {
hoveredElement,
type: TooltipTypes.DATAPOINT,
});
})
.on("click", function (datum) {
// Dispatch mouse event
Expand Down
Loading