web: histogram

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-01-07 21:56:30 +01:00
parent 31a4e1a277
commit 39827226d8
29 changed files with 523 additions and 321 deletions

View File

@@ -10,16 +10,16 @@ export function ReportDateRange() {
return (
<RadioGroup className="overflow-auto">
{timeRanges.map((item) => {
{Object.values(timeRanges).map((key) => {
return (
<RadioGroupItem
key={item.range}
active={item.range === range}
key={key}
active={key === range}
onClick={() => {
dispatch(changeDateRanges(item.range));
dispatch(changeDateRanges(key));
}}
>
{item.title}
{key}
</RadioGroupItem>
);
})}

View File

@@ -38,7 +38,11 @@ export function ReportInterval() {
{
value: 'month',
label: 'Month',
disabled: range < 1,
disabled:
range === 'today' ||
range === '24h' ||
range === '1h' ||
range === '30min',
},
]}
/>

View File

@@ -0,0 +1,107 @@
import { useEffect, useRef, useState } from 'react';
import { AutoSizer } from '@/components/AutoSizer';
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
import type { IChartData, IInterval } from '@/types';
import { alphabetIds } from '@/utils/constants';
import { getChartColor } from '@/utils/theme';
import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
import { useChartContext } from './ChartProvider';
import { ReportLineChartTooltip } from './ReportLineChartTooltip';
import { ReportTable } from './ReportTable';
interface ReportHistogramChartProps {
data: IChartData;
interval: IInterval;
}
export function ReportHistogramChart({
interval,
data,
}: ReportHistogramChartProps) {
const { editMode } = useChartContext();
const [visibleSeries, setVisibleSeries] = useState<string[]>([]);
const formatDate = useFormatDateInterval(interval);
const ref = useRef(false);
useEffect(() => {
if (!ref.current && data) {
const max = 20;
setVisibleSeries(
data?.series?.slice(0, max).map((serie) => serie.name) ?? []
);
// ref.current = true;
}
}, [data]);
const rel = data.series[0]?.data.map(({ date }) => {
return {
date,
...data.series.reduce((acc, serie, idx) => {
return {
...acc,
...serie.data.reduce(
(acc2, item) => {
const id = alphabetIds[idx];
if (item.date === date) {
acc2[`${id}:count`] = item.count;
acc2[`${id}:label`] = item.label;
}
return acc2;
},
{} as Record<string, any>
),
};
}, {}),
};
});
return (
<>
<div className="max-sm:-mx-3">
<AutoSizer disableHeight>
{({ width }) => (
<BarChart
width={width}
height={Math.min(Math.max(width * 0.5, 250), 400)}
data={rel}
>
<CartesianGrid strokeDasharray="3 3" />
<Tooltip content={<ReportLineChartTooltip />} />
<XAxis
fontSize={12}
dataKey="date"
tickFormatter={formatDate}
tickLine={false}
/>
{data.series.map((serie, index) => {
const id = alphabetIds[index];
return (
<>
<YAxis dataKey={`${id}:count`} fontSize={12}></YAxis>
<Bar
stackId={id}
key={serie.name}
isAnimationActive={false}
name={serie.name}
dataKey={`${id}:count`}
fill={getChartColor(index)}
/>
</>
);
})}
</BarChart>
)}
</AutoSizer>
</div>
{editMode && (
<ReportTable
data={data}
visibleSeries={visibleSeries}
setVisibleSeries={setVisibleSeries}
/>
)}
</>
);
}

View File

@@ -2,6 +2,7 @@ import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
import { useMappings } from '@/hooks/useMappings';
import { useSelector } from '@/redux';
import type { IToolTipProps } from '@/types';
import { alphabetIds } from '@/utils/constants';
type ReportLineChartTooltipProps = IToolTipProps<{
color: string;
@@ -10,7 +11,7 @@ type ReportLineChartTooltipProps = IToolTipProps<{
date: Date;
count: number;
label: string;
};
} & Record<string, any>;
}>;
export function ReportLineChartTooltip({
@@ -34,11 +35,13 @@ export function ReportLineChartTooltip({
const visible = sorted.slice(0, limit);
const hidden = sorted.slice(limit);
const first = visible[0]!;
const isBarChart = first.payload.count === undefined;
return (
<div className="flex flex-col gap-2 rounded-xl border bg-white p-3 text-sm shadow-xl">
{formatDate(new Date(first.payload.date))}
{visible.map((item) => {
{visible.map((item, index) => {
const id = alphabetIds[index];
return (
<div key={item.payload.label} className="flex gap-2">
<div
@@ -47,9 +50,13 @@ export function ReportLineChartTooltip({
></div>
<div className="flex flex-col">
<div className="min-w-0 max-w-[200px] overflow-hidden text-ellipsis whitespace-nowrap font-medium">
{getLabel(item.payload.label)}
{isBarChart
? item.payload[`${id}:label`]
: getLabel(item.payload.label)}
</div>
<div>
{isBarChart ? item.payload[`${id}:count`] : item.payload.count}
</div>
<div>{item.payload.count}</div>
</div>
</div>
);

View File

@@ -6,6 +6,7 @@ import { api } from '@/utils/api';
import { ChartAnimation, ChartAnimationContainer } from './ChartAnimation';
import { withChartProivder } from './ChartProvider';
import { ReportBarChart } from './ReportBarChart';
import { ReportHistogramChart } from './ReportHistogramChart';
import { ReportLineChart } from './ReportLineChart';
export type ReportChartProps = IChartInput;
@@ -88,6 +89,10 @@ export const Chart = memo(
);
}
if (chartType === 'histogram') {
return <ReportHistogramChart interval={interval} data={chart.data} />;
}
if (chartType === 'bar') {
return <ReportBarChart data={chart.data} />;
}

View File

@@ -24,7 +24,7 @@ const initialState: InitialState = {
interval: 'day',
breakdowns: [],
events: [],
range: 30,
range: '1m',
startDate: null,
endDate: null,
};
@@ -149,11 +149,11 @@ export const reportSlice = createSlice({
changeDateRanges: (state, action: PayloadAction<IChartRange>) => {
state.dirty = true;
state.range = action.payload;
if (action.payload === 0.3 || action.payload === 0.6) {
if (action.payload === '30min' || action.payload === '1h') {
state.interval = 'minute';
} else if (action.payload === 0 || action.payload === 1) {
} else if (action.payload === 'today' || action.payload === '24h') {
state.interval = 'hour';
} else if (action.payload <= 30) {
} else if (action.payload === '7d' || action.payload === '14d') {
state.interval = 'day';
} else {
state.interval = 'month';

View File

@@ -108,6 +108,10 @@ export function ReportEvents() {
value: 'user_average',
label: 'Unique users (average)',
},
{
value: 'one_event_per_user',
label: 'One event per user',
},
]}
label="Segment"
>
@@ -118,7 +122,11 @@ export function ReportEvents() {
</>
) : event.segment === 'user_average' ? (
<>
<Users size={12} /> Average per user
<Users size={12} /> Unique users (average)
</>
) : event.segment === 'one_event_per_user' ? (
<>
<Users size={12} /> One event per user
</>
) : (
<>