Skip to content

Commit a77a63b

Browse files
authored
Bugfix: return nearest non-null point on interaction when spanGaps=true (#11986)
* First step in fixing the bug of spanGaps null point interaction * Complete bugfix of spanGaps null point interaction * Add two tests in core.interaction.tests for the bugfix change * Remove odd line break * Use isNullOrUndef helper for point value checks * Add 10 more test cases for nearest interaction when spanGaps=true
1 parent 1e3d6e5 commit a77a63b

File tree

3 files changed

+115
-4
lines changed

3 files changed

+115
-4
lines changed

src/core/core.interaction.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {_lookupByKey, _rlookupByKey} from '../helpers/helpers.collection.js';
22
import {getRelativePosition} from '../helpers/helpers.dom.js';
33
import {_angleBetween, getAngleFromPoint} from '../helpers/helpers.math.js';
4-
import {_isPointInArea} from '../helpers/index.js';
4+
import {_isPointInArea, isNullOrUndef} from '../helpers/index.js';
55

66
/**
77
* @typedef { import('./core.controller.js').default } Chart
@@ -22,10 +22,30 @@ import {_isPointInArea} from '../helpers/index.js';
2222
function binarySearch(metaset, axis, value, intersect) {
2323
const {controller, data, _sorted} = metaset;
2424
const iScale = controller._cachedMeta.iScale;
25+
const spanGaps = metaset.dataset ? metaset.dataset.options ? metaset.dataset.options.spanGaps : null : null;
26+
2527
if (iScale && axis === iScale.axis && axis !== 'r' && _sorted && data.length) {
2628
const lookupMethod = iScale._reversePixels ? _rlookupByKey : _lookupByKey;
2729
if (!intersect) {
28-
return lookupMethod(data, axis, value);
30+
const result = lookupMethod(data, axis, value);
31+
if (spanGaps) {
32+
const {vScale} = controller._cachedMeta;
33+
const {_parsed} = metaset;
34+
35+
const distanceToDefinedLo = (_parsed
36+
.slice(0, result.lo + 1)
37+
.reverse()
38+
.findIndex(
39+
point => !isNullOrUndef(point[vScale.axis])));
40+
result.lo -= Math.max(0, distanceToDefinedLo);
41+
42+
const distanceToDefinedHi = (_parsed
43+
.slice(result.hi - 1)
44+
.findIndex(
45+
point => !isNullOrUndef(point[vScale.axis])));
46+
result.hi += Math.max(0, distanceToDefinedHi);
47+
}
48+
return result;
2949
} else if (controller._sharedOptions) {
3050
// _sharedOptions indicates that each element has equal options -> equal proportions
3151
// So we can do a ranged binary search based on the range of first element and

src/helpers/helpers.extras.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {ChartMeta, PointElement} from '../types/index.js';
22

33
import {_limitValue} from './helpers.math.js';
44
import {_lookupByKey} from './helpers.collection.js';
5+
import {isNullOrUndef} from './helpers.core.js';
56

67
export function fontString(pixelSize: number, fontStyle: string, fontFamily: string) {
78
return fontStyle + ' ' + pixelSize + 'px ' + fontFamily;
@@ -107,7 +108,7 @@ export function _getStartAndCountOfVisiblePoints(meta: ChartMeta<'line' | 'scatt
107108
.slice(0, start + 1)
108109
.reverse()
109110
.findIndex(
110-
point => point[vScale.axis] || point[vScale.axis] === 0));
111+
point => !isNullOrUndef(point[vScale.axis])));
111112
start -= Math.max(0, distanceToDefinedLo);
112113
}
113114
start = _limitValue(start, 0, pointCount - 1);
@@ -122,7 +123,7 @@ export function _getStartAndCountOfVisiblePoints(meta: ChartMeta<'line' | 'scatt
122123
const distanceToDefinedHi = (_parsed
123124
.slice(end - 1)
124125
.findIndex(
125-
point => point[vScale.axis] || point[vScale.axis] === 0));
126+
point => !isNullOrUndef(point[vScale.axis])));
126127
end += Math.max(0, distanceToDefinedHi);
127128
}
128129
count = _limitValue(end, start, pointCount) - start;

test/specs/core.interaction.tests.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,4 +912,94 @@ describe('Core.Interaction', function() {
912912
expect(elements).toContain(firstElement);
913913
});
914914
});
915+
916+
const testCases = [
917+
{
918+
data: [12, 19, null, null, null, null, 5, 2],
919+
clickPointIndex: 0,
920+
expectedNearestPointIndex: 0
921+
},
922+
{
923+
data: [12, 19, null, null, null, null, 5, 2],
924+
clickPointIndex: 1,
925+
expectedNearestPointIndex: 1},
926+
{
927+
data: [12, 19, null, null, null, null, 5, 2],
928+
clickPointIndex: 2,
929+
expectedNearestPointIndex: 1
930+
},
931+
{
932+
data: [12, 19, null, null, null, null, 5, 2],
933+
clickPointIndex: 3,
934+
expectedNearestPointIndex: 1
935+
},
936+
{
937+
data: [12, 19, null, null, null, null, 5, 2],
938+
clickPointIndex: 4,
939+
expectedNearestPointIndex: 6
940+
},
941+
{
942+
data: [12, 19, null, null, null, null, 5, 2],
943+
clickPointIndex: 5,
944+
expectedNearestPointIndex: 6
945+
},
946+
{
947+
data: [12, 19, null, null, null, null, 5, 2],
948+
clickPointIndex: 6,
949+
expectedNearestPointIndex: 6
950+
},
951+
{
952+
data: [12, 19, null, null, null, null, 5, 2],
953+
clickPointIndex: 7,
954+
expectedNearestPointIndex: 7
955+
},
956+
{
957+
data: [12, 0, null, null, null, null, 0, 2],
958+
clickPointIndex: 3,
959+
expectedNearestPointIndex: 1
960+
},
961+
{
962+
data: [12, 0, null, null, null, null, 0, 2],
963+
clickPointIndex: 4,
964+
expectedNearestPointIndex: 6
965+
},
966+
{
967+
data: [12, -1, null, null, null, null, -1, 2],
968+
clickPointIndex: 3,
969+
expectedNearestPointIndex: 1
970+
},
971+
{
972+
data: [12, -1, null, null, null, null, -1, 2],
973+
clickPointIndex: 4,
974+
expectedNearestPointIndex: 6
975+
}
976+
];
977+
testCases.forEach(({data, clickPointIndex, expectedNearestPointIndex}, i) => {
978+
it(`should select nearest non-null element with index ${expectedNearestPointIndex} when clicking on element with index ${clickPointIndex} in test case ${i + 1} if spanGaps=true`, function() {
979+
const chart = window.acquireChart({
980+
type: 'line',
981+
data: {
982+
labels: [1, 2, 3, 4, 5, 6, 7, 8, 9],
983+
datasets: [{
984+
data: data,
985+
spanGaps: true,
986+
}]
987+
}
988+
});
989+
chart.update();
990+
const meta = chart.getDatasetMeta(0);
991+
const point = meta.data[clickPointIndex];
992+
993+
const evt = {
994+
type: 'click',
995+
chart: chart,
996+
native: true, // needed otherwise things its a DOM event
997+
x: point.x,
998+
y: point.y,
999+
};
1000+
1001+
const elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'x', intersect: false}).map(item => item.element);
1002+
expect(elements).toEqual([meta.data[expectedNearestPointIndex]]);
1003+
});
1004+
});
9151005
});

0 commit comments

Comments
 (0)