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, range,
series: reportSeries, series: reportSeries,
breakdowns, breakdowns,
options: reportOptions,
}, },
options: { hideXAxis, hideYAxis }, options: { hideXAxis, hideYAxis },
} = useReportChartContext(); } = useReportChartContext();
const histogramOptions =
reportOptions?.type === 'histogram' ? reportOptions : undefined;
const isStacked = histogramOptions?.stacked ?? false;
const trpc = useTRPC(); const trpc = useTRPC();
const references = useQuery( const references = useQuery(
trpc.reference.getChartReferences.queryOptions( trpc.reference.getChartReferences.queryOptions(
@@ -155,68 +160,70 @@ export function Chart({ data }: Props) {
<div className={cn('h-full w-full', isEditMode && 'card p-4')}> <div className={cn('h-full w-full', isEditMode && 'card p-4')}>
<ResponsiveContainer> <ResponsiveContainer>
<BarChart data={rechartData}> <BarChart data={rechartData}>
<CartesianGrid <CartesianGrid
strokeDasharray="3 3" strokeDasharray="3 3"
vertical={false} vertical={false}
className="stroke-def-200" 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}
/> />
))} <Tooltip
</BarChart> content={<ReportChartTooltip.Tooltip />}
</ResponsiveContainer> cursor={<BarHover />}
</div> />
{isEditMode && ( <YAxis {...yAxisProps} />
<ReportTable <XAxis {...xAxisProps} scale={'auto'} type="category" />
data={data} {previous
visibleSeries={series} ? series.map((serie) => {
setVisibleSeries={setVisibleSeries} 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> </ChartClickMenu>
</ReportChartTooltip.TooltipProvider> </ReportChartTooltip.TooltipProvider>
); );

View File

@@ -361,6 +361,17 @@ export const reportSlice = createSlice({
state.options.include = action.payload; 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( reorderEvents(
state, state,
action: PayloadAction<{ fromIndex: number; toIndex: number }>, action: PayloadAction<{ fromIndex: number; toIndex: number }>,
@@ -406,6 +417,7 @@ export const {
changeSankeySteps, changeSankeySteps,
changeSankeyExclude, changeSankeyExclude,
changeSankeyInclude, changeSankeyInclude,
changeStacked,
reorderEvents, reorderEvents,
} = reportSlice.actions; } = reportSlice.actions;

View File

@@ -17,6 +17,7 @@ import {
changeSankeyInclude, changeSankeyInclude,
changeSankeyMode, changeSankeyMode,
changeSankeySteps, changeSankeySteps,
changeStacked,
changeUnit, changeUnit,
} from '../reportSlice'; } from '../reportSlice';
@@ -33,6 +34,9 @@ export function ReportSettings() {
const funnelGroup = funnelOptions?.funnelGroup; const funnelGroup = funnelOptions?.funnelGroup;
const funnelWindow = funnelOptions?.funnelWindow; const funnelWindow = funnelOptions?.funnelWindow;
const histogramOptions = options?.type === 'histogram' ? options : undefined;
const stacked = histogramOptions?.stacked ?? false;
const dispatch = useDispatch(); const dispatch = useDispatch();
const { projectId } = useAppParams(); const { projectId } = useAppParams();
const eventNames = useEventNames({ projectId }); const eventNames = useEventNames({ projectId });
@@ -61,6 +65,10 @@ export function ReportSettings() {
fields.push('sankeyInclude'); fields.push('sankeyInclude');
} }
if (chartType === 'histogram') {
fields.push('stacked');
}
return fields; return fields;
}, [chartType]); }, [chartType]);
@@ -259,6 +267,15 @@ export function ReportSettings() {
/> />
</div> </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>
</div> </div>
); );

View File

@@ -126,14 +126,21 @@ export const zSankeyOptions = z.object({
include: z.array(z.string()).optional(), include: z.array(z.string()).optional(),
}); });
export const zHistogramOptions = z.object({
type: z.literal('histogram'),
stacked: z.boolean().default(false),
});
export const zReportOptions = z.discriminatedUnion('type', [ export const zReportOptions = z.discriminatedUnion('type', [
zFunnelOptions, zFunnelOptions,
zRetentionOptions, zRetentionOptions,
zSankeyOptions, zSankeyOptions,
zHistogramOptions,
]); ]);
export type IReportOptions = z.infer<typeof zReportOptions>; export type IReportOptions = z.infer<typeof zReportOptions>;
export type ISankeyOptions = z.infer<typeof zSankeyOptions>; export type ISankeyOptions = z.infer<typeof zSankeyOptions>;
export type IHistogramOptions = z.infer<typeof zHistogramOptions>;
export const zWidgetType = z.enum(['realtime', 'counter']); export const zWidgetType = z.enum(['realtime', 'counter']);
export type IWidgetType = z.infer<typeof zWidgetType>; export type IWidgetType = z.infer<typeof zWidgetType>;