Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add zoom & pan to charts #23183

Merged
merged 13 commits into from
Dec 12, 2024
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"app-datepicker": "5.1.1",
"barcode-detector": "2.3.1",
"chart.js": "4.4.7",
"chartjs-plugin-zoom": "2.2.0",
"color-name": "2.0.0",
"comlink": "4.4.2",
"core-js": "3.39.0",
Expand Down
94 changes: 91 additions & 3 deletions src/components/chart/ha-chart-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { clamp } from "../../common/number/clamp";
import type { HomeAssistant } from "../../types";
import { debounce } from "../../common/util/debounce";
import { isMac } from "../../util/is_mac";

export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;

Expand Down Expand Up @@ -64,6 +65,8 @@ export class HaChartBase extends LitElement {

@state() private _hiddenDatasets: Set<number> = new Set();

@state() private _showZoomHint = false;

private _paddingUpdateCount = 0;

private _paddingUpdateLock = false;
Expand Down Expand Up @@ -201,7 +204,9 @@ export class HaChartBase extends LitElement {
}
this.chart.data = this.data;
}
if (changedProps.has("options")) {
if (changedProps.has("options") && !this.chart.isZoomedOrPanned()) {
// this resets the chart zoom because min/max scales changed
// so we only do it if the user is not zooming or panning
this.chart.options = this._createOptions();
}
this.chart.update("none");
Expand Down Expand Up @@ -249,7 +254,7 @@ export class HaChartBase extends LitElement {
})}
>
<div
class="chartContainer"
class="chart-container"
style=${styleMap({
height: `${
this.height ?? this._chartHeight ?? this.clientWidth / 2
Expand All @@ -259,8 +264,22 @@ export class HaChartBase extends LitElement {
"padding-inline-start": `${this._paddingYAxisInternal}px`,
"padding-inline-end": 0,
})}
@wheel=${this._handleChartScroll}
>
<canvas></canvas>
<div
class="zoom-hint ${classMap({
visible: this._showZoomHint,
})}"
>
<div>
${isMac
? this.hass.localize(
"ui.components.history_charts.zoom_hint_mac"
)
: this.hass.localize("ui.components.history_charts.zoom_hint")}
</div>
</div>
${this._tooltip
? html`<div
class="chartTooltip ${classMap({
Expand Down Expand Up @@ -343,7 +362,8 @@ export class HaChartBase extends LitElement {
}
}

private _createOptions() {
private _createOptions(): ChartOptions {
const modifierKey = isMac ? "meta" : "ctrl";
return {
maintainAspectRatio: false,
...this.options,
Expand All @@ -358,6 +378,36 @@ export class HaChartBase extends LitElement {
...this.options?.plugins?.legend,
display: false,
},
zoom: {
...this.options?.plugins?.zoom,
pan: {
enabled: true,
},
zoom: {
pinch: {
enabled: true,
},
drag: {
enabled: true,
modifierKey,
},
wheel: {
enabled: true,
modifierKey,
},
mode: "x",
},
limits: {
x: {
min: "original",
max: "original",
},
y: {
min: "original",
max: "original",
},
},
},
},
};
}
Expand All @@ -382,6 +432,16 @@ export class HaChartBase extends LitElement {
];
}

private _handleChartScroll(ev: MouseEvent) {
const modifier = isMac ? "metaKey" : "ctrlKey";
if (!ev[modifier] && !this._showZoomHint) {
this._showZoomHint = true;
setTimeout(() => {
this._showZoomHint = false;
}, 1000);
}
}

private _legendClick(ev) {
if (!this.chart) {
return;
Expand Down Expand Up @@ -450,6 +510,9 @@ export class HaChartBase extends LitElement {
height: 0;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
.chart-container {
position: relative;
}
canvas {
max-height: var(--chart-max-height, 400px);
}
Expand Down Expand Up @@ -539,6 +602,31 @@ export class HaChartBase extends LitElement {
font-weight: 300;
word-break: break-all;
}
.zoom-hint {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 500ms cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
.zoom-hint.visible {
opacity: 1;
}
.zoom-hint > div {
color: white;
font-size: 1.5em;
font-weight: 500;
padding: 8px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.3);
box-shadow: 0 0 32px 32px rgba(0, 0, 0, 0.3);
}
`;
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/resources/chartjs.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ZoomPlugin from "chartjs-plugin-zoom";
import {
LineController,
TimeScale,
Expand Down Expand Up @@ -35,5 +36,6 @@ Chart.register(
TextBarElement,
TimelineController,
CategoryScale,
LogarithmicScale
LogarithmicScale,
ZoomPlugin
);
4 changes: 3 additions & 1 deletion src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,9 @@
"error": "Unable to load history",
"duration": "Duration",
"source_history": "Source: History",
"source_stats": "Source: Long term statistics"
"source_stats": "Source: Long term statistics",
"zoom_hint": "Use ctrl + scroll to zoom in/out",
"zoom_hint_mac": "Use ⌘ + scroll to zoom in/out"
},
"map": {
"error": "Unable to load map"
Expand Down
1 change: 1 addition & 0 deletions src/util/is_mac.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isMac = /Mac/i.test(navigator.userAgent);
28 changes: 24 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4585,10 +4585,10 @@ __metadata:
languageName: node
linkType: hard

"@types/hammerjs@npm:^2.0.36":
version: 2.0.45
resolution: "@types/hammerjs@npm:2.0.45"
checksum: 10/8d7f8791789853a9461f6445e625f18922a823a61042161dde5513f4a2c15ecd6361fa6f9b457ce13bfb6b518489b892fedb9e2cebb4420523cb45f1cbb4ee88
"@types/hammerjs@npm:^2.0.36, @types/hammerjs@npm:^2.0.45":
version: 2.0.46
resolution: "@types/hammerjs@npm:2.0.46"
checksum: 10/1b6502d668f45ca49fb488c01f7938d3aa75e989d70c64801c8feded7d659ca1a118f745c1b604d220efe344c93231767d5cc68c05e00e069c14539b6143cfd9
languageName: node
linkType: hard

Expand Down Expand Up @@ -6417,6 +6417,18 @@ __metadata:
languageName: node
linkType: hard

"chartjs-plugin-zoom@npm:2.2.0":
version: 2.2.0
resolution: "chartjs-plugin-zoom@npm:2.2.0"
dependencies:
"@types/hammerjs": "npm:^2.0.45"
hammerjs: "npm:^2.0.8"
peerDependencies:
chart.js: ">=3.2.0"
checksum: 10/4a549b1b21ed5433f9ba67038d6176ed545b2881521e12d6b8024cd2ab08fb008c36fe388ab2ac7ee2ac334bf44a8d785703570388fa0e0b4c22c18602536f9c
languageName: node
linkType: hard

"check-error@npm:^2.1.1":
version: 2.1.1
resolution: "check-error@npm:2.1.1"
Expand Down Expand Up @@ -9019,6 +9031,13 @@ __metadata:
languageName: node
linkType: hard

"hammerjs@npm:^2.0.8":
version: 2.0.8
resolution: "hammerjs@npm:2.0.8"
checksum: 10/9155d056f252ef35e8ca258dbb5ee2c9d8794f6805d083da7d1d9763d185e3e149459ecc2b36ccce584e3cd5f099fd9fa55056e3bcc7724046390f2e5ae25815
languageName: node
linkType: hard

"handle-thing@npm:^2.0.0":
version: 2.0.1
resolution: "handle-thing@npm:2.0.1"
Expand Down Expand Up @@ -9229,6 +9248,7 @@ __metadata:
barcode-detector: "npm:2.3.1"
browserslist-useragent-regexp: "npm:4.1.3"
chart.js: "npm:4.4.7"
chartjs-plugin-zoom: "npm:2.2.0"
color-name: "npm:2.0.0"
comlink: "npm:4.4.2"
core-js: "npm:3.39.0"
Expand Down
Loading