Data Structure

Headless Chart uses consistent and intuitive data structures. This chapter covers how to pass data to charts and the type system.

Basic Data Structure

CartesianData Type

Most 2D charts (Bar, Line, Area, Scatter) use a common data structure:

type CartesianData = {
  labels: string[];
  datasets: {
    legend: string;
    values: number[];
  }[];
};

Basic Example

const data = {
  labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
  datasets: [
    {
      legend: '2023 Sales',
      values: [120, 190, 300, 250, 420]
    },
    {
      legend: '2024 Sales',
      values: [150, 230, 280, 320, 480]
    }
  ]
};

Data Structure by Chart Type

Bar Chart

Uses the basic CartesianData structure as is:

const barData = {
  labels: ['Seoul', 'Busan', 'Daegu', 'Incheon', 'Gwangju'],
  datasets: [
    {
      legend: 'Population',
      values: [9.7, 3.4, 2.4, 2.9, 1.5]
    }
  ]
};

BarChart({ data: barData });

Line Chart

Line Chart also uses the same structure:

const lineData = {
  labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
  datasets: [
    {
      legend: 'Visitors',
      values: [1200, 1900, 3000, 2500, 4200]
    },
    {
      legend: 'Page Views',
      values: [3400, 4800, 7200, 6100, 9800]
    }
  ]
};

LineChart({ data: lineData });

Scatter Chart

Scatter Chart has a different structure using x, y coordinates:

const scatterData = {
  datasets: [
    {
      legend: 'Group A',
      data: [
        { x: 10, y: 20 },
        { x: 15, y: 35 },
        { x: 20, y: 30 }
      ]
    },
    {
      legend: 'Group B',
      data: [
        { x: 25, y: 40 },
        { x: 30, y: 50 },
        { x: 35, y: 45 }
      ]
    }
  ]
};

ScatterChart({ data: scatterData });

Bubble Chart

Bubble Chart adds size information:

const bubbleData = {
  datasets: [
    {
      legend: 'Product A',
      data: [
        { x: 10, y: 20, r: 5 },   // r is radius
        { x: 15, y: 35, r: 10 },
        { x: 20, y: 30, r: 15 }
      ]
    }
  ]
};

BubbleChart({ data: bubbleData });

Heatmap Chart

Heatmap uses a 2D array structure:

const heatmapData = {
  xLabels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
  yLabels: ['Morning', 'Afternoon', 'Evening'],
  data: [
    [10, 20, 30, 25, 15],  // Morning
    [25, 30, 35, 40, 30],  // Afternoon
    [15, 10, 20, 15, 10]   // Evening
  ]
};

HeatmapChart({ data: heatmapData });

Data Validation and Type Safety

Using TypeScript Types

import type { CartesianData } from '@meursyphus/headless-chart';

// Type validation
const validateData = (data: CartesianData): boolean => {
  // Check if labels and values have the same length
  return data.datasets.every(
    dataset => dataset.values.length === data.labels.length
  );
};

// Type-safe data
const typedData: CartesianData = {
  labels: ['A', 'B', 'C'],
  datasets: [
    {
      legend: 'Series 1',
      values: [1, 2, 3]  // Same length as labels
    }
  ]
};

Data Transformation Helpers

// Transform object array to chart data
const transformData = (rawData) => {
  const labels = rawData.map(item => item.month);
  const salesValues = rawData.map(item => item.sales);
  const profitValues = rawData.map(item => item.profit);
  
  return {
    labels,
    datasets: [
      { legend: 'Sales', values: salesValues },
      { legend: 'Profit', values: profitValues }
    ]
  };
};

// Usage example
const rawData = [
  { month: 'Jan', sales: 100, profit: 20 },
  { month: 'Feb', sales: 120, profit: 25 },
  { month: 'Mar', sales: 140, profit: 30 }
];

const chartData = transformData(rawData);

Dynamic Data Processing

Real-time Data Updates

function RealtimeChart() {
  const [data, setData] = useState({
    labels: [],
    datasets: [{ legend: 'Real-time Data', values: [] }]
  });

  useEffect(() => {
    const interval = setInterval(() => {
      setData(prev => {
        const newLabels = [...prev.labels, new Date().toLocaleTimeString()];
        const newValues = [...prev.datasets[0].values, Math.random() * 100];
        
        // Keep only the latest 10 entries
        if (newLabels.length > 10) {
          newLabels.shift();
          newValues.shift();
        }
        
        return {
          labels: newLabels,
          datasets: [{ legend: 'Real-time Data', values: newValues }]
        };
      });
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <Widget widget={LineChart({ data })} />;
}

Filtering and Sorting

// Data filtering
const filterByDateRange = (data, startDate, endDate) => {
  const startIndex = data.labels.findIndex(label => label >= startDate);
  const endIndex = data.labels.findIndex(label => label > endDate);
  
  return {
    labels: data.labels.slice(startIndex, endIndex),
    datasets: data.datasets.map(dataset => ({
      ...dataset,
      values: dataset.values.slice(startIndex, endIndex)
    }))
  };
};

// Data sorting
const sortByValue = (data, ascending = true) => {
  const indices = [...Array(data.labels.length).keys()];
  indices.sort((a, b) => {
    const sumA = data.datasets.reduce((sum, d) => sum + d.values[a], 0);
    const sumB = data.datasets.reduce((sum, d) => sum + d.values[b], 0);
    return ascending ? sumA - sumB : sumB - sumA;
  });
  
  return {
    labels: indices.map(i => data.labels[i]),
    datasets: data.datasets.map(dataset => ({
      ...dataset,
      values: indices.map(i => dataset.values[i])
    }))
  };
};

Scale Settings

Auto Scale vs Manual Scale

// Auto scale (default)
BarChart({ data: myData });

// Manual scale setting
BarChart({
  data: myData,
  getScale: ({ data }) => {
    // Set upper limit to 110% of max value
    const maxValue = Math.max(
      ...data.datasets.flatMap(d => d.values)
    );
    
    return {
      min: 0,
      max: Math.ceil(maxValue * 1.1),
      step: Math.ceil(maxValue / 5)
    };
  }
});

Log Scale Implementation

const logScale = ({ data }) => {
  const values = data.datasets.flatMap(d => d.values);
  const minValue = Math.min(...values.filter(v => v > 0));
  const maxValue = Math.max(...values);
  
  return {
    min: Math.pow(10, Math.floor(Math.log10(minValue))),
    max: Math.pow(10, Math.ceil(Math.log10(maxValue))),
    transform: (value) => Math.log10(value),
    inverse: (value) => Math.pow(10, value)
  };
};

Data Formatting

Label Formatters

// Number formatting
const formatNumber = (value) => {
  if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
  if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;
  return value.toString();
};

// Date formatting
const formatDate = (dateString) => {
  const date = new Date(dateString);
  return date.toLocaleDateString('en-US', {
    month: 'short',
    day: 'numeric'
  });
};

// Usage
BarChart({
  data: myData,
  custom: {
    yAxisLabel: ({ name }) => Text(formatNumber(name)),
    xAxisLabel: ({ name }) => Text(formatDate(name))
  }
});

Empty Data Handling

// Empty data check
const EmptyAwareChart = ({ data }) => {
  if (!data.datasets.length || !data.labels.length) {
    return Container({
      child: Center({
        child: Text('No data available', {
          style: new TextStyle({
            fontSize: 16,
            color: '#6b7280'
          })
        })
      })
    });
  }
  
  return BarChart({ data });
};

Next Steps

Now that you understand data structures, it’s time to create actual charts!

Start implementing real charts in the Bar Chart Guide.