Daniel Gray

Thoughts, Notes, Ideas, Projects

Contact

Skia - Basic Charts and Data Visualization

This article demonstrates how to create basic data visualizations with Skia, including line charts, bar charts, and scatter plots. We'll cover the fundamental drawing operations and show how to apply them to real data visualization tasks.

Setting Up a Basic Canvas

Before we can draw charts, we need to set up a canvas. Here's a basic setup for web applications using CanvasKit:

// Initialize CanvasKit
const ck = await CanvasKitInit({
  locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@latest/bin/' + file
});

// Create surface
const surface = ck.MakeCanvasSurface('chart-canvas');
const canvas = surface.getCanvas();

// Set up dimensions
const width = 800;
const height = 600;
const padding = 50; // Padding around the chart area

Drawing Primitives

Skia provides several basic drawing primitives that we'll use to build charts:

Lines

Draw lines using drawLine():

const paint = new ck.Paint();
paint.setColor(ck.Color(0, 0, 0, 1.0)); // Black
paint.setStrokeWidth(2);
paint.setStyle(ck.PaintStyle.Stroke);

// Draw a line from (x1, y1) to (x2, y2)
canvas.drawLine(x1, y1, x2, y2, paint);

Rectangles

Draw rectangles using drawRect():

const paint = new ck.Paint();
paint.setColor(ck.Color(100, 150, 200, 1.0)); // Blue
paint.setStyle(ck.PaintStyle.Fill);

// Draw a filled rectangle
const rect = ck.LTRBRect(left, top, right, bottom);
canvas.drawRect(rect, paint);

Paths

Create complex shapes using paths:

const path = new ck.Path();
path.moveTo(x1, y1);
path.lineTo(x2, y2);
path.lineTo(x3, y3);
path.close();

canvas.drawPath(path, paint);

Creating a Line Chart

A line chart connects data points with lines. Here's how to create one:

function drawLineChart(canvas, data, width, height, padding) {
  const ck = canvas.getCanvasKit();
  
  // Calculate chart area
  const chartWidth = width - 2 * padding;
  const chartHeight = height - 2 * padding;
  const chartLeft = padding;
  const chartTop = padding;
  const chartRight = width - padding;
  const chartBottom = height - padding;
  
  // Find data range
  const xValues = data.map(d => d.x);
  const yValues = data.map(d => d.y);
  const xMin = Math.min(...xValues);
  const xMax = Math.max(...xValues);
  const yMin = Math.min(...yValues);
  const yMax = Math.max(...yValues);
  
  // Scale function to map data to screen coordinates
  const scaleX = (x) => chartLeft + ((x - xMin) / (xMax - xMin)) * chartWidth;
  const scaleY = (y) => chartBottom - ((y - yMin) / (yMax - yMin)) * chartHeight;
  
  // Draw axes
  const axisPaint = new ck.Paint();
  axisPaint.setColor(ck.Color(0, 0, 0, 1.0));
  axisPaint.setStrokeWidth(2);
  axisPaint.setStyle(ck.PaintStyle.Stroke);
  
  // X-axis
  canvas.drawLine(chartLeft, chartBottom, chartRight, chartBottom, axisPaint);
  // Y-axis
  canvas.drawLine(chartLeft, chartTop, chartLeft, chartBottom, axisPaint);
  
  // Draw data line
  const linePaint = new ck.Paint();
  linePaint.setColor(ck.Color(0, 100, 200, 1.0)); // Blue
  linePaint.setStrokeWidth(3);
  linePaint.setStyle(ck.PaintStyle.Stroke);
  
  const path = new ck.Path();
  for (let i = 0; i < data.length; i++) {
    const x = scaleX(data[i].x);
    const y = scaleY(data[i].y);
    
    if (i === 0) {
      path.moveTo(x, y);
    } else {
      path.lineTo(x, y);
    }
  }
  
  canvas.drawPath(path, linePaint);
  
  // Draw data points
  const pointPaint = new ck.Paint();
  pointPaint.setColor(ck.Color(0, 100, 200, 1.0));
  pointPaint.setStyle(ck.PaintStyle.Fill);
  
  for (const point of data) {
    const x = scaleX(point.x);
    const y = scaleY(point.y);
    canvas.drawCircle(x, y, 4, pointPaint);
  }
}

Creating a Bar Chart

