2023-06-11 18:29:31 +02:00
|
|
|
const { STORE_KEYS } = require("../model/stores");
|
2023-06-15 21:12:01 +02:00
|
|
|
const { settings } = require("../model");
|
2023-06-20 00:22:22 +02:00
|
|
|
const { today, log, deltaTime, isMobile } = require("../js/misc");
|
2023-06-11 18:29:31 +02:00
|
|
|
const { View } = require("./view");
|
|
|
|
require("./custom-checkbox");
|
2023-06-14 04:06:25 +02:00
|
|
|
const moment = require("moment");
|
2023-06-11 18:29:31 +02:00
|
|
|
const { Chart, registerables } = require("chart.js");
|
2023-06-14 04:06:25 +02:00
|
|
|
require("chartjs-adapter-moment");
|
2023-06-11 18:29:31 +02:00
|
|
|
Chart.register(...registerables);
|
|
|
|
|
|
|
|
class ItemsChart extends View {
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
|
2023-06-16 16:01:13 +02:00
|
|
|
this.unitPrice = false;
|
2023-06-11 18:29:31 +02:00
|
|
|
this.innerHTML = /*html*/ `
|
2023-06-20 00:22:22 +02:00
|
|
|
<div class="bg-stone-200 p-4 mx-auto md:rounded-none md:mb-0 rounded-xl mb-4">
|
|
|
|
<div class="w-full h-[calc(100vh*0.50)] md:h-[calc(100vh*0.60)] lg:h-[calc(100vh*0.60)]" style="position: relative;">
|
|
|
|
<canvas x-id="canvas" class="bg-white rounded-lg"></canvas>
|
2023-06-15 19:25:35 +02:00
|
|
|
<div x-id="noData" class="hidden flex items-center justify-center h-full">Keine Daten ausgewählt</div>
|
|
|
|
</div>
|
2023-06-11 18:29:31 +02:00
|
|
|
<div class="filters flex items-center flex-wrap justify-center gap-2 pt-2">
|
2023-06-11 23:49:18 +02:00
|
|
|
<custom-checkbox x-id="sumTotal" x-change x-state label="Preissumme Gesamt"></custom-checkbox>
|
|
|
|
<custom-checkbox x-id="sumStores" x-change x-state label="Preissumme Ketten"></custom-checkbox>
|
|
|
|
<custom-checkbox x-id="onlyToday" x-change x-state label="Nur heutige Preise"></custom-checkbox>
|
2023-06-11 18:29:31 +02:00
|
|
|
<div
|
|
|
|
class="cursor-pointer inline-flex items-center gap-x-1 rounded-full bg-white border border-gray-400 px-2 py-1 text-xs font-medium text-gray-600">
|
2023-06-14 17:07:02 +02:00
|
|
|
<input x-id="startDate" x-change x-state type="date" value="2017-01-01" />
|
2023-06-11 18:29:31 +02:00
|
|
|
-
|
2023-06-11 23:49:18 +02:00
|
|
|
<input x-id="endDate" x-change x-state type="date" value="${today()}"/>
|
2023-06-11 18:29:31 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`;
|
2023-06-15 21:12:01 +02:00
|
|
|
this.elements.startDate.value = settings.startDate;
|
2023-06-11 18:29:31 +02:00
|
|
|
this.setupEventHandlers();
|
2023-06-11 23:49:18 +02:00
|
|
|
this.addEventListener("x-change", () => {
|
2023-06-11 18:29:31 +02:00
|
|
|
this.render();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
calculateOverallPriceChanges(items, onlyToday, startDate, endDate) {
|
|
|
|
if (items.length == 0) return { dates: [], changes: [] };
|
|
|
|
|
2023-06-16 16:01:13 +02:00
|
|
|
const getPrice = this.unitPrice ? (o) => o.unitPrice : (o) => o.price;
|
|
|
|
|
2023-06-11 18:29:31 +02:00
|
|
|
if (onlyToday) {
|
|
|
|
let sum = 0;
|
2023-06-16 16:01:13 +02:00
|
|
|
for (const item of items) sum += getPrice(item);
|
2023-06-11 18:29:31 +02:00
|
|
|
return [{ date: today(), price: sum }];
|
|
|
|
}
|
|
|
|
|
2023-06-14 17:07:02 +02:00
|
|
|
const allDates = items.flatMap((product) =>
|
|
|
|
product.priceHistory.filter((price) => price.date >= startDate && price.date <= endDate).map((item) => item.date)
|
|
|
|
);
|
2023-06-11 18:29:31 +02:00
|
|
|
let uniqueDates = [...new Set(allDates)];
|
|
|
|
uniqueDates.sort();
|
|
|
|
|
2023-06-14 17:07:02 +02:00
|
|
|
let priceChanges = new Array(uniqueDates.length);
|
|
|
|
for (let i = 0; i < uniqueDates.length; i++) {
|
|
|
|
priceChanges[i] = { date: uniqueDates[i], price: 0 };
|
|
|
|
}
|
|
|
|
const priceScratch = new Array(uniqueDates.length);
|
|
|
|
items.forEach((product) => {
|
2023-06-11 18:29:31 +02:00
|
|
|
let price = null;
|
2023-06-14 17:07:02 +02:00
|
|
|
priceScratch.fill(null);
|
|
|
|
if (!product.priceHistoryLookup) {
|
|
|
|
product.priceHistoryLookup = {};
|
|
|
|
product.priceHistory.forEach((price) => (product.priceHistoryLookup[price.date] = price));
|
|
|
|
}
|
|
|
|
for (let i = 0; i < uniqueDates.length; i++) {
|
|
|
|
const priceObj = product.priceHistoryLookup[uniqueDates[i]];
|
2023-06-16 16:01:13 +02:00
|
|
|
if (!price && priceObj) price = getPrice(priceObj);
|
|
|
|
priceScratch[i] = priceObj ? getPrice(priceObj) : null;
|
2023-06-14 17:07:02 +02:00
|
|
|
}
|
2023-06-11 18:29:31 +02:00
|
|
|
|
2023-06-14 17:07:02 +02:00
|
|
|
for (let i = 0; i < priceScratch.length; i++) {
|
|
|
|
if (!priceScratch[i]) {
|
|
|
|
priceScratch[i] = price;
|
2023-06-11 18:29:31 +02:00
|
|
|
} else {
|
2023-06-14 17:07:02 +02:00
|
|
|
price = priceScratch[i];
|
2023-06-11 18:29:31 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-14 17:07:02 +02:00
|
|
|
for (let i = 0; i < priceScratch.length; i++) {
|
|
|
|
const price = priceScratch[i];
|
|
|
|
priceChanges[i].price += price;
|
2023-06-11 18:29:31 +02:00
|
|
|
}
|
2023-06-14 17:07:02 +02:00
|
|
|
});
|
2023-06-11 18:29:31 +02:00
|
|
|
|
2023-06-14 17:07:02 +02:00
|
|
|
priceChanges = priceChanges.filter((price) => price.date >= startDate && price.date <= endDate);
|
2023-06-11 18:29:31 +02:00
|
|
|
return priceChanges;
|
|
|
|
}
|
|
|
|
|
|
|
|
renderChart(items, chartType) {
|
2023-06-16 16:01:13 +02:00
|
|
|
const getPrice = this.unitPrice ? (o) => o.unitPrice : (o) => o.price;
|
2023-06-11 18:29:31 +02:00
|
|
|
const canvasDom = this.elements.canvas;
|
2023-06-15 19:25:35 +02:00
|
|
|
const noData = this.elements.noData;
|
2023-06-11 18:29:31 +02:00
|
|
|
if (items.length === 0) {
|
|
|
|
canvasDom.classList.add("hidden");
|
2023-06-15 19:25:35 +02:00
|
|
|
noData.classList.remove("hidden");
|
2023-06-11 18:29:31 +02:00
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
canvasDom.classList.remove("hidden");
|
2023-06-15 19:25:35 +02:00
|
|
|
noData.classList.add("hidden");
|
2023-06-11 18:29:31 +02:00
|
|
|
}
|
|
|
|
|
2023-06-14 04:06:25 +02:00
|
|
|
const startDate = this.elements.startDate.value;
|
|
|
|
const endDate = this.elements.endDate.value;
|
2023-06-11 18:29:31 +02:00
|
|
|
|
2023-06-14 17:07:02 +02:00
|
|
|
const now = performance.now();
|
2023-06-14 01:52:35 +02:00
|
|
|
const datasets = items.map((item) => {
|
2023-06-16 00:13:12 +02:00
|
|
|
const prices = item.priceHistory.filter((price) => price.date >= startDate && price.date <= endDate);
|
2023-06-14 04:06:25 +02:00
|
|
|
|
|
|
|
const dataset = {
|
2023-06-14 01:52:35 +02:00
|
|
|
label: (item.store ? item.store + " " : "") + item.name,
|
2023-06-16 00:13:12 +02:00
|
|
|
data: prices.map((price) => {
|
2023-06-14 04:06:25 +02:00
|
|
|
return {
|
|
|
|
x: moment(price.date),
|
2023-06-16 16:01:13 +02:00
|
|
|
y: getPrice(price),
|
2023-06-14 04:06:25 +02:00
|
|
|
};
|
|
|
|
}),
|
2023-06-11 18:29:31 +02:00
|
|
|
};
|
2023-06-15 21:12:01 +02:00
|
|
|
if (settings.chartType == "stepped") {
|
2023-06-16 00:48:51 +02:00
|
|
|
// I don't know why this is necessary...
|
|
|
|
if (dataset.label.startsWith("Preissumme")) dataset.stepped = "before";
|
|
|
|
else dataset.stepped = "after";
|
2023-06-15 21:12:01 +02:00
|
|
|
}
|
2023-06-14 04:06:25 +02:00
|
|
|
|
|
|
|
return dataset;
|
2023-06-11 18:29:31 +02:00
|
|
|
});
|
2023-06-14 17:07:02 +02:00
|
|
|
log("ItemsChart - Calculating datasets took " + ((performance.now() - now) / 1000).toFixed(2) + " secs");
|
2023-06-11 18:29:31 +02:00
|
|
|
|
|
|
|
const ctx = canvasDom.getContext("2d");
|
|
|
|
let scrollTop = -1;
|
|
|
|
if (canvasDom.lastChart) {
|
|
|
|
scrollTop = document.documentElement.scrollTop;
|
|
|
|
canvasDom.lastChart.destroy();
|
|
|
|
}
|
|
|
|
canvasDom.lastChart = new Chart(ctx, {
|
|
|
|
type: chartType ? chartType : "line",
|
|
|
|
data: {
|
|
|
|
datasets: datasets,
|
|
|
|
},
|
|
|
|
options: {
|
2023-06-14 00:09:17 +02:00
|
|
|
layout: {
|
2023-06-20 00:22:22 +02:00
|
|
|
padding: 16,
|
2023-06-14 00:09:17 +02:00
|
|
|
},
|
2023-06-15 19:25:35 +02:00
|
|
|
animation: false,
|
2023-06-11 18:29:31 +02:00
|
|
|
responsive: true,
|
2023-06-15 19:25:35 +02:00
|
|
|
maintainAspectRatio: false,
|
2023-06-11 18:29:31 +02:00
|
|
|
scales: {
|
2023-06-14 04:06:25 +02:00
|
|
|
x: {
|
|
|
|
type: "time",
|
|
|
|
adapters: {
|
|
|
|
date: moment,
|
|
|
|
},
|
|
|
|
time: {
|
|
|
|
unit: "day",
|
|
|
|
displayFormats: {
|
|
|
|
day: "YYYY-MM-D",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
title: {
|
|
|
|
display: true,
|
|
|
|
text: "Date",
|
|
|
|
},
|
|
|
|
},
|
2023-06-11 18:29:31 +02:00
|
|
|
y: {
|
2023-06-14 00:09:17 +02:00
|
|
|
ticks: {
|
|
|
|
callback: function (value, index, ticks) {
|
|
|
|
return value.toLocaleString("de-DE", {
|
|
|
|
minimumFractionDigits: 2,
|
|
|
|
style: "currency",
|
|
|
|
currency: "EUR",
|
|
|
|
});
|
|
|
|
},
|
2023-06-11 18:29:31 +02:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
if (scrollTop != -1) document.documentElement.scrollTop = scrollTop;
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
if (!this.model) return;
|
2023-06-11 23:49:18 +02:00
|
|
|
const start = performance.now();
|
2023-06-11 18:29:31 +02:00
|
|
|
const items = this.model.filteredItems;
|
|
|
|
const elements = this.elements;
|
|
|
|
const onlyToday = this.elements.onlyToday.checked;
|
|
|
|
const startDate = this.elements.startDate.value;
|
|
|
|
const endDate = this.elements.endDate.value;
|
|
|
|
const itemsToShow = [];
|
|
|
|
|
|
|
|
if (elements.sumTotal.checked && items.length > 0) {
|
2023-06-14 17:07:02 +02:00
|
|
|
const now = performance.now();
|
2023-06-11 18:29:31 +02:00
|
|
|
itemsToShow.push({
|
|
|
|
name: "Preissumme Gesamt",
|
|
|
|
priceHistory: this.calculateOverallPriceChanges(items, onlyToday, startDate, endDate),
|
|
|
|
});
|
2023-06-14 17:07:02 +02:00
|
|
|
log("ItemsChart - Calculating overall sum total " + ((performance.now() - now) / 1000).toFixed(2) + " secs");
|
2023-06-11 18:29:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (elements.sumStores.checked && items.length > 0) {
|
2023-06-14 17:07:02 +02:00
|
|
|
const now = performance.now();
|
2023-06-11 18:29:31 +02:00
|
|
|
STORE_KEYS.forEach((store) => {
|
|
|
|
const storeItems = items.filter((item) => item.store === store);
|
|
|
|
if (storeItems.length > 0) {
|
|
|
|
itemsToShow.push({
|
|
|
|
name: "Preissumme " + store,
|
|
|
|
priceHistory: this.calculateOverallPriceChanges(storeItems, onlyToday, startDate, endDate),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2023-06-14 17:07:02 +02:00
|
|
|
log("ItemsChart - Calculating overall sum per store took " + ((performance.now() - now) / 1000).toFixed(2) + " secs");
|
2023-06-11 18:29:31 +02:00
|
|
|
}
|
|
|
|
|
2023-06-16 16:01:13 +02:00
|
|
|
const getPrice = this.unitPrice ? (o) => o.unitPrice : (o) => o.price;
|
2023-06-11 18:29:31 +02:00
|
|
|
items.forEach((item) => {
|
|
|
|
if (item.chart) {
|
2023-06-14 04:06:25 +02:00
|
|
|
const chartItem = {
|
2023-06-11 18:29:31 +02:00
|
|
|
name: item.store + " " + item.name,
|
2023-06-16 16:01:13 +02:00
|
|
|
priceHistory: onlyToday ? [{ date: today(), price: getPrice(item) }] : item.priceHistory,
|
2023-06-14 04:06:25 +02:00
|
|
|
};
|
|
|
|
itemsToShow.push(chartItem);
|
2023-06-11 18:29:31 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.renderChart(itemsToShow, onlyToday ? "bar" : "line");
|
2023-06-11 23:49:18 +02:00
|
|
|
log(`ItemsChart - charted ${itemsToShow.length} items in ${deltaTime(start).toFixed(2)} secs`);
|
2023-06-11 18:29:31 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
customElements.define("items-chart", ItemsChart);
|