From 1389d8597e56a690d01f469a160c6b0bff8df1f1 Mon Sep 17 00:00:00 2001 From: Jae Sung Park Date: Fri, 21 Oct 2022 12:01:36 +0900 Subject: [PATCH] fix(interaction): Fix incorrect data.onover/out event call Fix onover/onout event call on same x axis bar shape. Fix #2901 --- src/ChartInternal/data/IData.ts | 10 ++- src/ChartInternal/interactions/interaction.ts | 45 ++++++---- test/interactions/interaction-spec.ts | 83 ++++++++++++++++++- 3 files changed, 116 insertions(+), 22 deletions(-) diff --git a/src/ChartInternal/data/IData.ts b/src/ChartInternal/data/IData.ts index 8dff9c350..0ef164f70 100644 --- a/src/ChartInternal/data/IData.ts +++ b/src/ChartInternal/data/IData.ts @@ -2,12 +2,18 @@ * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */ -export interface IDataRow { - x: number; +type TDataRow = { value: number | null; id: string; index: number; name?: string; +}; + +export interface IDataRow extends TDataRow { + x: number; +} +export interface IArcDataRow extends TDataRow { + ratio: number; } export interface IData { diff --git a/src/ChartInternal/interactions/interaction.ts b/src/ChartInternal/interactions/interaction.ts index 2382c2a85..14c9e3f8f 100644 --- a/src/ChartInternal/interactions/interaction.ts +++ b/src/ChartInternal/interactions/interaction.ts @@ -7,6 +7,7 @@ import {drag as d3Drag} from "d3-drag"; import {$ARC, $AXIS, $COMMON, $SHAPE} from "../../config/classes"; import {KEY} from "../../module/Cache"; import {emulateEvent, getPointer, isNumber, isObject} from "../../module/util"; +import type {IArcDataRow} from "../data/IData"; export default { selectRectForSingle(context, eventRect, index: number): void { @@ -107,42 +108,52 @@ export default { * @param {number|object} d data object * @private */ - setOverOut(isOver: boolean, d): void { + setOverOut(isOver: boolean, d: number | IArcDataRow): void { const $$ = this; const {config, state: {hasRadar}, $el: {main}} = $$; const isArc = isObject(d); // Call event handler if (isArc || d !== -1) { - let callback = config[isOver ? "data_onover" : "data_onout"].bind($$.api); + const callback = config[isOver ? "data_onover" : "data_onout"].bind($$.api); config.color_onover && $$.setOverColor(isOver, d, isArc); - if (isArc) { - callback(d, main.select(`.${$ARC.arc}${$$.getTargetSelectorSuffix(d.id)}`).node()); + if (isArc && "id") { + callback(d, main.select(`.${$ARC.arc}${$$.getTargetSelectorSuffix((d as IArcDataRow).id)}`).node()); } else if (!config.tooltip_grouped) { - let last = $$.cache.get(KEY.setOverOut) || []; + const last = $$.cache.get(KEY.setOverOut) || []; - const shape = main.selectAll(`.${$SHAPE.shape}-${d}`) + // select based on the index + const shapesAtIndex = main.selectAll(`.${$SHAPE.shape}-${d}`) .filter(function(d) { return $$.isWithinShape(this, d); }); - shape - .each(function(d) { - if (last.length === 0 || last.every(v => v !== this)) { - callback(d, this); - last.push(this); - } - }); + // filter if has new selection + const shape = shapesAtIndex.filter(function() { + return last.every(v => v !== this); + }); - if (last.length > 0 && shape.empty()) { - callback = config.data_onout.bind($$.api); + // call onout callback + if (!isOver || shapesAtIndex.empty() || ( + last.length === shape.size() && shape.nodes().every(((v, i) => v !== last[i])) + )) { + while (last.length) { + const target = last.pop(); - last.forEach(v => callback(d3Select(v).datum(), v)); - last = []; + config.data_onout.bind($$.api)(d3Select(target).datum(), target); + } } + // call onover callback + shape.each(function() { + if (isOver) { + callback(d3Select(this).datum(), this); + last.push(this); + } + }); + $$.cache.add(KEY.setOverOut, last); } else { if (isOver) { diff --git a/test/interactions/interaction-spec.ts b/test/interactions/interaction-spec.ts index 7ec553b8e..2328513ca 100644 --- a/test/interactions/interaction-spec.ts +++ b/test/interactions/interaction-spec.ts @@ -3,13 +3,13 @@ * billboard.js project is licensed under the MIT license */ /* eslint-disable */ +// @ts-nocheck /* global describe, beforeEach, it, expect */ import {expect} from "chai"; import sinon from "sinon"; import {select as d3Select} from "d3-selection"; import util from "../assets/util"; import {$ARC, $AXIS, $BAR, $CIRCLE, $COMMON, $FOCUS, $EVENT, $SELECT, $SHAPE} from "../../src/config/classes"; -import bb from "../../src"; describe("INTERACTION", () => { let chart; @@ -479,13 +479,90 @@ describe("INTERACTION", () => { expect(v.d.x).to.be.equal(index); expect(v.element.tagName).to.be.equal("circle"); - expect(itemOut[i].d).to.be.deep.equal(v.d); - expect(itemOut[i].element).to.be.deep.equal(v.element); + expect(itemOut.some(t => JSON.stringify(t) === JSON.stringify(v))).to.be.true; + expect(itemOut.some(t => t.element === v.element)).to.be.true; }); done(); }, 500); }); + + it("set options", () => { + args = { + data: { + columns: [ + ["data1", 30, 200, 200, 130, 150, 250], + ["data2", 130, 100, 140, 150, 150, 50], + ["data3", 130, 100, 140, 220, 150, 50] + ], + groups: [ + ["data1", "data2"] + ], + type: "bar", + onover: spyOver, + onout: spyOut + }, + bar: { + sensitivity: 0 + }, + tooltip: { + show: false, + grouped: false, + } + }; + }); + + it("callback should called correctly on same x Axis for bar type.", done => { + new Promise(resolve => { + util.hoverChart(chart, "mousemove", { + clientX: 360, + clientY: 300 + }); + + setTimeout(resolve, 300); + }).then(() => { + new Promise(resolve => { + util.hoverChart(chart, "mousemove", { + clientX: 340, + clientY: 300 + }); + + setTimeout(resolve, 300); + }); + }).then(() => { + new Promise(resolve => { + util.hoverChart(chart, "mousemove", { + clientX: 340, + clientY: 200 + }); + + setTimeout(resolve, 300); + }); + }) + .then(() => { + const expectedX = 3; + const expectedFlow = { + over: ["data3", "data2", "data1"], + out: ["data3", "data2"] + }; + + itemOver + .map(({d: {x, id}}) => ({x, id})) + .forEach((v, i) => { + expect(v.x).to.be.equal(expectedX); + expect(expectedFlow.over[i]).to.be.equal(v.id); + }); + + itemOut + .map(({d: {x, id}}) => ({x, id})) + .forEach((v, i) => { + expect(v.x).to.be.equal(expectedX); + expect(expectedFlow.out[i]).to.be.equal(v.id); + }); + + done(); + }); + }); }); describe("check for data.onclick", () => {