mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-29 17:15:20 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c350d46d44 | ||
|
|
e5a43b48c2 |
No files matched your search
@@ -89,6 +89,7 @@ export const DEFAULT_CHART_WIDTH = 500;
|
||||
// is short enough for a whole screenful; on a desktop that would cover the page.
|
||||
const VISIBILITY_ROOT_MARGIN_NARROW = "100%";
|
||||
const VISIBILITY_ROOT_MARGIN = "300px";
|
||||
const CLICK_DRAG_TOLERANCE = 4;
|
||||
|
||||
export type ZoomSliderMode = "auto" | "always" | "never";
|
||||
const DEFERRED_PROPS = [
|
||||
@@ -175,6 +176,10 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
@property({ attribute: "grow-for-zoom-slider", type: Boolean })
|
||||
public growForZoomSlider = false;
|
||||
|
||||
// Dragging selects a time range instead of zooming
|
||||
@property({ attribute: "select-range", type: Boolean })
|
||||
public selectRange = false;
|
||||
|
||||
// extraComponents is not reactive and should not trigger updates
|
||||
public extraComponents?: any[];
|
||||
|
||||
@@ -200,6 +205,8 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
|
||||
@state() private _isZoomed = false;
|
||||
|
||||
private _pointerDownX?: number;
|
||||
|
||||
// Separate from _isZoomed, whose touch handling reruns graph layouts
|
||||
@state() private _isGraphRoamed = false;
|
||||
|
||||
@@ -453,6 +460,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("selectRange")) {
|
||||
this._updateSelectRange();
|
||||
}
|
||||
const themeChanged = changedProps.has("_themes") && this.hasUpdated;
|
||||
if (!themeChanged && !DEFERRED_PROPS.some((p) => changedProps.has(p))) {
|
||||
return;
|
||||
@@ -904,6 +914,32 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
this.chart.on("click", (e: ECElementEvent) => {
|
||||
fireEvent(this, "chart-click", e);
|
||||
});
|
||||
this.chart.getZr().on("mousedown", (e: ECElementEvent) => {
|
||||
this._pointerDownX = e.offsetX;
|
||||
});
|
||||
this.chart.getZr().on("click", (e: ECElementEvent) => {
|
||||
const point = [e.offsetX, e.offsetY];
|
||||
// A drag that ends on the plot is not a click
|
||||
const dragged =
|
||||
this._pointerDownX !== undefined &&
|
||||
Math.abs(e.offsetX - this._pointerDownX) > CLICK_DRAG_TOLERANCE;
|
||||
if (dragged || !this.chart?.containPixel("grid", point)) {
|
||||
return;
|
||||
}
|
||||
const [x, y] = this.chart.convertFromPixel("grid", point) as number[];
|
||||
fireEvent(this, "chart-grid-click", { x, y });
|
||||
});
|
||||
this.chart.on("brushEnd", (e: any) => {
|
||||
const range = e.areas?.[0]?.coordRange as number[] | undefined;
|
||||
// The caller shows the selection, snapped to its data
|
||||
this.chart?.dispatchAction({ type: "brush", areas: [] });
|
||||
if (range) {
|
||||
fireEvent(this, "chart-range-select", {
|
||||
start: range[0],
|
||||
end: range[1],
|
||||
});
|
||||
}
|
||||
});
|
||||
this.chart.on("sankeyroam", () => {
|
||||
const option = this.chart!.getOption();
|
||||
const series = option.series as any[];
|
||||
@@ -969,6 +1005,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
this._legendItemCount = this._getLegendItems()?.length;
|
||||
this._updateSankeyRoam();
|
||||
this._updateDragToZoom();
|
||||
if (this.selectRange) {
|
||||
this._updateSelectRange();
|
||||
}
|
||||
if (zoomStart !== 0 || zoomEnd !== 100) {
|
||||
this.zoom(zoomStart, zoomEnd, true);
|
||||
}
|
||||
@@ -1020,7 +1059,8 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
|
||||
// A chart can switch type (e.g. pie and bar) without being rebuilt
|
||||
private _updateDragToZoom() {
|
||||
const active = !this._touchInput && this._hasZoomableXAxis();
|
||||
const active =
|
||||
!this._touchInput && this._hasZoomableXAxis() && !this.selectRange;
|
||||
if (!this.chart || active === this._dragToZoom) {
|
||||
return;
|
||||
}
|
||||
@@ -1032,6 +1072,33 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _updateSelectRange() {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (this.selectRange) {
|
||||
this.chart.setOption({
|
||||
brush: { xAxisIndex: 0, transformable: false },
|
||||
});
|
||||
}
|
||||
const brushCursor = {
|
||||
type: "takeGlobalCursor",
|
||||
key: "brush",
|
||||
brushOption: {
|
||||
brushType: this.selectRange ? "lineX" : false,
|
||||
brushMode: "single",
|
||||
},
|
||||
};
|
||||
// Brush and drag to zoom share the global cursor, the last one taken wins
|
||||
if (this.selectRange) {
|
||||
this._updateDragToZoom();
|
||||
this.chart.dispatchAction(brushCursor);
|
||||
} else {
|
||||
this.chart.dispatchAction(brushCursor);
|
||||
this._updateDragToZoom();
|
||||
}
|
||||
}
|
||||
|
||||
private _hasZoomableXAxis() {
|
||||
const xAxis = ensureArray(this.options?.xAxis)?.[0] as
|
||||
XAXisOption | undefined;
|
||||
@@ -1538,7 +1605,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _handleClickZoom = (e: ECElementEvent) => {
|
||||
if (!this.chart) {
|
||||
if (!this.chart || this.selectRange) {
|
||||
return;
|
||||
}
|
||||
// Handle sankey chart double-click zoom
|
||||
@@ -1751,7 +1818,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
// it, so a quick drag out would lose the release and its selection
|
||||
if (
|
||||
ev.type === "pointerdown" &&
|
||||
this._dragToZoom &&
|
||||
(this._dragToZoom || this.selectRange) &&
|
||||
this._isZoomPress(ev)
|
||||
) {
|
||||
this.chart
|
||||
@@ -2323,6 +2390,8 @@ declare global {
|
||||
"dataset-hidden": { id: string };
|
||||
"dataset-unhidden": { id: string };
|
||||
"chart-click": ECElementEvent;
|
||||
"chart-grid-click": { x: number; y: number };
|
||||
"chart-range-select": { start: number; end: number };
|
||||
"legend-label-click": { id: string };
|
||||
"chart-zoom": {
|
||||
start: number;
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
import type { HassConfig, HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
@@ -49,7 +49,11 @@ import type {
|
||||
HomeAssistantUI,
|
||||
} from "../../types";
|
||||
import { getPeriodicAxisLabelConfig } from "./axis-label";
|
||||
import type { CustomLegendOption } from "./ha-chart-base";
|
||||
import type {
|
||||
CustomLegendOption,
|
||||
HaChartBase,
|
||||
ZoomSliderMode,
|
||||
} from "./ha-chart-base";
|
||||
import "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
@@ -128,6 +132,19 @@ export class StatisticsChart extends LitElement {
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@property({ attribute: "select-range", type: Boolean })
|
||||
public selectRange = false;
|
||||
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
|
||||
@property({ attribute: false }) public highlight?: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
|
||||
@query("ha-chart-base") private _chartBase?: HaChartBase;
|
||||
|
||||
@state() private _chartData: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
|
||||
@state() private _legendData: CustomLegendOption["data"];
|
||||
@@ -243,9 +260,15 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-chart-base
|
||||
.data=${this._chartData}
|
||||
.data=${this._highlightedChartData(
|
||||
this._chartData,
|
||||
this.highlight,
|
||||
getComputedStyle(this).getPropertyValue("--primary-color").trim()
|
||||
)}
|
||||
.options=${this._chartOptions}
|
||||
.height=${this.height}
|
||||
.selectRange=${this.selectRange}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
style=${styleMap({ height: this.height })}
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
@@ -259,6 +282,59 @@ export class StatisticsChart extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public resetZoom() {
|
||||
this._chartBase?.zoom(0, 100);
|
||||
}
|
||||
|
||||
private _highlightedChartData = memoizeOne(
|
||||
(
|
||||
data: (LineSeriesOption | BarSeriesOption)[],
|
||||
highlight: { start: number; end: number } | undefined,
|
||||
color: string
|
||||
): (LineSeriesOption | BarSeriesOption)[] => {
|
||||
if (!data.length) {
|
||||
return data;
|
||||
}
|
||||
if (data[0].type === "bar") {
|
||||
if (!highlight) {
|
||||
return data;
|
||||
}
|
||||
// Bars outside the highlight fade, so the highlighted ones stand out.
|
||||
// Some points are already objects, like negative bars with round caps.
|
||||
return data.map((series) => ({
|
||||
...series,
|
||||
data: (series.data as any[]).map((item) => {
|
||||
const point =
|
||||
item && typeof item === "object" && "value" in item
|
||||
? item
|
||||
: { value: item };
|
||||
// Current state bars only carry their time
|
||||
const start = new Date(point.value[2] ?? point.value[0]).getTime();
|
||||
return start >= highlight.start && start < highlight.end
|
||||
? item
|
||||
: {
|
||||
...point,
|
||||
itemStyle: { ...point.itemStyle, opacity: 0.3 },
|
||||
};
|
||||
}),
|
||||
})) as BarSeriesOption[];
|
||||
}
|
||||
// Every series gets an area, empty when not highlighted, as the chart
|
||||
// merges series and would keep an old one
|
||||
return data.map((series, index) => ({
|
||||
...series,
|
||||
markArea: {
|
||||
silent: true,
|
||||
itemStyle: { color, opacity: 0.15 },
|
||||
data:
|
||||
highlight && index === 0
|
||||
? [[{ xAxis: highlight.start }, { xAxis: highlight.end }]]
|
||||
: [],
|
||||
},
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
private _datasetHidden(ev: HASSDomEvent<HASSDomEvents["dataset-hidden"]>) {
|
||||
this._hiddenStats.add(ev.detail.id);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
DataZoomComponent,
|
||||
VisualMapComponent,
|
||||
ToolboxComponent,
|
||||
MarkAreaComponent,
|
||||
BrushComponent,
|
||||
} from "echarts/components";
|
||||
|
||||
// Features like Universal Transition and Label Layout
|
||||
@@ -48,6 +50,8 @@ import type {
|
||||
GridComponentOption,
|
||||
DataZoomComponentOption,
|
||||
VisualMapComponentOption,
|
||||
MarkAreaComponentOption,
|
||||
BrushComponentOption,
|
||||
} from "echarts/components";
|
||||
import type { ComposeOption } from "echarts/core";
|
||||
|
||||
@@ -62,6 +66,8 @@ export type ECOption = ComposeOption<
|
||||
| GridComponentOption
|
||||
| DataZoomComponentOption
|
||||
| VisualMapComponentOption
|
||||
| MarkAreaComponentOption
|
||||
| BrushComponentOption
|
||||
| SankeySeriesOption
|
||||
| GraphSeriesOption
|
||||
| SunburstSeriesOption
|
||||
@@ -91,6 +97,8 @@ echarts.use([
|
||||
UniversalTransition,
|
||||
CanvasRenderer,
|
||||
ToolboxComponent,
|
||||
MarkAreaComponent,
|
||||
BrushComponent,
|
||||
]);
|
||||
|
||||
export { LinearGradient };
|
||||
|
||||
Reference in new issue
Block a user