Skip to main content

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

chartPath
string
required
SVG path data representing the price movement. Should be in the format generated by the path generation utility.
chartData
number[]
default:"[]"
Array of price values used for tooltip display. Each value represents a data point in the time series.
priceLabel
string
required
Formatted current price (e.g., “$67,234.56”). Displayed in header when tooltip is not active.
changeLabel
string
required
Formatted 24-hour price change (e.g., “+2.4%” or “-1.2%”)
positive
boolean
required
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)

Tooltip Hook

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"

PortfolioPerformanceChart

Portfolio value chart with area fill gradient, showing total portfolio performance over 30 days.

Props

chartPath
string
required
SVG path data for the portfolio value line
chartData
number[]
default:"[]"
Array of portfolio values for tooltip display
totalValueLabel
string
required
Formatted total portfolio value (e.g., “$125,430.67”)
changeLabel
string
required
Formatted change percentage (e.g., “+8.4%”)
positive
boolean
required
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:
  1. Follows the chart line
  2. Lines to bottom-right corner (100, 30)
  3. Lines to bottom-left corner (0, 30)
  4. 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

useChartTooltip Hook

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

Formatter Functions

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

  1. Consistent data length - Ensure chartPath and chartData represent the same points
  2. Handle empty data - Provide empty array defaults
  3. Validate numeric values - Filter out NaN/null/undefined
  4. Reasonable data range - Too many points (>1000) may impact performance

Performance

  1. Memoize path generation - Cache generated paths when possible
  2. Use requestAnimationFrame - For smooth tooltip updates
  3. Debounce hover updates - If experiencing performance issues
  4. Keep SVG simple - Avoid complex gradients and filters

Accessibility

  1. Provide alt descriptions - Use ARIA labels for chart context
  2. Support keyboard navigation - Consider adding keyboard tooltip controls
  3. High contrast colors - Ensure chart lines are visible
  4. Respect prefers-reduced-motion - Disable animations if requested

Responsive Design

  1. Test at multiple widths - Charts should scale gracefully
  2. Adjust stroke width - Use vectorEffect="non-scaling-stroke"
  3. Scale font sizes - Use responsive text classes
  4. Consider mobile touch - Larger touch targets for interactions

Dark Mode

  1. Invert chart backgrounds - Light backgrounds in dark mode
  2. Adjust opacity - Increase contrast for dark themes
  3. Test readability - Ensure lines and text are visible
  4. Use theme-aware colors - Leverage Tailwind’s dark: prefix

Internationalization

  1. Format numbers properly - Use Intl.NumberFormat for locale support
  2. Translate labels - Use translation hook for all text
  3. Handle RTL layouts - Consider right-to-left languages
  4. 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} />
)}

Custom Tooltip Formatting

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)

Build docs developers (and LLMs) love