refactor(dashboard): the chart component is now cleaned up and easier to extend

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-09-12 09:30:48 +02:00
parent 3e19f90e51
commit 558761ca9d
76 changed files with 2910 additions and 2475 deletions

View File

@@ -1,9 +1,8 @@
'use client';
import { StickyBelowHeader } from '@/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header';
import { FullPageEmptyState } from '@/components/full-page-empty-state';
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
import { LazyChart } from '@/components/report/chart/LazyChart';
import { ReportChart } from '@/components/report-chart';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@@ -133,17 +132,22 @@ export function ListReports({ reports, dashboard }: ListReportsProps) {
/>
</div>
</Link>
<div className={cn('p-4')}>
<LazyChart
<div
className={cn('p-4', report.chartType === 'metric' && 'p-0')}
>
<ReportChart
{...report}
range={range ?? report.range}
startDate={startDate}
endDate={endDate}
interval={
getDefaultIntervalByDates(startDate, endDate) ||
(range ? getDefaultIntervalByRange(range) : report.interval)
}
editMode={false}
report={{
...report,
range: range ?? report.range,
startDate: startDate ?? report.startDate,
endDate: endDate ?? report.endDate,
interval:
getDefaultIntervalByDates(startDate, endDate) ||
(range
? getDefaultIntervalByRange(range)
: report.interval),
}}
/>
</div>
</div>

View File

@@ -2,7 +2,7 @@
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
import { ChartRootShortcut } from '@/components/report/chart';
import { ReportChartShortcut } from '@/components/report-chart/shortcut';
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
import {
useEventQueryFilters,
@@ -44,7 +44,7 @@ function Charts({ projectId }: Props) {
<span className="title">Events per day</span>
</WidgetHead>
<WidgetBody>
<ChartRootShortcut
<ReportChartShortcut
projectId={projectId}
range="30d"
chartType="histogram"
@@ -67,7 +67,7 @@ function Charts({ projectId }: Props) {
<span className="title">Event distribution</span>
</WidgetHead>
<WidgetBody>
<ChartRootShortcut
<ReportChartShortcut
projectId={projectId}
range="30d"
chartType="pie"
@@ -104,7 +104,7 @@ function Charts({ projectId }: Props) {
<span className="title">Event distribution</span>
</WidgetHead>
<WidgetBody>
<ChartRootShortcut
<ReportChartShortcut
projectId={projectId}
range="30d"
chartType="bar"
@@ -141,7 +141,7 @@ function Charts({ projectId }: Props) {
<span className="title">Event distribution</span>
</WidgetHead>
<WidgetBody>
<ChartRootShortcut
<ReportChartShortcut
projectId={projectId}
range="30d"
chartType="linear"

View File

@@ -12,8 +12,10 @@ export default function LayoutContent({
const segments = useSelectedLayoutSegments();
if (segments[0] && NOT_MIGRATED_PAGES.includes(segments[0])) {
return <div className="transition-all lg:pl-72">{children}</div>;
return <div className="pb-20 transition-all lg:pl-72">{children}</div>;
}
return <div className="transition-all max-lg:mt-12 lg:pl-72">{children}</div>;
return (
<div className="pb-20 transition-all max-lg:mt-12 lg:pl-72">{children}</div>
);
}

View File

@@ -1,7 +1,7 @@
'use client';
import { memo } from 'react';
import { LazyChart } from '@/components/report/chart/LazyChart';
import { ReportChart } from '@/components/report-chart';
import { useNumber } from '@/hooks/useNumerFormatter';
import { cn } from '@/utils/cn';
import isEqual from 'lodash.isequal';
@@ -71,41 +71,46 @@ export const PagesTable = memo(
index === data.length - 1 && 'rounded-br-md'
)}
>
<LazyChart
hideYAxis
hideXAxis
className="w-full"
lineType="linear"
breakdowns={[]}
name="screen_view"
metric="sum"
range="30d"
interval="day"
previous
aspectRatio={0.15}
chartType="linear"
projectId={item.project_id}
events={[
{
id: 'A',
name: 'screen_view',
segment: 'event',
filters: [
{
id: 'path',
name: 'path',
value: [item.path],
operator: 'is',
},
{
id: 'origin',
name: 'origin',
value: [item.origin],
operator: 'is',
},
],
},
]}
<ReportChart
options={{
hideID: true,
hideXAxis: true,
hideYAxis: true,
aspectRatio: 0.15,
}}
report={{
lineType: 'linear',
breakdowns: [],
name: 'screen_view',
metric: 'sum',
range: '30d',
interval: 'day',
previous: true,
chartType: 'linear',
projectId: item.project_id,
events: [
{
id: 'A',
name: 'screen_view',
segment: 'event',
filters: [
{
id: 'path',
name: 'path',
value: [item.path],
operator: 'is',
},
{
id: 'origin',
name: 'origin',
value: [item.origin],
operator: 'is',
},
],
},
],
}}
/>
</div>
</div>

View File

@@ -1,7 +1,7 @@
'use client';
import { memo } from 'react';
import { ChartRoot } from '@/components/report/chart';
import { ReportChart } from '@/components/report-chart';
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
import type { IChartProps } from '@openpanel/validation';
@@ -85,7 +85,7 @@ const ProfileCharts = ({ profileId, projectId }: Props) => {
<span className="title">Page views</span>
</WidgetHead>
<WidgetBody>
<ChartRoot {...pageViewsChart} />
<ReportChart report={pageViewsChart} />
</WidgetBody>
</Widget>
<Widget className="col-span-6 md:col-span-3">
@@ -93,7 +93,7 @@ const ProfileCharts = ({ profileId, projectId }: Props) => {
<span className="title">Events per day</span>
</WidgetHead>
<WidgetBody>
<ChartRoot {...eventsChart} />
<ReportChart report={eventsChart} />
</WidgetBody>
</Widget>
</>

View File

@@ -18,7 +18,7 @@ function Card({ title, value }: { title: string; value: string }) {
return (
<div className="col gap-2 p-4 ring-[0.5px] ring-border">
<div className="text-muted-foreground">{title}</div>
<div className="font-mono truncate text-2xl font-bold">{value}</div>
<div className="truncate font-mono text-2xl font-bold">{value}</div>
</div>
);
}
@@ -27,7 +27,7 @@ function Info({ title, value }: { title: string; value: string }) {
return (
<div className="col gap-2">
<div className="capitalize text-muted-foreground">{title}</div>
<div className="font-mono truncate">{value || '-'}</div>
<div className="truncate font-mono">{value || '-'}</div>
</div>
);
}
@@ -40,7 +40,7 @@ const ProfileMetrics = ({ data, profile }: Props) => {
const number = useNumber();
return (
<div className="@container">
<div className="grid grid-cols-2 overflow-hidden whitespace-nowrap rounded-md border bg-background @xl:grid-cols-3 @4xl:grid-cols-6">
<div className="grid grid-cols-2 overflow-hidden whitespace-nowrap rounded-md border bg-background @xl:grid-cols-3 @4xl:grid-cols-6">
<div className="col-span-2 @xl:col-span-3 @4xl:col-span-6">
<div className="row border-b">
<button

View File

@@ -4,9 +4,8 @@ import {
FullscreenClose,
FullscreenOpen,
} from '@/components/fullscreen-toggle';
import { LazyChart } from '@/components/report/chart/LazyChart';
import { ReportChart } from '@/components/report-chart';
import PageLayout from '../page-layout';
import RealtimeMap from './map';
import RealtimeLiveEventsServer from './realtime-live-events';
import { RealtimeLiveHistogram } from './realtime-live-histogram';
@@ -42,9 +41,11 @@ export default function Page({ params: { projectId } }: Props) {
<div className="mb-6">
<div className="font-bold">Pages</div>
</div>
<LazyChart
hideID
{...{
<ReportChart
options={{
hideID: true,
}}
report={{
projectId,
events: [
{
@@ -74,9 +75,11 @@ export default function Page({ params: { projectId } }: Props) {
<div className="mb-6">
<div className="font-bold">Cities</div>
</div>
<LazyChart
hideID
{...{
<ReportChart
options={{
hideID: true,
}}
report={{
projectId,
events: [
{
@@ -106,9 +109,11 @@ export default function Page({ params: { projectId } }: Props) {
<div className="mb-6">
<div className="font-bold">Referrers</div>
</div>
<LazyChart
hideID
{...{
<ReportChart
options={{
hideID: true,
}}
report={{
projectId,
events: [
{

View File

@@ -2,7 +2,7 @@
import { useEffect } from 'react';
import { StickyBelowHeader } from '@/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header';
import { ChartRoot } from '@/components/report/chart';
import { ReportChart } from '@/components/report-chart';
import { ReportChartType } from '@/components/report/ReportChartType';
import { ReportInterval } from '@/components/report/ReportInterval';
import { ReportLineType } from '@/components/report/ReportLineType';
@@ -99,7 +99,7 @@ export default function ReportEditor({
</StickyBelowHeader>
<div className="flex flex-col gap-4 p-4" id="report-editor">
{report.ready && (
<ChartRoot {...report} projectId={projectId} editMode />
<ReportChart report={{ ...report, projectId }} isEditMode />
)}
</div>
<SheetContent className="!max-w-lg" side="left">

View File

@@ -1,13 +1,15 @@
'use client';
import { getYAxisWidth } from '@/components/report/chart/chart-utils';
import { ResponsiveContainer } from '@/components/report/chart/ResponsiveContainer';
import { useNumber } from '@/hooks/useNumerFormatter';
import {
useXAxisProps,
useYAxisProps,
} from '@/components/report-chart/common/axis';
import { getChartColor } from '@/utils/theme';
import {
Area,
AreaChart,
Tooltip as RechartTooltip,
ResponsiveContainer,
XAxis,
YAxis,
} from 'recharts';
@@ -39,69 +41,62 @@ function Tooltip(props: any) {
}
const Chart = ({ data }: Props) => {
const max = Math.max(...data.map((d) => d.users));
const number = useNumber();
const xAxisProps = useXAxisProps();
const yAxisProps = useYAxisProps({
data: data.map((d) => d.users),
});
return (
<div className="p-4">
<div className="aspect-video max-h-[300px] w-full p-4">
<ResponsiveContainer>
{({ width, height }) => (
<AreaChart data={data} width={width} height={height}>
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={getChartColor(0)}
stopOpacity={0.8}
></stop>
<stop
offset="100%"
stopColor={getChartColor(0)}
stopOpacity={0.1}
></stop>
</linearGradient>
</defs>
<AreaChart data={data}>
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={getChartColor(0)}
stopOpacity={0.8}
></stop>
<stop
offset="100%"
stopColor={getChartColor(0)}
stopOpacity={0.1}
></stop>
</linearGradient>
</defs>
<RechartTooltip content={<Tooltip />} />
<RechartTooltip content={<Tooltip />} />
<Area
dataKey="users"
stroke={getChartColor(0)}
strokeWidth={2}
fill={`url(#bg)`}
isAnimationActive={false}
/>
<XAxis
dataKey="days"
axisLine={false}
fontSize={12}
// type="number"
tickLine={false}
label={{
value: 'DAYS',
position: 'insideBottom',
offset: 0,
fontSize: 10,
}}
/>
<YAxis
label={{
value: 'USERS',
angle: -90,
position: 'insideLeft',
offset: 0,
fontSize: 10,
}}
dataKey="users"
fontSize={12}
axisLine={false}
tickLine={false}
width={getYAxisWidth(max)}
allowDecimals={false}
domain={[0, max]}
tickFormatter={number.short}
/>
</AreaChart>
)}
<Area
dataKey="users"
stroke={getChartColor(0)}
strokeWidth={2}
fill={`url(#bg)`}
isAnimationActive={false}
/>
<XAxis
{...xAxisProps}
dataKey="days"
scale="auto"
type="category"
label={{
value: 'DAYS',
position: 'insideBottom',
offset: 0,
fontSize: 10,
}}
/>
<YAxis
{...yAxisProps}
label={{
value: 'USERS',
angle: -90,
position: 'insideLeft',
offset: 0,
fontSize: 10,
}}
dataKey="users"
/>
</AreaChart>
</ResponsiveContainer>
</div>
);

View File

@@ -55,7 +55,7 @@ const Retention = ({ params: { projectId } }: Props) => {
</AlertDescription>
</Alert>
<LastActiveUsersServer projectId={projectId} />
<UsersRetentionSeries projectId={projectId} />
{/* <UsersRetentionSeries projectId={projectId} /> */}
<WeeklyCohortsServer projectId={projectId} />
</div>
</Padding>

View File

@@ -1,13 +1,15 @@
'use client';
import { getYAxisWidth } from '@/components/report/chart/chart-utils';
import { ResponsiveContainer } from '@/components/report/chart/ResponsiveContainer';
import { useNumber } from '@/hooks/useNumerFormatter';
import {
useXAxisProps,
useYAxisProps,
} from '@/components/report-chart/common/axis';
import { getChartColor } from '@/utils/theme';
import {
Area,
AreaChart,
Tooltip as RechartTooltip,
ResponsiveContainer,
XAxis,
YAxis,
} from 'recharts';
@@ -50,106 +52,94 @@ function Tooltip(props: any) {
}
const Chart = ({ data }: Props) => {
const max = Math.max(...data.monthly.map((d) => d.users));
const number = useNumber();
const rechartData = data.daily.map((d) => ({
date: d.date,
date: new Date(d.date).getTime(),
dau: d.users,
wau: data.weekly.find((w) => w.date === d.date)?.users,
mau: data.monthly.find((m) => m.date === d.date)?.users,
}));
const xAxisProps = useXAxisProps({ interval: 'day' });
const yAxisProps = useYAxisProps({
data: data.monthly.map((d) => d.users),
});
return (
<div className="p-4">
<div className="aspect-video max-h-[300px] w-full p-4">
<ResponsiveContainer>
{({ width, height }) => (
<AreaChart data={rechartData} width={width} height={height}>
<defs>
<linearGradient id="dau" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={getChartColor(0)}
stopOpacity={0.8}
></stop>
<stop
offset="100%"
stopColor={getChartColor(0)}
stopOpacity={0.1}
></stop>
</linearGradient>
<linearGradient id="wau" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={getChartColor(1)}
stopOpacity={0.8}
></stop>
<stop
offset="100%"
stopColor={getChartColor(1)}
stopOpacity={0.1}
></stop>
</linearGradient>
<linearGradient id="mau" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={getChartColor(2)}
stopOpacity={0.8}
></stop>
<stop
offset="100%"
stopColor={getChartColor(2)}
stopOpacity={0.1}
></stop>
</linearGradient>
</defs>
<AreaChart data={rechartData}>
<defs>
<linearGradient id="dau" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={getChartColor(0)}
stopOpacity={0.8}
></stop>
<stop
offset="100%"
stopColor={getChartColor(0)}
stopOpacity={0.1}
></stop>
</linearGradient>
<linearGradient id="wau" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={getChartColor(1)}
stopOpacity={0.8}
></stop>
<stop
offset="100%"
stopColor={getChartColor(1)}
stopOpacity={0.1}
></stop>
</linearGradient>
<linearGradient id="mau" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={getChartColor(2)}
stopOpacity={0.8}
></stop>
<stop
offset="100%"
stopColor={getChartColor(2)}
stopOpacity={0.1}
></stop>
</linearGradient>
</defs>
<RechartTooltip content={<Tooltip />} />
<RechartTooltip content={<Tooltip />} />
<Area
dataKey="dau"
stroke={getChartColor(0)}
strokeWidth={2}
fill={`url(#dau)`}
isAnimationActive={false}
/>
<Area
dataKey="wau"
stroke={getChartColor(1)}
strokeWidth={2}
fill={`url(#wau)`}
isAnimationActive={false}
/>
<Area
dataKey="mau"
stroke={getChartColor(2)}
strokeWidth={2}
fill={`url(#mau)`}
isAnimationActive={false}
/>
<XAxis
dataKey="date"
axisLine={false}
fontSize={12}
// type="number"
tickLine={false}
/>
<YAxis
label={{
value: 'UNIQUE USERS',
angle: -90,
position: 'insideLeft',
offset: 0,
fontSize: 10,
}}
fontSize={12}
axisLine={false}
tickLine={false}
width={getYAxisWidth(max)}
allowDecimals={false}
domain={[0, max]}
tickFormatter={number.short}
/>
</AreaChart>
)}
<Area
dataKey="dau"
stroke={getChartColor(0)}
strokeWidth={2}
fill={`url(#dau)`}
isAnimationActive={false}
/>
<Area
dataKey="wau"
stroke={getChartColor(1)}
strokeWidth={2}
fill={`url(#wau)`}
isAnimationActive={false}
/>
<Area
dataKey="mau"
stroke={getChartColor(2)}
strokeWidth={2}
fill={`url(#mau)`}
isAnimationActive={false}
/>
<XAxis {...xAxisProps} dataKey="date" />
<YAxis
{...yAxisProps}
label={{
value: 'UNIQUE USERS',
angle: -90,
position: 'insideLeft',
offset: 0,
fontSize: 10,
}}
/>
</AreaChart>
</ResponsiveContainer>
</div>
);

View File

@@ -1,15 +1,17 @@
'use client';
import { getYAxisWidth } from '@/components/report/chart/chart-utils';
import { ResponsiveContainer } from '@/components/report/chart/ResponsiveContainer';
import {
useXAxisProps,
useYAxisProps,
} from '@/components/report-chart/common/axis';
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
import { useNumber } from '@/hooks/useNumerFormatter';
import { formatDate } from '@/utils/date';
import { getChartColor } from '@/utils/theme';
import {
Area,
AreaChart,
Tooltip as RechartTooltip,
ResponsiveContainer,
XAxis,
YAxis,
} from 'recharts';
@@ -54,69 +56,61 @@ function Tooltip({ payload }: any) {
}
const Chart = ({ data }: Props) => {
const max = Math.max(...data.map((d) => d.retention));
const number = useNumber();
const xAxisProps = useXAxisProps();
const yAxisProps = useYAxisProps({
data: data.map((d) => d.retention),
});
return (
<div className="p-4">
<div className="aspect-video max-h-[300px] w-full p-4">
<ResponsiveContainer>
{({ width, height }) => (
<AreaChart data={data} width={width} height={height}>
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={getChartColor(0)}
stopOpacity={0.8}
></stop>
<stop
offset="100%"
stopColor={getChartColor(0)}
stopOpacity={0.1}
></stop>
</linearGradient>
</defs>
<AreaChart data={data}>
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={getChartColor(0)}
stopOpacity={0.8}
></stop>
<stop
offset="100%"
stopColor={getChartColor(0)}
stopOpacity={0.1}
></stop>
</linearGradient>
</defs>
<RechartTooltip content={<Tooltip />} />
<RechartTooltip content={<Tooltip />} />
<Area
dataKey="retention"
stroke={getChartColor(0)}
strokeWidth={2}
fill={`url(#bg)`}
isAnimationActive={false}
/>
<XAxis
axisLine={false}
fontSize={12}
dataKey="date"
tickFormatter={(m: string) => formatDate(new Date(m))}
tickLine={false}
allowDuplicatedCategory={false}
label={{
value: 'DATE',
position: 'insideBottom',
offset: 0,
fontSize: 10,
}}
/>
<YAxis
label={{
value: 'RETENTION (%)',
angle: -90,
position: 'insideLeft',
offset: 0,
fontSize: 10,
}}
fontSize={12}
axisLine={false}
tickLine={false}
width={getYAxisWidth(max)}
allowDecimals={false}
domain={[0, max]}
tickFormatter={number.short}
/>
</AreaChart>
)}
<Area
dataKey="retention"
stroke={getChartColor(0)}
strokeWidth={2}
fill={`url(#bg)`}
isAnimationActive={false}
/>
<XAxis
{...xAxisProps}
dataKey="date"
tickFormatter={(m: string) => formatDate(new Date(m))}
allowDuplicatedCategory={false}
label={{
value: 'DATE',
position: 'insideBottom',
offset: 0,
fontSize: 10,
}}
/>
<YAxis
{...yAxisProps}
label={{
value: 'RETENTION (%)',
angle: -90,
position: 'insideLeft',
offset: 0,
fontSize: 10,
}}
/>
</AreaChart>
</ResponsiveContainer>
</div>
);