diff --git a/demo/demo.js b/demo/demo.js index 3ea3bd75d..daea3d85d 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -4284,6 +4284,103 @@ d3.select(".chart_area") } } }, + TooltipPosition: [ + { + options: { + data: { + columns: [ + ["data1", 30, 200, 200, 400, 150, 250] + ], + type: "area" + }, + padding: { + top: 35 + }, + axis: { + x: { + padding: { + left: 15, + right: 15, + unit: "px" + } + }, + y2: { + show: true + } + }, + tooltip: { + position: function(data, width, height, element, pos) { + // when has single dataseries, 'pos.yAxis' is number value + return { + top: pos.yAxis - (height + 10), + left: pos.xAxis - (width / 2) + }; + } + } + } + }, + { + options: { + data: { + columns: [ + ["data1", 30, 200, 200, 400, 150, 250], + ["data2", 130, 100, 100, 200, 150, 50], + ["data3", 230, 200, 200, 300, 250, 250] + ], + type: "bar", + groups: [ + ["data1", "data2", "data3"] + ] + }, + axis: { + rotated: false, + y2: { + show: true + } + }, + tooltip: { + position: function(data, width, height, element, pos) { + const total = data.reduce((a, {value}) => a + value, 0); + + // when has multiple dataseries, 'pos.yAxis' is function + return this.config("axis.rotated") ? { + top: pos.xAxis - (width / 2), + left: pos.yAxis(total) + } : { + top: pos.yAxis(total) - height, + left: pos.xAxis - (width / 2) + }; + } + } + } + }, + { + options: { + data: { + columns: [ + ["data1", 30, 200, 200, 400, 150, 250] + ], + type: "bar" + }, + padding: { + right: 80 + }, + axis: { + rotated: true + }, + tooltip: { + position: function(data, width, height, element, pos) { + const total = data.reduce((a, {value}) => a + value, 0); + + return { + top: pos.xAxis - (height / 2), + left: pos.yAxis + 10 + } + } + } + } + } + ], TooltipTemplate: { options: { data: { diff --git a/src/ChartInternal/internals/tooltip.ts b/src/ChartInternal/internals/tooltip.ts index 95298c343..d27b7357f 100644 --- a/src/ChartInternal/internals/tooltip.ts +++ b/src/ChartInternal/internals/tooltip.ts @@ -259,25 +259,45 @@ export default { * @param {SVGElement} eventTarget Event element * @private */ - setTooltipPosition(dataToShow: IDataRow, eventTarget: SVGElement): void { + setTooltipPosition(dataToShow: IDataRow[], eventTarget: SVGElement): void { const $$ = this; const {config, scale, state, $el: {eventRect, tooltip}} = $$; const {bindto} = config.tooltip_contents; + const isRotated = config.axis_rotated; const datum = tooltip?.datum(); if (!bindto && datum) { + const data = dataToShow ?? JSON.parse(datum.current); const [x, y] = getPointer(state.event, eventTarget ?? eventRect?.node()); // get mouse event position - const currPos: {x: number, y: number, xAxis?: number} = {x, y}; + const currPos: { + x: number, y: number, xAxis?: number, yAxis?: number | ( + (value: number, id?: string, axisId?: string) => number + )} = {x, y}; + + if (state.hasAxis && scale.x && datum && "x" in datum) { + const getYPos = (value = 0, id?: string, axisId = "y"): number => { + const scaleFn = scale[id ? $$.axis?.getId(id) : axisId]; + + return scaleFn ? scaleFn(value) + (isRotated ? state.margin.left : state.margin.top) : 0; + }; - if (scale.x && datum && "x" in datum) { - currPos.xAxis = scale.x(datum.x); + currPos.xAxis = scale.x(datum.x) + ( + // add margin only when user specified tooltip.position function + config.tooltip_position ? (isRotated ? state.margin.top : state.margin.left) : 0 + ); + + if (data.length === 1) { + currPos.yAxis = getYPos(data[0].value as number, data[0].id); + } else { + currPos.yAxis = getYPos; + } } const {width = 0, height = 0} = datum; // Get tooltip position const pos = config.tooltip_position?.bind($$.api)( - dataToShow ?? JSON.parse(datum.current), + data, width, height, eventRect?.node(), currPos ) ?? $$.getTooltipPosition.bind($$)(width, height, currPos); diff --git a/src/config/Options/common/tooltip.ts b/src/config/Options/common/tooltip.ts index 0927472c1..2be5d8ef3 100644 --- a/src/config/Options/common/tooltip.ts +++ b/src/config/Options/common/tooltip.ts @@ -80,6 +80,7 @@ export default { * @see [Demo: Tooltip Grouping](https://naver.github.io/billboard.js/demo/#Tooltip.TooltipGrouping) * @see [Demo: Tooltip Format](https://naver.github.io/billboard.js/demo/#Tooltip.TooltipFormat) * @see [Demo: Linked Tooltip](https://naver.github.io/billboard.js/demo/#Tooltip.LinkedTooltips) + * @see [Demo: Tooltip Position](https://naver.github.io/billboard.js/demo/#Tooltip.TooltipPosition) * @see [Demo: Tooltip Template](https://naver.github.io/billboard.js/demo/#Tooltip.TooltipTemplate) * @example * tooltip: { @@ -103,8 +104,31 @@ export default { * // x: Current mouse event x position, * // y: Current mouse event y position, * // xAxis: Current x Axis position (the value is given for axis based chart type only) + * // yAxis: Current y Axis position value or function(the value is given for axis based chart type only) * // } - * return {top: 0, left: 0} + * + * // yAxis will work differently per data lenghts + * // - a) Single data: `yAxis` will return `number` value + * // - b) Multiple data: `yAxis` will return a function with property value + * + * // a) Single data: + * // Get y coordinate + * pos.yAxis; // y axis coordinate value of current data point + * + * // b) Multiple data: + * // Get y coordinate of value 500, where 'data1' scales(y or y2). + * // When 'data.axes' option is used, data can bound to different axes. + * // - when "data.axes={data1: 'y'}", wil return y value from y axis scale. + * // - when "data.axes={data1: 'y2'}", wil return y value from y2 axis scale. + * pos.yAxis(500, "data1"); // will return y coordinate value of data1 + * + * pos.yAxis(500); // get y coordinate with value of 500, using y axis scale + * pos.yAxis(500, null, "y2"); // get y coordinate with value of 500, using y2 axis scale + * + * return { + * top: 0, + * left: 0 + * } * }, * * contents: function(d, defaultTitleFormat, defaultValueFormat, color) { diff --git a/test/internals/tooltip-spec.ts b/test/internals/tooltip-spec.ts index b22e8770c..7d8fe1ad9 100644 --- a/test/internals/tooltip-spec.ts +++ b/test/internals/tooltip-spec.ts @@ -312,8 +312,7 @@ describe("TOOLTIP", function() { describe("do not overlap data point", () => { it("should show tooltip on proper position", () => { - const tooltip = chart.$.tooltip; - const circles = chart.$.circles; + const {circles, tooltip} = chart.$; const getCircleRectX = x => circles.filter(`.${$SHAPE.shape}-${x}`) .node().getBoundingClientRect().x; @@ -838,10 +837,40 @@ describe("TOOLTIP", function() { }); it("set option tooltip.position", () => { + args.data.axes = { + data3: "y2" + }; + args.axis = { + y2: { + show: true + } + } + args.tooltip.position = function(data, width, height, element, pos) { + const {scale: {y, y2}, state: {margin}} = this.internal; + expect(pos.x).to.be.equal(99.5); expect(pos.y).to.be.equal(100.5); - expect(pos.xAxis).to.be.equal(this.internal.scale.x(data[0].x)); + + expect(pos.xAxis).to.be.equal( + this.internal.scale.x(data[0].x) + margin.left + ); + + data.forEach(({id, value}) => { + const isY2 = id === "data3"; + const scale = isY2 ? y2 : y; + + expect(pos.yAxis(value, id)).to.be.equal( + scale(value) + this.internal.state.margin.top + ); + + if (isY2) { + expect(y2(value) + margin.top).to.be.equal(pos.yAxis(value, null, "y2")); + } else { + expect(y(value) + margin.top).to.be.equal(pos.yAxis(value, null, "y")); + } + + }) return { top: 50, left: 300 @@ -866,6 +895,10 @@ describe("TOOLTIP", function() { done(); }, 200); }); + + it("", () => { + + }) }); describe("tooltip order", () => { diff --git a/types/options.d.ts b/types/options.d.ts index c3bb55d72..d9fee0791 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -598,6 +598,7 @@ export interface TooltipOptions { x: number; y: number; xAxis?: number; + yAxis?: number | ((value: number, id?: string, axisId?: string) => number); } ) => { top: number; left: number });