Bar charts represent data using rectangular bars:

function drawBarChart(canvas, data, width, height, padding) {
  const ck = canvas.getCanvasKit();
  
  // Calculate chart area
  const chartWidth = width - 2 * padding;
  const chartHeight = height - 2 * padding;
  const chartLeft = padding;
  const chartBottom = height - padding;
  
  // Find data range
  const values = data.map(d => d.value);
  const maxValue = Math.max(...values);
  const barWidth = chartWidth / data.length;
  const barSpacing = barWidth * 0.2; // 20% spacing between bars
  const actualBarWidth = barWidth - barSpacing;
  
  // Draw bars
  data.forEach((item, index) => {
    const barHeight = (item.value / maxValue) * chartHeight;
    const barLeft = chartLeft + index * barWidth + barSpacing / 2;
    const barTop = chartBottom - barHeight;
    const barRight = barLeft + actualBarWidth;
    const barBottom = chartBottom;
    
    const barPaint = new ck.Paint();
    barPaint.setColor(ck.Color(100, 150, 200, 1.0));
    barPaint.setStyle(ck.PaintStyle.Fill);
    
    const rect = ck.LTRBRect(barLeft, barTop, barRight, barBottom);
    canvas.drawRect(rect, barPaint);
  });
  
  // Draw axes
  const axisPaint = new ck.Paint();
  axisPaint.setColor(ck.Color(0, 0, 0, 1.0));
  axisPaint.setStrokeWidth(2);
  axisPaint.setStyle(ck.PaintStyle.Stroke);
  
  canvas.drawLine(chartLeft, chartBottom, chartLeft + chartWidth, chartBottom, axisPaint);
  canvas.drawLine(chartLeft, chartBottom, chartLeft, padding, axisPaint);
}

Creating a Scatter Plot

Scatter plots show relationships between two variables:

function drawScatterPlot(canvas, data, width, height, padding) {
  const ck = canvas.getCanvasKit();
  
  // Calculate chart area
  const chartWidth = width - 2 * padding;
  const chartHeight = height - 2 * padding;
  const chartLeft = padding;
  const chartTop = padding;
  const chartRight = width - padding;
  const chartBottom = height - padding;
  
  // Find data range
  const xValues = data.map(d => d.x);
  const yValues = data.map(d => d.y);
  const xMin = Math.min(...xValues);
  const xMax = Math.max(...xValues);
  const yMin = Math.min(...yValues);
  const yMax = Math.max(...yValues);
  
  // Scale functions
  const scaleX = (x) => chartLeft + ((x - xMin) / (xMax - xMin)) * chartWidth;
  const scaleY = (y) => chartBottom - ((y - yMin) / (yMax - yMin)) * chartHeight;
  
  // Draw axes
  const axisPaint = new ck.Paint();
  axisPaint.setColor(ck.Color(0, 0, 0, 1.0));
  axisPaint.setStrokeWidth(2);
  axisPaint.setStyle(ck.PaintStyle.Stroke);
  
  canvas.drawLine(chartLeft, chartBottom, chartRight, chartBottom, axisPaint);
  canvas.drawLine(chartLeft, chartTop, chartLeft, chartBottom, axisPaint);
  
  // Draw data points
  const pointPaint = new ck.Paint();
  pointPaint.setColor(ck.Color(200, 50, 50, 1.0)); // Red
  pointPaint.setStyle(ck.PaintStyle.Fill);
  
  data.forEach(point => {
    const x = scaleX(point.x);
    const y = scaleY(point.y);
    canvas.drawCircle(x, y, 5, pointPaint);
  });
}

Adding Labels and Titles

Charts need labels and titles to be useful:

function drawText(canvas, text, x, y, fontSize = 16, color = [0, 0, 0, 1.0]) {
  const ck = canvas.getCanvasKit();
  
  const font = new ck.Font(null, fontSize);
  const paint = new ck.Paint();
  paint.setColor(ck.Color(...color));
  paint.setAntiAlias(true);
  
  canvas.drawText(text, x, y, font, paint);
}

// Draw title
drawText(canvas, "Sales Over Time", width / 2, 30, 24, [0, 0, 0, 1.0]);

// Draw axis labels
drawText(canvas, "Time", width / 2, height - 10, 14);
drawText(canvas, "Sales", 10, height / 2, 14);

Styling and Customization

