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 (
+
+ {rows.map((row) => {
+ const start = xScale(row.date, { position: "start" });
+ const end = xScale(row.date, { position: "end" });
+ const x = start ?? xScale(row.date);
+ if (x === undefined || !Number.isFinite(x)) return null;
+ const width =
+ end !== undefined && Number.isFinite(end)
+ ? Math.max(1, end - x)
+ : fallbackWidth;
+ return (
+ onBucketClick(row)}
+ />
+ );
+ })}
+
+ );
+}
+
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";
}