Overview
CryptoDash includes specialized chart components for visualizing cryptocurrency price movements and portfolio performance. These components use SVG for crisp rendering at any resolution and include interactive tooltips.
MainPerformanceChart
Primary dashboard chart displaying Bitcoin price performance over 30 days with interactive tooltips.
Props
SVG path data representing the price movement. Should be in the format generated by the path generation utility.
Array of price values used for tooltip display. Each value represents a data point in the time series.
Formatted current price (e.g., “$67,234.56”). Displayed in header when tooltip is not active.
Formatted 24-hour price change (e.g., “+2.4%” or “-1.2%”)
Whether the price change is positive. Controls chart line color (green for true, red for false).
Features
Interactive Tooltips
- Hover over chart to see price at specific points
- Vertical guideline shows hover position
- Dot indicator marks exact data point
- Price updates in header during hover
- Uses
useChartTooltip custom hook
Responsive Design
- SVG with
preserveAspectRatio="none" for flexible scaling
- ViewBox coordinates:
0 0 100 30
- Container adapts to parent width
- Fixed height:
h-56 (224px)
Visual Styling
- Color-coded line (green for gains, red for losses)
- Smooth strokeLinecap and strokeLinejoin
- Non-scaling stroke for consistent line width
- Crosshair cursor on hover
Usage Example
import MainPerformanceChart from './components/dashboard/MainPerformanceChart'
import { useOutletContext } from 'react-router-dom'
export default function DashboardPage() {
const {
mainPerformancePath,
mainPerformanceSeries,
mainPerformancePriceLabel,
mainPerformanceChangeLabel,
mainPerformancePositive,
} = useOutletContext()
return (
<MainPerformanceChart
chartPath={mainPerformancePath}
chartData={mainPerformanceSeries}
priceLabel={mainPerformancePriceLabel}
changeLabel={mainPerformanceChangeLabel}
positive={mainPerformancePositive}
/>
)
}
Chart Data Structure
The chartPath prop should be generated from price data:
// Example price data
const prices = [67000, 67200, 66800, 67500, ...] // 30 days of prices
// Generate path (simplified example)
function generatePath(data) {
const min = Math.min(...data)
const max = Math.max(...data)
const range = max - min
const points = data.map((value, index) => {
const x = (index / (data.length - 1)) * 100
const y = 30 - ((value - min) / range) * 30
return `${x},${y}`
})
return `M ${points.join(' L ')}`
}
const chartPath = generatePath(prices)
The component uses useChartTooltip hook:
import { useChartTooltip } from '../../hooks/useChartTooltip'
import { formatPrice } from '../../utils/dashboardFormatters'
const { tooltip, svgRef, handleMouseMove, handleMouseLeave } =
useChartTooltip(chartData, formatPrice)
Tooltip state:
{
visible: boolean,
index: number | null,
value: string // Formatted price
}
Component Structure
<div className="rounded-3xl border bg-white shadow-xl">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b">
<div>
<h2>Performance</h2>
<p>Bitcoin 30d</p>
</div>
<div className="text-right">
<p className="text-xl font-bold">
{tooltip.visible ? tooltip.value : priceLabel}
</p>
<p className={positive ? 'text-emerald-600' : 'text-red-500'}>
{changeLabel}
</p>
</div>
</div>
{/* Chart Area */}
<div className="p-6">
<div className="h-56 rounded-2xl border bg-slate-50 p-4">
<svg
ref={svgRef}
viewBox="0 0 100 30"
preserveAspectRatio="none"
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{ cursor: 'crosshair' }}
>
{/* Price line */}
<path d={chartPath} />
{/* Tooltip indicator */}
{tooltip.visible && (
<>
<line /> {/* Vertical guideline */}
<circle /> {/* Dot at data point */}
</>
)}
</svg>
</div>
</div>
</div>
Styling Details
Container
rounded-3xl /* Large border radius */
border border-slate-200 /* Light border */
bg-white /* White background */
shadow-xl /* Large shadow */
dark:border-white/5 /* Dark mode border */
dark:bg-[#14281d] /* Dark mode background */
Chart Line
/* Positive (green) */
stroke: currentColor
text-emerald-600 dark:text-[#2bee79]
/* Negative (red) */
stroke: currentColor
text-red-500
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
vectorEffect="non-scaling-stroke"
Tooltip Guideline
stroke="currentColor"
strokeWidth="0.5"
strokeDasharray="2,2"
opacity="0.5"
Portfolio value chart with area fill gradient, showing total portfolio performance over 30 days.
Props
SVG path data for the portfolio value line
Array of portfolio values for tooltip display
Formatted total portfolio value (e.g., “$125,430.67”)
Formatted change percentage (e.g., “+8.4%”)
Whether the change is positive. Controls badge color and trend icon.
Features
Area Fill Gradient
- Linear gradient from solid to transparent
- Creates filled area under the line
- Color:
#2bee79 (brand green)
- Opacity gradient: 35% → 0%
Interactive Tooltips
- Same hover behavior as MainPerformanceChart
- Shows portfolio value at any point in time
- Vertical guideline with dot indicator
Trend Badge
- Colored badge showing percentage change
- Up/down arrow icon
- Green for positive, red for negative
Usage Example
import PortfolioPerformanceChart from './components/portfolio/PortfolioPerformanceChart'
export default function PortfolioPage() {
const {
portfolioPath,
portfolioSeries,
portfolioValueLabel,
portfolioChangeLabel,
portfolioPositive
} = usePortfolioData()
return (
<div className="grid gap-6">
<PortfolioPerformanceChart
chartPath={portfolioPath}
chartData={portfolioSeries}
totalValueLabel={portfolioValueLabel}
changeLabel={portfolioChangeLabel}
positive={portfolioPositive}
/>
{/* Other portfolio components */}
</div>
)
}
Gradient Definition
<svg>
<defs>
<linearGradient id="portfolio-area" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor="currentColor" stopOpacity="0.35" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
</defs>
{/* Line */}
<path d={chartPath} fill="none" stroke="currentColor" />
{/* Area fill */}
<path d={`${chartPath} L 100 30 L 0 30 Z`} fill="url(#portfolio-area)" />
</svg>
The area path:
- Follows the chart line
- Lines to bottom-right corner (100, 30)
- Lines to bottom-left corner (0, 30)
- Closes path (Z)
Component Structure
<section className="rounded-2xl border bg-white p-6">
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div>
<p className="text-sm font-semibold text-slate-600">
Performance 30d
</p>
<p className="text-2xl font-black mt-1">
{tooltip.visible ? tooltip.value : totalValueLabel}
</p>
</div>
{/* Trend badge */}
<span className={`inline-flex items-center gap-1 rounded-lg px-2 py-1 text-xs font-bold ${
positive ? 'bg-[#2bee79]/10 text-[#2bee79]' : 'bg-red-500/10 text-red-500'
}`}>
<span className="material-symbols-outlined text-sm">
{positive ? 'trending_up' : 'trending_down'}
</span>
{changeLabel}
</span>
</div>
{/* Chart */}
<div className="h-52 rounded-xl border bg-slate-50 p-4">
<svg
ref={svgRef}
className="text-[#2bee79]" {/* Green color */}
viewBox="0 0 100 30"
preserveAspectRatio="none"
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<defs>
<linearGradient id="portfolio-area" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor="currentColor" stopOpacity="0.35" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
</defs>
{/* Line and area */}
<path d={chartPath} fill="none" stroke="currentColor" strokeWidth="0.1" />
<path d={`${chartPath} L 100 30 L 0 30 Z`} fill="url(#portfolio-area)" />
{/* Tooltip */}
{tooltip.visible && tooltip.index !== null && (
<>
<line /> {/* Guideline */}
<circle /> {/* Dot */}
</>
)}
</svg>
</div>
</section>
Styling Details
Card Container
rounded-2xl
border border-slate-200 dark:border-[#1a2e23]
bg-white dark:bg-[#14281d]
p-6
Trend Badge (Positive)
bg-[#2bee79]/10 /* Light green background */
text-[#2bee79] /* Green text */
rounded-lg
px-2 py-1
text-xs font-bold
Trend Badge (Negative)
bg-red-500/10 /* Light red background */
text-red-500 /* Red text */
Chart Area
h-52 /* 208px height */
rounded-xl
border border-slate-200 dark:border-[#1a2e23]
bg-slate-50 dark:bg-[#102217]/70
p-4
Chart Line
- Color: Always
#2bee79 (green)
- Stroke width:
0.1 (thinner than main chart)
- No scaling stroke
Chart Utilities
Custom hook for managing chart tooltip state and interactions.
Import:
import { useChartTooltip } from '../../hooks/useChartTooltip'
Usage:
const { tooltip, svgRef, handleMouseMove, handleMouseLeave } =
useChartTooltip(chartData, formatter)
Parameters:
chartData - Array of numeric values
formatter - Function to format values (e.g., formatPrice, formatCurrency)
Returns:
tooltip - Object with { visible, index, value }
svgRef - Ref to attach to SVG element
handleMouseMove - Mouse move event handler
handleMouseLeave - Mouse leave event handler
formatPrice - For cryptocurrency prices
import { formatPrice } from '../../utils/dashboardFormatters'
formatPrice(67234.56) // "$67,234.56"
formatCompactCurrency - For large values
import { formatCompactCurrency } from '../../utils/dashboardFormatters'
formatCompactCurrency(125430.67) // "$125.4K"
formatCompactCurrency(2400000) // "$2.4M"
Path Generation
While not included in the component files, here’s the typical pattern for generating chart paths:
function generateChartPath(data) {
if (!data || data.length === 0) return ''
const min = Math.min(...data)
const max = Math.max(...data)
const range = max - min || 1 // Prevent division by zero
const points = data.map((value, index) => {
const x = (index / (data.length - 1)) * 100
const y = 30 - ((value - min) / range) * 30
return `${x},${y}`
})
return `M ${points.join(' L ')}`
}
// Usage
const chartPath = generateChartPath(priceData)
Explanation:
- Normalizes data to viewBox coordinates (0-100 x, 0-30 y)
M = MoveTo first point
L = LineTo subsequent points
- Y-axis is inverted (30 - value) because SVG origin is top-left
Best Practices
Data Requirements
- Consistent data length - Ensure
chartPath and chartData represent the same points
- Handle empty data - Provide empty array defaults
- Validate numeric values - Filter out NaN/null/undefined
- Reasonable data range - Too many points (>1000) may impact performance
- Memoize path generation - Cache generated paths when possible
- Use requestAnimationFrame - For smooth tooltip updates
- Debounce hover updates - If experiencing performance issues
- Keep SVG simple - Avoid complex gradients and filters
Accessibility
- Provide alt descriptions - Use ARIA labels for chart context
- Support keyboard navigation - Consider adding keyboard tooltip controls
- High contrast colors - Ensure chart lines are visible
- Respect prefers-reduced-motion - Disable animations if requested
Responsive Design
- Test at multiple widths - Charts should scale gracefully
- Adjust stroke width - Use
vectorEffect="non-scaling-stroke"
- Scale font sizes - Use responsive text classes
- Consider mobile touch - Larger touch targets for interactions
Dark Mode
- Invert chart backgrounds - Light backgrounds in dark mode
- Adjust opacity - Increase contrast for dark themes
- Test readability - Ensure lines and text are visible
- Use theme-aware colors - Leverage Tailwind’s dark: prefix
Internationalization
- Format numbers properly - Use
Intl.NumberFormat for locale support
- Translate labels - Use translation hook for all text
- Handle RTL layouts - Consider right-to-left languages
- Locale-specific dates - If showing time-based data
Advanced Examples
Multiple Charts in Grid
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<PortfolioPerformanceChart {...portfolioProps} />
<MainPerformanceChart {...btcProps} />
</div>
Loading States
import { ChartSkeleton } from './components/common/SkeletonLoader'
{loading ? (
<ChartSkeleton height="h-56" />
) : (
<MainPerformanceChart {...chartProps} />
)}
Error Handling
{error ? (
<div className="rounded-2xl border border-red-200 bg-red-50 p-8 text-center">
<span className="material-symbols-outlined text-4xl text-red-500">error</span>
<p className="mt-2 text-sm text-red-600">Failed to load chart data</p>
</div>
) : (
<MainPerformanceChart {...chartProps} />
)}
const customFormatter = (value) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value)
}
const { tooltip, svgRef, handleMouseMove, handleMouseLeave } =
useChartTooltip(chartData, customFormatter)