PeriodSelector
@coinbase/cds-web-visualization@3.4.0-beta.1
Related components
Basic Example
Loading...
Live Codefunction BasicExample() { const tabs = [ { id: '1H', label: '1H' }, { id: '1D', label: '1D' }, { id: '1W', label: '1W' }, { id: '1M', label: '1M' }, { id: '1Y', label: '1Y' }, { id: 'YTD', label: 'YTD' }, { id: 'All', label: 'All' }, ]; const [activeTab, setActiveTab] = useState(tabs[0]); return <PeriodSelector activeTab={activeTab} onChange={setActiveTab} tabs={tabs} />; }
Minimum Width
You can set the width
prop to fit-content
to make the period selector as small as possible.
Loading...
Live Codefunction MinimumWidthExample() { const tabs = [ { id: '1W', label: '1W' }, { id: '1M', label: '1M' }, { id: 'YTD', label: 'YTD' }, ]; const [activeTab, setActiveTab] = useState(tabs[0]); return ( <PeriodSelector activeTab={activeTab} onChange={setActiveTab} tabs={tabs} width="fit-content" gap={2} /> ); }
Many Periods with Overflow
Loading...
Live Codefunction ManyPeriodsExample() { const tabs = useMemo( () => [ { id: '1H', label: '1H' }, { id: '1D', label: '1D' }, { id: '1W', label: '1W' }, { id: '1M', label: '1M' }, { id: 'YTD', label: 'YTD' }, { id: '1Y', label: '1Y' }, { id: '5Y', label: '5Y' }, { id: 'All', label: 'All' }, ], [], ); const [activeTab, setActiveTab] = useState(tabs[0]); const isLive = useMemo(() => activeTab?.id === '1H', [activeTab]); return ( <HStack alignItems="center" justifyContent="space-between" maxWidth="100%" overflow="hidden" width="100%" > <Box flexGrow={1} overflow="hidden" position="relative"> <style>{` .scrollContainer { scrollbar-width: none; overflow-x: auto; &::-webkit-scrollbar { display: none; } } `}</style> <Box className="scrollContainer" paddingEnd={2}> <PeriodSelector activeTab={activeTab} gap={1} justifyContent="flex-start" onChange={setActiveTab} tabs={tabs} width="fit-content" /> </Box> <Box position="absolute" style={{ background: 'linear-gradient(to left, var(--color-bg), transparent 100%)', right: 0, bottom: 0, top: 0, width: 'var(--space-4)', pointerEvents: 'none', }} /> </Box> <IconButton compact accessibilityLabel="Configure chart" flexShrink={0} height={36} name="filter" variant="secondary" /> </HStack> ); }
Live Indicator
Loading...
Live Codefunction LiveExample() { const tabs = useMemo( () => [ // LiveTabLabel is exported from PeriodSelector { id: '1H', label: <LiveTabLabel /> }, { id: '1D', label: '1D' }, { id: '1W', label: '1W' }, { id: '1M', label: '1M' }, { id: '1Y', label: '1Y' }, { id: 'All', label: 'All' }, ], [], ); const [activeTab, setActiveTab] = useState(tabs[0]); const isLive = useMemo(() => activeTab?.id === '1H', [activeTab]); const activeBackground = useMemo(() => (isLive ? 'bgNegativeWash' : 'bgPrimaryWash'), [isLive]); return ( <PeriodSelector activeBackground={activeBackground} activeTab={activeTab} onChange={setActiveTab} tabs={tabs} /> ); }
Customization
Custom Colors
Loading...
Live Codefunction LiveExample() { const tabs = useMemo( () => [ { id: '1H', label: '1H' }, { id: '1D', label: '1D' }, { id: '1W', label: '1W' }, { id: '1M', label: '1M' }, { id: '1Y', label: '1Y' }, { id: 'All', label: 'All' }, ], [], ); const [activeTab, setActiveTab] = useState(tabs[0]); const isLive = useMemo(() => activeTab?.id === 'live', [activeTab]); const activeBackground = useMemo(() => (isLive ? 'bgNegativeWash' : 'bgPrimaryWash'), [isLive]); return ( <PeriodSelector activeBackground={activeBackground} activeTab={activeTab} onChange={setActiveTab} tabs={tabs} /> ); }
Color Shifting
Loading...
Live Codefunction ColorShiftingExample() { const TabLabel = memo(({ label }) => ( <Text font="label1" style={{ color: 'var(--chartActiveColor)' }}> {label} </Text> )); const tabs = useMemo( () => [ { id: '1H', label: <TabLabel label="1H" />, }, { id: '1D', label: <TabLabel label="1D" />, }, { id: '1W', label: <TabLabel label="1W" />, }, { id: '1M', label: <TabLabel label="1M" />, }, { id: '1Y', label: <TabLabel label="1Y" />, }, { id: 'All', label: <TabLabel label="All" />, }, ], [], ); const [activeTab, setActiveTab] = useState(tabs[0]); const [chartActiveColor, setChartActiveColor] = useState('positive'); const toggleColor = useCallback(() => { setChartActiveColor((activeColor) => (activeColor === 'positive' ? 'negative' : 'positive')); }, []); const activeForegroundColor = useMemo(() => { return chartActiveColor === 'positive' ? 'var(--color-fgPositive)' : 'var(--color-fgNegative)'; }, [chartActiveColor]); const activeBackground = useMemo(() => { return chartActiveColor === 'positive' ? 'bgPositiveWash' : 'bgNegativeWash'; }, [chartActiveColor]); return ( <VStack gap={2}> <m.div animate={{ '--chartActiveColor': activeForegroundColor }} style={{ '--chartActiveColor': activeForegroundColor }} transition={{ duration: 0.3 }} > <PeriodSelector activeBackground={activeBackground} activeTab={activeTab} onChange={setActiveTab} tabs={tabs} /> </m.div> <Button onClick={toggleColor}>Toggle Color</Button> </VStack> ); }
Within Section Header
Loading...
Live Codefunction SectionHeaderExample() { const tabs = [ { id: '1D', label: '1D' }, { id: '1W', label: '1W' }, { id: '1M', label: '1M' }, ]; const [activeTab, setActiveTab] = useState(tabs[0]); return ( <SectionHeader end={ <HStack alignItems="center"> <PeriodSelector activeTab={activeTab} onChange={setActiveTab} tabs={tabs} width="fit-content" /> </HStack> } title={<Text font="label1">Portfolio Balance</Text>} balance={ <Text font="display3" color="fgMuted"> $10,023.82 </Text> } /> ); }
Asset Price Chart
You can use a PeriodSelector to control the time period of a LineChart, with a settings icon to enable extra customization for users.
Loading...
Live Codefunction CustomizableAssetPriceExample() { const tabs = [ { id: 'hour', label: '1H' }, { id: 'day', label: '1D' }, { id: 'week', label: '1W' }, { id: 'month', label: '1M' }, { id: 'year', label: '1Y' }, { id: 'all', label: 'All' }, ]; const PeriodSelectorWrapper = memo(({ activeTab, setActiveTab, tabs, onClickSettings }) => ( <HStack alignItems="center" justifyContent="space-between" maxWidth="100%" overflow="hidden" width="100%" > <Box flexGrow={1} overflow="hidden" position="relative"> <style>{` .scrollContainer { scrollbar-width: none; overflow-x: auto; &::-webkit-scrollbar { display: none; } } `}</style> <Box className="scrollContainer" paddingEnd={2}> <PeriodSelector activeTab={activeTab} gap={1} justifyContent="flex-start" onChange={setActiveTab} tabs={tabs} width="fit-content" /> </Box> <Box position="absolute" style={{ background: 'linear-gradient(to left, var(--color-bg), transparent 100%)', right: 0, bottom: 0, top: 0, width: 'var(--space-4)', pointerEvents: 'none', }} /> </Box> <IconButton compact accessibilityLabel="Chart settings" flexShrink={0} height={36} name="settings" variant="secondary" onClick={onClickSettings} /> </HStack> )); const AssetPriceChart = memo(() => { const [activeTab, setActiveTab] = useState(tabs[0]); const [showSettings, setShowSettings] = useState(false); const [showYAxis, setShowYAxis] = useState(true); const [showXAxis, setShowXAxis] = useState(true); const [scrubIndex, setScrubIndex] = useState(); const breakpoints = useBreakpoints(); const formatPrice = useCallback((price: number) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(price); }, []); const toggleShowYAxis = useCallback(() => setShowYAxis((show) => !show), []); const toggleShowXAxis = useCallback(() => setShowXAxis((show) => !show), []); const data = useMemo(() => sparklineInteractiveData[activeTab.id], [activeTab.id]); const currentPrice = useMemo(() => sparklineInteractiveData.hour[sparklineInteractiveData.hour.length - 1].value, []); const currentTimePrice = useMemo(() => { if (scrubIndex !== undefined) { return data[scrubIndex].value; } return currentPrice; }, [data, scrubIndex, currentPrice]); const formatDate = useCallback((date) => { const dayOfWeek = date.toLocaleDateString('en-US', { weekday: 'short' }); const monthDay = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', }); const time = date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, }); return `${dayOfWeek}, ${monthDay}, ${time}`; }, []); const scrubberLabel = useMemo(() => { if (scrubIndex === undefined) return; return formatDate(data[scrubIndex].date); }, [scrubIndex, data, formatDate]); const accessibilityLabel = useMemo(() => { if (scrubIndex === undefined) return; const price = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(data[scrubIndex].value); const date = formatDate(data[scrubIndex].date); return `Asset price: ${price} USD on ${date}`; }, [scrubIndex, data, formatDate]); const onClickSettings = useCallback(() => setShowSettings(!showSettings), [showSettings]); const seriesData = useMemo(() => [{ id: 'price', data: data.map((d) => d.value) }], [data]); const getFormattingConfigForPeriod = useCallback((period) => { switch (period) { case 'hour': case 'day': return { hour: 'numeric', minute: 'numeric', }; case 'week': case 'month': return { month: 'numeric', day: 'numeric', }; case 'year': case 'all': return { month: 'numeric', year: 'numeric', }; } }, []); const formatXAxisDate = useCallback((index) => { if (!data[index]) return ''; const date = data[index].date; const formatConfig = getFormattingConfigForPeriod(activeTab.id); if (activeTab.id === 'hour' || activeTab.id === 'day') { return date.toLocaleTimeString('en-US', formatConfig); } else { return date.toLocaleDateString('en-US', formatConfig); } }, [data, activeTab.id, getFormattingConfigForPeriod]); const isMobile = breakpoints.isPhone || breakpoints.isTabletPortrait; return ( <VStack gap={2}> <SectionHeader padding={0} title={<Text font="label1">Asset Price</Text>} balance={<RollingNumber format={{ style: 'currency', currency: 'USD' }} font="display3" color="fgMuted" value={currentTimePrice} />} end={isMobile ? undefined : ( <HStack alignItems="center"> <PeriodSelectorWrapper activeTab={activeTab} setActiveTab={setActiveTab} tabs={tabs} onClickSettings={onClickSettings} /> </HStack>) } /> <LineChart enableScrubbing height={300} onScrubberPositionChange={setScrubIndex} series={seriesData} yAxis={{ domainLimit: 'strict', showGrid: true, tickLabelFormatter: formatPrice, width: 80 }} xAxis={{ tickLabelFormatter: formatXAxisDate, }} showYAxis={showYAxis} showXAxis={showXAxis} accessibilityLabel={accessibilityLabel} > <Scrubber label={scrubberLabel} /> </LineChart> {isMobile && ( <HStack alignItems="center"> <PeriodSelectorWrapper activeTab={activeTab} setActiveTab={setActiveTab} tabs={tabs} onClickSettings={onClickSettings} /> </HStack> )} {showSettings && ( <Tray title="Chart Settings" onCloseComplete={() => setShowSettings(false)}> {({ handleClose }) => ( <VStack gap={2} paddingX={3} paddingBottom={3}> <HStack justifyContent="space-between" alignItems="center"> <Text font="label1">Show Y-Axis</Text> <Switch checked={showYAxis} onChange={toggleShowYAxis} /> </HStack> <HStack justifyContent="space-between" alignItems="center"> <Text font="label1">Show X-Axis</Text> <Switch checked={showXAxis} onChange={toggleShowXAxis} /> </HStack> </VStack> )} </Tray> )} </VStack> ); }, []); return <AssetPriceChart />; }