Daniel Gray

Thoughts, Notes, Ideas, Projects

Contact

Skia - Advanced Data Visualization Techniques

This article explores advanced techniques for creating sophisticated data visualizations with Skia, including animations, interactions, multi-series charts, and performance optimization. We'll build on the basics covered in the previous article and create more complex, interactive visualizations.

Animations and Transitions

Smooth animations make data visualizations more engaging and help users understand changes over time. Skia's performance makes it ideal for animated visualizations.

Basic Animation Loop

let animationFrame = 0;
const animationDuration = 60; // frames

function animate() {
  const progress = animationFrame / animationDuration;
  
  // Update chart based on progress
  updateChart(progress);
  
  animationFrame++;
  if (animationFrame < animationDuration) {
    requestAnimationFrame(animate);
  }
}

function updateChart(progress) {
  // Clear canvas
  canvas.clear(ck.Color(255, 255, 255, 1.0));
  
  // Interpolate data or transform based on progress
  const animatedData = interpolateData(data, progress);
  
  // Redraw chart
  drawChart(canvas, animatedData);
  
  surface.flush();
}

Easing Functions

Use easing functions for natural-looking animations:

// Ease-in-out cubic
function easeInOutCubic(t) {
  return t < 0.5
    ? 4 * t * t * t
    : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

// Apply easing
const easedProgress = easeInOutCubic(progress);

Animated Line Chart

Animate a line chart drawing from left to right:

function drawAnimatedLineChart(canvas, data, progress) {
  const ck = canvas.getCanvasKit();
  
  // Calculate how many points to show
  const visiblePoints = Math.floor(data.length * progress);
  
  // Draw visible portion
  const path = new ck.Path();
  for (let i = 0; i < visiblePoints; 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);
    }
  }
  
  const linePaint = new ck.Paint();
  linePaint.setColor(ck.Color(0, 100, 200, 1.0));
  linePaint.setStrokeWidth(3);
  linePaint.setStyle(ck.PaintStyle.Stroke);
  
  canvas.drawPath(path, linePaint);
  
  // Draw animated point
  if (visiblePoints > 0) {
    const lastPoint = data[visiblePoints - 1];
    const x = scaleX(lastPoint.x);
    const y = scaleY(lastPoint.y);
    
    const pointPaint = new ck.Paint();
    pointPaint.setColor(ck.Color(200, 50, 50, 1.0));
    pointPaint.setStyle(ck.PaintStyle.Fill);
    
    canvas.drawCircle(x, y, 8, pointPaint);
  }
}

Interactive Visualizations

Make visualizations interactive by responding to user input:

Mouse Interaction

function setupMouseInteraction(canvas, data) {
  canvas.addEventListener('mousemove', (e) => {
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    
    // Find nearest data point
    const nearestPoint = findNearestPoint(data, x, y);
    
    // Highlight point
    highlightPoint(canvas, nearestPoint);
  });
}

function findNearestPoint(data, mouseX, mouseY) {
  let nearest = null;
  let minDistance = Infinity;
  
  data.forEach(point => {
    const x = scaleX(point.x);
    const y = scaleY(point.y);
    const distance = Math.sqrt(
      Math.pow(x - mouseX, 2) + Math.pow(y - mouseY, 2)
    );
    
    if (distance < minDistance) {
      minDistance = distance;
      nearest = point;
    }
  });
  
  return nearest;
}

Tooltips

Display information on hover:

function drawTooltip(canvas, point, mouseX, mouseY) {
  const ck = canvas.getCanvasKit();
  
  const tooltipText = `Value: ${point.y}\nTime: ${point.x}`;
  const tooltipWidth = 120;
  const tooltipHeight = 50;
  
  // Draw tooltip background
  const bgPaint = new ck.Paint();
  bgPaint.setColor(ck.Color(0, 0, 0, 0.8));
  bgPaint.setStyle(ck.PaintStyle.Fill);
  
  const tooltipRect = ck.LTRBRect(
    mouseX + 10,
    mouseY - tooltipHeight - 10,
    mouseX + 10 + tooltipWidth,
    mouseY - 10
  );
  
  canvas.drawRect(tooltipRect, bgPaint);
  
  // Draw tooltip text
  const font = new ck.Font(null, 12);
  const textPaint = new ck.Paint();
  textPaint.setColor(ck.Color(255, 255, 255, 1.0));
  textPaint.setAntiAlias(true);
  
  canvas.drawText(tooltipText, mouseX + 15, mouseY - 25, font, textPaint);
}

Multi-Series Charts

Display multiple data series on the same chart:

