-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add series percentage chart (#103)
* feat: add series percentage chart * chore: export chart
- Loading branch information
1 parent
c69e10c
commit fbd3fb9
Showing
4 changed files
with
265 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import React, { type ReactElement, useMemo } from 'react'; | ||
import { | ||
AreaChart, | ||
Area, | ||
XAxis, | ||
YAxis, | ||
CartesianGrid, | ||
Tooltip, | ||
ResponsiveContainer, | ||
} from 'recharts'; | ||
import Decimal from 'decimal.js'; | ||
import { Box, type SxProps } from '@mui/material'; | ||
|
||
import { formatDateMMYY, formatExtendedDate } from '../../../utils/date'; | ||
|
||
interface KeyValue { | ||
key: string; | ||
name: string; | ||
color: string; | ||
} | ||
|
||
interface ChartDataPoint { | ||
date: number; | ||
[key: string]: number; // Allow dynamic key access | ||
} | ||
|
||
interface SeriesChartData { | ||
uuid: string; | ||
chartData: ChartDataPoint[]; | ||
} | ||
|
||
interface SeriesPercentageChartProps { | ||
data: SeriesChartData[]; | ||
filter: { timezone: string }; | ||
keyValues: KeyValue[]; | ||
sx?: SxProps; | ||
} | ||
|
||
interface FormattedChartData { | ||
date: string; | ||
total: number; | ||
oneClickSuccess: number; | ||
oneClickCreated: number; | ||
oneClickCancelled: number; | ||
[key: string]: string | number; // Allow dynamic key access | ||
} | ||
|
||
const formatChartData = ( | ||
data: SeriesChartData[], | ||
keyValues: KeyValue[], | ||
): FormattedChartData[] => { | ||
// Create a map to store all unique dates | ||
const dateMap = new Map<number, FormattedChartData>(); | ||
|
||
// Collect all unique dates from all series | ||
data.forEach((series) => { | ||
series.chartData.forEach((point) => { | ||
if (!dateMap.has(point.date)) { | ||
const entry: FormattedChartData = { | ||
date: point.date.toString(), | ||
total: 0, | ||
oneClickSuccess: 0, | ||
oneClickCreated: 0, | ||
oneClickCancelled: 0, | ||
}; | ||
// Initialize all keyValues with 0 | ||
keyValues.forEach(({ key }) => { | ||
entry[key] = 0; | ||
}); | ||
dateMap.set(point.date, entry); | ||
} | ||
|
||
// Add the values for each key | ||
keyValues.forEach(({ key }) => { | ||
const currentEntry = dateMap.get( | ||
point.date, | ||
) as unknown as FormattedChartData; | ||
currentEntry[key] = point[key] || 0; | ||
}); | ||
}); | ||
}); | ||
|
||
// Convert the map to array and add totals | ||
return Array.from(dateMap.values()) | ||
.map((entry) => { | ||
const total = keyValues.reduce( | ||
(sum, { key }) => sum + ((entry[key] as number) || 0), | ||
0, | ||
); | ||
return { | ||
...entry, | ||
total, | ||
}; | ||
}) | ||
.sort((a, b) => parseInt(a.date) - parseInt(b.date)); | ||
}; | ||
|
||
export function SeriesPercentageChart( | ||
props: SeriesPercentageChartProps, | ||
): ReactElement { | ||
const formattedData = useMemo(() => { | ||
if (!props.data) return []; | ||
return formatChartData(props.data, props.keyValues); | ||
}, [props.data, props.keyValues]); | ||
|
||
return ( | ||
<Box sx={{ width: '100%', height: '100%', ...props.sx }}> | ||
<ResponsiveContainer width='100%' height='100%'> | ||
<AreaChart | ||
data={formattedData} | ||
stackOffset='expand' | ||
width={500} | ||
height={300} | ||
margin={{ | ||
top: 5, | ||
right: 60, | ||
left: 20, | ||
bottom: 5, | ||
}} | ||
> | ||
<CartesianGrid strokeDasharray='3 3' /> | ||
<XAxis | ||
dataKey='date' | ||
type='number' | ||
domain={['dataMin', 'dataMax']} | ||
tickFormatter={(value) => | ||
formatDateMMYY(value, { | ||
timeZone: props.filter.timezone, | ||
hour12: false, | ||
hour: 'numeric', | ||
}) | ||
} | ||
allowDuplicatedCategory={false} | ||
tickLine={false} | ||
fontSize={12} | ||
tickMargin={12} | ||
/> | ||
<YAxis | ||
textAnchor='end' | ||
tickLine={false} | ||
tickFormatter={(value) => `${(value * 100).toFixed(2)}%`} | ||
/> | ||
<Tooltip | ||
formatter={(value, name, item) => { | ||
const total = item.payload.total; | ||
const percentage = | ||
total === 0 | ||
? '0.00' | ||
: new Decimal(value.toString()) | ||
.div(total) | ||
.mul(100) | ||
.toFixed(2, Decimal.ROUND_DOWN); | ||
|
||
return [`${value.toString()} (${percentage}%)`, name]; | ||
}} | ||
labelFormatter={(value) => | ||
formatExtendedDate(value, { | ||
timeZone: props.filter.timezone, | ||
hour12: false, | ||
}) | ||
} | ||
/> | ||
{props.keyValues.map((keyValue) => ( | ||
<Area | ||
key={keyValue.key} | ||
type='monotone' | ||
dataKey={keyValue.key} | ||
name={keyValue.name} | ||
stackId='1' | ||
stroke={keyValue.color} | ||
fill={keyValue.color} | ||
strokeWidth={2} | ||
/> | ||
))} | ||
</AreaChart> | ||
</ResponsiveContainer> | ||
</Box> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './SeriesChart'; | ||
export * from './SeriesChartLegend'; | ||
export * from './SeriesPercentageChart'; |
82 changes: 82 additions & 0 deletions
82
src/stories/components/chart/SeriesPercentageChart.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { SeriesPercentageChart } from '../../../components/chart/SeriesPercentageChart'; | ||
|
||
const meta = { | ||
title: 'components/chart/SeriesPercentageChart', | ||
component: SeriesPercentageChart, | ||
parameters: { | ||
layout: 'centered', | ||
}, | ||
tags: ['autodocs'], | ||
} satisfies Meta<typeof SeriesPercentageChart>; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
const generateRandomData = (days = 30) => { | ||
const data: any = []; | ||
const now = new Date().getTime(); // Using provided current time | ||
|
||
for (let i = 0; i < days; i++) { | ||
// Generate timestamp for each day, starting from the most recent | ||
const date = now - i * 24 * 60 * 60 * 1000; | ||
|
||
// Generate random values between 0-5 for each series | ||
const oneClickCreated = Math.floor(Math.random() * 6); | ||
const oneClickSuccess = Math.floor(Math.random() * (oneClickCreated + 1)); // Success should be <= Created | ||
const oneClickCancelled = Math.floor( | ||
Math.random() * (oneClickCreated - oneClickSuccess + 1), | ||
); // Cancelled should be <= (Created - Success) | ||
|
||
data.push({ | ||
date, | ||
oneClickCreated, | ||
oneClickSuccess, | ||
oneClickCancelled, | ||
}); | ||
} | ||
|
||
// Sort by date ascending | ||
return data.sort((a, b) => a.date - b.date); | ||
}; | ||
|
||
const sampleData = [ | ||
{ | ||
uuid: 'b07cbc37-fe8f-4920-a6b9-c4e9dfe193cd', | ||
chartData: generateRandomData(30), | ||
}, | ||
]; | ||
|
||
const keyValues = [ | ||
{ key: 'oneClickSuccess', name: 'Success', color: '#0DBC3D' }, | ||
{ key: 'oneClickCreated', name: 'Created', color: '#fbbc05' }, | ||
{ key: 'oneClickCancelled', name: 'Cancelled', color: '#808080' }, | ||
]; | ||
|
||
export const Default: Story = { | ||
args: { | ||
data: sampleData, | ||
filter: { | ||
timezone: 'UTC', | ||
}, | ||
keyValues: keyValues, | ||
sx: { | ||
width: 800, | ||
height: 400, | ||
}, | ||
}, | ||
}; | ||
|
||
export const Empty: Story = { | ||
args: { | ||
data: [], | ||
filter: { | ||
timezone: 'UTC', | ||
}, | ||
keyValues: keyValues, | ||
sx: { | ||
width: 800, | ||
height: 400, | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters