evcc-io/assets/js/components/Forecast/PriceChart.vue

174 lines
4.8 KiB
Vue

<template>
<div ref="scrollEl" class="forecast-chart-scroll" @scroll="onScroll">
<div ref="chartEl" :style="{ height: '200px', width: chartWidth + 'px' }"></div>
</div>
</template>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import {
FONT_FAMILY,
markPointLabel,
tooltipStyle,
forecastGrid,
forecastXAxes,
forecastYAxis,
clampStart,
filterForecastSlots,
minSlotIndex,
maxSlotIndex,
} from "./echarts";
import colors from "@/colors";
import formatter from "@/mixins/formatter";
import chartMixin from "./chartMixin";
import type { CURRENCY } from "@/types/evcc";
import type { ForecastSlot } from "./types";
export default defineComponent({
name: "PriceChart",
mixins: [formatter, chartMixin],
props: {
grid: { type: Array as PropType<ForecastSlot[]>, required: true },
currency: { type: String as PropType<CURRENCY> },
zoom: { type: Boolean, default: false },
},
data() {
return {
hoveredIndex: -1 as number,
};
},
computed: {
slots(): ForecastSlot[] {
return filterForecastSlots(this.grid, this.startDate, this.endDate);
},
markPoints(): { coord: [string, number]; value: string }[] {
const slots = this.slots;
if (!slots.length) return [];
const minIdx = minSlotIndex(slots);
const maxIdx = maxSlotIndex(slots);
const points: { coord: [string, number]; value: string }[] = [];
if (slots[minIdx]) {
points.push({
coord: [clampStart(slots[minIdx]!.start, this.startDate), slots[minIdx]!.value],
value: this.fmtPricePerKWh(slots[minIdx]!.value, this.currency, true, true),
});
}
if (maxIdx !== minIdx && slots[maxIdx]) {
points.push({
coord: [clampStart(slots[maxIdx]!.start, this.startDate), slots[maxIdx]!.value],
value: this.fmtPricePerKWh(slots[maxIdx]!.value, this.currency, true, true),
});
}
return points;
},
yAxisConfig(): Record<string, unknown> {
const values = this.slots.map((s) => s.value);
const dataMin = Math.min(...values);
const dataMax = Math.max(...values);
const fullMin = Math.min(0, dataMin);
const fullRange = dataMax - fullMin || 1;
const rawInterval = fullRange / 5;
const magnitude = Math.pow(10, Math.floor(Math.log10(rawInterval)));
const nice = [1, 2, 2.5, 5, 10].find((n) => n * magnitude >= rawInterval) || 10;
const interval = nice * magnitude;
if (this.zoom) {
return {
min: Math.floor(dataMin / interval) * interval,
max: Math.ceil(dataMax / interval) * interval,
interval,
};
}
return {
min: fullMin,
max: Math.ceil(dataMax / interval) * interval,
interval,
};
},
chartOption(): Record<string, unknown> {
const priceColor = colors.price || "";
// eslint-disable-next-line @typescript-eslint/no-this-alias
const vThis = this;
return {
animationDuration: 0,
textStyle: { fontFamily: FONT_FAMILY },
grid: forecastGrid(),
tooltip: {
trigger: "axis",
axisPointer: { type: "line", snap: true, lineStyle: { color: "transparent" } },
...tooltipStyle(priceColor, () => this.chart),
formatter(params: { value: [string, number] }[]) {
const p = params[0];
if (!p) return "";
const d = new Date(p.value[0]);
const time = `${vThis.weekdayShort(d)} ${vThis.fmtHourMinute(d)}`;
return `${time}<br/>${vThis.fmtPricePerKWh(p.value[1], vThis.currency, false, true)}`;
},
},
xAxis: forecastXAxes(this.startDate, this.endDate, this.weekdayShort),
yAxis: forecastYAxis({
...this.yAxisConfig,
axisLabel: {
color: colors.muted,
formatter: (value: number) => {
const v =
this.currency && this.energyPriceSubunit(this.currency)
? value * 100
: value;
return `${Math.round(v)}`;
},
},
}),
series: [
{
type: "bar",
cursor: "default",
data: this.slots.map((s, i) => ({
value: [clampStart(s.start, this.startDate), s.value],
itemStyle:
this.hoveredIndex >= 0 && i !== this.hoveredIndex
? { opacity: 0.33 }
: undefined,
})),
barMaxWidth: 4,
barMinWidth: 4,
itemStyle: {
color: priceColor,
borderRadius: 2,
},
emphasis: { disabled: true },
markPoint: markPointLabel(
priceColor,
this.tooltipVisible ? [] : this.markPoints,
this.startDate,
this.endDate
),
},
],
};
},
},
mounted() {
this.chart!.on("showTip", (params: unknown) => {
const p = params as { dataIndex?: number };
if (p.dataIndex != null) this.hoveredIndex = p.dataIndex;
});
this.chart!.on("hideTip", () => {
this.hoveredIndex = -1;
});
this.chart!.getZr().on("mouseout", () => {
this.tooltipVisible = false;
this.hoveredIndex = -1;
});
},
});
</script>
<style scoped>
.forecast-chart-scroll {
overflow-x: auto;
margin-bottom: 1.5rem;
padding-bottom: 4px;
}
</style>