Compare commits

...
3 changed files with 159 additions and 6 deletions

No files matched your search

+72 -3
View File
@@ -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;
+79 -3
View File
@@ -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");
+8
View File
@@ -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 };