Stats: column charts for per-period activity, Payments card last on My Orders
Build and Push Lunchtime Images (Kaniko) / build-and-push (push) Successful in 1m50s
Build and Push Lunchtime Images (Kaniko) / build-and-push (push) Successful in 1m50s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c50b61e5e8
commit
cfe145f94f
3 files changed
+25
-20
No files matched your search
@@ -25,7 +25,7 @@ import { FactGrid, Headline } from "./Highlights";
|
||||
import ItemRanking from "./ItemRanking";
|
||||
import { useRange } from "./range";
|
||||
import TimeSeriesPanel from "./TimeSeriesPanel";
|
||||
import { CardGrid, ChartCard, PageStack } from "./StatCard";
|
||||
import { ChartCard, PageStack } from "./StatCard";
|
||||
|
||||
const load = () => apiService.stats.getMine();
|
||||
|
||||
@@ -144,23 +144,22 @@ export default function MyOrdersStats() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<CardGrid>
|
||||
<ItemRanking title="Favourite items" items={countItems(submissions.flatMap((s) => s.items))} />
|
||||
<ChartCard title="Payments" extra={open.length ? `${formatMoney(sum(open.map((s) => s.total ?? 0)))} to pay` : "all settled"}>
|
||||
<Table
|
||||
size="small"
|
||||
pagination={{ pageSize: 6, hideOnSinglePage: true, size: "small" }}
|
||||
rowKey="order_id"
|
||||
dataSource={[...open, ...paid].sort((x, y) => Number(x.paid) - Number(y.paid) || y.submitted_at.localeCompare(x.submitted_at))}
|
||||
columns={paymentColumns}
|
||||
locale={{ emptyText: "No payments yet" }}
|
||||
/>
|
||||
</ChartCard>
|
||||
</CardGrid>
|
||||
<ItemRanking title="Favourite items" items={countItems(submissions.flatMap((s) => s.items))} />
|
||||
|
||||
<ChartCard title="When you order" extra="weekday and hour">
|
||||
<WeekdayHourHeatmap grid={weekdayHourGrid(times)} />
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard title="Payments" extra={open.length ? `${formatMoney(sum(open.map((s) => s.total ?? 0)))} to pay` : "all settled"}>
|
||||
<Table
|
||||
size="small"
|
||||
pagination={{ pageSize: 6, hideOnSinglePage: true, size: "small" }}
|
||||
rowKey="order_id"
|
||||
dataSource={[...open, ...paid].sort((x, y) => Number(x.paid) - Number(y.paid) || y.submitted_at.localeCompare(x.submitted_at))}
|
||||
columns={paymentColumns}
|
||||
locale={{ emptyText: "No payments yet" }}
|
||||
/>
|
||||
</ChartCard>
|
||||
</PageStack>
|
||||
</AsyncContent>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { Switch, Typography } from "antd";
|
||||
import { Bucket, periodBuckets, sumIn, TimedValue } from "../../lib/stats";
|
||||
import { LineChart } from "./charts";
|
||||
import { ColumnChart, LineChart } from "./charts";
|
||||
import { useRange } from "./range";
|
||||
import { CardGrid, ChartCard, Delta } from "./StatCard";
|
||||
|
||||
@@ -17,7 +17,7 @@ const accumulate = (buckets: Bucket[]): Bucket[] => {
|
||||
return buckets.map((b) => ({ ...b, value: (total += b.value) }));
|
||||
};
|
||||
|
||||
/** Per-period values (days, weeks or months, depending on the timeframe) as line charts, optionally cumulative, each with its change vs. the previous period. */
|
||||
/** Per-period values (days, weeks or months, depending on the timeframe) as columns (one per day for ranges up to ~3 months) or, when cumulative, as lines, each with its change vs. the previous period. */
|
||||
export default function TimeSeriesPanel({ title = "Activity", metrics }: { title?: string; metrics: TimeMetric[] }) {
|
||||
const [cumulative, setCumulative] = useState(false);
|
||||
const earliestMs = Math.min(Infinity, ...metrics.flatMap((m) => m.events.map((e) => e.date.getTime())));
|
||||
@@ -38,7 +38,7 @@ export default function TimeSeriesPanel({ title = "Activity", metrics }: { title
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<CardGrid minWidth={260} balance={false}>
|
||||
<CardGrid minWidth={480} balance={false}>
|
||||
{metrics.map((metric) => {
|
||||
const format = metric.format ?? String;
|
||||
const total = sumIn(metric.events, range);
|
||||
@@ -52,7 +52,11 @@ export default function TimeSeriesPanel({ title = "Activity", metrics }: { title
|
||||
<span style={{ fontSize: 20, fontWeight: 600, lineHeight: 1.3, fontVariantNumeric: "tabular-nums" }}>{format(total)}</span>
|
||||
<Delta current={total} previous={previous ? sumIn(metric.events, previous) : null} />
|
||||
</div>
|
||||
<LineChart data={cumulative ? accumulate(buckets) : buckets} height={110} format={format} />
|
||||
{cumulative ? (
|
||||
<LineChart data={accumulate(buckets)} height={110} format={format} />
|
||||
) : (
|
||||
<ColumnChart data={buckets} height={110} labelEvery={Math.ceil(buckets.length / 8)} format={format} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -19,6 +19,8 @@ export function ColumnChart({
|
||||
}) {
|
||||
const { token } = theme.useToken();
|
||||
const max = Math.max(1, ...data.map((d) => d.value + (d.extra ?? 0)));
|
||||
// Dense series (a bar per day) need thinner gaps to leave the bars visible
|
||||
const gap = data.length > 60 ? 1 : data.length > 30 ? 2 : 3;
|
||||
if (data.every((d) => d.value + (d.extra ?? 0) === 0)) return noData;
|
||||
|
||||
return (
|
||||
@@ -44,7 +46,7 @@ export function ColumnChart({
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 3,
|
||||
gap,
|
||||
alignItems: "flex-end",
|
||||
height,
|
||||
borderBottom: `1px solid ${token.colorBorder}`,
|
||||
@@ -69,7 +71,7 @@ export function ColumnChart({
|
||||
))}
|
||||
</div>
|
||||
{/* Labels sit in their own row. With every n-th label shown, each one spans its n slots and is truncated there, so long names never collide. */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: `repeat(${data.length}, minmax(0, 1fr))`, columnGap: 3, marginTop: 6, height: 14 }}>
|
||||
<div style={{ display: "grid", gridTemplateColumns: `repeat(${data.length}, minmax(0, 1fr))`, columnGap: gap, marginTop: 6, height: 14 }}>
|
||||
{data.map((d, i) =>
|
||||
i % labelEvery === 0 ? (
|
||||
<span
|
||||
|
||||
Reference in new issue
Block a user