From 0c97b26c8c36bad0ccb2b782b8e95c3084dfb7d8 Mon Sep 17 00:00:00 2001 From: tobomobo <57799306+tobomobo@users.noreply.github.com> Date: Mon, 25 May 2026 20:00:40 +0200 Subject: [PATCH 1/2] Filter transactions by chart bucket --- .../dashboard/TransactionWorkbench.tsx | 84 ++++++++++++++++++- .../transactions/dashboard/model.test.ts | 65 ++++++++++++++ .../transactions/dashboard/model.ts | 9 +- 3 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 ui-tauri/src/components/transactions/dashboard/model.test.ts diff --git a/ui-tauri/src/components/transactions/dashboard/TransactionWorkbench.tsx b/ui-tauri/src/components/transactions/dashboard/TransactionWorkbench.tsx index 482cb0ac..aa133a14 100644 --- a/ui-tauri/src/components/transactions/dashboard/TransactionWorkbench.tsx +++ b/ui-tauri/src/components/transactions/dashboard/TransactionWorkbench.tsx @@ -16,6 +16,8 @@ import { LabelList, ReferenceLine, Tooltip, + usePlotArea, + useXAxisScale, XAxis, YAxis, } from "recharts"; @@ -123,6 +125,47 @@ interface ChartTooltipProps { metric: FlowChartMetric; } +function FlowBucketClickAreas({ + rows, + onBucketClick, +}: { + rows: FlowChartPoint[]; + onBucketClick: (point: FlowChartPoint) => void; +}) { + const plotArea = usePlotArea(); + const xScale = useXAxisScale(); + if (!plotArea || !xScale || rows.length === 0) return null; + + const fallbackWidth = plotArea.width / rows.length; + + return ( + + ); +} + const TransactionWorkbench = ({ period, records, @@ -213,10 +256,15 @@ const TransactionWorkbench = ({ const yDomain = flowAxisDomain(visibleChartRows, chartMetric); const flowChartCellProps = React.useCallback( (row: FlowChartPoint, segment: FlowChartSegment) => { - const selected = - chartSelection?.segment === segment && - (chartSelection.bucketKey === null || - chartSelection.bucketKey === row.bucketKey); + const sameBucket = + chartSelection?.bucketKey === null || + chartSelection?.bucketKey === row.bucketKey; + const selected = Boolean( + chartSelection && + sameBucket && + (chartSelection.segment === null || + chartSelection.segment === segment), + ); const dimmed = Boolean(chartSelection && !selected); return { fillOpacity: dimmed ? 0.32 : 1, @@ -226,6 +274,30 @@ const TransactionWorkbench = ({ }, [chartSelection], ); + const selectFlowBucket = React.useCallback( + (point: FlowChartPoint) => { + if (flowPointTotal(point) === 0) return; + onQuickFilterChange(null); + onBreakdownSelectionChange(null); + onTableFiltersReset(); + onFlowSelectionChange({ + id: `${period}:${point.bucketKey}:all:${chartMode}`, + period, + bucketKey: point.bucketKey, + bucketLabel: point.date, + segment: null, + mode: chartMode, + }); + }, + [ + chartMode, + onBreakdownSelectionChange, + onFlowSelectionChange, + onQuickFilterChange, + onTableFiltersReset, + period, + ], + ); const handleFlowChartClick = React.useCallback( (data: FlowChartClickData, segment: FlowChartSegment) => { const point = data.payload ?? data.activePayload?.[0]?.payload; @@ -687,6 +759,10 @@ const TransactionWorkbench = ({ zIndex: 30, }} /> + = {}, +): Transaction { + return { + id: "tx-1", + txnId: "txid-1", + amount: 10, + amountBtc: 0.01, + counterparty: "Alice", + counterpartyInitials: "AL", + direction: "Receive", + paymentMethod: "On-chain", + date: "2026-04-15T12:00:00Z", + status: "completed", + ...overrides, + }; +} + +describe("transaction dashboard chart selection", () => { + it("labels and matches a whole bucket selection across flows", () => { + const bucket = bucketTransactionDate( + new Date("2026-04-15T12:00:00Z"), + "1year", + ); + const selection: FlowChartSelection = { + id: `1year:${bucket.key}:all:all`, + period: "1year", + bucketKey: bucket.key, + bucketLabel: bucket.label, + segment: null, + mode: "all", + }; + + expect(flowChartSelectionLabel(selection)).toBe(`${bucket.label} · All flows · All`); + expect( + matchesFlowChartSelection( + transaction({ flow: "incoming" }), + selection, + (txn) => txn.flow as TransactionFlow, + ), + ).toBe(true); + expect( + matchesFlowChartSelection( + transaction({ + id: "tx-2", + txnId: "txid-2", + date: "2026-05-15T12:00:00Z", + flow: "incoming", + }), + selection, + (txn) => txn.flow as TransactionFlow, + ), + ).toBe(false); + }); +}); diff --git a/ui-tauri/src/components/transactions/dashboard/model.ts b/ui-tauri/src/components/transactions/dashboard/model.ts index 6a108dcb..2165958f 100644 --- a/ui-tauri/src/components/transactions/dashboard/model.ts +++ b/ui-tauri/src/components/transactions/dashboard/model.ts @@ -51,7 +51,7 @@ type FlowChartSelection = { period: PeriodKey; bucketKey: string | null; bucketLabel: string; - segment: FlowChartSegment; + segment: FlowChartSegment | null; mode: FlowChartMode; }; @@ -926,7 +926,10 @@ function matchesTransactionDeepLink(txn: Transaction, transactionId: string) { } function flowChartSelectionLabel(selection: FlowChartSelection) { - return `${selection.bucketLabel} · ${flowChartSegmentLabels[selection.segment]} · ${ + const segmentLabel = selection.segment + ? flowChartSegmentLabels[selection.segment] + : "All flows"; + return `${selection.bucketLabel} · ${segmentLabel} · ${ flowChartModeLabels[selection.mode] }`; } @@ -954,6 +957,8 @@ function matchesFlowChartSelection( if (bucket.key !== selection.bucketKey) return false; } + if (selection.segment === null) return true; + const flow = displayFlow(txn); if (selection.segment === "transfers") { return flow === "transfer" || flow === "layer-transition"; From 0b8d90a48292f086979a5d1f4e8dd43735dac01d Mon Sep 17 00:00:00 2001 From: tobomobo <57799306+tobomobo@users.noreply.github.com> Date: Thu, 28 May 2026 14:27:46 +0200 Subject: [PATCH 2/2] Respect external chart bucket filters --- .../transactions/dashboard/model.test.ts | 41 ++++++++++++++++++- .../transactions/dashboard/model.ts | 10 ++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/ui-tauri/src/components/transactions/dashboard/model.test.ts b/ui-tauri/src/components/transactions/dashboard/model.test.ts index 8cc9cfc9..29d0b1cc 100644 --- a/ui-tauri/src/components/transactions/dashboard/model.test.ts +++ b/ui-tauri/src/components/transactions/dashboard/model.test.ts @@ -41,7 +41,9 @@ describe("transaction dashboard chart selection", () => { mode: "all", }; - expect(flowChartSelectionLabel(selection)).toBe(`${bucket.label} · All flows · All`); + expect(flowChartSelectionLabel(selection)).toBe( + `${bucket.label} · All flows · All`, + ); expect( matchesFlowChartSelection( transaction({ flow: "incoming" }), @@ -62,4 +64,41 @@ describe("transaction dashboard chart selection", () => { ), ).toBe(false); }); + + it("limits whole bucket selections to visible flows in external mode", () => { + const bucket = bucketTransactionDate( + new Date("2026-04-15T12:00:00Z"), + "1year", + ); + const selection: FlowChartSelection = { + id: `1year:${bucket.key}:all:external`, + period: "1year", + bucketKey: bucket.key, + bucketLabel: bucket.label, + segment: null, + mode: "external", + }; + + expect( + matchesFlowChartSelection( + transaction({ flow: "incoming" }), + selection, + (txn) => txn.flow as TransactionFlow, + ), + ).toBe(true); + expect( + matchesFlowChartSelection( + transaction({ flow: "transfer" }), + selection, + (txn) => txn.flow as TransactionFlow, + ), + ).toBe(false); + expect( + matchesFlowChartSelection( + transaction({ flow: "swap" }), + selection, + (txn) => txn.flow as TransactionFlow, + ), + ).toBe(false); + }); }); diff --git a/ui-tauri/src/components/transactions/dashboard/model.ts b/ui-tauri/src/components/transactions/dashboard/model.ts index 2165958f..72a7d6cf 100644 --- a/ui-tauri/src/components/transactions/dashboard/model.ts +++ b/ui-tauri/src/components/transactions/dashboard/model.ts @@ -957,9 +957,17 @@ function matchesFlowChartSelection( if (bucket.key !== selection.bucketKey) return false; } + const flow = displayFlow(txn); + if ( + selection.mode === "external" && + flow !== "incoming" && + flow !== "outgoing" + ) { + return false; + } + if (selection.segment === null) return true; - const flow = displayFlow(txn); if (selection.segment === "transfers") { return flow === "transfer" || flow === "layer-transition"; }