Commit 43a3a998 authored by tom's avatar tom

basic graph implementation

parent ede913d3
__snapshots__/** filter=lfs diff=lfs merge=lfs -text __snapshots__/** filter=lfs diff=lfs merge=lfs -text
data/charts_eth_txs.json filter=lfs diff=lfs merge=lfs -text
This source diff could not be displayed because it is stored in LFS. You can view the blob instead.
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import Graph from 'ui/pages/Graph';
const GraphPage: NextPage = () => {
return (
<>
<Head><title>Graph Page</title></Head>
<Graph/>
</>
);
};
export default GraphPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import React from 'react';
import json from 'data/charts_eth_txs.json';
import Area from 'ui/shared/graphs/Area';
import Axis from 'ui/shared/graphs/Axis';
import GridLine from 'ui/shared/graphs/GridLine';
import Line from 'ui/shared/graphs/Line';
import useController from 'ui/shared/graphs/useController';
const dimensions = {
width: 600,
height: 300,
margin: { top: 30, right: 30, bottom: 30, left: 60 },
};
const data = {
name: 'VCIT',
color: '#5e4fa2',
items: json.map((d) => ({ ...d, date: new Date(d.date) })),
};
const EthereumDailyTxsChart = () => {
const { width, height, margin } = dimensions;
const svgWidth = width + margin.left + margin.right;
const svgHeight = height + margin.top + margin.bottom;
const controller = useController({ data, width, height });
const { yTickFormat, xScale, yScale } = controller;
return (
<svg width={ svgWidth } height={ svgHeight }>
<g transform={ `translate(${ margin.left },${ margin.top })` }>
{ /* base grid line */ }
<GridLine
type="horizontal"
scale={ yScale }
ticks={ 1 }
size={ width }
disableAnimation
/>
<GridLine
type="vertical"
scale={ xScale }
ticks={ 5 }
size={ height }
transform={ `translate(0, ${ height })` }
/>
<GridLine
type="horizontal"
scale={ yScale }
ticks={ 5 }
size={ width }
/>
<Line
data={ data.items }
xScale={ xScale }
yScale={ yScale }
color={ data.color }
animation="left"
/>
<Area
data={ data.items }
color={ data.color }
xScale={ xScale }
yScale={ yScale }
/>
<Axis
type="left"
scale={ yScale }
transform="translate(0, -10)"
ticks={ 5 }
tickFormat={ yTickFormat }
/>
<Axis
type="bottom"
scale={ xScale }
transform={ `translate(10, ${ height - height / 6 })` }
ticks={ 5 }
/>
</g>
</svg>
);
};
export default EthereumDailyTxsChart;
import React from 'react';
import EthereumDailyTxsChart from 'ui/charts/EthereumDailyTxsChart';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const Graph = () => {
return (
<Page>
<PageTitle text="Ethereum Daily Transactions Chart"/>
<EthereumDailyTxsChart/>
</Page>
);
};
export default Graph;
import * as d3 from 'd3';
import React from 'react';
interface DataItem {
date: Date;
value: number;
}
interface Props {
xScale: d3.ScaleTime<number, number> | d3.ScaleLinear<number, number>;
yScale: d3.ScaleTime<number, number> | d3.ScaleLinear<number, number>;
color: string;
data: Array<DataItem>;
disableAnimation?: boolean;
}
const Area = ({ xScale, yScale, color, data, disableAnimation, ...props }: Props) => {
const ref = React.useRef(null);
React.useEffect(() => {
if (disableAnimation) {
d3.select(ref.current).attr('opacity', 1);
return;
}
d3.select(ref.current).transition()
.duration(750)
.ease(d3.easeBackIn)
.attr('opacity', 1);
}, [ disableAnimation ]);
const d = React.useMemo(() => {
const area = d3.area<DataItem>()
.x(({ date }) => xScale(date))
.y1(({ value }) => yScale(value))
.y0(() => yScale(yScale.domain()[0]));
return area(data) || undefined;
}, [ xScale, yScale, data ]);
return (
<>
<path ref={ ref } d={ d } fill={ `url(#gradient-${ color })` } opacity={ 0 } { ...props }/>
<defs>
<linearGradient id={ `gradient-${ color }` } x1="0%" x2="0%" y1="0%" y2="100%">
<stop offset="0%" stopColor={ color } stopOpacity={ 0.5 }/>
<stop offset="100%" stopColor={ color } stopOpacity={ 0 }/>
</linearGradient>
</defs>
</>
);
};
export default React.memo(Area);
import * as d3 from 'd3';
import React from 'react';
interface Props {
type: 'left' | 'bottom';
scale: d3.ScaleTime<number, number> | d3.ScaleLinear<number, number>;
disableAnimation?: boolean;
transform?: string;
ticks: number;
tickFormat?: (domainValue: d3.AxisDomain, index: number) => string;
}
const Axis = ({ type, scale, ticks, transform, tickFormat, disableAnimation, ...props }: Props) => {
const ref = React.useRef<SVGGElement>(null);
React.useEffect(() => {
if (!ref.current) {
return;
}
const axisGenerator = type === 'left' ? d3.axisLeft : d3.axisBottom;
const axis = tickFormat ?
axisGenerator(scale).ticks(ticks).tickFormat(tickFormat) :
axisGenerator(scale).ticks(ticks);
const axisGroup = d3.select(ref.current);
if (disableAnimation) {
axisGroup.call(axis);
} else {
axisGroup.transition().duration(750).ease(d3.easeLinear).call(axis);
}
axisGroup.select('.domain').remove();
axisGroup.selectAll('line').remove();
axisGroup.selectAll('text')
.attr('opacity', 0.5)
.attr('color', 'white')
.attr('font-size', '0.75rem');
}, [ scale, ticks, tickFormat, disableAnimation, type ]);
return <g ref={ ref } transform={ transform } { ...props }/>;
};
export default React.memo(Axis);
import * as d3 from 'd3';
import React from 'react';
interface Props {
type: 'vertical' | 'horizontal';
scale: d3.ScaleTime<number, number> | d3.ScaleLinear<number, number>;
disableAnimation?: boolean;
size: number;
transform?: string;
ticks: number;
}
const GridLine = ({ type, scale, ticks, size, transform, disableAnimation, ...props }: Props) => {
const ref = React.useRef<SVGGElement>(null);
React.useEffect(() => {
if (!ref.current) {
return;
}
const axisGenerator = type === 'vertical' ? d3.axisBottom : d3.axisLeft;
const axis = axisGenerator(scale).ticks(ticks).tickSize(-size);
const gridGroup = d3.select(ref.current);
if (disableAnimation) {
gridGroup.call(axis);
} else {
gridGroup.transition().duration(750).ease(d3.easeLinear).call(axis);
}
gridGroup.select('.domain').remove();
gridGroup.selectAll('text').remove();
gridGroup.selectAll('line').attr('stroke', 'rgba(255, 255, 255, 0.1)');
}, [ scale, ticks, size, disableAnimation, type ]);
return <g ref={ ref } transform={ transform } { ...props }/>;
};
export default React.memo(GridLine);
import * as d3 from 'd3';
import React from 'react';
interface DataItem {
date: Date;
value: number;
}
interface Props {
xScale: d3.ScaleTime<number, number> | d3.ScaleLinear<number, number>;
yScale: d3.ScaleTime<number, number> | d3.ScaleLinear<number, number>;
color: string;
data: Array<DataItem>;
animation: 'left' | 'fadeIn' | 'none';
}
const Line = ({ xScale, yScale, color, data, animation, ...props }: Props) => {
const ref = React.useRef<SVGPathElement>(null);
// Define different types of animation that we can use
const animateLeft = React.useCallback(() => {
const totalLength = ref.current?.getTotalLength() || 0;
d3.select(ref.current)
.attr('opacity', 1)
.attr('stroke-dasharray', `${ totalLength },${ totalLength }`)
.attr('stroke-dashoffset', totalLength)
.transition()
.duration(750)
.ease(d3.easeLinear)
.attr('stroke-dashoffset', 0);
}, []);
const animateFadeIn = React.useCallback(() => {
d3.select(ref.current)
.transition()
.duration(750)
.ease(d3.easeLinear)
.attr('opacity', 1);
}, []);
const noneAnimation = React.useCallback(() => {
d3.select(ref.current).attr('opacity', 1);
}, []);
React.useEffect(() => {
switch (animation) {
case 'left':
animateLeft();
break;
case 'fadeIn':
animateFadeIn();
break;
case 'none':
default:
noneAnimation();
break;
}
}, [ animateLeft, animateFadeIn, noneAnimation, animation ]);
// Recalculate line length if scale has changed
React.useEffect(() => {
if (animation === 'left') {
const totalLength = ref.current?.getTotalLength();
d3.select(ref.current).attr(
'stroke-dasharray',
`${ totalLength },${ totalLength }`,
);
}
}, [ xScale, yScale, animation ]);
const line = d3.line<DataItem>()
.x((d) => xScale(d.date))
.y((d) => yScale(d.value));
return (
<path
ref={ ref }
d={ line(data) || undefined }
stroke={ color }
strokeWidth={ 1 }
fill="none"
opacity={ 0 }
{ ...props }
/>
);
};
export default React.memo(Line);
import * as d3 from 'd3';
import { useMemo } from 'react';
interface DataItem {
date: Date;
value: number;
}
interface Props {
data: {
items: Array<DataItem>;
};
width: number;
height: number;
}
const useController = ({ data, width, height }: Props) => {
const xMin = useMemo(
() => d3.min(data.items, ({ date }) => date) || new Date(),
[ data ],
);
const xMax = useMemo(
() => d3.max(data.items, ({ date }) => date) || new Date(),
[ data ],
);
const xScale = useMemo(
() => d3.scaleTime().domain([ xMin, xMax ]).range([ 0, width ]),
[ xMin, xMax, width ],
);
const yMin = useMemo(
() => d3.min(data.items, ({ value }) => value) || 0,
[ data ],
);
const yMax = useMemo(
() => d3.max(data.items, ({ value }) => value) || 0,
[ data ],
);
const yScale = useMemo(() => {
const indention = (yMax - yMin) * 0.5;
return d3.scaleLinear()
.domain([ Math.max(yMin - indention, 0), yMax + indention ])
.range([ height, 0 ]);
}, [ height, yMin, yMax ]);
const yScaleForAxis = useMemo(
() => d3.scaleBand().domain([ String(yMin), String(yMax) ]).range([ height, 0 ]),
[ height, yMin, yMax ],
);
const yTickFormat = (d: d3.AxisDomain) => d.toLocaleString();
return {
yTickFormat,
xScale,
yScale,
yScaleForAxis,
};
};
export default useController;
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment