diff --git a/src/ChartInternal/interactions/interaction.ts b/src/ChartInternal/interactions/interaction.ts index 34e9dacaa..53a546464 100644 --- a/src/ChartInternal/interactions/interaction.ts +++ b/src/ChartInternal/interactions/interaction.ts @@ -180,6 +180,7 @@ export default { if (element) { const isMultipleX = $$.isMultipleX(); + const isRotated = config.axis_rotated; let {width, left, top} = element.getBoundingClientRect(); if (hasAxis && !hasRadar && !isMultipleX) { @@ -197,9 +198,11 @@ export default { } const x = left + (mouse ? mouse[0] : 0) + ( - isMultipleX || config.axis_rotated ? 0 : (width / 2) + isMultipleX || isRotated ? 0 : (width / 2) ); - const y = top + (mouse ? mouse[1] : 0); + + // value 4, is to adjust coordinate value set from: scale.ts - updateScales(): $$.getResettedPadding(1) + const y = top + (mouse ? mouse[1] : 0) + (isRotated ? 4 : 0); const params = { screenX: x, screenY: y, diff --git a/src/ChartInternal/internals/size.ts b/src/ChartInternal/internals/size.ts index 7c8c6c155..b6e1cbb7d 100644 --- a/src/ChartInternal/internals/size.ts +++ b/src/ChartInternal/internals/size.ts @@ -4,7 +4,7 @@ */ import {document} from "../../module/browser"; import {$AXIS, $SUBCHART} from "../../config/classes"; -import {ceil10, capitalize, isNumber, isEmpty, isUndefined} from "../../module/util"; +import {ceil10, capitalize, isNumber, isEmpty, isString, isUndefined} from "../../module/util"; export default { /** @@ -87,13 +87,26 @@ export default { getSvgLeft(withoutRecompute?: boolean): number { const $$ = this; const {config, $el} = $$; - const hasLeftAxisRect = config.axis_rotated || (!config.axis_rotated && !config.axis_y_inner); - const leftAxisClass = config.axis_rotated ? $AXIS.axisX : $AXIS.axisY; + const isRotated = config.axis_rotated; + const hasLeftAxisRect = isRotated || (!isRotated && !config.axis_y_inner); + const leftAxisClass = isRotated ? $AXIS.axisX : $AXIS.axisY; const leftAxis = $el.main.select(`.${leftAxisClass}`).node(); + const leftLabel = config[`axis_${isRotated ? "x" : "y"}_label`]; + let labelWidth = 0; + + // if axis label position set to inner, exclude from the value + if (isString(leftLabel) || isString(leftLabel.text) || /^inner-/.test(leftLabel?.position)) { + const label = $el.main.select(`.${leftAxisClass}-label`); + + if (!label.empty()) { + labelWidth = label.node().getBoundingClientRect().left; + } + } + const svgRect = leftAxis && hasLeftAxisRect ? leftAxis.getBoundingClientRect() : {right: 0}; - const chartRect = $el.chart.node().getBoundingClientRect(); + const chartRectLeft = $el.chart.node().getBoundingClientRect().left + labelWidth; const hasArc = $$.hasArcType(); - const svgLeft = svgRect.right - chartRect.left - + const svgLeft = svgRect.right - chartRectLeft - (hasArc ? 0 : $$.getCurrentPaddingByDirection("left", withoutRecompute)); return svgLeft > 0 ? svgLeft : 0; @@ -204,7 +217,7 @@ export default { padding += isRotated ? ( !isFitPadding && isUndefined(paddingOption) ? 10 : 2 ) : !isAxisShow || isAxisInner ? (isFitPadding ? 2 : 1) : 0; - } else if (type === "left" && isRotated) { + } else if (type === "left" && isRotated && isUndefined(paddingOption)) { padding = !config.axis_x_show ? 1 : (isFitPadding ? axisSize : Math.max(axisSize, 40)); } diff --git a/src/ChartInternal/internals/tooltip.ts b/src/ChartInternal/internals/tooltip.ts index d27b7357f..5bb6eb7ff 100644 --- a/src/ChartInternal/internals/tooltip.ts +++ b/src/ChartInternal/internals/tooltip.ts @@ -330,34 +330,39 @@ export default { const hasGauge = $$.hasType("gauge") && !config.gauge_fullCircle; const hasTreemap = state.hasTreemap; const isRotated = config.axis_rotated; + const hasArcType = $$.hasArcType(); const svgLeft = $$.getSvgLeft(true); let chartRight = svgLeft + current.width - $$.getCurrentPaddingByDirection("right"); - const chartLeft = $$.getCurrentPaddingByDirection("left", true); const size = 20; let {x, y} = currPos; // Determine tooltip position - if ($$.hasArcType()) { + if (hasArcType) { const raw = inputType === "touch" || $$.hasType("radar"); if (!raw) { - y += hasGauge ? height : height / 2; x += (width - (isLegendRight ? $$.getLegendWidth() : 0)) / 2; + y += hasGauge ? height : height / 2; } } else if (!hasTreemap) { + const padding = { + top: $$.getCurrentPaddingByDirection("top", true), + left: $$.getCurrentPaddingByDirection("left", true) + }; + if (isRotated) { - y = currPos.xAxis + size; - x += svgLeft; + x += svgLeft + padding.left + size; + y = padding.top + currPos.xAxis + size; chartRight -= svgLeft; } else { - y -= 5; - x = svgLeft + chartLeft + size + (scale.zoom ? x : currPos.xAxis); + x = svgLeft + padding.left + size + (scale.zoom ? x : currPos.xAxis); + y += padding.top - 5; } } // when tooltip left + tWidth > chart's width if ((x + tWidth + 15) > chartRight) { - x -= isRotated ? tWidth - chartLeft : tWidth + (hasTreemap ? 0 : chartLeft); + x -= tWidth + (hasTreemap || hasArcType ? 0 : (isRotated ? size * 2 : 38)); } if (y + tHeight > current.height) { diff --git a/src/config/Options/axis/x.ts b/src/config/Options/axis/x.ts index 4f2b12ea0..c61f34f7b 100644 --- a/src/config/Options/axis/x.ts +++ b/src/config/Options/axis/x.ts @@ -120,7 +120,7 @@ export default { * @memberof Options * @type {Function|string} * @default undefined - * @see [D3's time specifier](https://github.com/d3/d3-time-format#locale_format) + * @see [D3's time specifier](https://d3js.org/d3-time-format#locale_format) * @example * axis: { * x: { diff --git a/src/config/Options/data/axis.ts b/src/config/Options/data/axis.ts index 4e8425cc0..63c3d13f8 100644 --- a/src/config/Options/data/axis.ts +++ b/src/config/Options/data/axis.ts @@ -46,7 +46,7 @@ export default { * type: "timeseries" * } * } - * @see [D3's time specifier](https://github.com/d3/d3-time-format#locale_format) + * @see [D3's time specifier](https://d3js.org/d3-time-format#locale_format) */ data_xFormat: "%Y-%m-%d", diff --git a/test/api/tooltip-spec.ts b/test/api/tooltip-spec.ts index a8423d20e..42afe7d89 100644 --- a/test/api/tooltip-spec.ts +++ b/test/api/tooltip-spec.ts @@ -363,4 +363,76 @@ describe("API tooltip", () => { expect(tooltip.select(".value").text()).to.be.equal("34.6%"); }); }); + + describe("on rotated axis", () => { + before(() => { + args = { + data: { + columns: [ + ["data1", 150, 140, 110, 100, 300], + ["data1", 30, 200, 100, 400, 150], + ["data2", 130, 340, 200, 500, 250] + ], + type: "line" + }, + axis: { + rotated: true + } + }; + }); + + function checkTooltip(x, categoryName?) { + const type = chart.config("axis.x.type"); + let value = x; + + // when + chart.tooltip.show({x}); + + let th = chart.$.tooltip.select("th").text(); + + if (type === "timeseries") { + value = chart.internal.format.xAxisTick(x); + } + + expect(isNaN(th) ? th : +th).to.be.equal(categoryName ?? value); + } + + it("check for indexed x axis type", () => { + chart.xs().data1.forEach(v => { + checkTooltip(v); + }); + }); + + it("set options", () => { + args.data.x = "x"; + args.data.columns.unshift( + ["x", "2023-01-01", "2023-01-02", "2023-01-03", "2023-01-04", "2023-01-05"] + ); + args.axis.x = { + type: "timeseries", + tick: { + format: "%Y-%m-%d" + } + }; + }); + + it("check for timeseries x axis type", () => { + chart.xs().data1.forEach(v => { + checkTooltip(+v); + }); + }); + + it("set options", () => { + args.data.columns[0] = ["x", "a", "b", "c", "d", "e"]; + args.axis.x = { + type: "category" + }; + }); + + it("check for category x axis type", () => { + chart.categories().forEach((v, i) => { + checkTooltip(i, v); + }); + }); + }); }); diff --git a/test/internals/text-spec.ts b/test/internals/text-spec.ts index 805dc6b9c..a63ac16b1 100644 --- a/test/internals/text-spec.ts +++ b/test/internals/text-spec.ts @@ -838,7 +838,7 @@ describe("TEXT", () => { }); it("should locate labels above each data point", () => { - const expectedXs = [72, 537, 72, 527]; // 72.50132230092231 + const expectedXs = [72, 527, 72, 527]; // 72.50132230092231 const expectedYs = [9, 157, 305, 434]; chart.$.main.selectAll(`.${$TEXT.texts}-data1 text`) diff --git a/test/internals/tooltip-spec.ts b/test/internals/tooltip-spec.ts index 7d8fe1ad9..f76ce74cf 100644 --- a/test/internals/tooltip-spec.ts +++ b/test/internals/tooltip-spec.ts @@ -11,7 +11,7 @@ import { namespaces as d3Namespaces } from "d3-selection"; import util from "../assets/util"; -import {$SHAPE, $TOOLTIP} from "../../src/config/classes"; +import {$CIRCLE, $SHAPE, $TOOLTIP} from "../../src/config/classes"; import {isNumber, isUndefined, isString} from "../../src/module/util"; describe("TOOLTIP", function() { @@ -667,6 +667,135 @@ describe("TOOLTIP", function() { }); + describe("with padding", () => { + before(() => { + args = { + data: { + columns: [ + ["data1", 30, 200, 100, 400, 150, 250], + ["data2", 130, 340, 200, 500, 250, 350] + ], + type: "line" + }, + padding: { + left: 400 + }, + axis: { + rotated: false, + y: { + label: "y axis label" + } + } + } + }); + + it("should displayed in correct position with padding.left", () => { + const x = 4; + + // when + chart.tooltip.show({x}); + + const tooltip = chart.$.tooltip.node().getBoundingClientRect(); + const pointLeft = chart.$.circles.filter(`.${$CIRCLE.circle}-${x}`).node().getBoundingClientRect().left; + + expect(tooltip.left + tooltip.width).to.be.below(pointLeft); + expect(tooltip.left + tooltip.width).to.be.closeTo(pointLeft, 15); + }); + + it("set options: axis.y.label", () => { + args.axis.y.label = { + text: "y axis label" + }; + }); + + it("should displayed in correct position with padding.left", () => { + const x = 4; + + // when + chart.tooltip.show({x}); + + const tooltip = chart.$.tooltip.node().getBoundingClientRect(); + const pointLeft = chart.$.circles.filter(`.${$CIRCLE.circle}-${x}`).node().getBoundingClientRect().left; + + expect(tooltip.left + tooltip.width).to.be.below(pointLeft); + expect(tooltip.left + tooltip.width).to.be.closeTo(pointLeft, 15); + }); + + it("set options: axis.y.label", () => { + args.axis.y.label = { + text: "y axis label", + position: "inner-middle" + }; + }); + + it("should displayed in correct position with padding.left", () => { + const x = 4; + + // when + chart.tooltip.show({x}); + + const tooltip = chart.$.tooltip.node().getBoundingClientRect(); + const pointLeft = chart.$.circles.filter(`.${$CIRCLE.circle}-${x}`).node().getBoundingClientRect().left; + + expect(tooltip.left + tooltip.width).to.be.below(pointLeft); + expect(tooltip.left + tooltip.width).to.be.closeTo(pointLeft, 15); + }); + + it("set options: axis.rotated=true", () => { + args.axis.rotated = true; + }); + + it("should displayed in correct position with padding.left on rotated axis", () => { + const x = 2; + + // when + util.hoverChart(chart, "mousemove", {clientX: 0, clientY: 200}); + + const tooltip = chart.$.tooltip.node().getBoundingClientRect(); + const pointLeft = chart.$.circles.filter(`.${$CIRCLE.circle}-${x}`).node().getBoundingClientRect().left; + + expect(pointLeft).to.be.below(tooltip.left + tooltip.width); + expect(tooltip.left).to.be.closeTo(pointLeft, 30); + }); + + it("set options: axis.rotated=true", () => { + args.axis.rotated = false; + args.padding = { + top: 100 + }; + }); + + it("should displayed in correct position with padding.top", () => { + const x = 3; + + // when + chart.tooltip.show({x}); + + const tooltip = chart.$.tooltip.node().getBoundingClientRect(); + const pointTop = chart.$.circles.filter(`.${$CIRCLE.circle}-${x}`).node().getBoundingClientRect().top; + + expect(tooltip.top).to.be.below(pointTop); + expect(tooltip.top + tooltip.height).to.be.closeTo(pointTop, 30); + }); + + it("set options: axis.rotated=true", () => { + args.axis.rotated = true; + }); + + it("should displayed in correct position with padding.top on rotated axis", () => { + const x = 2; + + // when + util.hoverChart(chart, "mousemove", {clientX: 0, clientY: 250}); + + const tooltip = chart.$.tooltip.node().getBoundingClientRect(); + const pointTop = chart.$.circles.filter(`.${$CIRCLE.circle}-${x}`).node().getBoundingClientRect().top; + + expect(pointTop).to.be.below(tooltip.top + tooltip.height); + expect(tooltip.top).to.be.closeTo(pointTop, 30); + }); + }); + describe("on rotated axis", () => { before(() => { args = { @@ -1758,7 +1887,7 @@ describe("TOOLTIP", function() { chart.$.chart.style("margin-top", "100px"); chart.tooltip.show({index:1}); - expect(chart.$.tooltip.select("th").text()).to.be.equal("0"); + expect(chart.$.tooltip.select("th").text()).to.be.equal("1"); chart.$.chart.style("margin-top", null); });