Sessions UI: visualization, month/year charts (#15902)

This commit is contained in:
Michael Geers 2024-10-21 12:13:45 +02:00 • committed by GitHub
parent 1bd521fc24
commit cfa35f295b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 3271 additions and 715 deletions

View file

@ -0,0 +1,237 @@
<template>
<div>
<div style="position: relative; height: 300px" class="my-3">
<Bar :data="chartData" :options="options" />
</div>
<LegendList :legends="legends" />
</div>
</template>
<script>
import { Bar } from "vue-chartjs";
import { BarController, BarElement, CategoryScale, LinearScale, Legend, Tooltip } from "chart.js";
import { registerChartComponents, commonOptions, tooltipLabelColor } from "./chartConfig";
import LegendList from "./LegendList.vue";
import formatter, { POWER_UNIT } from "../../mixins/formatter";
import colors from "../../colors";
import { GROUPS, PERIODS } from "./types";
registerChartComponents([BarController, BarElement, CategoryScale, LinearScale, Legend, Tooltip]);
export default {
name: "EnergyHistoryChart",
components: { Bar, LegendList },
props: {
sessions: { type: Array, default: () => [] },
groupBy: { type: String, default: GROUPS.NONE },
period: { type: String, default: PERIODS.TOTAL },
colorMappings: { type: Object, default: () => ({ loadpoint: {}, vehicle: {} }) },
},
mixins: [formatter],
computed: {
firstDay() {
if (this.sessions.length === 0) {
return null;
}
return new Date(this.sessions[0].created);
},
month() {
return this.firstDay?.getMonth() + 1;
},
year() {
return this.firstDay?.getFullYear();
},
lastDay() {
if (this.sessions.length === 0) {
return null;
}
return new Date(this.sessions[this.sessions.length - 1].created);
},
chartData() {
console.log("update energy history data");
const result = {};
const groups = new Set();
if (this.sessions.length > 0) {
//const lastDay = new Date(this.year, this.month, 0);
//const daysInMonth = this.lastDay.getDate();
let xFrom, xTo;
if (this.period === PERIODS.TOTAL) {
xFrom = this.firstDay.getFullYear();
xTo = this.lastDay.getFullYear();
} else if (this.period === PERIODS.YEAR) {
xFrom = 1;
xTo = 12;
} else {
xFrom = 1;
xTo = new Date(
this.lastDay.getFullYear(),
this.lastDay.getMonth() + 1,
0
).getDate();
}
// initialize result with empty arrays
for (let i = xFrom; i <= xTo; i++) {
result[i] = {};
}
// Populate with actual data
this.sessions.forEach((session) => {
let index;
const date = new Date(session.created);
if (this.period === PERIODS.MONTH) {
index = date.getDate();
} else if (this.period === PERIODS.YEAR) {
index = date.getMonth() + 1;
} else {
index = date.getFullYear();
}
if (this.groupBy === GROUPS.NONE) {
groups.add("grid");
groups.add("self");
const charged = session.chargedEnergy;
const self = (charged / 100) * session.solarPercentage;
const grid = charged - self;
result[index].self = (result[index].self || 0) + self;
result[index].grid = (result[index].grid || 0) + grid;
} else {
const groupKey = session[this.groupBy];
groups.add(groupKey);
result[index][groupKey] =
(result[index][groupKey] || 0) + session.chargedEnergy;
}
});
}
const datasets = Array.from(groups).map((group) => {
const colorGroup = this.groupBy === GROUPS.NONE ? "solar" : this.groupBy;
const backgroundColor = this.colorMappings[colorGroup][group];
const label =
this.groupBy === GROUPS.NONE ? this.$t(`sessions.group.${group}`) : group;
return {
backgroundColor,
label,
data: Object.values(result).map((day) => day[group] || 0),
borderRadius: (context) => {
const threshold = 0.04; // 400 Wh
const { dataIndex, datasetIndex } = context;
const currentValue = context.dataset.data[dataIndex];
const previousValuesExist = context.chart.data.datasets
.slice(datasetIndex + 1)
.some((dataset) => (dataset?.data[dataIndex] || 0) > threshold);
return currentValue > threshold && !previousValuesExist
? { topLeft: 10, topRight: 10 }
: { topLeft: 0, topRight: 0 };
},
};
});
return {
labels: Object.keys(result),
datasets: datasets,
};
},
legends() {
return this.chartData.datasets.map((dataset) => ({
label: dataset.label,
color: dataset.backgroundColor,
value: this.fmtWh(
dataset.data.reduce((acc, curr) => acc + curr, 0) * 1e3,
POWER_UNIT.AUTO
),
}));
},
options() {
return {
...commonOptions,
locale: this.$i18n?.locale,
color: colors.text,
borderSkipped: false,
maxBarThickness: 40,
animation: false,
plugins: {
...commonOptions.plugins,
tooltip: {
...commonOptions.plugins.tooltip,
mode: "index",
intersect: false,
positioner: (context) => {
const { chart, tooltipPosition } = context;
const { tooltip } = chart;
const { width, height } = tooltip;
const { x, y } = tooltipPosition();
const { innerWidth, innerHeight } = window;
return {
x: Math.min(x, innerWidth - width),
y: Math.min(y, innerHeight - height),
};
},
callbacks: {
title: (tooltipItem) => {
const { label } = tooltipItem[0];
if (this.period === PERIODS.TOTAL) {
return label;
} else if (this.period === PERIODS.YEAR) {
const date = new Date(this.year, label - 1, 1);
return this.fmtMonth(date);
} else {
const date = new Date(this.year, this.month, label);
return this.fmtDayMonth(date);
}
},
label: (tooltipItem) => {
const datasetLabel = tooltipItem.dataset.label || "";
const value = tooltipItem.raw || 0;
return (
datasetLabel + ": " + this.fmtWh(value * 1e3, POWER_UNIT.AUTO)
);
},
labelColor: tooltipLabelColor(false),
labelPointStyle: function () {
return {
pointStyle: "circle",
};
},
labelTextColor: (item) => {
return !item.raw ? colors.muted : "#fff";
},
},
itemSort: function (a, b) {
return b.datasetIndex - a.datasetIndex;
},
},
},
scales: {
x: {
stacked: true,
border: { display: false },
grid: { display: false },
ticks: { color: colors.muted },
},
y: {
stacked: true,
border: { display: false },
grid: { color: colors.border },
title: {
text: "kWh",
display: true,
color: colors.muted,
},
ticks: {
callback: (value) => this.fmtWh(value * 1e3, POWER_UNIT.KW, false, 0),
color: colors.muted,
maxTicksLimit: 6,
},
position: "right",
min: 0,
},
},
};
},
},
};
</script>