Skia provides extensive styling options:

Colors

// RGB color
paint.setColor(ck.Color(255, 0, 0, 1.0)); // Red

// HSB color
paint.setColor(ck.Color(0, 1.0, 1.0, 1.0, ck.ColorSpace.SRGB)); // Red in HSB

// With alpha
paint.setColor(ck.Color(255, 0, 0, 0.5)); // Semi-transparent red

Gradients

const colors = [
  ck.Color(100, 150, 200, 1.0),
  ck.Color(200, 100, 150, 1.0)
];
const positions = [0.0, 1.0];
const shader = ck.Shader.MakeLinearGradient(
  [0, 0],
  [0, height],
  colors,
  positions,
  ck.TileMode.Clamp
);
paint.setShader(shader);

Strokes and Fills

// Filled shape
paint.setStyle(ck.PaintStyle.Fill);

// Outlined shape
paint.setStyle(ck.PaintStyle.Stroke);
paint.setStrokeWidth(3);

// Filled and outlined
paint.setStyle(ck.PaintStyle.StrokeAndFill);

Coordinate System Transformations

Transform the coordinate system for easier drawing:

// Save current transformation
canvas.save();

// Translate to center
canvas.translate(width / 2, height / 2);

// Scale
canvas.scale(2, 2); // Make everything 2x bigger

// Rotate
canvas.rotate(45); // Rotate 45 degrees

// Draw (now in transformed coordinates)
canvas.drawCircle(0, 0, 50, paint);

// Restore transformation
canvas.restore();

Example: Complete Line Chart

Here's a complete example that puts it all together:

async function createLineChart() {
  const ck = await CanvasKitInit({
    locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@latest/bin/' + file
  });
  
  const surface = ck.MakeCanvasSurface('chart');
  const canvas = surface.getCanvas();
  
  const width = 800;
  const height = 600;
  const padding = 60;
  
  // Sample data
  const data = [
    { x: 0, y: 10 },
    { x: 1, y: 25 },
    { x: 2, y: 15 },
    { x: 3, y: 30 },
    { x: 4, y: 20 },
    { x: 5, y: 35 }
  ];
  
  // Draw the chart
  drawLineChart(canvas, data, width, height, padding);
  
  // Draw title
  const font = new ck.Font(null, 24);
  const titlePaint = new ck.Paint();
  titlePaint.setColor(ck.Color(0, 0, 0, 1.0));
  titlePaint.setAntiAlias(true);
  canvas.drawText("Sample Data", width / 2 - 80, 30, font, titlePaint);
  
  surface.flush();
}

Best Practices

When creating charts with Skia:

  1. Calculate ranges first - Determine data ranges before drawing
  2. Use consistent padding - Maintain margins for readability
  3. Reuse Paint objects - Create paint objects once and reuse them
  4. Group drawing operations - Draw similar elements together
  5. Handle edge cases - Empty data, single points, etc.
  6. Add labels - Always include axis labels and titles
  7. Consider accessibility - Use colors that work for colorblind users

Performance Tips

For better performance:

  • Reuse objects - Don't create new Paint/Path objects in loops
  • Batch operations - Group similar drawing commands
  • Use hardware acceleration - Ensure GPU acceleration is enabled
  • Minimize redraws - Only redraw when data changes
  • Optimize paths - Simplify complex paths when possible

Next Steps

Now that you can create basic charts, the next article will explore advanced techniques including animations, interactions, and more complex visualization types.

Conclusion

Skia provides the building blocks needed to create custom data visualizations with precise control and excellent performance. By combining basic drawing primitives with coordinate transformations and styling, you can create any chart type you need.

For related topics:


This article is part of the Skia Framework series. Previous: Skia - Introduction and Overview. Next: Skia - Advanced Data Visualization Techniques.

Related Content

Skia - Introduction and Overview

Skia - Introduction and Overview Skia is Google's open-source 2D graphics library that powers some of the most widely used applications in the world. From Chrome's rendering engine to Android's UI fra...

Skia Series Index

Skia Series Index This series introduces the Skia graphics framework and demonstrates its power for creating beautiful, performant data visualizations. Skia is Google's open-source 2D graphics library...

Coding Projects

Coding Projects Building interactive web applications, exploring new technologies, and solving problems. This section covers web development, software engineering, coding projects, procedural generati...