function drawMultiSeriesLineChart(canvas, seriesArray, width, height, padding) {
  const ck = canvas.getCanvasKit();
  
  // Calculate combined data range
  const allXValues = seriesArray.flatMap(s => s.data.map(d => d.x));
  const allYValues = seriesArray.flatMap(s => s.data.map(d => d.y));
  const xMin = Math.min(...allXValues);
  const xMax = Math.max(...allXValues);
  const yMin = Math.min(...allYValues);
  const yMax = Math.max(...allYValues);
  
  // Scale functions
  const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * (width - 2 * padding);
  const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * (height - 2 * padding);
  
  // Draw each series
  seriesArray.forEach((series, index) => {
    const path = new ck.Path();
    
    series.data.forEach((point, i) => {
      const x = scaleX(point.x);
      const y = scaleY(point.y);
      
      if (i === 0) {
        path.moveTo(x, y);
      } else {
        path.lineTo(x, y);
      }
    });
    
    const linePaint = new ck.Paint();
    linePaint.setColor(ck.Color(...series.color));
    linePaint.setStrokeWidth(series.width || 2);
    linePaint.setStyle(ck.PaintStyle.Stroke);
    
    canvas.drawPath(path, linePaint);
  });
  
  // Draw legend
  drawLegend(canvas, seriesArray, width, height);
}

Custom Chart Types

Create unique visualizations that don't exist in standard libraries:

Heatmap

function drawHeatmap(canvas, data, width, height, padding) {
  const ck = canvas.getCanvasKit();
  
  const rows = data.length;
  const cols = data[0].length;
  const cellWidth = (width - 2 * padding) / cols;
  const cellHeight = (height - 2 * padding) / rows;
  
  // Find value range
  const allValues = data.flat();
  const minValue = Math.min(...allValues);
  const maxValue = Math.max(...allValues);
  
  data.forEach((row, rowIndex) => {
    row.forEach((value, colIndex) => {
      // Calculate color based on value
      const normalized = (value - minValue) / (maxValue - minValue);
      const color = interpolateColor([255, 0, 0], [0, 0, 255], normalized);
      
      const x = padding + colIndex * cellWidth;
      const y = padding + rowIndex * cellHeight;
      
      const cellPaint = new ck.Paint();
      cellPaint.setColor(ck.Color(...color, 1.0));
      cellPaint.setStyle(ck.PaintStyle.Fill);
      
      const rect = ck.LTRBRect(x, y, x + cellWidth, y + cellHeight);
      canvas.drawRect(rect, cellPaint);
    });
  });
}

function interpolateColor(color1, color2, t) {
  return [
    color1[0] + (color2[0] - color1[0]) * t,
    color1[1] + (color2[1] - color1[1]) * t,
    color1[2] + (color2[2] - color1[2]) * t
  ];
}

Radar Chart

function drawRadarChart(canvas, data, width, height) {
  const ck = canvas.getCanvasKit();
  
  const centerX = width / 2;
  const centerY = height / 2;
  const radius = Math.min(width, height) / 2 - 50;
  const numAxes = data.length;
  const angleStep = (2 * Math.PI) / numAxes;
  
  // Find max value
  const maxValue = Math.max(...data.map(d => d.value));
  
  // Draw axes
  const axisPaint = new ck.Paint();
  axisPaint.setColor(ck.Color(200, 200, 200, 1.0));
  axisPaint.setStrokeWidth(1);
  axisPaint.setStyle(ck.PaintStyle.Stroke);
  
  for (let i = 0; i < numAxes; i++) {
    const angle = i * angleStep - Math.PI / 2;
    const x = centerX + Math.cos(angle) * radius;
    const y = centerY + Math.sin(angle) * radius;
    canvas.drawLine(centerX, centerY, x, y, axisPaint);
  }
  
  // Draw data polygon
  const path = new ck.Path();
  data.forEach((point, i) => {
    const angle = i * angleStep - Math.PI / 2;
    const distance = (point.value / maxValue) * radius;
    const x = centerX + Math.cos(angle) * distance;
    const y = centerY + Math.sin(angle) * distance;
    
    if (i === 0) {
      path.moveTo(x, y);
    } else {
      path.lineTo(x, y);
    }
  });
  path.close();
  
  const fillPaint = new ck.Paint();
  fillPaint.setColor(ck.Color(100, 150, 200, 0.3));
  fillPaint.setStyle(ck.PaintStyle.Fill);
  canvas.drawPath(path, fillPaint);
  
  const strokePaint = new ck.Paint();
  strokePaint.setColor(ck.Color(100, 150, 200, 1.0));
  strokePaint.setStrokeWidth(2);
  strokePaint.setStyle(ck.PaintStyle.Stroke);
  canvas.drawPath(path, strokePaint);
}

Performance Optimization

For large datasets or real-time updates, optimize your rendering:

Efficient Redraws

Only redraw what changed:

let lastDataHash = null;

function updateChart(newData) {
  const dataHash = JSON.stringify(newData);
  
  if (dataHash !== lastDataHash) {
    redrawChart(newData);
    lastDataHash = dataHash;
  }
}

Clipping

Use clipping to avoid drawing outside visible areas:

canvas.save();
const clipRect = ck.LTRBRect(padding, padding, width - padding, height - padding);
canvas.clipRect(clipRect, ck.ClipOp.Intersect, true);

