Skip to content

Commit 61cc647

Browse files
committed
migrate ProductMoveRateGraph to uPlot
1 parent 4139806 commit 61cc647

7 files changed

Lines changed: 176 additions & 104 deletions

File tree

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@
7979
"sanitize-html": "^2.12.1",
8080
"svelte-confetti": "^2.3.2",
8181
"svelte-turnstile": "^0.8.0",
82-
"typesense": "^1.8.2"
82+
"typesense": "^1.8.2",
83+
"uplot": "^1.6.32",
84+
"uplot-svelte": "^1.2.4"
8385
},
8486
"imports": {
8587
"#app.css": "./src/app.css"

pnpm-lock.yaml

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22

3-
export function typed<Type>(): Type | undefined;
3+
export function typed<Type>(): Type;
44
export function typed<Type>(def: Type): Type;
5-
export function typed<Type>(def?: Type): Type | undefined {
6-
return def;
5+
export function typed<Type>(def?: Type): Type {
6+
return def as Type;
77
}

src/lib/lttstore/product/ProductMoveRateGraph.svelte

Lines changed: 94 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
<script lang="ts">
2-
import { run } from 'svelte/legacy';
3-
42
import { browser } from '$app/environment';
53
import { onMount } from 'svelte';
64
import { commas } from '$lib/utils.ts';
75
import { fade } from 'svelte/transition';
86
import { getTimePreference } from '$lib/prefUtils.ts';
97
import type { StockCounts } from '$lib/lttstore/lttstore_types.ts';
108
import { typed } from '$lib';
9+
import UplotSvelte from 'uplot-svelte';
10+
import uPlot from "uplot";
11+
import 'uplot/dist/uPlot.min.css';
12+
import { stockGaps, timeFormat, stockColors } from './ProductStockHistoryGraph.svelte';
1113
1214
let {
1315
productName = typed<string | undefined>(),
14-
stockHistory = typed<
16+
stockHistory: stockHistoryRaw = typed<
1517
{
1618
handle: string;
1719
id: number;
@@ -22,60 +24,94 @@
2224
chartUpdateNumber = typed<number>(1)
2325
} = $props();
2426
25-
let chart = $state();
27+
let stockHistory: {
28+
handle: string;
29+
id: number;
30+
timestamp: number;
31+
stock: StockCounts;
32+
}[] = $derived(stockHistoryRaw
33+
.map(h => ({
34+
...h,
35+
stock: JSON.parse(h.stock)
36+
}))
37+
.toSorted((a, b) => a.timestamp - b.timestamp)
38+
);
39+
40+
let someStock = $derived(
41+
Object.keys(stockHistory).length >= 1
42+
? stockHistory
43+
.map((h) => h.stock)
44+
.reduce((p, c) => {
45+
return {
46+
...c,
47+
...p
48+
};
49+
}, {})
50+
: {}
51+
);
2652
2753
let onlyTotalCheck = $state(false);
54+
// show only the total for items where the stock is just the default + the total
55+
let onlyTotal = $derived(Object.keys(someStock).length <= 2 || onlyTotalCheck);
56+
57+
let data = $derived.by(() => {
58+
const allTimestamps = new Set<number>(stockHistory.map(h => h.timestamp));
59+
return [
60+
[...allTimestamps].map(t => Math.round(t/1e3)), // uPlot expects epoch in seconds
61+
...(onlyTotal ? ["total"] : Object.keys(someStock))
62+
.map(k => stockHistory.map((h, i, a) => {
63+
if(i > 0) {
64+
const previous = a[i-1];
65+
const previousStock = previous.stock[k]
66+
const currentStock = h.stock[k];
67+
if(previousStock === null || currentStock === null) return null;
68+
const timeDiff = h.timestamp - previous.timestamp;
69+
const stockDiff = previousStock - currentStock;
70+
const rate = stockDiff / (timeDiff / (60 * 60e3));
71+
if(rate < 0) return null;
72+
return rate;
73+
} else {
74+
return null;
75+
}
76+
}))
77+
]
78+
});
2879
29-
function getSeries() {
30-
if(!onlyTotal) {
31-
return Object.keys(someStock).map(k => {
32-
return {
33-
name: k,
34-
data: stockHistory.map((h, i, a) => {
35-
if(i > 0) {
36-
const previous = a[i-1];
37-
const previousStock = JSON.parse(previous.stock)[k]
38-
const timeDiff = h.timestamp - previous.timestamp;
39-
const stockDiff = previousStock - JSON.parse(h.stock)[k]
40-
return {
41-
x: h.timestamp,
42-
y: stockDiff / (timeDiff / (60 * 60e3))
43-
}
44-
} else {
45-
return {
46-
x: h.timestamp,
47-
y: -1
48-
}
49-
}
50-
}).filter(h => h.y >= 0)
51-
}
52-
})
53-
} else {
54-
return [{
55-
name: "total",
56-
data: stockHistory.map((h, i, a) => {
57-
if(i > 0) {
58-
const previous = a[i-1];
59-
const previousStock = JSON.parse(previous.stock)["total"]
60-
const timeDiff = h.timestamp - previous.timestamp;
61-
const stockDiff = previousStock - JSON.parse(h.stock)["total"]
62-
return {
63-
x: h.timestamp,
64-
y: stockDiff / (timeDiff / (60 * 60e3))
65-
}
66-
} else {
67-
return {
68-
x: h.timestamp,
69-
y: -1
70-
}
71-
}
72-
}).filter(h => h.y >= 0)
73-
}]
74-
}
75-
}
7680
77-
const options = $state({
78-
chart: {
81+
const options: uPlot.Options = $derived({
82+
title: productName ? 'Move Rate History - ' + productName : 'Move Rate History',
83+
id: productName + "-move-rate-" + chartUpdateNumber,
84+
height: browser ? document.documentElement.clientHeight / 1.5 : 740,
85+
width: browser ? document.documentElement.clientWidth / 1.3 : 1490,
86+
series: [
87+
{
88+
label: "Time",
89+
// value: timeFormat,
90+
},
91+
...Object.keys(someStock).map((k, i) => ({
92+
show: true,
93+
gaps: stockGaps,
94+
label: k,
95+
value: (_, rawValue: number | null) => rawValue === null ? "" : commas(Math.round(rawValue))!,
96+
stroke: stockColors[i % stockColors.length],
97+
points: {
98+
show: false
99+
}
100+
}) satisfies uPlot.Series)
101+
],
102+
axes: [
103+
{
104+
stroke: "rgba(255, 255, 255, 0.5)",
105+
grid: {stroke: "rgba(255, 255, 255, 0.025)"},
106+
// values: ((self, splits) => splits.map(s => new Date(s))) satisfies uPlot.Axis.Values
107+
108+
},
109+
{
110+
stroke: "rgba(255, 255, 255, 0.5)",
111+
grid: {stroke: "rgba(255, 255, 255, 0.025)"},
112+
}
113+
]
114+
/*chart: {
79115
type: 'area',
80116
stacked: false,
81117
height: browser ? document.documentElement.clientHeight / 1.5 : undefined,
@@ -91,17 +127,12 @@
91127
enabled: false
92128
}
93129
},
94-
series: {},
95130
dataLabels: {
96131
enabled: false
97132
},
98133
markers: {
99134
size: 0
100135
},
101-
title: {
102-
text: productName ? 'Move Rate History - ' + productName : 'Move Rate History',
103-
align: 'left'
104-
},
105136
fill: {
106137
type: 'gradient',
107138
gradient: {
@@ -151,53 +182,20 @@
151182
},
152183
grid: {
153184
borderColor: '#535A6C'
154-
}
185+
}*/
155186
});
156187
157-
let chartDiv: HTMLDivElement = $state();
158-
159-
let ApexCharts;
160188
let mounted = $state(false);
161189
onMount(async () => {
162-
options.series = getSeries();
163-
mounted = true;
164-
ApexCharts = (await import('apexcharts')).default;
165-
chart = new ApexCharts(chartDiv, options);
166-
chart.render();
167-
168-
// console.log({options})
169-
});
170-
171-
// let style = browser ? : undefined;
172-
let someStock = $derived(
173-
Object.keys(stockHistory).length >= 1
174-
? stockHistory
175-
.map((h) => JSON.parse(h.stock ?? '{}') as StockCounts)
176-
.reduce((p, c) => {
177-
return {
178-
...c,
179-
...p
180-
};
181-
}, {})
182-
: {}
183-
);
184-
// show only the total for items where the stock is just the default + the total
185-
let onlyTotal = $derived(Object.keys(someStock).length <= 2 || onlyTotalCheck);
186-
run(() => {
187-
console.debug({ onlyTotal, length: Object.keys(someStock).length, someStock, stockHistory });
188-
});
189-
run(() => {
190-
onlyTotal;
191-
chartUpdateNumber;
192-
options.series = getSeries();
193-
// console.debug("Series:", options.series)
194-
if (chart) chart.updateSeries(options.series);
190+
setTimeout(() => mounted = true, 1);
195191
});
196192
</script>
197193

198194
<div style="min-height: 69vh">
199195
{#if mounted}
200-
<div bind:this={chartDiv} in:fade></div>
196+
<div in:fade>
197+
<UplotSvelte {data} {options} onCreate={() => {}} onDelete={() => {}}/>
198+
</div>
201199
{/if}
202200
</div>
203201
{#if Object.keys(someStock).length > 2}

src/lib/lttstore/product/ProductStockHistoryGraph.svelte

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,52 @@
1+
<script module lang="ts">
2+
import uPlot from "uplot";
3+
const isNum = Number.isFinite;
4+
export const stockGaps: uPlot.Series.GapsRefiner = (u: uPlot, sidx: number, idx0: number, idx1: number, nullGaps: uPlot.Series.Gaps) => {
5+
let xData = u.data[0];
6+
let yData = u.data[sidx];
7+
8+
let addGaps: uPlot.Series.Gaps = [];
9+
10+
for (let i = idx0 + 1; i <= idx1; i++) {
11+
const now = yData[i];
12+
const previous = yData[i - 1];
13+
14+
if (typeof now === "number" && typeof previous === "number" && isNum(now) && isNum(previous)) {
15+
// adds a gap if the gap is more than 7 days
16+
if (now - previous > 7 * 24 * 60 * 60e3) {
17+
uPlot.addGap(
18+
addGaps,
19+
Math.round(u.valToPos(xData[i - 1], 'x', true)),
20+
Math.round(u.valToPos(xData[i], 'x', true)),
21+
);
22+
}
23+
}
24+
}
25+
26+
nullGaps.push(...addGaps);
27+
nullGaps.sort((a, b) => a[0] - b[0]);
28+
29+
return nullGaps;
30+
};
31+
32+
export const timeFormat: uPlot.Series.Value = ((_, val: number | null) => {
33+
if(val === null) return "";
34+
const date = new Date(val);
35+
return (
36+
date.toLocaleDateString(undefined, { dateStyle: 'medium' }) +
37+
' ' +
38+
date.toLocaleTimeString(undefined, { timeStyle: 'short', hour12: getTimePreference() })
39+
)
40+
});
41+
42+
export const stockColors = [
43+
"#008FFB",
44+
"#00E396",
45+
"orange",
46+
"#FF4560",
47+
"#775DD0"
48+
]
49+
</script>
150
<script lang="ts">
251
import { browser } from '$app/environment';
352
import { onMount } from 'svelte';

src/routes/(info)/lttstore/createTables.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { D1Database, D1DatabaseSession } from "@cloudflare/workers-types";
22

33
export async function createTables(DB: D1Database | D1DatabaseSession) {
4-
await DB.prepare("create table if not exists products (handle text, id integer PRIMARY KEY, title text, product text, stock string, metadataUpdate integer, stockChecked integer, lastRestock integer, purchasesPerHour integer, purchasesPerDay integer, regularPrice integer, currentPrice integer, firstSeen integer, available integer, backorderAlerts text, productDetailModules text, productDiscount text)")
4+
await DB.prepare("create table if not exists products (handle text, id integer PRIMARY KEY, title text, product text, stock string, metadataUpdate integer, stockChecked integer, lastRestock integer, purchasesPerHour integer, purchasesPerDay integer, regularPrice integer, currentPrice integer, firstSeen integer, available integer, backorderAlerts text, productDetailModules text, productDiscount text, differences integer)")
55
.run();
66
await DB.prepare("create table if not exists stock_history (handle text, id integer, timestamp integer, stock string)")
77
.run();
8-
await DB.prepare("create table if not exists change_history (id integer, timestamp integer, field TEXT, old TEXT, new TEXT)")
8+
await DB.prepare("create table if not exists change_history (id integer, timestamp integer, field TEXT, old TEXT, new TEXT, UNIQUE(timestamp, field) ON CONFLICT REPLACE)")
99
.run();
1010
await DB.prepare("create table if not exists collections (id integer PRIMARY KEY, title text, handle text, description text, published_at integer, updated_at integer, image text, reportedCount integer, products text, available integer)")
1111
.run();

src/routes/api/lttstore/dev/fetch/+server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const GET = (async ({platform}) => {
3131
let i = 0;
3232
for (const product of data.products) {
3333
console.log("Inserting (" + ++i + "/" + data.products.length + ") " + product.title);
34-
await db.prepare("insert or replace into products(handle, id, title, product, stock, stockChecked, metadataUpdate, lastRestock, purchasesPerHour, purchasesPerDay, regularPrice, currentPrice, firstSeen, available, backorderAlerts, productDetailModules, productDiscount) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
34+
await db.prepare("insert or replace into products(handle, id, title, product, stock, stockChecked, metadataUpdate, lastRestock, purchasesPerHour, purchasesPerDay, regularPrice, currentPrice, firstSeen, available, backorderAlerts, productDetailModules, productDiscount, differences) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
3535
.bind(
3636
product.handle,
3737
product.id,
@@ -49,7 +49,8 @@ export const GET = (async ({platform}) => {
4949
product.available,
5050
product.backorderAlerts,
5151
product.productDetailModules,
52-
product.productDiscount
52+
product.productDiscount,
53+
product.differences
5354
)
5455
.run();
5556
}

0 commit comments

Comments
 (0)