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
- Performance First - Optimize for large datasets
- Smooth Animations - Use requestAnimationFrame and easing
- Responsive Design - Adapt to different screen sizes
- Accessibility - Support keyboard navigation and screen readers
- Error Handling - Gracefully handle missing or invalid data
- Modular Code - Break visualizations into reusable components
- 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:
- Skia - Introduction and Overview - Learn about Skia fundamentals
- Skia - Basic Charts and Data Visualization - Basic chart creation
- Skia Series Index - Series overview
- Coding Projects - Overview of coding projects
This article is part of the Skia Framework series. Previous: Skia - Basic Charts and Data Visualization.