Refactor items-chart.js, add support for percentage change charts. Closes #139

This commit is contained in:
Mario Zechner 2023-07-18 20:48:33 +02:00
parent 2dc7cebef7
commit 0dad9c34fb

View File

@ -25,6 +25,7 @@ class ItemsChart extends View {
<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>
<custom-checkbox x-id="percentageChange" x-change x-state label="Änderung in % seit"></custom-checkbox>
<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">
<input x-id="startDate" x-change x-state type="date" value="2017-01-01" />
@ -41,7 +42,69 @@ class ItemsChart extends View {
});
}
calculateOverallPriceChanges(items, onlyToday, startDate, endDate) {
uniqueDates(items, startDate, endDate) {
const allDates = items.flatMap((product) =>
product.priceHistory.filter((price) => price.date >= startDate && price.date <= endDate).map((item) => item.date)
);
let uniqueDates = new Set(allDates);
uniqueDates.add(startDate);
uniqueDates.add(endDate);
uniqueDates = [...uniqueDates];
uniqueDates.sort();
return uniqueDates;
}
calculateItemPriceTimeSeries(product, percentageChange, startDate, uniqueDates) {
const getPrice = this.unitPrice ? (o) => o.unitPrice : (o) => o.price;
const priceScratch = new Array(uniqueDates.length);
let startPrice = null;
const priceHistoryLookup = {};
priceScratch.fill(null);
if (!product.priceHistoryLookup) {
product.priceHistory.forEach((price) => {
priceHistoryLookup[price.date] = price;
if (!startPrice && price.date <= startDate) {
startPrice = getPrice(price);
}
});
}
if (startPrice == null) {
const firstPrice = product.priceHistory[product.priceHistory.length - 1];
startPrice = getPrice(firstPrice);
}
for (let i = 0; i < uniqueDates.length; i++) {
const priceObj = priceHistoryLookup[uniqueDates[i]];
priceScratch[i] = priceObj ? getPrice(priceObj) : null;
}
for (let i = 0; i < priceScratch.length; i++) {
if (priceScratch[i] == null) {
priceScratch[i] = startPrice;
} else {
startPrice = priceScratch[i];
}
}
if (priceScratch.some((price) => price == null)) {
return null;
}
if (percentageChange) {
const firstPrice = priceScratch.find((price) => price != 0);
if (firstPrice == 0) return null;
for (let i = 0; i < priceScratch.length; i++) {
priceScratch[i] = ((priceScratch[i] - firstPrice) / firstPrice) * 100;
}
}
if (priceScratch.some((price) => isNaN(price))) {
return null;
}
return priceScratch;
}
calculateOverallPriceChanges(items, onlyToday, percentageChange, startDate, endDate) {
if (items.length == 0) return { dates: [], changes: [] };
const getPrice = this.unitPrice ? (o) => o.unitPrice : (o) => o.price;
@ -52,37 +115,18 @@ class ItemsChart extends View {
return [{ date: today(), price: sum }];
}
const allDates = items.flatMap((product) =>
product.priceHistory.filter((price) => price.date >= startDate && price.date <= endDate).map((item) => item.date)
);
let uniqueDates = [...new Set(allDates)];
uniqueDates.sort();
const dates = this.uniqueDates(items, startDate, endDate);
let priceChanges = new Array(uniqueDates.length);
for (let i = 0; i < uniqueDates.length; i++) {
priceChanges[i] = { date: uniqueDates[i], price: 0, unitPrice: 0 };
let priceChanges = new Array(dates.length);
for (let i = 0; i < dates.length; i++) {
priceChanges[i] = { date: dates[i], price: 0, unitPrice: 0 };
}
const priceScratch = new Array(uniqueDates.length);
items.forEach((product) => {
let price = null;
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]];
if (!price && priceObj) price = getPrice(priceObj);
priceScratch[i] = priceObj ? getPrice(priceObj) : null;
}
for (let i = 0; i < priceScratch.length; i++) {
if (!priceScratch[i]) {
priceScratch[i] = price;
} else {
price = priceScratch[i];
}
}
let numItems = 0;
items.forEach((product) => {
const priceScratch = this.calculateItemPriceTimeSeries(product, percentageChange, startDate, dates);
if (priceScratch == null) return;
numItems++;
for (let i = 0; i < priceScratch.length; i++) {
const price = priceScratch[i];
@ -91,11 +135,22 @@ class ItemsChart extends View {
}
});
priceChanges = priceChanges.filter((price) => price.date >= startDate && price.date <= endDate);
if (percentageChange) {
for (let i = 0; i < priceChanges.length; i++) {
priceChanges[i].price /= numItems;
priceChanges[i].unitPrice /= numItems;
}
for (let i = 0; i < priceChanges.length; i++) {
priceChanges[i].price = priceChanges[i].price.toFixed(2);
priceChanges[i].unitPrice = priceChanges[i].unitPrice.toFixed(2);
}
}
return priceChanges;
}
renderChart(items, chartType) {
renderChart(items, chartType, startDate) {
const getPrice = this.unitPrice ? (o) => o.unitPrice : (o) => o.price;
const canvasDom = this.elements.canvas;
const noData = this.elements.noData;
@ -108,12 +163,11 @@ class ItemsChart extends View {
noData.classList.add("hidden");
}
const startDate = this.elements.startDate.value;
const endDate = this.elements.endDate.value;
const percentageChange = this.elements.percentageChange.checked;
const now = performance.now();
const datasets = items.map((item) => {
const prices = item.priceHistory.filter((price) => price.date >= startDate && price.date <= endDate);
const prices = item.priceHistory;
const dataset = {
label: (item.store ? item.store + " " : "") + item.name,
@ -125,9 +179,7 @@ class ItemsChart extends View {
}),
};
if (settings.chartType == "stepped") {
// I don't know why this is necessary...
if (dataset.label.startsWith("Preissumme")) dataset.stepped = "before";
else dataset.stepped = "after";
dataset.stepped = prices.length % 2 == 0 ? "after" : "before";
}
return dataset;
@ -140,6 +192,32 @@ class ItemsChart extends View {
scrollTop = document.documentElement.scrollTop;
canvasDom.lastChart.destroy();
}
let yAxis = {
ticks: {
callback: function (value, index, ticks) {
return value.toLocaleString("de-DE", {
minimumFractionDigits: 2,
style: "currency",
currency: "EUR",
});
},
},
};
if (percentageChange) {
yAxis = {
title: {
display: true,
text: "Änderung in % seit " + startDate,
},
ticks: {
callback: (value) => {
return value + "%";
},
},
};
}
canvasDom.lastChart = new Chart(ctx, {
type: chartType ? chartType : "line",
data: {
@ -169,17 +247,7 @@ class ItemsChart extends View {
text: "Date",
},
},
y: {
ticks: {
callback: function (value, index, ticks) {
return value.toLocaleString("de-DE", {
minimumFractionDigits: 2,
style: "currency",
currency: "EUR",
});
},
},
},
y: yAxis,
},
},
});
@ -192,15 +260,19 @@ class ItemsChart extends View {
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;
let startDate = this.elements.startDate.value;
let endDate = this.elements.endDate.value;
let validDate = /^20\d{2}-\d{2}-\d{2}$/;
if (!validDate.test(startDate)) startDate = "2017-01-01";
if (!validDate.test(endDate)) endDate = today();
const percentageChange = this.elements.percentageChange.checked;
const itemsToShow = [];
if (elements.sumTotal.checked && items.length > 0) {
const now = performance.now();
itemsToShow.push({
name: "Preissumme Gesamt",
priceHistory: this.calculateOverallPriceChanges(items, onlyToday, startDate, endDate),
priceHistory: this.calculateOverallPriceChanges(items, onlyToday, percentageChange, startDate, endDate),
});
log("ItemsChart - Calculating overall sum total " + ((performance.now() - now) / 1000).toFixed(2) + " secs");
}
@ -212,25 +284,32 @@ class ItemsChart extends View {
if (storeItems.length > 0) {
itemsToShow.push({
name: "Preissumme " + store,
priceHistory: this.calculateOverallPriceChanges(storeItems, onlyToday, startDate, endDate),
priceHistory: this.calculateOverallPriceChanges(storeItems, onlyToday, percentageChange, startDate, endDate),
});
}
});
log("ItemsChart - Calculating overall sum per store took " + ((performance.now() - now) / 1000).toFixed(2) + " secs");
}
const getPrice = this.unitPrice ? (o) => o.unitPrice : (o) => o.price;
items.forEach((item) => {
if (item.chart) {
const dates = this.uniqueDates([item], startDate, endDate);
const prices = this.calculateItemPriceTimeSeries(item, percentageChange, startDate, dates).map((price) =>
percentageChange ? price.toFixed(2) : price
);
const priceHistory = [];
for (let i = 0; i < dates.length; i++) {
priceHistory.push({ date: dates[i], price: prices[i] });
}
const chartItem = {
name: item.store + " " + item.name,
priceHistory: onlyToday ? [{ date: today(), price: getPrice(item) }] : item.priceHistory,
priceHistory,
};
itemsToShow.push(chartItem);
}
});
this.renderChart(itemsToShow, onlyToday ? "bar" : "line");
this.renderChart(itemsToShow, onlyToday ? "bar" : "line", startDate);
log(`ItemsChart - charted ${itemsToShow.length} items in ${deltaTime(start).toFixed(2)} secs`);
}
}