mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-07 18:03:29 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
337f0e9f34 |
40
src/components/chart/chart-tooltip-position.ts
Normal file
40
src/components/chart/chart-tooltip-position.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { TooltipPositionCallback } from "echarts/types/dist/shared";
|
||||
|
||||
export const TOOLTIP_GAP_PX = 12;
|
||||
export const TOOLTIP_TOP_OFFSET_PX = 10;
|
||||
|
||||
/**
|
||||
* Pins the tooltip near the top of the chart and offsets it horizontally
|
||||
* from the cursor so it never covers the data point being inspected.
|
||||
* For axis-trigger time-series tooltips where the cursor's Y is uncorrelated
|
||||
* with the displayed content.
|
||||
*/
|
||||
export const sideTooltipPosition: TooltipPositionCallback = (
|
||||
point,
|
||||
_params,
|
||||
dom,
|
||||
_rect,
|
||||
size
|
||||
) => {
|
||||
const [cursorX] = point;
|
||||
const [viewW, viewH] = size.viewSize;
|
||||
const [tipW, tipH] = size.contentSize;
|
||||
|
||||
const rtl =
|
||||
dom instanceof HTMLElement && getComputedStyle(dom).direction === "rtl";
|
||||
|
||||
const rightOfCursor = cursorX + TOOLTIP_GAP_PX;
|
||||
const leftOfCursor = cursorX - TOOLTIP_GAP_PX - tipW;
|
||||
|
||||
let x = rtl ? leftOfCursor : rightOfCursor;
|
||||
const overflowsRight = x + tipW > viewW;
|
||||
const overflowsLeft = x < 0;
|
||||
if (overflowsRight || overflowsLeft) {
|
||||
x = rtl ? rightOfCursor : leftOfCursor;
|
||||
}
|
||||
x = Math.max(0, Math.min(x, viewW - tipW));
|
||||
|
||||
const y = Math.max(0, Math.min(TOOLTIP_TOP_OFFSET_PX, viewH - tipH));
|
||||
|
||||
return [x, y];
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { LineChartEntity, LineChartState } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
@@ -410,8 +411,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
renderMode: "html",
|
||||
position: "bottom",
|
||||
align: "center",
|
||||
position: sideTooltipPosition,
|
||||
confine: true,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { TimelineEntity } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import { computeTimelineColor } from "./timeline-color";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import echarts from "../../resources/echarts/echarts";
|
||||
@@ -256,8 +257,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
},
|
||||
tooltip: {
|
||||
renderMode: "html",
|
||||
position: "bottom",
|
||||
align: "center",
|
||||
position: sideTooltipPosition,
|
||||
confine: true,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
|
||||
@@ -37,6 +37,7 @@ import type { HomeAssistant } from "../../types";
|
||||
import { getPeriodicAxisLabelConfig } from "./axis-label";
|
||||
import type { CustomLegendOption } from "./ha-chart-base";
|
||||
import "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import { fillDataGapsAndRoundCaps } from "./round-caps";
|
||||
|
||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||
@@ -398,8 +399,7 @@ export class StatisticsChart extends LitElement {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
renderMode: "html",
|
||||
position: "bottom",
|
||||
align: "center",
|
||||
position: sideTooltipPosition,
|
||||
confine: true,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
|
||||
66
test/components/chart/chart-tooltip-position.test.ts
Normal file
66
test/components/chart/chart-tooltip-position.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
TOOLTIP_GAP_PX,
|
||||
TOOLTIP_TOP_OFFSET_PX,
|
||||
sideTooltipPosition,
|
||||
} from "../../../src/components/chart/chart-tooltip-position";
|
||||
|
||||
const callPosition = (
|
||||
cursorX: number,
|
||||
options: {
|
||||
viewSize?: [number, number];
|
||||
contentSize?: [number, number];
|
||||
rtl?: boolean;
|
||||
} = {}
|
||||
) => {
|
||||
const dom = document.createElement("div");
|
||||
if (options.rtl) {
|
||||
dom.setAttribute("dir", "rtl");
|
||||
document.body.appendChild(dom);
|
||||
}
|
||||
const result = sideTooltipPosition([cursorX, 0], [], dom, null, {
|
||||
viewSize: options.viewSize ?? [800, 400],
|
||||
contentSize: options.contentSize ?? [200, 120],
|
||||
}) as [number, number];
|
||||
if (options.rtl) {
|
||||
document.body.removeChild(dom);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
describe("sideTooltipPosition", () => {
|
||||
it("places tooltip to the right of the cursor by default", () => {
|
||||
const [x, y] = callPosition(100);
|
||||
expect(x).toBe(100 + TOOLTIP_GAP_PX);
|
||||
expect(y).toBe(TOOLTIP_TOP_OFFSET_PX);
|
||||
});
|
||||
|
||||
it("flips to the left when right side overflows the chart", () => {
|
||||
const [x] = callPosition(700, {
|
||||
viewSize: [800, 400],
|
||||
contentSize: [200, 120],
|
||||
});
|
||||
expect(x).toBe(700 - TOOLTIP_GAP_PX - 200);
|
||||
});
|
||||
|
||||
it("clamps to chart bounds when neither side fits", () => {
|
||||
const [x] = callPosition(50, {
|
||||
viewSize: [120, 400],
|
||||
contentSize: [200, 120],
|
||||
});
|
||||
expect(x).toBe(0);
|
||||
});
|
||||
|
||||
it("clamps Y when chart is shorter than the tooltip", () => {
|
||||
const [, y] = callPosition(100, {
|
||||
viewSize: [800, 100],
|
||||
contentSize: [200, 120],
|
||||
});
|
||||
expect(y).toBe(0);
|
||||
});
|
||||
|
||||
it("prefers the left of the cursor in RTL mode", () => {
|
||||
const [x] = callPosition(400, { rtl: true });
|
||||
expect(x).toBe(400 - TOOLTIP_GAP_PX - 200);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user