import { faker } from "@faker-js/faker" import { Meta, StoryObj } from "@storybook/react" import { addDays, addHours, parseISO, startOfDay, startOfToday, subDays, subMinutes, } from "date-fns" import { list } from "radash" import React, { useState } from "react" import { useDateTimeFormatter } from "../../../hooks" import { useProminentColors } from "../../../hooks/useProminentColors/useProminentColors" import { Button } from "../../Button" import { List } from "../../List" import { NumberDisplay } from "../../NumberDisplay" import { TakeoverModal } from "../../TakeoverModal" import { Text } from "../../Text" import { ChartPanel } from "../ChartPanel" import { FloorPriceDataPoint, MOCK_FLOOR_PRICE_DATA, MOCK_PRICE_AND_AVERAGE_PRICE, MOCK_VOLUME_DATA, PriceAndAveragePriceDataPoint, SCATTER_LINE_AND_VOLUME, VolumeDataPoint, } from "./__mocks__/volumeData" import { TimeSeriesChart, TimeSeriesChartProps } from "./TimeSeriesChart" type Story = StoryObj> const RenderTooltip = ({ item }: { item: VolumeDataPoint }) => { const formatDate = useDateTimeFormatter() return ( <>
{formatDate(new Date(item.date))}
Avg. price:
Volume:
) } const meta: Meta> = { title: "Design System/TimeSeriesChart", component: TimeSeriesChart, args: { className: "h-[400px]", data: MOCK_VOLUME_DATA, getDate: (d: VolumeDataPoint) => new Date(d.date), getValue: (d: VolumeDataPoint) => d.averagePrice, renderTooltip: RenderTooltip, }, } export default meta export const AveragePrice: Story = {} export const AveragePriceWithDelay: Story = { args: { tooltipDelay: 1_000, }, } export const AreaSplineChart: Story = { args: { lineType: "areaspline", }, } export const AreaChart: Story = { args: { lineType: "area", }, } export const AveragePriceWithVolume: Story = { args: { getColumnValue: (d: VolumeDataPoint) => d.volume, }, } const WithScatterTemplate = ( args: TimeSeriesChartProps, ) => { return } const LOWER_BOUND = 2000 const UPPER_BOUND = 10000 export const SalesWithAveragePriceAndOutliers = { render: WithScatterTemplate, args: { data: MOCK_PRICE_AND_AVERAGE_PRICE, getScatterValue: (d: PriceAndAveragePriceDataPoint) => Math.max(LOWER_BOUND, Math.min(UPPER_BOUND, d.price)), getDate: (d: PriceAndAveragePriceDataPoint) => new Date(d.date), getValue: (d: PriceAndAveragePriceDataPoint) => d.priceMovingAverage, getIsOutlier: (d: PriceAndAveragePriceDataPoint) => d.price != Math.max(LOWER_BOUND, Math.min(UPPER_BOUND, d.price)), yAxisMax: UPPER_BOUND, yAxisMin: LOWER_BOUND, formatYAxisLabels: (value: number) => `${value} ETH`, scatterOptions: { tooltipDelay: 200, }, }, } export const FixedWidth: Story = { args: { className: "w-[400px]", }, } export const AxesHidden: Story = { args: { hideAxes: true, }, } export const YAxisHidden: Story = { args: { hideYAxis: true, }, } export const InteractiveAveragePrice: Story = { args: { interactive: true, }, } export const InteractiveAveragePriceWithVolume: Story = { args: { interactive: true, getColumnValue: (d: VolumeDataPoint) => d.volume, }, } export const ConstantSeries: Story = { args: { data: list(0, 6).map(index => ({ volume: 100, averagePrice: 100, date: subMinutes(addHours(startOfToday(), 1), 7 - index).toISOString(), })), }, } export const AddData: Story = { render: function Render(args: TimeSeriesChartProps) { const [data, setData] = useState(args.data) const lastDataPoint = data[data.length - 1] return ( <> ) }, } export const Extremes: Story = { args: { ...AveragePriceWithVolume.args, data: list(0, 6).map(index => ({ ...MOCK_VOLUME_DATA[index], date: subDays(startOfToday(), 7 - index).toISOString(), })), rangeEnd: addDays(new Date(), 1), rangeStart: subDays(new Date(), 10), }, } const FloorPriceTemplate = ( args: TimeSeriesChartProps, ) => export const FloorPrice = { render: FloorPriceTemplate, args: { interactive: true, data: MOCK_FLOOR_PRICE_DATA, getDate: (dp: FloorPriceDataPoint) => new Date(dp.timestamp), getValue: (dp: FloorPriceDataPoint) => dp.price, renderTooltip: function RenderTooltip(d: FloorPriceDataPoint) { const formatDate = useDateTimeFormatter() return ( <>
{formatDate(parseISO(d.timestamp))}
Floor price:
) }, }, } export const Linear = { render: FloorPriceTemplate, args: { ...FloorPrice.args, data: list(0, 450).map(index => ({ price: index, timestamp: addDays(startOfDay(new Date()), index).toISOString(), })), }, } const PanelTemplate = (args: TimeSeriesChartProps) => { return (
Volume and price Last 30 days
) } export const WithPanel: Story = { render: PanelTemplate, args: { ...AveragePriceWithVolume.args, interactive: true, }, } export const WithAxisTitles: Story = { render: PanelTemplate, args: { ...AveragePriceWithVolume.args, interactive: true, yAxisTitle: "Average price", columnAxisTitle: "Volume (ETH)", xAxisTitle: "Time", formatYAxisLabels: value => `${value} ETH`, }, } export const WithPanelExtremes: Story = { render: PanelTemplate, args: { ...Extremes.args, interactive: true, }, } const ListTemplate = (args: TimeSeriesChartProps) => { return ( {list(100).map(index => ( Chart {index + 1} ))} ) } export const InList: Story = { render: ListTemplate, args: { lineWidth: 2, markerRadius: 2, hideAxes: true, data: MOCK_VOLUME_DATA.slice(3, 10), lineType: "spline", overrides: { tooltip: { style: { // Need to provide tooltip width here because highcharts limits it to the chart width width: 150, }, // No hide delay so tooltips in list dont overlap hideDelay: 0, }, }, }, } export const AreaChartInList: Story = { render: ListTemplate, args: { ...InList.args, lineType: "areaspline", }, } export const ScatterLineAndVolume: StoryObj< typeof TimeSeriesChart< (typeof SCATTER_LINE_AND_VOLUME)["collectionSales"][number], (typeof SCATTER_LINE_AND_VOLUME)["collectionVolumes"][number] & { metadata: string } > > = { args: { className: "h-[400px]", data: SCATTER_LINE_AND_VOLUME.collectionSales, getDate: d => new Date(d.date), getValue: d => +d.priceMovingAverage, getScatterValue: d => +d.price, scatterOptions: { cursor: "pointer", events: { click: () => { alert("You clicked a point") }, }, }, standaloneColumn: { items: SCATTER_LINE_AND_VOLUME.collectionVolumes.map(d => ({ metadata: "metadata", ...d, })), getDate: d => new Date(d.date), getValue: d => +d.volume, }, renderTooltip: function RenderTooltip({ item, matchedColumnData }) { const formatDate = useDateTimeFormatter() return ( <>
{formatDate(parseISO(item.date))}
Avg. price:
Price:
{matchedColumnData ? ( <>
Volume:
Metadata: {matchedColumnData.metadata}
) : null} ) }, }, } const TakeoverModalTemplate = (args: TimeSeriesChartProps) => { const [item, setItem] = useState(null) return ( <> setItem(null)} /> ) } export const TakeoverModalOnClick: Story = { render: TakeoverModalTemplate, } const CustomColorTemplate = (args: TimeSeriesChartProps) => { const { prominentColors } = useProminentColors( "https://i2.seadn.io/ethereum/ede40ccd9bcb4528838f1d029be13224/5f1b200a067938f507cbe12bbbabc2/0d5f1b200a067938f507cbe12bbbabc2.jpeg?h=250&w=250", ) return ( ) } export const CustomColors: Story = { render: CustomColorTemplate, } export const Skeleton: Story = { render: () => ( Loadign Time Series ), } export const EmptyState: Story = { render: () => ( Empty Time Series ), } // Create type for scatter-only data points type ScatterDataPoint = { date: string value: number } // Create mock data for scatter-only example const MOCK_SCATTER_ONLY_DATA: ScatterDataPoint[] = list(20).map((_, i) => ({ date: new Date(addDays(startOfToday(), i - 10).getTime()).toISOString(), value: Math.random() * 300 + 200, })) const ScatterOnlyTooltip = ({ item }: { item: ScatterDataPoint }) => { const formatDate = useDateTimeFormatter() return ( <>
{formatDate(new Date(item.date))}
Value:
) } const ScatterOnlyTemplate = (args: TimeSeriesChartProps) => { return } export const ScatterOnly = { render: ScatterOnlyTemplate, args: { className: "h-[400px]", data: MOCK_SCATTER_ONLY_DATA, getDate: (d: ScatterDataPoint) => new Date(d.date), // No getValue prop - using only getScatterValue getScatterValue: (d: ScatterDataPoint) => d.value, interactive: true, renderTooltip: ScatterOnlyTooltip, scatterOptions: { marker: { enabled: true, radius: 5, }, color: "rgb(var(--color-blue-4))", }, }, }