Skip to content

Commit

Permalink
wip chart
Browse files Browse the repository at this point in the history
  • Loading branch information
naltatis committed Feb 11, 2025
1 parent 17bc7ea commit 5b03c0c
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 69 deletions.
4 changes: 3 additions & 1 deletion assets/js/colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const dimColor = (color) => {
};

export const lighterColor = (color) => {
return color.toLowerCase().replace(/ff$/, "99");
return color.toLowerCase().replace(/ff$/, "aa");
};

export const fullColor = (color) => {
Expand All @@ -63,6 +63,8 @@ function updateCssColors() {
// update colors on theme change
const darkModeMatcher = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)");
darkModeMatcher?.addEventListener("change", updateCssColors);
// initialize colors
updateCssColors();
window.requestAnimationFrame(updateCssColors);

export default colors;
4 changes: 2 additions & 2 deletions assets/js/components/Energyflow/Energyflow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ import AnimatedNumber from "../AnimatedNumber.vue";
import settings from "../../settings";
import { CO2_TYPE } from "../../units";
import collector from "../../mixins/collector";
import { todaysEnergy } from "../../utils/forecast";
import { energyByDay } from "../../utils/forecast";
export default {
name: "Energyflow",
components: {
Expand Down Expand Up @@ -392,7 +392,7 @@ export default {
solarForecastToday() {
const slots = this.forecast.solar;
if (!slots?.length) return null;
return todaysEnergy(slots);
return energyByDay(slots, 0);
},
},
watch: {
Expand Down
124 changes: 84 additions & 40 deletions assets/js/components/ForecastChart.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<template>
<div>
<div style="position: relative; height: 300px" class="my-3">
<Bar :data="chartData" :options="options" />
<div class="overflow-x-auto overflow-x-md-auto chart-container">
<div style="position: relative; height: 220px" class="chart">
<Bar :data="chartData" :options="options" />
</div>
</div>
</div>
</template>
Expand All @@ -21,11 +23,13 @@ import {
PointElement,
Filler,
} from "chart.js";
import ChartDataLabels 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 { PriceSlot } from "../utils/forecast";
import { energyByDay, highestSlotIndexByDay, type PriceSlot } from "../utils/forecast";
registerChartComponents([
BarController,
BarElement,
Expand All @@ -37,6 +41,7 @@ registerChartComponents([
Legend,
Tooltip,
PointElement,
ChartDataLabels,
]);
export default defineComponent({
Expand Down Expand Up @@ -67,45 +72,60 @@ export default defineComponent({
gridSlots() {
return this.filterSlots(this.grid);
},
maxPriceIndex() {
return this.gridSlots.reduce((max, slot, index) => {
return slot.price > this.gridSlots[max].price ? index : max;
}, 0);
},
minPriceIndex() {
return this.gridSlots.reduce((min, slot, index) => {
return slot.price < this.gridSlots[min].price ? index : min;
}, 0);
},
solarHighlights() {
return [0, 1, 2].map((day) => {
const energy = energyByDay(this.solarSlots, day);
const index = highestSlotIndexByDay(this.solarSlots, day);
return { index, energy };
});
},
chartData() {
const datasets = [];
const vThis = this;

Check failure on line 93 in assets/js/components/ForecastChart.vue

View workflow job for this annotation

GitHub Actions / UI

Unexpected aliasing of 'this' to local variable

Check failure on line 93 in assets/js/components/ForecastChart.vue

View workflow job for this annotation

GitHub Actions / UI

'vThis' is assigned a value but never used
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const datasets: any[] = [];
if (this.solarSlots.length > 0) {
datasets.push({
label: "solar forecast",
label: "solar",
type: "line",
data: this.solarSlots.map((slot) => ({
data: this.solarSlots.map((slot, index) => ({
y: slot.price,
x: new Date(slot.start),
highlight: this.solarHighlights.find(({ index: i }) => i === index)?.energy,
})),
yAxisID: "yForecast",
backgroundColor: lighterColor(colors.self),
borderColor: colors.self,
fill: "origin",
tension: 0.25,
tension: 0.5,
pointRadius: 0,
pointHoverRadius: 6,
borderWidth: 2,
spanGaps: true,
});
}
if (this.gridSlots.length > 0) {
datasets.push({
label: "grid price",
data: this.gridSlots.map((slot) => ({
label: "price",
data: this.gridSlots.map((slot, index) => ({
y: slot.price,
x: new Date(slot.start),
highlight: index === this.maxPriceIndex || index === this.minPriceIndex,
})),
yAxisID: "yPrice",
borderRadius: 2,
borderRadius: 8,
backgroundColor: colors.light,
borderColor: colors.light,
});
}
return {
labels: Array.from(
{ length: 48 },
(_, i) => new Date(this.startDate.getTime() + i * 60 * 60 * 1000)
),
datasets,
};
},
Expand All @@ -114,15 +134,38 @@ export default defineComponent({
return {
...commonOptions,
locale: this.$i18n?.locale,
layout: { padding: { top: 32 } },
color: colors.text,
borderSkipped: false,
maxBarThickness: 40,
animation: false,
interaction: {
mode: "nearest",
},
categoryPercentage: 0.7,
plugins: {
...commonOptions.plugins,
datalabels: {
backgroundColor: function (context) {
return context.dataset.borderColor;
},
align: "end",
anchor: "end",
borderRadius: 4,
color: "white",
font: {
weight: "bold",
},
formatter: function (data, ctx) {
if (data.highlight) {
if (ctx.dataset.label === "price") {
return vThis.fmtPricePerKWh(data.y, vThis.currency, true, true);
}
if (ctx.dataset.label === "solar") {
return vThis.fmtWh(data.highlight, POWER_UNIT.AUTO);
}
return null;
}
return null;
},
padding: 6,
},
tooltip: {
...commonOptions.plugins.tooltip,
axis: "x",
Expand Down Expand Up @@ -175,6 +218,7 @@ export default defineComponent({
grid: {
display: true,
color: colors.border,
offset: false,
lineWidth: function (context) {
if (context.type !== "tick") {
return 0;
Expand All @@ -188,15 +232,11 @@ export default defineComponent({
autoSkip: false,
maxRotation: 0,
minRotation: 0,
source: "data",
align: "center",
callback: function (value) {
const date = new Date(value);
const hour = date.getHours();
const mins = date.getMinutes();
console.log(date, hour, mins);
if (mins !== 0) {
return "";
}
if (hour === 0) {
return [hour, vThis.weekdayShort(date)];
}
Expand All @@ -208,37 +248,29 @@ export default defineComponent({
},
},
yForecast: {
display: false,
border: { display: false },
grid: { color: colors.border },
title: {
text: "kW",
display: true,
color: colors.muted,
},
grid: { color: colors.border, drawOnChartArea: false },
ticks: {
callback: (value) => this.fmtW(value, POWER_UNIT.KW, false),
callback: (value) => this.fmtW(value, POWER_UNIT.KW, true),
color: colors.muted,
maxTicksLimit: 6,
maxTicksLimit: 3,
},
position: "right",
min: 0,
},
yPrice: {
title: {
text: this.pricePerKWhUnit(this.currency),
display: true,
color: colors.muted,
},
display: false,
border: { display: false },
grid: {
color: colors.border,
drawOnChartArea: false,
},
ticks: {
callback: (value) =>
this.fmtPricePerKWh(value, this.currency, true, false),
this.fmtPricePerKWh(value, this.currency, true, true),
color: colors.muted,
maxTicksLimit: 6,
maxTicksLimit: 3,
},
position: "left",
},
Expand All @@ -253,3 +285,15 @@ export default defineComponent({
},
});
</script>

<style scoped>
.chart {
width: 780px;
}
@media (min-width: 992px) {
.chart {
width: 100%;
}
}
</style>
58 changes: 45 additions & 13 deletions assets/js/components/ForecastModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,28 @@
@open="modalVisible"
@closed="modalInvisible"
>
<ForecastChart :grid="forecast.grid" :solar="forecast.solar" :co2="forecast.co2" />
<div class="d-flex justify-content-start">
<IconSelectGroup>
<IconSelectItem
v-for="type in types"
:key="type"
:active="selectedType === type"
:label="$t(`forecast.type.${type}`)"
hideLabelOnMobile
@click="updateType(type)"
>
<component :is="typeIcons[type]"></component>
</IconSelectItem>
</IconSelectGroup>
</div>

<h6>Solar</h6>
<p>{{ solarToday }}<br />{{ solarTomorrow }}</p>
<ForecastChart
class="my-3"
:grid="forecast.grid"
:solar="forecast.solar"
:co2="forecast.co2"
:currency="currency"
/>
</GenericModal>
</template>

Expand All @@ -19,18 +37,28 @@ import { defineComponent } from "vue";
import type { PropType } from "vue";
import GenericModal from "./GenericModal.vue";
import ForecastChart from "./ForecastChart.vue";
import { todaysEnergy, tomorrowsEnergy, type PriceSlot } from "../utils/forecast";
import formatter, { POWER_UNIT } from "../mixins/formatter";
import IconSelectItem from "./IconSelectItem.vue";
import IconSelectGroup from "./IconSelectGroup.vue";
import DynamicPriceIcon from "./MaterialIcon/DynamicPrice.vue";
import { type PriceSlot } from "../utils/forecast";
import formatter from "../mixins/formatter";
interface Forecast {
grid?: PriceSlot[];
solar?: PriceSlot[];
co2?: PriceSlot[];
currency?: string;
}
export const TYPES = {
SOLAR: "solar",
PRICE: "price",
CO2: "co2",
};
export default defineComponent({
name: "ForecastModal",
components: { GenericModal, ForecastChart },
components: { GenericModal, ForecastChart, IconSelectItem, IconSelectGroup },
mixins: [formatter],
props: {
forecast: { type: Object as PropType<Forecast>, default: () => ({}) },
Expand All @@ -39,6 +67,8 @@ export default defineComponent({
data: function () {
return {
isModalVisible: false,
selectedType: TYPES.PRICE,
types: Object.values(TYPES),
};
},
computed: {
Expand All @@ -48,13 +78,12 @@ export default defineComponent({
solarSlots() {
return this.forecast?.solar || [];
},
solarToday() {
const energy = this.fmtWh(todaysEnergy(this.solarSlots), POWER_UNIT.KW);
return `${energy} remaining today`;
},
solarTomorrow() {
const energy = this.fmtWh(tomorrowsEnergy(this.solarSlots), POWER_UNIT.KW);
return `${energy} tomorrow`;
typeIcons() {
return {
[TYPES.SOLAR]: "shopicon-regular-sun",
[TYPES.PRICE]: DynamicPriceIcon,
[TYPES.CO2]: "shopicon-regular-eco1",
};
},
},
methods: {
Expand All @@ -64,6 +93,9 @@ export default defineComponent({
modalInvisible: function () {
this.isModalVisible = false;
},
updateType: function (type: string) {
this.selectedType = type;
},
},
});
</script>
Expand Down
Loading

0 comments on commit 5b03c0c

Please sign in to comment.