// Draw chart (only visible portion will render)
drawChart(canvas, data);

canvas.restore();

Object Reuse

Reuse objects to minimize allocations:

// Create once, reuse
const paint = new ck.Paint();
const path = new ck.Path();

function drawFrame() {
  // Reset and reuse
  path.reset();
  paint.setColor(ck.Color(0, 0, 0, 1.0));
  
  // Draw with reused objects
  // ...
}

Level of Detail

Reduce detail for better performance:

function drawOptimizedLineChart(canvas, data, pixelThreshold = 2) {
  // Skip points that are too close together
  const simplified = simplifyData(data, pixelThreshold);
  drawLineChart(canvas, simplified);
}

function simplifyData(data, threshold) {
  const simplified = [data[0]];
  
  for (let i = 1; i < data.length - 1; i++) {
    const prev = simplified[simplified.length - 1];
    const curr = data[i];
    const next = data[i + 1];
    
    // Calculate distance
    const dx = scaleX(next.x) - scaleX(prev.x);
    const dy = scaleY(next.y) - scaleY(prev.y);
    const distance = Math.sqrt(dx * dx + dy * dy);
    
    if (distance > threshold) {
      simplified.push(curr);
    }
  }
  
  simplified.push(data[data.length - 1]);
  return simplified;
}

Real-Time Data Visualization

Handle streaming data efficiently:

class StreamingChart {
  constructor(canvas, maxPoints = 1000) {
    this.canvas = canvas;
    this.data = [];
    this.maxPoints = maxPoints;
  }
  
  addDataPoint(point) {
    this.data.push(point);
    
    // Keep only recent points
    if (this.data.length > this.maxPoints) {
      this.data.shift();
    }
    
    this.redraw();
  }
  
  redraw() {
    // Clear canvas
    this.canvas.clear(ck.Color(255, 255, 255, 1.0));
    
    // Draw only visible portion (sliding window)
    const visibleData = this.data.slice(-500); // Last 500 points
    drawLineChart(this.canvas, visibleData);
    
    this.surface.flush();
  }
}

Export and Sharing

Export visualizations as images or PDFs:

// Export as PNG
function exportAsPNG(surface, filename) {
  const image = surface.makeImageSnapshot();
  const data = image.encodeToData();
  
  // Download or save data
  downloadFile(data, filename + '.png');
}

// Export as PDF
function exportAsPDF(data, filename) {
  const pdfDoc = ck.MakePDFDocument();
  const pdfCanvas = pdfDoc.beginPage(800, 600);
  
  // Draw chart to PDF canvas
  drawChart(pdfCanvas, data);
  
  pdfDoc.endPage();
  const pdfData = pdfDoc.close();
  
  downloadFile(pdfData, filename + '.pdf');
}

Best Practices for Advanced Visualizations

  1. Performance First - Optimize for large datasets
  2. Smooth Animations - Use requestAnimationFrame and easing
  3. Responsive Design - Adapt to different screen sizes
  4. Accessibility - Support keyboard navigation and screen readers
  5. Error Handling - Gracefully handle missing or invalid data
  6. Modular Code - Break visualizations into reusable components
  7. Documentation - Comment complex calculations and transformations

Example: Interactive Dashboard

Here's a complete example combining multiple techniques:

class InteractiveDashboard {
  constructor(canvasId) {
    this.surface = ck.MakeCanvasSurface(canvasId);
    this.canvas = this.surface.getCanvas();
    this.data = [];
    this.hoveredPoint = null;
    this.animationProgress = 0;
  }
  
  updateData(newData) {
    this.data = newData;
    this.animateUpdate();
  }
  
  animateUpdate() {
    this.animationProgress = 0;
    this.animate();
  }
  
  animate() {
    const progress = easeInOutCubic(this.animationProgress);
    
    this.canvas.clear(ck.Color(255, 255, 255, 1.0));
    this.drawChart(this.data, progress);
    this.drawTooltip();
    this.surface.flush();
    
    this.animationProgress += 0.02;
    if (this.animationProgress < 1.0) {
      requestAnimationFrame(() => this.animate());
    }
  }
  
  drawChart(data, progress) {
    // Draw animated chart
    drawAnimatedLineChart(this.canvas, data, progress);
  }
  
  drawTooltip() {
    if (this.hoveredPoint) {
      drawTooltip(this.canvas, this.hoveredPoint, this.mouseX, this.mouseY);
    }
  }
  
  handleMouseMove(x, y) {
    this.mouseX = x;
    this.mouseY = y;
    this.hoveredPoint = findNearestPoint(this.data, x, y);
    this.redraw();
  }
}

Conclusion

Skia's powerful API and excellent performance make it ideal for creating sophisticated, interactive data visualizations. By combining animations, interactions, and custom chart types, you can create visualizations that go far beyond what standard libraries offer.

For related topics:


This article is part of the Skia Framework series. Previous: Skia - Basic Charts and Data Visualization.

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 - Basic Charts and Data Visualization

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 fundamenta...

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...