Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
LabelList,
ReferenceLine,
Tooltip,
usePlotArea,
useXAxisScale,
XAxis,
YAxis,
} from "recharts";
Expand Down Expand Up @@ -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 (
<g aria-hidden="true">
{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 (
<rect
key={`bucket-click-${row.bucketKey}`}
x={x}
y={plotArea.y}
width={width}
height={plotArea.height}
fill="transparent"
cursor="pointer"
onClick={() => onBucketClick(row)}
/>
);
})}
</g>
);
}

const TransactionWorkbench = ({
period,
records,
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -687,6 +759,10 @@ const TransactionWorkbench = ({
zIndex: 30,
}}
/>
<FlowBucketClickAreas
rows={visibleChartRows}
onBucketClick={selectFlowBucket}
/>
<Bar
dataKey="incoming"
fill={flowColors.incoming}
Expand Down
104 changes: 104 additions & 0 deletions ui-tauri/src/components/transactions/dashboard/model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { describe, expect, it } from "vitest";

import {
bucketTransactionDate,
flowChartSelectionLabel,
matchesFlowChartSelection,
type FlowChartSelection,
} from "./model";
import type { Transaction, TransactionFlow } from "@/components/transactions";

function transaction(
overrides: Partial<Transaction> = {},
): 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);
});
});
17 changes: 15 additions & 2 deletions ui-tauri/src/components/transactions/dashboard/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ type FlowChartSelection = {
period: PeriodKey;
bucketKey: string | null;
bucketLabel: string;
segment: FlowChartSegment;
segment: FlowChartSegment | null;
mode: FlowChartMode;
};

Expand Down Expand Up @@ -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]
}`;
}
Expand Down Expand Up @@ -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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect chart mode for bucket-wide selections

When a bucket is selected via the new segment: null path, matchesFlowChartSelection returns true immediately after date matching, so flow filtering is skipped entirely. In external chart mode this means clicking a bucket can include transfer/swap rows that were not part of the visible chart slice, even though the selection carries mode: "external" and the chip label implies that context. This creates a misleading filter result for users who clicked the external-only view.

Useful? React with 👍 / 👎.


if (selection.segment === "transfers") {
return flow === "transfer" || flow === "layer-transition";
}
Expand Down
Loading