-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
425 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,12 @@ | ||
{ | ||
"presets": [ | ||
[ | ||
"@nx/react/babel", | ||
{ | ||
"runtime": "automatic", | ||
"useBuiltIns": "usage" | ||
} | ||
] | ||
], | ||
"plugins": [] | ||
} |
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,18 @@ | ||
{ | ||
"extends": ["plugin:@nx/react", "../../.eslintrc.json"], | ||
"ignorePatterns": ["!**/*"], | ||
"overrides": [ | ||
{ | ||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"], | ||
"rules": {} | ||
}, | ||
{ | ||
"files": ["*.ts", "*.tsx"], | ||
"rules": {} | ||
}, | ||
{ | ||
"files": ["*.js", "*.jsx"], | ||
"rules": {} | ||
} | ||
] | ||
} |
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,7 @@ | ||
# analytics | ||
|
||
This library was generated with [Nx](https://nx.dev). | ||
|
||
## Running unit tests | ||
|
||
Run `nx test analytics` to execute the unit tests via [Jest](https://jestjs.io). |
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,16 @@ | ||
{ | ||
"name": "analytics", | ||
"$schema": "../../node_modules/nx/schemas/project-schema.json", | ||
"sourceRoot": "libs/analytics/src", | ||
"projectType": "library", | ||
"tags": [], | ||
"targets": { | ||
"lint": { | ||
"executor": "@nx/linter:eslint", | ||
"outputs": ["{options.outputFile}"], | ||
"options": { | ||
"lintFilePatterns": ["libs/analytics/**/*.{ts,tsx,js,jsx}"] | ||
} | ||
} | ||
} | ||
} |
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 @@ | ||
export * from './lib/analytics'; |
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,63 @@ | ||
import { faker } from '@faker-js/faker'; | ||
import { Container } from '@mui/material'; | ||
import dayjs from 'dayjs'; | ||
|
||
import { Analytics } from './analytics'; | ||
|
||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
faker.seed(4548); | ||
|
||
function array<T>(n: number, initial: T, fn: (last: T) => T) { | ||
let last: T = initial; | ||
return new Array(n).fill(0).map((_, index) => { | ||
if (index === 0) return last; | ||
return (last = fn(last)); | ||
}); | ||
} | ||
|
||
const ema = (p: number) => { | ||
let last: number; | ||
return (val: number) => { | ||
return (last = (val + (last ?? val) * (p - 1)) / p); | ||
}; | ||
}; | ||
|
||
const smooth = ema(7); | ||
|
||
const meta: Meta<typeof Analytics> = { | ||
component: Analytics, | ||
title: 'Analytics/test', | ||
args: { | ||
title: 'APY', | ||
data: { | ||
datasets: [ | ||
{ | ||
type: 'line', | ||
data: array(180, { x: new Date('2023-01-01'), y: 0.05 }, (last) => ({ | ||
x: dayjs(last.x).add(1, 'day').toDate(), | ||
y: faker.number.float({ | ||
min: last.y * 0.9, | ||
max: last.y * 1.1, | ||
}), | ||
})).map((d) => ({ | ||
x: d.x, | ||
y: smooth(d.y), | ||
})), | ||
}, | ||
], | ||
}, | ||
formatValues: (val) => { | ||
return `${Math.floor(Number(val) * 10000) / 100}%`; | ||
}, | ||
}, | ||
render: (args) => ( | ||
<Container> | ||
<Analytics {...args} /> | ||
</Container> | ||
), | ||
}; | ||
|
||
export default meta; | ||
|
||
export const Default: StoryObj<typeof Analytics> = {}; |
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,232 @@ | ||
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; | ||
|
||
import { useState } from 'react'; | ||
|
||
import { Box, Paper, Stack, useTheme } from '@mui/material'; | ||
import { Chart, TimeScale } from 'chart.js'; | ||
import dayjs from 'dayjs'; | ||
import { mergeDeepRight } from 'ramda'; | ||
import { Line } from 'react-chartjs-2'; | ||
|
||
import type { ChartData, ChartOptions, Plugin } from 'chart.js'; | ||
import type { ComponentProps } from 'react'; | ||
|
||
Chart.register(TimeScale); | ||
|
||
/** | ||
* TODO: Figure out a proper home for these? | ||
*/ | ||
const chartTheme = { | ||
primary1: '#CF75D5', | ||
primary2: '#FEDBA8', | ||
position: '#0074F0', | ||
background: '#828699', | ||
grid: '#2e2f3d', | ||
positive: '#4EBE96', | ||
negative: '#D44E66', | ||
}; | ||
|
||
const full = { xs: 1, sm: 2, md: 3 }; | ||
const half = { xs: 0.5, sm: 1, md: 1.5 }; | ||
const quarter = { xs: 0.25, sm: 0.5, md: 0.75 }; | ||
|
||
export function Analytics(props: { | ||
title: string; | ||
titleProps: ComponentProps<typeof Box>; | ||
data: ChartData<'line', { x: Date; y: number }[]>; | ||
formatValues?: (value: number | string) => string | number; | ||
}) { | ||
if (!props.formatValues) { | ||
props.formatValues = (value: number | string) => value; | ||
} | ||
const firstData = props.data.datasets.map((d) => d.data[0]); | ||
const lastData = props.data.datasets.map((d) => d.data[d.data.length - 1]); | ||
const [currentData, setCurrentData] = useState(lastData); | ||
const [hovering, setHovering] = useState(false); | ||
const change = currentData[0].y / firstData[0].y - 1; | ||
return ( | ||
<Paper> | ||
<Box sx={{ fontFamily: 'Inter' }}> | ||
<Stack gap={full}> | ||
<Stack | ||
direction="row" | ||
alignItems="center" | ||
justifyContent="space-between" | ||
p={full} | ||
> | ||
<Stack> | ||
<Box mb={half} fontSize={'0.875rem'} {...props.titleProps}> | ||
{props.title} | ||
</Box> | ||
<Stack direction={'row'} mb={quarter} gap={0.5}> | ||
<Box fontSize={'2.5rem'} color={'white'}> | ||
{props.formatValues(currentData[0].y)} | ||
</Box> | ||
{change ? ( | ||
<Box | ||
mt={0.5} | ||
fontSize={'.75rem'} | ||
color={ | ||
change > 0 | ||
? chartTheme.positive | ||
: change < 0 | ||
? chartTheme.negative | ||
: undefined | ||
} | ||
> | ||
{change > 0 ? '+' : null} | ||
{props.formatValues(change)} | ||
</Box> | ||
) : null} | ||
</Stack> | ||
<Box fontSize={'.75rem'} color={hovering ? '#ddd' : undefined}> | ||
{dayjs(currentData[0].x).format('lll')} | ||
</Box> | ||
</Stack> | ||
<Box fontSize={'0.875rem'}>Filter Options</Box> | ||
</Stack> | ||
<Box pr={half} pb={full}> | ||
<DatedLineChart | ||
data={props.data} | ||
plugins={[ | ||
{ | ||
id: 'hooks', | ||
afterDraw: (chart) => { | ||
const elements = chart.getActiveElements(); | ||
if (elements.length) { | ||
setCurrentData( | ||
elements.map( | ||
(element) => | ||
props.data.datasets[element.datasetIndex].data[ | ||
element.index | ||
], | ||
), | ||
); | ||
setHovering(true); | ||
} else { | ||
setCurrentData(lastData); | ||
setHovering(false); | ||
} | ||
}, | ||
}, | ||
]} | ||
options={{ | ||
scales: { | ||
y: { | ||
ticks: { | ||
callback: props.formatValues, | ||
}, | ||
}, | ||
}, | ||
}} | ||
/> | ||
</Box> | ||
</Stack> | ||
</Box> | ||
</Paper> | ||
); | ||
} | ||
|
||
export const DatedLineChart = (props: ComponentProps<typeof Line>) => { | ||
const theme = useTheme(); | ||
let options: ChartOptions<'line'> = { | ||
backgroundColor: chartTheme.background, | ||
color: theme.palette.text.secondary, | ||
responsive: true, | ||
interaction: { | ||
mode: 'nearest', | ||
axis: 'x', | ||
intersect: false, | ||
}, | ||
animation: { | ||
easing: 'easeInOutQuad', | ||
}, | ||
scales: { | ||
x: { | ||
grid: { | ||
display: false, | ||
}, | ||
border: { | ||
display: true, | ||
color: chartTheme.grid, | ||
}, | ||
type: 'time', | ||
ticks: { | ||
autoSkip: true, | ||
autoSkipPadding: 50, | ||
maxRotation: 0, | ||
align: 'start', | ||
}, | ||
time: { | ||
unit: 'day', | ||
}, | ||
}, | ||
y: { | ||
grid: { | ||
color: chartTheme.grid, | ||
drawTicks: false, | ||
}, | ||
border: { | ||
display: false, | ||
color: chartTheme.grid, | ||
dash: [3, 3], | ||
}, | ||
position: 'right', | ||
ticks: { | ||
crossAlign: 'far', | ||
}, | ||
}, | ||
}, | ||
elements: { | ||
line: { | ||
borderWidth: 1, | ||
}, | ||
point: { | ||
radius: 0, | ||
}, | ||
}, | ||
borderColor: (ctx) => { | ||
const gradient = ctx.chart.ctx.createLinearGradient( | ||
0, | ||
0, | ||
ctx.chart.width, | ||
ctx.chart.height, | ||
); | ||
gradient.addColorStop(0, chartTheme.primary2); | ||
gradient.addColorStop(1, chartTheme.primary1); | ||
return gradient; | ||
}, | ||
}; | ||
if (props.options) { | ||
options = mergeDeepRight(props.options, options) as ChartOptions<'line'>; | ||
} | ||
return ( | ||
<Line | ||
{...props} | ||
plugins={[verticalLinePlugin, ...(props.plugins ?? [])]} | ||
options={options} | ||
data={props.data} | ||
/> | ||
); | ||
}; | ||
|
||
const verticalLinePlugin: Plugin<'line'> = { | ||
id: 'verticalLineAtIndex', | ||
afterDraw: (chart) => { | ||
const active = chart.getActiveElements(); | ||
if (active[0]) { | ||
const ctx = chart.ctx; | ||
const x = active[0].element.x; | ||
|
||
ctx.save(); | ||
ctx.beginPath(); | ||
ctx.moveTo(x, chart.chartArea.top); | ||
ctx.lineTo(x, chart.chartArea.bottom); | ||
ctx.setLineDash([2, 2]); | ||
ctx.strokeStyle = chartTheme.position; | ||
ctx.lineWidth = 0.5; | ||
ctx.stroke(); | ||
ctx.restore(); | ||
} | ||
}, | ||
}; |
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,17 @@ | ||
{ | ||
"compilerOptions": { | ||
"jsx": "react-jsx", | ||
"allowJs": false, | ||
"esModuleInterop": false, | ||
"allowSyntheticDefaultImports": true, | ||
"strict": true | ||
}, | ||
"files": [], | ||
"include": [], | ||
"references": [ | ||
{ | ||
"path": "./tsconfig.lib.json" | ||
} | ||
], | ||
"extends": "../../tsconfig.base.json" | ||
} |
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,24 @@ | ||
{ | ||
"extends": "./tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "../../dist/out-tsc", | ||
"types": [ | ||
"node", | ||
|
||
"@nx/react/typings/cssmodule.d.ts", | ||
"@nx/react/typings/image.d.ts" | ||
] | ||
}, | ||
"exclude": [ | ||
"jest.config.ts", | ||
"src/**/*.spec.ts", | ||
"src/**/*.test.ts", | ||
"src/**/*.spec.tsx", | ||
"src/**/*.test.tsx", | ||
"src/**/*.spec.js", | ||
"src/**/*.test.js", | ||
"src/**/*.spec.jsx", | ||
"src/**/*.test.jsx" | ||
], | ||
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] | ||
} |
Oops, something went wrong.