mirror of
https://github.com/badlogic/heissepreise.git
synced 2024-06-04 15:55:30 +02:00
Refactor items-chart.js, add support for percentage change charts. Closes #139
This commit is contained in:
parent
2dc7cebef7
commit
0dad9c34fb
|
@ -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`);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user