evcc-io/assets/js/components/Forecast/echarts.ts

202 lines
5.3 KiB
TypeScript

import * as echarts from "echarts/core";
import colors from "@/colors";
import type { ForecastSlot } from "./types";
import { BarChart, LineChart } from "echarts/charts";
import {
GridComponent,
TooltipComponent,
MarkPointComponent,
AxisPointerComponent,
} from "echarts/components";
import { SVGRenderer } from "echarts/renderers";
echarts.use([
BarChart,
LineChart,
GridComponent,
TooltipComponent,
AxisPointerComponent,
MarkPointComponent,
SVGRenderer,
]);
export const FONT_FAMILY = "Montserrat, sans-serif";
export function markPointLabel(
color: string,
data: { coord: [string, number]; value: string; label?: { offset?: [number, number] } }[],
startDate?: Date,
endDate?: Date
) {
// threshold: points within the first 5% of the time range get shifted right
const threshold =
startDate && endDate
? startDate.getTime() + (endDate.getTime() - startDate.getTime()) * 0.05
: 0;
const adjustedData = data.map((d) => {
const ts = new Date(d.coord[0]).getTime();
if (threshold && ts < threshold) {
const baseOffset = d.label?.offset ?? [0, -2];
return {
...d,
label: {
...d.label,
offset: [baseOffset[0] + 30, baseOffset[1]] as [number, number],
},
};
}
return d;
});
return {
animation: true,
clip: false,
symbol: "rect",
symbolSize: 1,
label: {
show: true,
position: "top",
fontFamily: FONT_FAMILY,
fontSize: 14,
fontWeight: "bold" as const,
color: colors.background,
backgroundColor: color,
borderRadius: 4,
padding: [5, 10],
offset: [0, -2],
formatter: (p: { data: { value: string } }) => p.data.value,
},
data: adjustedData,
};
}
export function tooltipStyle(
color: string,
getChart?: () => { convertToPixel: echarts.ECharts["convertToPixel"] } | null
) {
return {
backgroundColor: color,
borderColor: color,
borderWidth: 0,
padding: [5, 10],
extraCssText: "box-shadow: none; border-radius: 4px; text-align: center;",
position(
point: [number, number],
params: { value: [string, number] }[] | { value: [string, number] },
el: HTMLElement
): [number, number] {
const w = el?.offsetWidth || 0;
const h = el?.offsetHeight || 0;
const margin = 8;
const arr = Array.isArray(params) ? params : [params];
const p = arr[0];
const chart = getChart?.();
if (chart && p?.value) {
const pixelY = chart.convertToPixel({ seriesIndex: 0 }, p.value)?.[1];
if (pixelY != null) {
return [point[0] - w / 2, pixelY - h - margin];
}
}
return [point[0] - w / 2, point[1] - h - margin];
},
textStyle: {
fontFamily: FONT_FAMILY,
fontSize: 14,
fontWeight: "bold" as const,
color: colors.background,
},
};
}
export function forecastGrid() {
return { top: 36, right: 16, bottom: 4, left: 48, borderWidth: 0 };
}
export function forecastXAxes(startDate: Date, endDate: Date, weekdayShort: (d: Date) => string) {
return [
{
type: "time",
min: startDate,
max: endDate,
minInterval: 3600 * 1000,
maxInterval: 3600 * 1000,
axisLabel: {
color: colors.muted,
fontSize: 14,
lineHeight: Math.round(14 * 1.1),
margin: 4,
formatter: (value: number) => {
const date = new Date(value);
const h = date.getHours();
if (h % 2 !== 0) return "";
if (h === 0) return `${h}\n${weekdayShort(date)}`;
return `${h}`;
},
},
splitLine: { show: false },
axisLine: { show: false },
axisTick: { show: false },
},
{
type: "time",
position: "bottom",
min: startDate,
max: endDate,
minInterval: 24 * 3600 * 1000,
maxInterval: 24 * 3600 * 1000,
axisLabel: { show: false },
axisLine: { show: false },
axisTick: { show: false },
splitLine: {
show: true,
showMinLine: false,
showMaxLine: false,
lineStyle: { color: colors.border || "", type: "dashed" },
},
},
];
}
export function forecastYAxis(overrides: Record<string, unknown> = {}) {
const { axisLabel, ...rest } = overrides;
return {
type: "value",
min: 0,
axisLine: { show: false },
axisTick: { show: false },
splitLine: {
showMinLine: false,
showMaxLine: false,
lineStyle: { color: colors.border || "" },
},
axisLabel: {
fontSize: 14,
...(axisLabel as Record<string, unknown>),
},
...rest,
};
}
export function clampStart(ts: string, startDate: Date): string {
return new Date(ts) < startDate ? startDate.toISOString() : ts;
}
export function filterForecastSlots(
slots: ForecastSlot[],
startDate: Date,
endDate: Date
): ForecastSlot[] {
if (!Array.isArray(slots)) return [];
return slots.filter((s) => new Date(s.end) >= startDate && new Date(s.start) <= endDate);
}
export function minSlotIndex(slots: ForecastSlot[]): number {
return slots.reduce((min, s, i) => (s.value < (slots[min]?.value || Infinity) ? i : min), 0);
}
export function maxSlotIndex(slots: ForecastSlot[]): number {
return slots.reduce((max, s, i) => (s.value > (slots[max]?.value || 0) ? i : max), 0);
}
export { echarts };