UI: hide chart tooltips on touch release (#32603)
This commit is contained in:
parent
ffb91cc335
commit
7317b3437a
9 changed files with 53 additions and 558 deletions
|
|
@ -9,6 +9,7 @@ import {
|
|||
FONT_FAMILY,
|
||||
forecastXAxes,
|
||||
forecastYAxis,
|
||||
registerTouchTooltipReset,
|
||||
tooltipStyle,
|
||||
tooltipTable,
|
||||
type TooltipRow,
|
||||
|
|
@ -241,6 +242,7 @@ export default defineComponent({
|
|||
const el = this.$refs["chartEl"] as HTMLElement;
|
||||
this.chart = markRaw(echarts.init(el));
|
||||
this.chart.setOption(this.chartOption);
|
||||
registerTouchTooltipReset(this.chart, el);
|
||||
this.$nextTick(() => this.updateGraphic());
|
||||
window.addEventListener("resize", this.resize);
|
||||
// initial render done here (not via the watcher), so paging animates from the first click
|
||||
|
|
|
|||
|
|
@ -1,548 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="overflow-x-auto overflow-x-md-hidden chart-container border-1"
|
||||
@mouseleave="onMouseLeave"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
position: 'relative',
|
||||
height: '240px',
|
||||
width: `${chartWidth}px`,
|
||||
}"
|
||||
class="user-select-none"
|
||||
>
|
||||
<!-- @vue-ignore -->
|
||||
<Bar ref="chart" :data="chartData" :options="options" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { Bar } from "vue-chartjs";
|
||||
import {
|
||||
BarController,
|
||||
BarElement,
|
||||
LineController,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
TimeSeriesScale,
|
||||
Legend,
|
||||
Tooltip,
|
||||
PointElement,
|
||||
Filler,
|
||||
type ChartEvent,
|
||||
type ActiveElement,
|
||||
Chart,
|
||||
} from "chart.js";
|
||||
import ChartDataLabels, { type Context } from "chartjs-plugin-datalabels";
|
||||
import "chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm";
|
||||
import { registerChartComponents, commonOptions } from "../Sessions/chartConfig";
|
||||
import formatter, { POWER_UNIT } from "@/mixins/formatter";
|
||||
import colors, { lighterColor } from "@/colors";
|
||||
import type { CURRENCY, UiForecastSlot, UiSolarDetails, UiTimeseriesEntry } from "@/types/evcc";
|
||||
import { ForecastType, highestSlotIndexByDay } from "@/utils/forecast";
|
||||
|
||||
registerChartComponents([
|
||||
BarController,
|
||||
BarElement,
|
||||
LineController,
|
||||
LineElement,
|
||||
Filler,
|
||||
LinearScale,
|
||||
TimeSeriesScale,
|
||||
Legend,
|
||||
Tooltip,
|
||||
PointElement,
|
||||
ChartDataLabels,
|
||||
]);
|
||||
|
||||
export default defineComponent({
|
||||
name: "ForecastChart",
|
||||
components: { Bar },
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
grid: { type: Array as PropType<UiForecastSlot[]> },
|
||||
solar: { type: Object as PropType<UiSolarDetails> },
|
||||
co2: { type: Array as PropType<UiForecastSlot[]> },
|
||||
currency: { type: String as PropType<CURRENCY> },
|
||||
selected: { type: String as PropType<ForecastType> },
|
||||
},
|
||||
emits: ["selected"],
|
||||
data(): {
|
||||
selectedIndex: number | null;
|
||||
startDate: Date;
|
||||
interval: ReturnType<typeof setTimeout> | null;
|
||||
ignoreEvents: boolean;
|
||||
ignoreEventsTimeout: ReturnType<typeof setTimeout> | null;
|
||||
animations: boolean;
|
||||
} {
|
||||
return {
|
||||
selectedIndex: null,
|
||||
startDate: new Date(),
|
||||
interval: null,
|
||||
ignoreEvents: false,
|
||||
ignoreEventsTimeout: null,
|
||||
animations: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
endDate() {
|
||||
const end = new Date(this.startDate);
|
||||
end.setHours(end.getHours() + 96);
|
||||
return end;
|
||||
},
|
||||
solarEntries() {
|
||||
return this.filterEntries(this.solar?.timeseries || []);
|
||||
},
|
||||
gridSlots() {
|
||||
return this.filterSlots(this.grid);
|
||||
},
|
||||
co2Slots() {
|
||||
return this.filterSlots(this.co2);
|
||||
},
|
||||
maxPriceIndex() {
|
||||
return this.maxIndex(this.gridSlots);
|
||||
},
|
||||
minPriceIndex() {
|
||||
return this.minIndex(this.gridSlots);
|
||||
},
|
||||
maxCo2Index() {
|
||||
return this.maxIndex(this.co2Slots);
|
||||
},
|
||||
minCo2Index() {
|
||||
return this.minIndex(this.co2Slots);
|
||||
},
|
||||
maxSolarIndex() {
|
||||
return this.maxEntryIndex(this.solarEntries);
|
||||
},
|
||||
solarHighlights() {
|
||||
const { today, tomorrow, dayAfterTomorrow } = this.solar || {};
|
||||
return [
|
||||
{
|
||||
index: highestSlotIndexByDay(this.solarEntries, 0),
|
||||
energy: today?.energy,
|
||||
},
|
||||
{
|
||||
index: highestSlotIndexByDay(this.solarEntries, 1),
|
||||
energy: tomorrow?.energy,
|
||||
},
|
||||
{
|
||||
index: highestSlotIndexByDay(this.solarEntries, 2),
|
||||
energy: dayAfterTomorrow?.energy,
|
||||
},
|
||||
];
|
||||
},
|
||||
chartData() {
|
||||
const datasets = [];
|
||||
if (this.solarEntries.length > 0) {
|
||||
const active = this.selected === ForecastType.Solar;
|
||||
const color = active ? colors.self : colors.border;
|
||||
datasets.push({
|
||||
label: ForecastType.Solar,
|
||||
type: "line",
|
||||
data: this.solarEntries.map((entry, index) => {
|
||||
return {
|
||||
y: entry.val,
|
||||
x: new Date(entry.ts),
|
||||
highlight:
|
||||
active &&
|
||||
(this.selectedIndex !== null
|
||||
? this.selectedIndex === index
|
||||
: this.solarHighlights.find(({ index: i }) => i === index)
|
||||
?.energy),
|
||||
};
|
||||
}),
|
||||
yAxisID: "yForecast",
|
||||
backgroundColor: lighterColor(color),
|
||||
borderColor: color,
|
||||
fill: "start",
|
||||
tension: 0.05,
|
||||
pointRadius: 0,
|
||||
animation: {
|
||||
y: { duration: this.animations ? 500 : 0 },
|
||||
},
|
||||
pointHoverRadius: active ? 4 : 0,
|
||||
spanGaps: true,
|
||||
order: active ? 0 : 1,
|
||||
});
|
||||
}
|
||||
if (this.gridSlots && this.gridSlots.length > 0) {
|
||||
const active = this.selected === ForecastType.Price;
|
||||
const color = active ? colors.price : colors.border;
|
||||
datasets.push({
|
||||
label: ForecastType.Price,
|
||||
data: this.gridSlots.map((slot, index) => ({
|
||||
y: slot.value,
|
||||
x: new Date(slot.start),
|
||||
highlight:
|
||||
active &&
|
||||
(this.selectedIndex !== null
|
||||
? this.selectedIndex === index
|
||||
: index === this.maxPriceIndex || index === this.minPriceIndex),
|
||||
})),
|
||||
yAxisID: "yPrice",
|
||||
borderRadius: 2,
|
||||
backgroundColor: color,
|
||||
borderColor: color,
|
||||
order: active ? 0 : 1,
|
||||
});
|
||||
}
|
||||
if (this.co2Slots && this.co2Slots.length > 0) {
|
||||
const active = this.selected === ForecastType.Co2;
|
||||
const color = active ? colors.co2 : colors.border;
|
||||
datasets.push({
|
||||
label: ForecastType.Co2,
|
||||
type: "line",
|
||||
data: this.co2Slots.map((slot, index) => {
|
||||
const dataActive =
|
||||
active &&
|
||||
(this.selectedIndex !== null
|
||||
? this.selectedIndex === index
|
||||
: index === this.maxCo2Index || index === this.minCo2Index);
|
||||
return {
|
||||
y: slot.value,
|
||||
x: new Date(slot.start),
|
||||
highlight: dataActive,
|
||||
active: dataActive,
|
||||
};
|
||||
}),
|
||||
yAxisID: "yCo2",
|
||||
backgroundColor: color,
|
||||
borderColor: color,
|
||||
tension: 0.05,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: active ? 4 : 0,
|
||||
spanGaps: true,
|
||||
order: active ? 0 : 1,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
datasets,
|
||||
};
|
||||
},
|
||||
chartDataMaxDate() {
|
||||
let result: Date | null = null;
|
||||
for (const dataset of this.chartData.datasets) {
|
||||
for (const data of dataset.data) {
|
||||
if (!result || data.x.getTime() > result.getTime()) result = data.x;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
chartWidth() {
|
||||
const minWidth = 780;
|
||||
const maxWidth = 1500; // allow diagram to to grow depending on available data
|
||||
const realEndDate = this.chartDataMaxDate;
|
||||
if (!realEndDate) return minWidth;
|
||||
const maxRange = this.endDate.getTime() - this.startDate.getTime();
|
||||
const realRange = realEndDate.getTime() - this.startDate.getTime();
|
||||
if (maxRange <= 0 || realRange <= 0) return minWidth;
|
||||
const scale = realRange / maxRange;
|
||||
return Math.round(Math.min(maxWidth, Math.max(minWidth, scale * maxWidth)));
|
||||
},
|
||||
options() {
|
||||
// oxlint-disable-next-line typescript/no-this-alias
|
||||
const vThis = this;
|
||||
return {
|
||||
...commonOptions,
|
||||
locale: this.$i18n?.locale,
|
||||
layout: { padding: { top: 32 } },
|
||||
color: colors.text,
|
||||
borderSkipped: false,
|
||||
animation: {
|
||||
duration: 500, // --evcc-transition-medium
|
||||
colors: true,
|
||||
numbers: false,
|
||||
},
|
||||
interaction: {
|
||||
mode: "index",
|
||||
axis: "x",
|
||||
intersect: false,
|
||||
},
|
||||
categoryPercentage: 0.7,
|
||||
events: ["mousemove", "click", "touchstart", "touchend"],
|
||||
onHover(event: ChartEvent, active: ActiveElement[], chart: Chart) {
|
||||
if (["touchend", "click"].includes(event.type)) {
|
||||
vThis.selectIndex(null, true);
|
||||
return;
|
||||
}
|
||||
const element = active.find(({ datasetIndex }) => {
|
||||
const { label } = chart.getDatasetMeta(datasetIndex);
|
||||
return label === vThis.selected;
|
||||
});
|
||||
vThis.selectIndex(element ? element.index : null);
|
||||
},
|
||||
plugins: {
|
||||
...commonOptions.plugins,
|
||||
datalabels: {
|
||||
backgroundColor(context: Context) {
|
||||
return context.dataset.borderColor;
|
||||
},
|
||||
align({ chart, dataset, dataIndex }: Context) {
|
||||
const scale = chart.scales["x"] as any;
|
||||
const { min, max } = scale;
|
||||
// @ts-expect-error no-explicit-any
|
||||
const time = new Date(dataset.data[dataIndex]?.x).getTime();
|
||||
|
||||
// percent along the x axis (0: start, 1: end)
|
||||
const percent = (time - min) / (max - min);
|
||||
let adjust = 0;
|
||||
const step = 20;
|
||||
|
||||
// tilt label left/right if it's close to the edge
|
||||
if (percent < 0.02) {
|
||||
adjust = 2;
|
||||
} else if (percent < 0.04) {
|
||||
adjust = 1;
|
||||
} else if (percent > 0.98) {
|
||||
adjust = -2;
|
||||
} else if (percent > 0.96) {
|
||||
adjust = -1;
|
||||
}
|
||||
|
||||
return -90 + adjust * step;
|
||||
},
|
||||
anchor: "end",
|
||||
offset: 8,
|
||||
padding(context: Context) {
|
||||
const data = context.dataset.data[context.dataIndex];
|
||||
// @ts-expect-error no-explicit-any
|
||||
const x = typeof data.highlight === "number" ? 32 : 8;
|
||||
return {
|
||||
x,
|
||||
y: 4,
|
||||
};
|
||||
},
|
||||
borderRadius: 4,
|
||||
color: colors.background,
|
||||
font: { weight: "bold" },
|
||||
// @ts-expect-error no-explicit-any
|
||||
formatter(value, context: Context) {
|
||||
if (value.highlight) {
|
||||
switch (context.dataset.label) {
|
||||
case ForecastType.Price:
|
||||
return vThis.fmtPricePerKWh(
|
||||
value.y,
|
||||
vThis.currency,
|
||||
true,
|
||||
true
|
||||
);
|
||||
case ForecastType.Co2:
|
||||
return vThis.fmtGrams(value.y);
|
||||
case ForecastType.Solar:
|
||||
if (value.highlight === true) {
|
||||
return vThis.fmtW(value.y, POWER_UNIT.AUTO);
|
||||
} else {
|
||||
return vThis.fmtWh(value.highlight, POWER_UNIT.AUTO);
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
tooltip: null,
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "timeseries",
|
||||
display: true,
|
||||
time: { unit: "day" },
|
||||
border: { display: false },
|
||||
grid: {
|
||||
display: true,
|
||||
color: colors.border,
|
||||
offset: false,
|
||||
// @ts-expect-error no-explicit-any
|
||||
lineWidth(context) {
|
||||
if (context.type !== "tick") {
|
||||
return 0;
|
||||
}
|
||||
const label = context.tick?.label;
|
||||
return Array.isArray(label) ? 1 : 0;
|
||||
},
|
||||
},
|
||||
min: this.startDate,
|
||||
max: this.endDate,
|
||||
ticks: {
|
||||
color: colors.muted,
|
||||
autoSkip: false,
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
source: "data",
|
||||
align: "center",
|
||||
callback(value: number) {
|
||||
const date = new Date(value);
|
||||
const hour = date.getHours();
|
||||
const minute = date.getMinutes();
|
||||
if (minute !== 0) {
|
||||
return "";
|
||||
}
|
||||
const hourFmt = vThis.hourShort(date);
|
||||
if (hour === 0) {
|
||||
return [hourFmt, vThis.weekdayShort(date)];
|
||||
}
|
||||
if (hour % 6 === 0) {
|
||||
return hourFmt;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
},
|
||||
},
|
||||
yForecast: {
|
||||
...this.yScaleOptions(ForecastType.Solar),
|
||||
min: 0,
|
||||
max: this.yMaxEntry(this.solarEntries, this.solar?.scale),
|
||||
beginAtZero: true,
|
||||
},
|
||||
yCo2: {
|
||||
...this.yScaleOptions(ForecastType.Co2),
|
||||
min: 0,
|
||||
max: this.yMax(this.co2Slots),
|
||||
},
|
||||
yPrice: {
|
||||
...this.yScaleOptions(ForecastType.Price),
|
||||
suggestedMin: 0,
|
||||
max: this.yMax(this.gridSlots),
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
selectedSlot() {
|
||||
if (this.selectedIndex === null || !this.selected) return null;
|
||||
|
||||
const slotMap = {
|
||||
[ForecastType.Solar]: this.solarEntries,
|
||||
[ForecastType.Price]: this.gridSlots,
|
||||
[ForecastType.Co2]: this.co2Slots,
|
||||
};
|
||||
|
||||
return slotMap[this.selected]?.[this.selectedIndex] ?? null;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedSlot(slot) {
|
||||
this.$emit("selected", slot);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.interval = setTimeout(() => {
|
||||
this.updateStartDate();
|
||||
}, 1000 * 60);
|
||||
this.updateStartDate();
|
||||
setTimeout(() => {
|
||||
this.animations = true;
|
||||
}, 1000);
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.interval) {
|
||||
clearTimeout(this.interval);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateStartDate() {
|
||||
const now = new Date();
|
||||
now.setMinutes(0);
|
||||
now.setSeconds(0);
|
||||
now.setMilliseconds(0);
|
||||
this.startDate = now;
|
||||
},
|
||||
filterSlots(slots: UiForecastSlot[] = []) {
|
||||
if (!slots) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return slots.filter(
|
||||
(slot) =>
|
||||
new Date(slot.end) >= this.startDate && new Date(slot.start) <= this.endDate
|
||||
);
|
||||
},
|
||||
filterEntries(entries: UiTimeseriesEntry[] = []) {
|
||||
return entries.filter(
|
||||
(entry) =>
|
||||
new Date(entry.ts) >= this.startDate && new Date(entry.ts) <= this.endDate
|
||||
);
|
||||
},
|
||||
onMouseLeave() {
|
||||
this.selectIndex(null, true);
|
||||
},
|
||||
selectIndex(index: number | null, timeout = false) {
|
||||
if (this.ignoreEvents) return;
|
||||
this.selectedIndex = index;
|
||||
|
||||
// reset hover state (points, highlights)
|
||||
if (this.selectedIndex === null) {
|
||||
this.$nextTick(() => {
|
||||
// @ts-expect-error unknown chart type
|
||||
this.$refs.chart?.chart?.setActiveElements([]);
|
||||
});
|
||||
}
|
||||
|
||||
// ignore events after selection reset because chart.js triggers delayed mousemove events
|
||||
if (timeout) {
|
||||
this.ignoreEvents = true;
|
||||
this.ignoreEventsTimeout = setTimeout(() => {
|
||||
this.ignoreEvents = false;
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
yMax(slots: UiForecastSlot[] = []): number | undefined {
|
||||
const max = this.maxValue(slots);
|
||||
if (!max) return undefined;
|
||||
const fixedValues = slots.every((slot) => slot.value === max);
|
||||
// add space to the top of the scale; shrink fixed-value datasets, they are not interesting and should not dominate the chart
|
||||
const topSpace = fixedValues ? 3 : 1.15;
|
||||
return max * topSpace;
|
||||
},
|
||||
yMaxEntry(entries: UiTimeseriesEntry[] = [], scale: number = 1): number | undefined {
|
||||
const maxValue = this.maxEntryValue(entries);
|
||||
if (!maxValue) return undefined;
|
||||
// use scale and unscaled to determine max scale
|
||||
return Math.max(maxValue * scale, maxValue) * 1.15;
|
||||
},
|
||||
maxIndex(slots: UiForecastSlot[] = []) {
|
||||
return slots.reduce((max, slot, index) => {
|
||||
return slot.value > (slots[max]?.value || 0) ? index : max;
|
||||
}, 0);
|
||||
},
|
||||
minIndex(slots: UiForecastSlot[] = []) {
|
||||
return slots.reduce((min, slot, index) => {
|
||||
return slot.value < (slots[min]?.value || 0) ? index : min;
|
||||
}, 0);
|
||||
},
|
||||
maxValue(slots: UiForecastSlot[] = []) {
|
||||
return slots[this.maxIndex(slots)]?.value || null;
|
||||
},
|
||||
maxEntryValue(entries: UiTimeseriesEntry[] = []) {
|
||||
return entries[this.maxEntryIndex(entries)]?.val || null;
|
||||
},
|
||||
maxEntryIndex(entries: UiTimeseriesEntry[] = []) {
|
||||
return entries.reduce((max, entry, index) => {
|
||||
return entry.val > (entries[max]?.val || 0) ? index : max;
|
||||
}, 0);
|
||||
},
|
||||
yScaleOptions(type: ForecastType) {
|
||||
return type === this.selected
|
||||
? {
|
||||
display: true,
|
||||
ticks: { display: false },
|
||||
border: { display: false },
|
||||
grid: {
|
||||
display: true,
|
||||
color: colors.border,
|
||||
// @ts-expect-error no-explicit-any
|
||||
lineWidth: (context) => {
|
||||
return context.tick?.value === 0 ? 1 : 0;
|
||||
},
|
||||
},
|
||||
}
|
||||
: { display: false };
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { defineComponent, markRaw } from "vue";
|
||||
import { echarts } from "./echarts";
|
||||
import { echarts, registerTouchTooltipReset } from "./echarts";
|
||||
import "./chartStyles.css";
|
||||
|
||||
// chartOption is provided by each consuming component's computed
|
||||
|
|
@ -63,12 +63,7 @@ export default defineComponent({
|
|||
this.chart.on("hideTip", () => {
|
||||
this.tooltipVisible = false;
|
||||
});
|
||||
const resetTouch = () => {
|
||||
this.chart?.dispatchAction({ type: "hideTip" });
|
||||
this.tooltipVisible = false;
|
||||
};
|
||||
el.addEventListener("touchend", resetTouch);
|
||||
el.addEventListener("touchcancel", resetTouch);
|
||||
registerTouchTooltipReset(this.chart, el, () => (this.tooltipVisible = false));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -117,6 +117,20 @@ export function tooltipStyle(
|
|||
};
|
||||
}
|
||||
|
||||
// hide tooltip when finger lifts; mouse hover unchanged
|
||||
export function registerTouchTooltipReset(
|
||||
chart: Pick<echarts.ECharts, "dispatchAction">,
|
||||
el: HTMLElement,
|
||||
onReset?: () => void
|
||||
) {
|
||||
const reset = () => {
|
||||
chart.dispatchAction({ type: "hideTip" });
|
||||
onReset?.();
|
||||
};
|
||||
el.addEventListener("touchend", reset);
|
||||
el.addEventListener("touchcancel", reset);
|
||||
}
|
||||
|
||||
export interface TooltipRow {
|
||||
name?: string;
|
||||
values: string[];
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
FONT_FAMILY,
|
||||
forecastGrid,
|
||||
forecastYAxis,
|
||||
registerTouchTooltipReset,
|
||||
tooltipStyle,
|
||||
tooltipTable,
|
||||
xAxisLabelStyle,
|
||||
|
|
@ -719,6 +720,7 @@ export default defineComponent({
|
|||
const zr = this.chart.getZr();
|
||||
zr.on("mousemove", this.onChartMouseMove);
|
||||
zr.on("globalout", this.clearHighlight);
|
||||
registerTouchTooltipReset(this.chart, el, this.clearHighlight);
|
||||
this.mediaQuery = window.matchMedia("(max-width: 575.98px)");
|
||||
this.isMobile = this.mediaQuery.matches;
|
||||
this.mediaQuery.addEventListener("change", this.onMediaChange);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import formatter from "@/mixins/formatter";
|
|||
import colors from "@/colors";
|
||||
import LegendList from "../Sessions/LegendList.vue";
|
||||
import type { Legend } from "../Sessions/types";
|
||||
import { touchDismissPlugin } from "../Sessions/chartConfig";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
|
|
@ -48,7 +49,8 @@ ChartJS.register(
|
|||
PointElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
ChartLegendPlugin
|
||||
ChartLegendPlugin,
|
||||
touchDismissPlugin
|
||||
);
|
||||
|
||||
export default defineComponent({
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import formatter from "@/mixins/formatter";
|
|||
import colors from "@/colors";
|
||||
import LegendList from "../Sessions/LegendList.vue";
|
||||
import type { Legend } from "../Sessions/types";
|
||||
import { touchDismissPlugin } from "../Sessions/chartConfig";
|
||||
|
||||
const tension = 0;
|
||||
|
||||
|
|
@ -46,7 +47,8 @@ ChartJS.register(
|
|||
PointElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
ChartLegendPlugin
|
||||
ChartLegendPlugin,
|
||||
touchDismissPlugin
|
||||
);
|
||||
|
||||
export default defineComponent({
|
||||
|
|
|
|||
|
|
@ -41,8 +41,17 @@ import type { EvoptData } from "./TimeSeriesDataTable.vue";
|
|||
import type { CURRENCY, BatteryDetail } from "@/types/evcc";
|
||||
import formatter from "@/mixins/formatter";
|
||||
import colors from "@/colors";
|
||||
import { touchDismissPlugin } from "../Sessions/chartConfig";
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, ChartLegendPlugin);
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
ChartLegendPlugin,
|
||||
touchDismissPlugin
|
||||
);
|
||||
|
||||
export default defineComponent({
|
||||
name: "SocChart",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
Tooltip,
|
||||
type ChartComponentLike,
|
||||
type ChartType,
|
||||
type Plugin,
|
||||
type Point,
|
||||
type TooltipItem,
|
||||
} from "chart.js";
|
||||
|
|
@ -20,6 +21,22 @@ Chart.defaults.font.family = window
|
|||
Chart.defaults.font.size = 14;
|
||||
Chart.defaults.layout.padding = 0;
|
||||
|
||||
// hide tooltip when finger lifts; mouse hover unchanged
|
||||
export const touchDismissPlugin: Plugin = {
|
||||
id: "touchDismiss",
|
||||
afterInit(chart) {
|
||||
const dismiss = () => {
|
||||
if (!chart.ctx) return; // destroyed
|
||||
chart.setActiveElements([]);
|
||||
chart.tooltip?.setActiveElements([], { x: 0, y: 0 });
|
||||
chart.update();
|
||||
};
|
||||
chart.canvas.addEventListener("touchend", dismiss);
|
||||
chart.canvas.addEventListener("touchcancel", dismiss);
|
||||
},
|
||||
};
|
||||
Chart.register(touchDismissPlugin);
|
||||
|
||||
// Custom tooltip positioners
|
||||
(Tooltip.positioners as any)["center"] = function () {
|
||||
const { chart } = this;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue