feat: add stacked option for histogram

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-01-19 21:41:36 +01:00
parent 0d1773eb74
commit 00f2e2937d
4 changed files with 106 additions and 63 deletions

View File

@@ -61,9 +61,14 @@ export function Chart({ data }: Props) {
range,
series: reportSeries,
breakdowns,
options: reportOptions,
},
options: { hideXAxis, hideYAxis },
} = useReportChartContext();
const histogramOptions =
reportOptions?.type === 'histogram' ? reportOptions : undefined;
const isStacked = histogramOptions?.stacked ?? false;
const trpc = useTRPC();
const references = useQuery(
trpc.reference.getChartReferences.queryOptions(
@@ -155,68 +160,70 @@ export function Chart({ data }: Props) {
<div className={cn('h-full w-full', isEditMode && 'card p-4')}>
<ResponsiveContainer>
<BarChart data={rechartData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
className="stroke-def-200"
/>
<Tooltip
content={<ReportChartTooltip.Tooltip />}
cursor={<BarHover />}
/>
<YAxis {...yAxisProps} />
<XAxis {...xAxisProps} scale={'auto'} type="category" />
{previous
? series.map((serie) => {
return (
<Bar
key={`${serie.id}:prev`}
name={`${serie.id}:prev`}
dataKey={`${serie.id}:prev:count`}
fill={getChartColor(serie.index)}
fillOpacity={0.3}
radius={5}
/>
);
})
: null}
{series.map((serie) => {
return (
<Bar
key={serie.id}
name={serie.id}
dataKey={`${serie.id}:count`}
fill={getChartColor(serie.index)}
radius={5}
fillOpacity={1}
/>
);
})}
{references.data?.map((ref) => (
<ReferenceLine
key={ref.id}
x={ref.date.getTime()}
stroke={'oklch(from var(--foreground) l c h / 0.1)'}
strokeDasharray={'3 3'}
label={{
value: ref.title,
position: 'centerTop',
fill: '#334155',
fontSize: 12,
}}
fontSize={10}
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
className="stroke-def-200"
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
{isEditMode && (
<ReportTable
data={data}
visibleSeries={series}
setVisibleSeries={setVisibleSeries}
/>
)}
<Tooltip
content={<ReportChartTooltip.Tooltip />}
cursor={<BarHover />}
/>
<YAxis {...yAxisProps} />
<XAxis {...xAxisProps} scale={'auto'} type="category" />
{previous
? series.map((serie) => {
return (
<Bar
key={`${serie.id}:prev`}
name={`${serie.id}:prev`}
dataKey={`${serie.id}:prev:count`}
fill={getChartColor(serie.index)}
fillOpacity={0.3}
radius={5}
stackId={isStacked ? 'prev' : undefined}
/>
);
})
: null}
{series.map((serie) => {
return (
<Bar
key={serie.id}
name={serie.id}
dataKey={`${serie.id}:count`}
fill={getChartColor(serie.index)}
radius={isStacked ? 0 : 4}
fillOpacity={1}
stackId={isStacked ? 'current' : undefined}
/>
);
})}
{references.data?.map((ref) => (
<ReferenceLine
key={ref.id}
x={ref.date.getTime()}
stroke={'oklch(from var(--foreground) l c h / 0.1)'}
strokeDasharray={'3 3'}
label={{
value: ref.title,
position: 'centerTop',
fill: '#334155',
fontSize: 12,
}}
fontSize={10}
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
{isEditMode && (
<ReportTable
data={data}
visibleSeries={series}
setVisibleSeries={setVisibleSeries}
/>
)}
</ChartClickMenu>
</ReportChartTooltip.TooltipProvider>
);

View File

@@ -361,6 +361,17 @@ export const reportSlice = createSlice({
state.options.include = action.payload;
}
},
changeStacked(state, action: PayloadAction<boolean>) {
state.dirty = true;
if (!state.options || state.options.type !== 'histogram') {
state.options = {
type: 'histogram',
stacked: action.payload,
};
} else {
state.options.stacked = action.payload;
}
},
reorderEvents(
state,
action: PayloadAction<{ fromIndex: number; toIndex: number }>,
@@ -406,6 +417,7 @@ export const {
changeSankeySteps,
changeSankeyExclude,
changeSankeyInclude,
changeStacked,
reorderEvents,
} = reportSlice.actions;

View File

@@ -17,6 +17,7 @@ import {
changeSankeyInclude,
changeSankeyMode,
changeSankeySteps,
changeStacked,
changeUnit,
} from '../reportSlice';
@@ -25,14 +26,17 @@ export function ReportSettings() {
const previous = useSelector((state) => state.report.previous);
const unit = useSelector((state) => state.report.unit);
const options = useSelector((state) => state.report.options);
const retentionOptions = options?.type === 'retention' ? options : undefined;
const criteria = retentionOptions?.criteria ?? 'on_or_after';
const funnelOptions = options?.type === 'funnel' ? options : undefined;
const funnelGroup = funnelOptions?.funnelGroup;
const funnelWindow = funnelOptions?.funnelWindow;
const histogramOptions = options?.type === 'histogram' ? options : undefined;
const stacked = histogramOptions?.stacked ?? false;
const dispatch = useDispatch();
const { projectId } = useAppParams();
const eventNames = useEventNames({ projectId });
@@ -61,6 +65,10 @@ export function ReportSettings() {
fields.push('sankeyInclude');
}
if (chartType === 'histogram') {
fields.push('stacked');
}
return fields;
}, [chartType]);
@@ -259,6 +267,15 @@ export function ReportSettings() {
/>
</div>
)}
{fields.includes('stacked') && (
<Label className="flex items-center justify-between mb-0">
<span className="whitespace-nowrap">Stack series</span>
<Switch
checked={stacked}
onCheckedChange={(val) => dispatch(changeStacked(!!val))}
/>
</Label>
)}
</div>
</div>
);