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);
+ });
+
+ 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 6a108dcb..72a7d6cf 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]
}`;
}
@@ -955,6 +958,16 @@ function matchesFlowChartSelection(
}
const flow = displayFlow(txn);
+ if (
+ selection.mode === "external" &&
+ flow !== "incoming" &&
+ flow !== "outgoing"
+ ) {
+ return false;
+ }
+
+ if (selection.segment === null) return true;
+
if (selection.segment === "transfers") {
return flow === "transfer" || flow === "layer-transition";
}