Feat/324 sparkline component (#381)
* move colors in ui-toolkit/config * fixing tests and add stories * fixing some tests and edit story * update sparkline story * rename story templates for sparkline * use tailwind colors and rename to *.spec.tsx * use tailwind only * remove test.tsx * Update libs/tailwindcss-config/src/theme.js Co-authored-by: Matthew Russell <mattrussell36@gmail.com> * remove gray from tailwind and use black/40 and white/40 for strokeCurrent Co-authored-by: madalinaraicu <“madalina@raygroup.uk”> Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
parent
19a9e9adb0
commit
319e14164d
@ -5,7 +5,6 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Colors } from '../../config';
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
@ -26,6 +25,9 @@ import {
|
||||
WalletCardRow,
|
||||
} from '../wallet-card';
|
||||
import { Button, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import { theme } from '@vegaprotocol/tailwindcss-config';
|
||||
|
||||
const Colors = theme.colors;
|
||||
|
||||
const removeLeadingAddressSymbol = (key: string) => {
|
||||
if (key && key.length > 2 && key.slice(0, 2) === '0x') {
|
||||
@ -68,13 +70,13 @@ const AssociatedAmounts = ({
|
||||
total={associationAmounts.total}
|
||||
leftLabel={t('associated')}
|
||||
rightLabel={t('notAssociated')}
|
||||
leftColor={Colors.WHITE}
|
||||
rightColor={Colors.BLACK}
|
||||
leftColor={Colors.white.DEFAULT}
|
||||
rightColor={Colors.black.DEFAULT}
|
||||
light={true}
|
||||
/>
|
||||
{vestingAssociationByVegaKey.length ? (
|
||||
<>
|
||||
<hr style={{ borderStyle: 'dashed', color: Colors.TEXT }} />
|
||||
<hr style={{ borderStyle: 'dashed', color: Colors.text }} />
|
||||
<WalletCardRow label="Associated with Vega keys" bold={true} />
|
||||
{vestingAssociationByVegaKey.map(([key, amount]) => {
|
||||
return (
|
||||
|
@ -2,9 +2,11 @@ import './locked-progress.scss';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Colors } from '../../config';
|
||||
import { formatNumber } from '../../lib/format-number';
|
||||
import type { BigNumber } from '../../lib/bignumber';
|
||||
import { theme } from '@vegaprotocol/tailwindcss-config';
|
||||
|
||||
const Colors = theme.colors;
|
||||
|
||||
export interface LockedProgressProps {
|
||||
total: BigNumber;
|
||||
@ -23,8 +25,8 @@ export const LockedProgress = ({
|
||||
unlocked,
|
||||
leftLabel,
|
||||
rightLabel,
|
||||
leftColor = Colors.PINK,
|
||||
rightColor = Colors.GREEN,
|
||||
leftColor = Colors.pink,
|
||||
rightColor = Colors.green.DEFAULT,
|
||||
light = false,
|
||||
}: LockedProgressProps) => {
|
||||
const lockedPercentage = React.useMemo(() => {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Colors, Links } from '../../config';
|
||||
import { Links } from '../../config';
|
||||
|
||||
export const DownloadWalletPrompt = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -9,7 +8,7 @@ export const DownloadWalletPrompt = () => {
|
||||
<h3>{t('getWallet')}</h3>
|
||||
<p style={{ margin: 0 }}>
|
||||
<a
|
||||
style={{ color: Colors.DEEMPHASISE }}
|
||||
className={'text-deemphasise'}
|
||||
href={Links.WALLET_GUIDE}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@ -19,7 +18,7 @@ export const DownloadWalletPrompt = () => {
|
||||
</p>
|
||||
<p style={{ margin: 0 }}>
|
||||
<a
|
||||
style={{ color: Colors.DEEMPHASISE }}
|
||||
className={'text-deemphasise'}
|
||||
href={Links.WALLET_RELEASES}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
|
@ -1,24 +0,0 @@
|
||||
export const Colors = {
|
||||
WHITE: '#FFF',
|
||||
RED: '#ED1515',
|
||||
PINK: '#ff2d5e',
|
||||
GREEN: '#26ff8a',
|
||||
TEXT: '#c7c7c7',
|
||||
|
||||
BLACK: '#000',
|
||||
DEEMPHASISE: '#8a9ba8',
|
||||
GRAY_DARK_1: '#292929',
|
||||
GRAY_DARK_2: '#333',
|
||||
GRAY: '#494949',
|
||||
GRAY_LIGHT: '#ccc',
|
||||
GRAY_LIGHT_1: '#6e6e6e',
|
||||
GREEN_DARK: '#246340', // Same as GREEN_TRANSPARENT given a background of #1f1f1f
|
||||
GREEN_TRANSPARENT: 'rgba(38, 255, 138, 0.3)',
|
||||
RED_TRANSPARENT: 'rgba(255, 38, 65, 0.3)',
|
||||
TRANSPARENT: 'rgba(0,0,0,0)',
|
||||
|
||||
VEGA_RED: '#ff261a',
|
||||
VEGA_ORANGE: '#d9822b',
|
||||
VEGA_GREEN: '#26ff8a',
|
||||
VEGA_YELLOW: '#daff0d',
|
||||
};
|
@ -1,4 +1,3 @@
|
||||
export * from './colors';
|
||||
export * from './flags';
|
||||
export * from './ethereum';
|
||||
export * from './links';
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Colors } from '../config';
|
||||
import { usePrevious } from './use-previous';
|
||||
import type { BigNumber } from '../lib/bignumber';
|
||||
import { theme as tailwindcss } from '@vegaprotocol/tailwindcss-config';
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
const Colors = tailwindcss.colors;
|
||||
|
||||
const FLASH_DURATION = 1200; // Duration of flash animation in milliseconds
|
||||
|
||||
@ -11,6 +13,7 @@ export function useAnimateValue(
|
||||
) {
|
||||
const shouldAnimate = React.useRef(false);
|
||||
const previous = usePrevious(value);
|
||||
const [theme] = useThemeSwitcher();
|
||||
|
||||
React.useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
@ -28,9 +31,17 @@ export function useAnimateValue(
|
||||
) {
|
||||
elRef.current?.animate(
|
||||
[
|
||||
{ backgroundColor: Colors.VEGA_RED, color: Colors.WHITE },
|
||||
{ backgroundColor: Colors.VEGA_RED, color: Colors.WHITE, offset: 0.8 },
|
||||
{ backgroundColor: Colors.GRAY_LIGHT, color: Colors.WHITE },
|
||||
{ backgroundColor: Colors.red.vega, color: Colors.white.DEFAULT },
|
||||
{
|
||||
backgroundColor: Colors.red.vega,
|
||||
color: Colors.white.DEFAULT,
|
||||
offset: 0.8,
|
||||
},
|
||||
{
|
||||
backgroundColor:
|
||||
theme === 'dark' ? Colors.white[60] : Colors.black[60],
|
||||
color: Colors.white.DEFAULT,
|
||||
},
|
||||
],
|
||||
FLASH_DURATION
|
||||
);
|
||||
@ -43,13 +54,20 @@ export function useAnimateValue(
|
||||
) {
|
||||
elRef.current?.animate(
|
||||
[
|
||||
{ backgroundColor: Colors.VEGA_GREEN, color: Colors.WHITE },
|
||||
{
|
||||
backgroundColor: Colors.VEGA_GREEN,
|
||||
color: Colors.WHITE,
|
||||
backgroundColor: Colors.green.vega,
|
||||
color: Colors.white.DEFAULT,
|
||||
},
|
||||
{
|
||||
backgroundColor: Colors.green.vega,
|
||||
color: Colors.white.DEFAULT,
|
||||
offset: 0.8,
|
||||
},
|
||||
{ backgroundColor: Colors.GRAY_LIGHT, color: Colors.WHITE },
|
||||
{
|
||||
backgroundColor:
|
||||
theme === 'dark' ? Colors.white[60] : Colors.black[60],
|
||||
color: Colors.white.DEFAULT,
|
||||
},
|
||||
],
|
||||
FLASH_DURATION
|
||||
);
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { Colors } from '../../config';
|
||||
|
||||
interface TargetAddressMismatchProps {
|
||||
connectedAddress: string;
|
||||
expectedAddress: string;
|
||||
@ -27,7 +25,7 @@ export const TargetAddressMismatch = ({
|
||||
}}
|
||||
components={{
|
||||
bold: <strong />,
|
||||
red: <span style={{ color: Colors.RED }} />,
|
||||
red: <span className={'text-red'} />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
@ -3,7 +3,6 @@ import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link, useParams, useOutletContext } from 'react-router-dom';
|
||||
|
||||
import { TransactionCallout } from '../../../components/transaction-callout';
|
||||
import { Colors } from '../../../config';
|
||||
import { ADDRESSES } from '../../../config';
|
||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||
import { useContracts } from '../../../contexts/contracts/contracts-context';
|
||||
@ -88,7 +87,7 @@ export const RedeemFromTranche = () => {
|
||||
{txState.txState !== TxState.Default ? (
|
||||
<TransactionCallout
|
||||
completeHeading={
|
||||
<strong style={{ color: Colors.WHITE }}>
|
||||
<strong className={'text-white'}>
|
||||
{t('Tokens from this Tranche have been redeemed')}
|
||||
</strong>
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { TokenInput } from '../../components/token-input';
|
||||
import { Colors, NetworkParams } from '../../config';
|
||||
import { NetworkParams } from '../../config';
|
||||
import { useAppState } from '../../contexts/app-state/app-state-context';
|
||||
import { useNetworkParam } from '../../hooks/use-network-param';
|
||||
import { useSearchParams } from '../../hooks/use-search-params';
|
||||
@ -203,11 +203,9 @@ export const StakingForm = ({
|
||||
availableStakeToRemove.isEqualTo(0)
|
||||
) {
|
||||
if (appState.lien.isGreaterThan(0)) {
|
||||
return (
|
||||
<span style={{ color: Colors.RED }}>{t('stakeNodeWrongVegaKey')}</span>
|
||||
);
|
||||
return <span className={'text-red'}>{t('stakeNodeWrongVegaKey')}</span>;
|
||||
} else {
|
||||
return <span style={{ color: Colors.RED }}>{t('stakeNodeNone')}</span>;
|
||||
return <span className={'text-red'}>{t('stakeNodeNone')}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { EpochCountdown } from '../../components/epoch-countdown';
|
||||
import { Colors } from '../../config';
|
||||
import type { VegaKeyExtended } from '@vegaprotocol/wallet';
|
||||
import { BigNumber } from '../../lib/bignumber';
|
||||
import type { Staking as StakingQueryResult } from './__generated__/Staking';
|
||||
@ -88,9 +87,7 @@ export const StakingNode = ({ vegaKey, data }: StakingNodeProps) => {
|
||||
|
||||
if (!nodeInfo) {
|
||||
return (
|
||||
<span style={{ color: Colors.RED }}>
|
||||
{t('stakingNodeNotFound', { node })}
|
||||
</span>
|
||||
<span className={'text-red'}>{t('stakingNodeNotFound', { node })}</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Colors } from '../../config';
|
||||
import { getAbbreviatedNumber } from '../../lib/abbreviate-number';
|
||||
import { ProgressBar } from './progress-bar';
|
||||
import type { BigNumber } from '../../lib/bignumber';
|
||||
import { theme } from '@vegaprotocol/tailwindcss-config';
|
||||
const Colors = theme.colors;
|
||||
|
||||
interface TrancheProgressProps {
|
||||
locked: BigNumber;
|
||||
@ -28,7 +29,7 @@ export const TrancheProgress = ({
|
||||
<span className="tranches__progress-title">{t('Locked')}</span>
|
||||
<ProgressBar
|
||||
width={220}
|
||||
color={Colors.PINK}
|
||||
color={Colors.pink}
|
||||
percentage={lockedPercentage}
|
||||
/>
|
||||
<span className="tranches__progress-numbers">
|
||||
@ -39,7 +40,7 @@ export const TrancheProgress = ({
|
||||
<span className="tranches__progress-title">{t('Redeemed')}</span>
|
||||
<ProgressBar
|
||||
width={220}
|
||||
color={Colors.GREEN}
|
||||
color={Colors.green.DEFAULT}
|
||||
percentage={removedPercentage}
|
||||
/>
|
||||
<span className="tranches__progress-numbers">
|
||||
|
@ -13,10 +13,12 @@ import {
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
|
||||
import { Colors } from '../../config';
|
||||
import { DATE_FORMAT_LONG } from '../../lib/date-formats';
|
||||
import data from './data.json';
|
||||
|
||||
import { theme } from '@vegaprotocol/tailwindcss-config';
|
||||
const Colors = theme.colors;
|
||||
|
||||
const ORDER = ['community', 'publicSale', 'earlyInvestors', 'team'];
|
||||
|
||||
export const VestingChart = () => {
|
||||
@ -31,10 +33,10 @@ export const VestingChart = () => {
|
||||
<AreaChart data={data}>
|
||||
<defs>
|
||||
{[
|
||||
['pink', Colors.PINK],
|
||||
['green', Colors.VEGA_GREEN],
|
||||
['orange', Colors.VEGA_ORANGE],
|
||||
['yellow', Colors.VEGA_YELLOW],
|
||||
['pink', Colors.pink],
|
||||
['green', Colors.green.vega],
|
||||
['orange', Colors.orange],
|
||||
['yellow', Colors.yellow.DEFAULT],
|
||||
].map(([key, color]) => (
|
||||
<linearGradient id={key} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={color} stopOpacity={0.85} />
|
||||
@ -43,7 +45,7 @@ export const VestingChart = () => {
|
||||
))}
|
||||
</defs>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: Colors.BLACK }}
|
||||
contentStyle={{ backgroundColor: Colors.black.DEFAULT }}
|
||||
separator=":"
|
||||
formatter={(value: number) => {
|
||||
return (
|
||||
@ -68,7 +70,7 @@ export const VestingChart = () => {
|
||||
value={t('VEGA').toString()}
|
||||
position="left"
|
||||
offset={-5}
|
||||
fill={Colors.WHITE}
|
||||
fill={Colors.white.DEFAULT}
|
||||
/>
|
||||
</YAxis>
|
||||
<XAxis dataKey="date">
|
||||
@ -76,24 +78,24 @@ export const VestingChart = () => {
|
||||
value={t('date').toString()}
|
||||
position="bottom"
|
||||
offset={5}
|
||||
fill={Colors.WHITE}
|
||||
fill={Colors.white.DEFAULT}
|
||||
/>
|
||||
</XAxis>
|
||||
<ReferenceLine
|
||||
x={currentDate}
|
||||
stroke={Colors.WHITE}
|
||||
stroke={Colors.white.DEFAULT}
|
||||
strokeWidth={2}
|
||||
label={{
|
||||
position: 'right',
|
||||
value: currentDate,
|
||||
fill: Colors.WHITE,
|
||||
fill: Colors.white.DEFAULT,
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
dot={false}
|
||||
type="linear"
|
||||
dataKey="team"
|
||||
stroke={Colors.PINK}
|
||||
stroke={Colors.pink}
|
||||
fill="url(#pink)"
|
||||
yAxisId={0}
|
||||
strokeWidth={2}
|
||||
@ -105,7 +107,7 @@ export const VestingChart = () => {
|
||||
dot={false}
|
||||
type="monotone"
|
||||
dataKey="earlyInvestors"
|
||||
stroke={Colors.VEGA_GREEN}
|
||||
stroke={Colors.green.vega}
|
||||
fill="url(#green)"
|
||||
yAxisId={0}
|
||||
strokeWidth={2}
|
||||
@ -117,7 +119,7 @@ export const VestingChart = () => {
|
||||
dot={false}
|
||||
type="monotone"
|
||||
dataKey="publicSale"
|
||||
stroke={Colors.VEGA_YELLOW}
|
||||
stroke={Colors.yellow.DEFAULT}
|
||||
fill="url(#yellow)"
|
||||
yAxisId={0}
|
||||
strokeWidth={2}
|
||||
@ -129,7 +131,7 @@ export const VestingChart = () => {
|
||||
dot={false}
|
||||
type="monotone"
|
||||
dataKey="community"
|
||||
stroke={Colors.VEGA_ORANGE}
|
||||
stroke={Colors.orange}
|
||||
fill="url(#orange)"
|
||||
yAxisId={0}
|
||||
strokeWidth={2}
|
||||
|
@ -71,8 +71,8 @@ export const FlashCell = memo(({ children, value }: FlashCellProps) => {
|
||||
if (value < previousValue) {
|
||||
ref.current?.animate(
|
||||
[
|
||||
{ color: theme.colors.vega.pink },
|
||||
{ color: theme.colors.vega.pink, offset: 0.8 },
|
||||
{ color: theme.colors.pink },
|
||||
{ color: theme.colors.pink, offset: 0.8 },
|
||||
{ color: 'inherit' },
|
||||
],
|
||||
FLASH_DURATION
|
||||
@ -80,8 +80,8 @@ export const FlashCell = memo(({ children, value }: FlashCellProps) => {
|
||||
} else if (value > previousValue) {
|
||||
ref.current?.animate(
|
||||
[
|
||||
{ color: theme.colors.vega.green },
|
||||
{ color: theme.colors.vega.green, offset: 0.8 },
|
||||
{ color: theme.colors.green.vega },
|
||||
{ color: theme.colors.green.vega, offset: 0.8 },
|
||||
{ color: 'inherit' },
|
||||
],
|
||||
FLASH_DURATION
|
||||
|
@ -10,6 +10,26 @@ module.exports = {
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
bullish: '#26FF8A',
|
||||
bearish: '#ED1515',
|
||||
vega: {
|
||||
yellow: '#EDFF22',
|
||||
pink: '#FF2D5E',
|
||||
green: '#00F780',
|
||||
},
|
||||
red: {
|
||||
DEFAULT: '#ED1515',
|
||||
transparent: 'rgba(255, 38, 65, 0.3)',
|
||||
vega: '#FF261A',
|
||||
},
|
||||
green: {
|
||||
DEFAULT: '#26FF8A',
|
||||
transparent: 'rgba(38, 255, 138, 0.3)',
|
||||
dark: '#246340',
|
||||
vega: '#00F780',
|
||||
},
|
||||
text: '#C7C7C7',
|
||||
deemphasise: '#8A9BA8',
|
||||
white: {
|
||||
DEFAULT: '#FFF',
|
||||
'02': 'rgba(255, 255, 255, 0.02)',
|
||||
@ -36,12 +56,12 @@ module.exports = {
|
||||
},
|
||||
blue: '#1DA2FB',
|
||||
coral: '#FF6057',
|
||||
vega: {
|
||||
yellow: '#EDFF22',
|
||||
pink: '#FF2D5E',
|
||||
green: '#00F780',
|
||||
pink: '#FF2D5E',
|
||||
orange: '#D9822B',
|
||||
yellow: {
|
||||
DEFAULT: '#EDFF22',
|
||||
dark: '#474B0A', // yellow 0.3 opacity on black
|
||||
},
|
||||
'vega-yellow-dark': '#474B0A', // yellow 0.3 opacity on black
|
||||
intent: {
|
||||
danger: '#FF261A',
|
||||
warning: '#FF7A1A',
|
||||
|
@ -18,7 +18,7 @@ const vegaCustomClasses = plugin(function ({ addUtilities }) {
|
||||
},
|
||||
'.dark .syntax-highlighter-wrapper .hljs': {
|
||||
background: '#2C2C2C',
|
||||
color: theme.colors.vega.green,
|
||||
color: theme.colors.vega.green.DEFAULT,
|
||||
},
|
||||
'.syntax-highlighter-wrapper .hljs-literal': {
|
||||
color: theme.colors.vega.pink,
|
||||
|
@ -9,7 +9,7 @@ const agGridLightVariables = `
|
||||
--ag-header-background-color: ${theme.colors.white[100]};
|
||||
--ag-odd-row-background-color: ${theme.colors.white[100]};
|
||||
--ag-row-border-color: ${theme.colors.white[100]};
|
||||
--ag-row-hover-color: ${theme.colors.vega.yellow};
|
||||
--ag-row-hover-color: ${theme.colors.yellow.DEFAULT};
|
||||
--ag-font-size: 12px;
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ const getClasses = (
|
||||
standardButtonPaddingRight,
|
||||
standardButtonBorderWidth,
|
||||
buttonHeight,
|
||||
'bg-vega-yellow dark:bg-vega-yellow hover:bg-vega-yellow-dark dark:hover:bg-vega-yellow/30 active:bg-white dark:active:bg-black',
|
||||
'bg-vega-yellow dark:bg-vega-yellow hover:bg-yellow/dark dark:hover:bg-vega-yellow/30 active:bg-white dark:active:bg-black',
|
||||
'text-ui uppercase text-black dark:text-black hover:text-white dark:hover:text-white active:text-black dark:active:text-white',
|
||||
'border-transparent dark:border-transparent',
|
||||
];
|
||||
|
@ -22,3 +22,4 @@ export * from './toggle';
|
||||
export * from './tooltip';
|
||||
export * from './vega-logo';
|
||||
export * from './syntax-highlighter';
|
||||
export * from './sparkline';
|
||||
|
1
libs/ui-toolkit/src/components/sparkline/index.ts
Normal file
1
libs/ui-toolkit/src/components/sparkline/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './sparkline';
|
73
libs/ui-toolkit/src/components/sparkline/sparkline.spec.tsx
Normal file
73
libs/ui-toolkit/src/components/sparkline/sparkline.spec.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { theme } from '@vegaprotocol/tailwindcss-config';
|
||||
|
||||
import { Sparkline } from './sparkline';
|
||||
export const Colors = theme.colors;
|
||||
|
||||
const props = {
|
||||
data: [
|
||||
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
|
||||
10, 11, 12,
|
||||
],
|
||||
};
|
||||
|
||||
it('Renders an svg with a single path', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
expect(screen.getByTestId('sparkline-svg')).toBeInTheDocument();
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toBeInTheDocument();
|
||||
expect(path).toHaveAttribute('d', expect.any(String));
|
||||
expect(path).toHaveAttribute('stroke', expect.any(String));
|
||||
expect(path).toHaveAttribute('stroke-width', '2');
|
||||
expect(path).toHaveAttribute('fill', 'transparent');
|
||||
});
|
||||
|
||||
it('Requires a data prop but width and height are optional', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
const svg = screen.getByTestId('sparkline-svg');
|
||||
expect(svg).toHaveAttribute('width', '60');
|
||||
expect(svg).toHaveAttribute('height', '15');
|
||||
});
|
||||
|
||||
it('Renders a red line if the last value is less than the first', () => {
|
||||
props.data[0] = 10;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass('stroke-bearish');
|
||||
});
|
||||
|
||||
it('Renders a green line if the last value is greater than the first', () => {
|
||||
props.data[0] = 5;
|
||||
props.data[props.data.length - 1] = 10;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass('stroke-bullish');
|
||||
});
|
||||
|
||||
it('Renders a white line if the first and last values are equal', () => {
|
||||
props.data[0] = 5;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders a gray line if there are not 24 values', () => {
|
||||
props.data = [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
|
||||
22, 23,
|
||||
];
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.queryAllByTestId('sparkline-path');
|
||||
expect(paths).toHaveLength(2);
|
||||
expect(paths[0]).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
|
||||
);
|
||||
});
|
@ -0,0 +1,56 @@
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { Sparkline } from './sparkline';
|
||||
|
||||
export default {
|
||||
component: Sparkline,
|
||||
title: 'Sparkline',
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => <Sparkline data={args['data']} {...args} />;
|
||||
|
||||
export const Grey = Template.bind({});
|
||||
Grey.args = {
|
||||
data: [
|
||||
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8,
|
||||
],
|
||||
width: 60,
|
||||
height: 30,
|
||||
points: 25,
|
||||
className: 'w-[113px]',
|
||||
};
|
||||
|
||||
export const Equal = Template.bind({});
|
||||
Equal.args = {
|
||||
data: [
|
||||
12, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
|
||||
10, 11, 12,
|
||||
],
|
||||
width: 60,
|
||||
height: 30,
|
||||
points: 25,
|
||||
className: 'w-[113px]',
|
||||
};
|
||||
|
||||
export const Increase = Template.bind({});
|
||||
Increase.args = {
|
||||
data: [
|
||||
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
|
||||
10, 11, 12,
|
||||
],
|
||||
width: 60,
|
||||
height: 30,
|
||||
points: 25,
|
||||
className: 'w-[113px]',
|
||||
};
|
||||
|
||||
export const Decrease = Template.bind({});
|
||||
Decrease.args = {
|
||||
data: [
|
||||
12, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
|
||||
10, 11, 1,
|
||||
],
|
||||
width: 60,
|
||||
height: 30,
|
||||
points: 25,
|
||||
className: 'w-[113px]',
|
||||
};
|
132
libs/ui-toolkit/src/components/sparkline/sparkline.tsx
Normal file
132
libs/ui-toolkit/src/components/sparkline/sparkline.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import { extent } from 'd3-array';
|
||||
import { scaleLinear } from 'd3-scale';
|
||||
import { line } from 'd3-shape';
|
||||
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-bullish'
|
||||
: 'stroke-bearish';
|
||||
}
|
||||
|
||||
export interface SparklineProps {
|
||||
data: number[];
|
||||
width?: number;
|
||||
height?: number;
|
||||
points?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SparklineView = ({
|
||||
data,
|
||||
width = 60,
|
||||
height = 15,
|
||||
points = 25,
|
||||
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 =
|
||||
data.length >= 24
|
||||
? colorByChange(firstVal, lastVal)
|
||||
: 'stroke-black/40 dark:stroke-white/40';
|
||||
|
||||
// Create paths
|
||||
const preMarketCreationPath = lineSeries(preMarketData);
|
||||
const mainPath = lineSeries(marketData);
|
||||
|
||||
return (
|
||||
<svg
|
||||
data-testid="sparkline-svg"
|
||||
className={`pt-px pr-0 w-full overflow-visible ${className}`}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
{preMarketCreationPath && (
|
||||
<path
|
||||
data-testid="sparkline-path"
|
||||
className={`[vector-effect:non-scaling-stroke] ${strokeClassName}`}
|
||||
d={preMarketCreationPath}
|
||||
stroke="strokeCurrent"
|
||||
strokeWidth={2}
|
||||
fill="transparent"
|
||||
/>
|
||||
)}
|
||||
{mainPath && (
|
||||
<path
|
||||
data-testid="sparkline-path"
|
||||
d={mainPath}
|
||||
className={strokeClassName}
|
||||
stroke="strokeCurrent"
|
||||
strokeWidth={1}
|
||||
fill="transparent"
|
||||
/>
|
||||
)}
|
||||
</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';
|
@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node", "@testing-libary/jest-dom"]
|
||||
"types": ["jest", "node", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.test.ts",
|
||||
|
Loading…
Reference in New Issue
Block a user