vega-frontend-monorepo/libs/ui-toolkit/src/components/sparkline/sparkline.tsx
2023-05-22 21:33:16 -07:00

128 lines
3.6 KiB
TypeScript

import { extent } from 'd3-array';
import { scaleLinear } from 'd3-scale';
import { line } from 'd3-shape';
import classNames from 'classnames';
import isEqual from 'lodash/isEqual';
import React from 'react';
function colorByChange(a: number, b: number) {
return a === b
? 'stroke-black/40 dark:stroke-white/40'
: a < b
? 'stroke-vega-green dark:stroke-vega-green'
: 'stroke-vega-pink dark:stroke-vega-pink';
}
export interface SparklineProps {
data: number[];
width?: number;
height?: number;
points?: number;
className?: string;
muted?: boolean;
}
export const SparklineView = ({
data,
width = 60,
height = 15,
points = 25,
muted = false,
className,
}: SparklineProps) => {
// How many points are missing. If market is 12 hours old the 25 - 12
const preMarketLength = points - data.length;
// Create two dimensional array for sparkline points [x, y]
const marketData: [number, number][] = data.map((d, i) => [
preMarketLength + i,
d,
]);
// Empty two dimensional array for gray, 'no data' line
let preMarketData: [number, number][] = [];
// Get the extent for our y value
const [min, max] = extent(marketData, (d) => d[1]);
if (typeof min !== 'number' || typeof max !== 'number') {
return null;
}
// Create a second set of data to render a gray line for any
// missing points if the market is less than 24 hours old
if (marketData.length < points) {
// Populate preMarketData with the average of our extents
// so that the line renders centered vertically
const fillValue = (min + max) / 2;
preMarketData = new Array(points - marketData.length)
.fill(fillValue)
.map((d: number, i) => [i, d] as [number, number]);
// Add the first point of or market data so that the two
// lines join up
preMarketData.push(marketData[0] as [number, number]);
}
const xScale = scaleLinear().domain([0, points]).range([0, 100]);
const yScale = scaleLinear().domain([min, max]).range([100, 0]);
const lineSeries = line()
.x((d) => xScale(d[0]))
.y((d) => yScale(d[1]));
// Get the color of the marketData line
const [firstVal, lastVal] = [data[0], data[data.length - 1]];
const strokeClassName = muted
? data.length >= 24
? colorByChange(firstVal, lastVal)
: 'stroke-black/40 dark:stroke-white/40'
: colorByChange(firstVal, lastVal);
// Create paths
const preMarketCreationPath = lineSeries(preMarketData);
const mainPath = lineSeries(marketData);
const pathProps = {
'data-testid': 'sparkline-path',
className: `[vector-effect:non-scaling-stroke] ${strokeClassName}`,
stroke: 'strokeCurrent',
strokeWidth: 1,
fill: 'transparent',
};
return (
<svg
data-testid="sparkline-svg"
className={classNames('w-full', className)}
width={width}
height={height}
viewBox="0 0 100 100"
preserveAspectRatio="none"
>
{preMarketCreationPath && (
<path {...pathProps} d={preMarketCreationPath} />
)}
{mainPath && <path {...pathProps} d={mainPath} />}
</svg>
);
};
SparklineView.displayName = 'SparklineView';
// Use react memo to only re-render if props change
export const Sparkline = React.memo(
SparklineView,
function (prevProps, nextProps) {
// Warning! The return value here is the opposite of shouldComponentUpdate.
// Return true if you DON NOT want a re-render
if (
prevProps.width !== nextProps.width ||
prevProps.height !== nextProps.height
) {
return false;
}
return isEqual(prevProps.data, nextProps.data);
}
);
Sparkline.displayName = 'Sparkline';