diff --git a/packages/demo/examples/01-xy-chart/index.jsx b/packages/demo/examples/01-xy-chart/index.jsx index 1d478970..0613a1fd 100644 --- a/packages/demo/examples/01-xy-chart/index.jsx +++ b/packages/demo/examples/01-xy-chart/index.jsx @@ -406,6 +406,7 @@ export default { ariaLabel="Required label" xScale={{ type: 'band' }} yScale={{ type: 'linear' }} + eventTrigger="container" > 0) { event.persist(); const args = { event, datum: closestDatum, series }; diff --git a/packages/xy-chart/src/utils/findClosestDatum.js b/packages/xy-chart/src/utils/findClosestDatum.js index 7b8e0169..b394b4ca 100644 --- a/packages/xy-chart/src/utils/findClosestDatum.js +++ b/packages/xy-chart/src/utils/findClosestDatum.js @@ -15,12 +15,9 @@ export default function findClosestDatum({ data, getX, xScale, event }) { // Ordinal scales don't have an invert function so we do it maually const xDomain = xScale.domain(); const scaledXValues = xDomain.map(val => xScale(val)); - const index = Math.min( - scaledXValues.length - 1, - d3BisectLeft(scaledXValues, mouseX), - ); + const index = d3BisectLeft(scaledXValues, mouseX); const d0 = data[index - 1]; - const d1 = data[index] || {}; + const d1 = data[index]; d = d0 || d1; } else { const dataX = xScale.invert(mouseX); diff --git a/packages/xy-chart/src/utils/findClosestDatums.js b/packages/xy-chart/src/utils/findClosestDatums.js index 0c5efdd0..d9235c71 100644 --- a/packages/xy-chart/src/utils/findClosestDatums.js +++ b/packages/xy-chart/src/utils/findClosestDatums.js @@ -25,17 +25,15 @@ export default function findClosestDatums({ Children.forEach(children, (Child, childIndex) => { const { disableMouseEvents, data, seriesKey } = Child.props; if (isSeries(componentName(Child)) && !disableMouseEvents) { - const adjustedGetX = xScale.invert ? getX : d => getX(d) + ((xScale.barWidth || 0) / 2); - // @TODO data should be sorted, come up with a way to enforce+cache instead of relying on user const datum = findClosestDatum({ data, - getX: adjustedGetX, + getX, xScale, event, }); - const deltaX = Math.abs(xScale(adjustedGetX(datum || {})) - mouseX); + const deltaX = Math.abs(xScale(getX(datum || {})) - mouseX); if (datum && deltaX <= maxXDistancePx) { const key = seriesKey || childIndex; // fall back to child index diff --git a/packages/xy-chart/test/utils/findClosestDatum.test.js b/packages/xy-chart/test/utils/findClosestDatum.test.js new file mode 100644 index 00000000..92344cd0 --- /dev/null +++ b/packages/xy-chart/test/utils/findClosestDatum.test.js @@ -0,0 +1,81 @@ +import scaleLinear from '@vx/scale/build/scales/linear'; +import scaleBand from '@vx/scale/build/scales/band'; +import findClosestDatum from '../../src/utils/findClosestDatum'; + +describe('findClosestDatum', () => { + beforeAll(() => { + // mock prototype attributes for vx's localPoint + Element.prototype.getBoundingClientRect = jest.fn(() => ({ + width: 10, + height: 10, + top: 0, + left: 0, + bottom: 0, + right: 0, + })); + + Element.prototype.clientLeft = 0; + Element.prototype.clientTop = 0; + }); + + const node = document.createElement('g'); + const event = { + // missing clientX + clientY: 0, + target: { + ownerSVGElement: { + firstChild: node, + }, + }, + }; + + test('it should be defined', () => { + expect(findClosestDatum).toBeDefined(); + }); + + test('it should return the closest datum', () => { + const props = { + data: [{ x: 0 }, { x: 5 }, { x: 10 }], + getX: d => d.x, + xScale: scaleLinear({ domain: [0, 10], range: [0, 10] }), + }; + + expect(findClosestDatum({ + ...props, + event: { ...event, clientX: 1 }, + })).toBe(props.data[0]); + + expect(findClosestDatum({ + ...props, + event: { ...event, clientX: 6 }, + })).toBe(props.data[1]); + + expect(findClosestDatum({ + ...props, + event: { ...event, clientX: 9 }, + })).toBe(props.data[2]); + }); + + test('it should work for ordinal scales', () => { + const props = { + data: [{ x: 'a' }, { x: 'b' }, { x: 'c' }], + getX: d => d.x, + xScale: scaleBand({ domain: ['a', 'b', 'c'], range: [0, 10] }), + }; + + expect(findClosestDatum({ + ...props, + event: { ...event, clientX: 0 }, + })).toBe(props.data[0]); + + expect(findClosestDatum({ + ...props, + event: { ...event, clientX: 5 }, + })).toBe(props.data[1]); + + expect(findClosestDatum({ + ...props, + event: { ...event, clientX: 10 }, + })).toBe(props.data[2]); + }); +});