From 4f72d17faf966d2283a156b2acd36e8f89ac9abe Mon Sep 17 00:00:00 2001 From: Yan <75355375+yancat160@users.noreply.github.com> Date: Fri, 8 May 2026 08:38:15 -0400 Subject: [PATCH 1/3] keep hidden events hidden across date-range changes Clicking a legend item on the events chart toggled hidden via chart.current.getDatasetMeta(idx).hidden, which lives on Chart.js's per-dataset meta object. Each time chartData changed (date-range switch, refetch, focusLabel update) the second useEffect in Chart.tsx replaces datasets wholesale, Chart.js regenerates the meta, and the hidden flags vanish, so previously-toggled-off events came back on their own. Lift the hidden state into React. EventsChart owns a Set of hidden labels and passes it down via a new optional hiddenLabels prop on Chart, plus an onLegendClick callback for controlled toggling. Chart re-applies hidden after the existing focusLabel pass so the set survives every data refresh, and falls back to the original meta-based behaviour when no callback is provided so other charts (website overview, revenue) keep their existing semantics. Verified with the seeded Demo SaaS data: hide signup_started in the Last 24 hours view, switch to Last 7 days then Last 30 days, the label stays greyed in the legend and absent from the bars; clicking again restores it. State is component-scoped, so it intentionally resets on reload or navigation away from the events page. --- src/components/charts/Chart.tsx | 26 +++++++++++++++++++++++++- src/components/metrics/EventsChart.tsx | 12 ++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/components/charts/Chart.tsx b/src/components/charts/Chart.tsx index b6ae9d794..6fe167690 100644 --- a/src/components/charts/Chart.tsx +++ b/src/components/charts/Chart.tsx @@ -18,6 +18,8 @@ export interface ChartProps extends BoxProps { updateMode?: UpdateMode; animationDuration?: number; onTooltip?: (model: any) => void; + hiddenLabels?: Set; + onLegendClick?: (label: string, willBeHidden: boolean) => void; } export function Chart({ @@ -27,6 +29,8 @@ export function Chart({ updateMode, onTooltip, chartOptions, + hiddenLabels, + onLegendClick, ...props }: ChartProps) { const canvas = useRef(null); @@ -61,6 +65,15 @@ export function Chart({ }, [chartOptions]); const handleLegendClick = (item: LegendItem) => { + if (onLegendClick && type === 'bar') { + // Controlled mode: caller owns the hidden state. We report the click + // and let the parent push a new hiddenLabels set on the next render. + const { datasetIndex } = item; + const ds = chart.current.data.datasets[datasetIndex]; + onLegendClick(ds.label, !ds.hidden); + return; + } + if (type === 'bar') { const { datasetIndex } = item; const meta = chart.current.getDatasetMeta(datasetIndex); @@ -111,13 +124,24 @@ export function Chart({ }); } + // Re-apply caller-driven hidden flags after focusLabel handling so a + // dataset stays hidden across data changes (e.g. date-range switches) + // even though Chart.js regenerates dataset meta on every replace. + if (hiddenLabels) { + chart.current.data.datasets.forEach((ds: { hidden: boolean; label: any }) => { + if (hiddenLabels.has(ds.label)) { + ds.hidden = true; + } + }); + } + chart.current.options = options; chart.current.update(updateMode); setLegendItems(chart.current.legend.legendItems); } - }, [chartData, options, updateMode]); + }, [chartData, options, updateMode, hiddenLabels]); return ( diff --git a/src/components/metrics/EventsChart.tsx b/src/components/metrics/EventsChart.tsx index db23dcc47..9fe9d449b 100644 --- a/src/components/metrics/EventsChart.tsx +++ b/src/components/metrics/EventsChart.tsx @@ -26,6 +26,16 @@ export function EventsChart({ websiteId, focusLabel, limit }: EventsChartProps) const { locale, dateLocale } = useLocale(); const { data, isLoading, error } = useWebsiteEventsSeriesQuery(websiteId, { limit }); const [label, setLabel] = useState(focusLabel); + const [hiddenLabels, setHiddenLabels] = useState>(() => new Set()); + + const handleLegendClick = useCallback((legendLabel: string, willBeHidden: boolean) => { + setHiddenLabels(prev => { + const next = new Set(prev); + if (willBeHidden) next.add(legendLabel); + else next.delete(legendLabel); + return next; + }); + }, []); const chartData: any = useMemo(() => { if (!data) return; @@ -87,6 +97,8 @@ export function EventsChart({ websiteId, focusLabel, limit }: EventsChartProps) stacked={true} renderXLabel={renderXLabel} height="400px" + hiddenLabels={hiddenLabels} + onLegendClick={handleLegendClick} /> )} From d0049e70b53d982176937f2e93bce601504a8dd4 Mon Sep 17 00:00:00 2001 From: Yan <75355375+yancat160@users.noreply.github.com> Date: Fri, 8 May 2026 09:35:09 -0400 Subject: [PATCH 2/3] explicitly reset ds.hidden in the hiddenLabels block Address Greptile P1 review on PR #4259. The hiddenLabels loop only wrote ds.hidden = true and never reset to false when a label was removed from the set. Toggle-off worked today only because the focusLabel block above ran first and unconditionally cleared all ds.hidden when focusLabel was falsy. That block is guarded by chartData.focusLabel !== null, so any caller passing focusLabel={null} would skip the reset and leave a stale true on reused dataset objects across effect re-runs, making un-hide silently fail. Add an else-if branch to the hiddenLabels loop that resets ds.hidden to false when the label is not in the set and no focusLabel is active. Behavior is unchanged for EventsChart (which never passes a null focusLabel) and the contract of the new props is now independent of execution order. --- src/components/charts/Chart.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/charts/Chart.tsx b/src/components/charts/Chart.tsx index 6fe167690..5ac44273a 100644 --- a/src/components/charts/Chart.tsx +++ b/src/components/charts/Chart.tsx @@ -131,6 +131,10 @@ export function Chart({ chart.current.data.datasets.forEach((ds: { hidden: boolean; label: any }) => { if (hiddenLabels.has(ds.label)) { ds.hidden = true; + } else if (!chartData.focusLabel) { + // Explicitly reset so un-hiding a label is always reflected, + // regardless of whether the focusLabel pass ran above. + ds.hidden = false; } }); } From 84fe25af928d7c1e97fc9bf8067c8111b6816913 Mon Sep 17 00:00:00 2001 From: Yan <75355375+yancat160@users.noreply.github.com> Date: Fri, 8 May 2026 09:35:43 -0400 Subject: [PATCH 3/3] read willBeHidden from hiddenLabels in handleLegendClick Address Greptile P2 review on PR #4259. willBeHidden was derived from !ds.hidden, but ds.hidden can be set by the focusLabel pass too, so the dataset flag is not a faithful read of the controlled hidden state. In a chart that uses both focusLabel and hiddenLabels at once, the callback could fire with a misleading toggle direction. Read directly from hiddenLabels instead, with optional chaining so the new path is a no-op when hiddenLabels is not provided. Behavior is unchanged for EventsChart (which does not use focusLabel today) but the controlled state is now the single source of truth. --- src/components/charts/Chart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/charts/Chart.tsx b/src/components/charts/Chart.tsx index 5ac44273a..cd4410b68 100644 --- a/src/components/charts/Chart.tsx +++ b/src/components/charts/Chart.tsx @@ -70,7 +70,7 @@ export function Chart({ // and let the parent push a new hiddenLabels set on the next render. const { datasetIndex } = item; const ds = chart.current.data.datasets[datasetIndex]; - onLegendClick(ds.label, !ds.hidden); + onLegendClick(ds.label, !hiddenLabels?.has(ds.label)); return; }