Skip to content

Commit 20e1ebd

Browse files
committed
add LineGraph
1 parent d3e2b79 commit 20e1ebd

4 files changed

Lines changed: 213 additions & 1 deletion

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
3+
import { LineGraph } from './LineGraph';
4+
5+
type GraphData = readonly {
6+
m1: number;
7+
m2: number;
8+
sd1: number;
9+
sd2: number;
10+
time: number;
11+
}[];
12+
13+
type Story = StoryObj<typeof LineGraph<GraphData>>;
14+
15+
const meta: Meta<typeof LineGraph> = {
16+
component: LineGraph,
17+
decorators: [
18+
(Story) => (
19+
<div className="container flex justify-center">
20+
<div style={{ width: 600 }}>
21+
<Story />
22+
</div>
23+
</div>
24+
)
25+
]
26+
};
27+
28+
export default meta;
29+
30+
export const Default: Story = {
31+
args: {
32+
data: [
33+
{
34+
m1: 1000,
35+
m2: 550,
36+
sd1: 100,
37+
sd2: 100,
38+
time: new Date(2000, 0, 1).getTime()
39+
},
40+
{
41+
m1: 1500,
42+
m2: 600,
43+
sd1: 100,
44+
sd2: 100,
45+
time: new Date(2000, 1, 1).getTime()
46+
},
47+
{
48+
m1: 1200,
49+
m2: 500,
50+
sd1: 100,
51+
sd2: 100,
52+
time: new Date(2000, 2, 1).getTime()
53+
},
54+
{
55+
m1: 1800,
56+
m2: 450,
57+
sd1: 100,
58+
sd2: 100,
59+
time: new Date(2000, 3, 1).getTime()
60+
}
61+
],
62+
lines: [
63+
{
64+
err: 'sd1',
65+
name: 'Mean 1',
66+
val: 'm1'
67+
},
68+
{
69+
err: 'sd2',
70+
name: 'Mean 2',
71+
val: 'm2'
72+
}
73+
],
74+
xAxis: {
75+
key: 'time',
76+
label: 'Month'
77+
}
78+
}
79+
};
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import * as React from 'react';
2+
3+
import { toBasicISOString } from '@douglasneuroinformatics/libjs';
4+
import { useTheme, useTranslation } from '@douglasneuroinformatics/libui/hooks';
5+
import type { Theme } from '@douglasneuroinformatics/libui/hooks';
6+
import {
7+
CartesianGrid,
8+
ErrorBar,
9+
Label,
10+
Legend,
11+
Line,
12+
LineChart,
13+
ResponsiveContainer,
14+
Tooltip,
15+
XAxis,
16+
YAxis
17+
} from 'recharts';
18+
import type { LineProps } from 'recharts';
19+
import type { ConditionalKeys } from 'type-fest';
20+
21+
/** An array of arbitrary objects with data to graph */
22+
23+
type LineGraphData = readonly { [key: string]: any }[];
24+
25+
/** Extract string keys from items in `T` where the value of `T[K]` extends `K` */
26+
type ExtractValidKeys<T extends LineGraphData, K> = Extract<ConditionalKeys<T[number], K>, string>;
27+
28+
type LineGraphLine<T extends LineGraphData = { [key: string]: any }[]> = Pick<
29+
LineProps,
30+
'legendType' | 'stroke' | 'strokeDasharray' | 'strokeWidth' | 'type'
31+
> & {
32+
err?: ExtractValidKeys<T, number>;
33+
name: string;
34+
val: ExtractValidKeys<T, number>;
35+
};
36+
37+
const strokeColors = {
38+
dark: '#cbd5e1', // slate-300
39+
light: '#475569' // slate-600
40+
};
41+
42+
const tooltipStyles: { [K in Theme]: React.CSSProperties } = {
43+
dark: {
44+
backgroundColor: '#0f172a', // slate-900
45+
borderColor: strokeColors.light,
46+
borderRadius: '2px'
47+
},
48+
light: {
49+
backgroundColor: '#f1f5f9', // slate-100
50+
borderColor: strokeColors.dark,
51+
borderRadius: '2px'
52+
}
53+
};
54+
55+
// eslint-disable-next-line react/function-component-definition
56+
function LineGraphComponent<const T extends LineGraphData>({
57+
data,
58+
lines,
59+
xAxis
60+
}: {
61+
/** An array of objects, where each object represents one point on the x-axis */
62+
data: T;
63+
lines: LineGraphLine<T>[];
64+
xAxis?: {
65+
key?: ExtractValidKeys<T, number>; // unix time
66+
label?: string;
67+
};
68+
}) {
69+
const { resolvedLanguage } = useTranslation('libui');
70+
const [theme] = useTheme();
71+
72+
return (
73+
<ResponsiveContainer height={400} width="100%">
74+
<LineChart data={[...data]} margin={{ bottom: 5, left: 15, right: 15, top: 5 }}>
75+
<CartesianGrid stroke="#64748b" strokeDasharray="5 5" />
76+
<XAxis
77+
axisLine={{ stroke: '#64748b' }}
78+
dataKey={xAxis?.key}
79+
domain={['auto', 'auto']}
80+
height={50}
81+
interval="preserveStartEnd"
82+
padding={{ left: 20, right: 20 }}
83+
stroke={strokeColors[theme]}
84+
tickFormatter={(time: number) => toBasicISOString(new Date(time))}
85+
tickLine={{ stroke: '#64748b' }}
86+
tickMargin={8}
87+
tickSize={8}
88+
type={'number'}
89+
>
90+
<Label fill={strokeColors[theme]} offset={0} position="insideBottom" value={xAxis?.label} />
91+
</XAxis>
92+
<YAxis
93+
axisLine={{ stroke: '#64748b' }}
94+
stroke={strokeColors[theme]}
95+
tickLine={{ stroke: '#64748b' }}
96+
tickMargin={5}
97+
tickSize={8}
98+
width={40}
99+
/>
100+
<Tooltip
101+
contentStyle={tooltipStyles[theme]}
102+
labelFormatter={(time: number) => {
103+
const date = new Date(time);
104+
return new Intl.DateTimeFormat(resolvedLanguage, {
105+
dateStyle: 'full',
106+
timeStyle: 'medium'
107+
}).format(date);
108+
}}
109+
labelStyle={{ color: strokeColors[theme], fontWeight: 500, whiteSpace: 'pre-wrap' }}
110+
/>
111+
{lines.map(({ err, name, stroke, type, val, ...props }) => (
112+
<Line
113+
{...props}
114+
dataKey={val}
115+
key={val}
116+
name={name}
117+
stroke={stroke ?? strokeColors[theme]}
118+
type={type ?? 'linear'}
119+
>
120+
{err && <ErrorBar dataKey={err} stroke="#64748b" />}
121+
</Line>
122+
))}
123+
<Legend wrapperStyle={{ paddingLeft: 40, paddingTop: 10 }} />
124+
</LineChart>
125+
</ResponsiveContainer>
126+
);
127+
}
128+
129+
export const LineGraph = React.memo(LineGraphComponent) as typeof LineGraphComponent;
130+
131+
export type { LineGraphData, LineGraphLine };
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './LineGraph';

apps/web/src/routes/_app/datahub/$subjectId/graph.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { useRef, useState } from 'react';
22

33
import { toBasicISOString } from '@douglasneuroinformatics/libjs';
4-
import { ActionDropdown, LineGraph, ListboxDropdown } from '@douglasneuroinformatics/libui/components';
4+
import { ActionDropdown, ListboxDropdown } from '@douglasneuroinformatics/libui/components';
55
import type { ListboxDropdownOption } from '@douglasneuroinformatics/libui/components';
66
import { useDownload, useTranslation } from '@douglasneuroinformatics/libui/hooks';
77
import type { AnyUnilingualFormInstrument } from '@opendatacapture/runtime-core';
88
import { createFileRoute } from '@tanstack/react-router';
99
import html2canvas from 'html2canvas';
1010

11+
import { LineGraph } from '@/components/LineGraph';
1112
import { SelectInstrument } from '@/components/SelectInstrument';
1213
import { TimeDropdown } from '@/components/TimeDropdown';
1314
import { useGraphData } from '@/hooks/useGraphData';

0 commit comments

Comments
 (0)