Skip to main content

Performance & Optimization

Understanding performance considerations and optimization strategies for D3.js visualizations.

Rendering Performance

DOM vs Canvas vs WebGL

Different rendering approaches have distinct performance characteristics and use cases.

SVG/DOM Rendering

Best for:

  • Interactive elements with individual event handlers
  • Small to medium datasets (< 1,000 elements)
  • Complex styling with CSS
  • Text rendering and typography
  • Accessibility requirements

Performance Characteristics:

// Each element is a DOM node
const circles = svg
.selectAll('circle')
.data(data) // Assume 500 data points
.join('circle') // Creates 500 DOM elements
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.on('click', handleClick); // 500 event listeners

// Memory usage grows linearly with element count
// Browser must track 500 individual DOM nodes

Limitations:

  • Performance degrades significantly with many elements (> 1,000)
  • Memory usage increases with element count
  • Style recalculation can be expensive

Canvas Rendering

Best for:

  • Large datasets (1,000 - 100,000 elements)
  • Real-time animations
  • Custom rendering algorithms
  • Performance-critical applications
// Canvas implementation for large datasets
function renderToCanvas(data) {
const canvas = d3
.select('#chart')
.append('canvas')
.attr('width', width)
.attr('height', height);

const context = canvas.node().getContext('2d');

// Single render pass for all elements
data.forEach(d => {
context.beginPath();
context.arc(xScale(d.x), yScale(d.y), radiusScale(d.value), 0, 2 * Math.PI);
context.fillStyle = colorScale(d.category);
context.fill();
});
}

// Performance benefits:
// - Single DOM element (canvas)
// - GPU acceleration
// - No event listener overhead

Limitations:

  • No individual element interaction
  • More complex event handling
  • No CSS styling
  • Accessibility challenges

WebGL Rendering

Best for:

  • Very large datasets (> 100,000 elements)
  • 3D visualizations
  • GPU-accelerated computations
  • Real-time graphics
// WebGL for massive datasets
function renderWithWebGL(data) {
const regl = require('regl')();

const drawPoints = regl({
frag: `
precision mediump float;
uniform vec3 color;
void main() {
gl_FragColor = vec4(color, 1.0);
}
`,
vert: `
precision mediump float;
attribute vec2 position;
uniform float pointSize;
uniform mat3 transform;
void main() {
vec3 pos = transform * vec3(position, 1.0);
gl_Position = vec4(pos.xy, 0.0, 1.0);
gl_PointSize = pointSize;
}
`,
attributes: {
position: data.map(d => [d.x, d.y]),
},
uniforms: {
color: [0.2, 0.6, 0.8],
pointSize: 4,
transform: computeTransform(),
},
count: data.length,
primitive: 'points',
});

drawPoints();
}

Optimization Strategies

1. Data Reduction and Sampling

// Adaptive sampling based on zoom level
function adaptiveSampling(data, zoomLevel) {
if (zoomLevel < 1) {
// Zoomed out: show every 10th point
return data.filter((d, i) => i % 10 === 0);
} else if (zoomLevel < 2) {
// Medium zoom: show every 5th point
return data.filter((d, i) => i % 5 === 0);
} else {
// Zoomed in: show all points
return data;
}
}

// Level of detail (LOD) based on dataset size
function levelOfDetail(data, maxPoints = 1000) {
if (data.length <= maxPoints) {
return data;
}

const step = Math.ceil(data.length / maxPoints);
return data.filter((d, i) => i % step === 0);
}

// Spatial sampling for geographic data
function spatialSampling(data, bounds, resolution) {
const grid = {};
const cellSize = resolution;

return data.filter(d => {
const x = Math.floor(d.longitude / cellSize);
const y = Math.floor(d.latitude / cellSize);
const key = `${x},${y}`;

if (!grid[key]) {
grid[key] = true;
return true;
}
return false;
});
}

2. Efficient Data Structures

// Use Maps for O(1) lookups instead of arrays
class EfficientDataManager {
constructor(data) {
// Index by ID for fast lookups
this.dataById = new Map(data.map(d => [d.id, d]));

// Spatial index for geographic data
this.spatialIndex = this.buildSpatialIndex(data);

// Time-based index for time series
this.timeIndex = this.buildTimeIndex(data);
}

buildSpatialIndex(data) {
// Quadtree for spatial queries
const quadtree = d3
.quadtree()
.x(d => d.x)
.y(d => d.y)
.addAll(data);

return quadtree;
}

buildTimeIndex(data) {
// Group by time periods for efficient time-based queries
return d3.group(data, d => d3.timeDay.floor(d.date));
}

findNearby(x, y, radius) {
const nearby = [];
this.spatialIndex.visit((node, x0, y0, x1, y1) => {
if (!node.length) {
const d = node.data;
const dx = x - d.x;
const dy = y - d.y;
if (dx * dx + dy * dy < radius * radius) {
nearby.push(d);
}
}
return false;
});
return nearby;
}
}

3. Virtualization

// Only render visible elements
class VirtualizedScatterPlot {
constructor(data, container, options) {
this.data = data;
this.container = container;
this.options = options;
this.visibleBounds = this.calculateVisibleBounds();
this.setupViewport();
}

calculateVisibleBounds() {
const transform = d3.zoomTransform(this.container.node());
const bounds = {
x0: transform.invertX(0),
y0: transform.invertY(0),
x1: transform.invertX(this.options.width),
y1: transform.invertY(this.options.height),
};
return bounds;
}

getVisibleData() {
const bounds = this.visibleBounds;
return this.data.filter(
d =>
d.x >= bounds.x0 &&
d.x <= bounds.x1 &&
d.y >= bounds.y0 &&
d.y <= bounds.y1
);
}

render() {
const visibleData = this.getVisibleData();

// Only render visible points
const circles = this.container
.selectAll('circle')
.data(visibleData, d => d.id);

circles.enter().append('circle').attr('r', 3);

circles.attr('cx', d => this.xScale(d.x)).attr('cy', d => this.yScale(d.y));

circles.exit().remove();
}

onZoom(event) {
this.visibleBounds = this.calculateVisibleBounds();
this.render();
}
}

4. Batch DOM Operations

// Inefficient: Multiple DOM updates
data.forEach(d => {
svg.append('circle').attr('cx', d.x).attr('cy', d.y).attr('r', d.r);
});

// Efficient: Single data join
svg
.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', d => d.r);

// For dynamic updates, batch changes
function batchUpdate(updates) {
const selection = svg.selectAll('circle');

// Apply all attribute changes in one operation
updates.forEach(update => {
selection.filter(d => d.id === update.id).datum(update.data);
});

// Single transition for all changes
selection
.transition()
.duration(300)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', d => d.r);
}

Memory Management

Avoiding Memory Leaks

// Properly clean up event listeners
class ChartComponent {
constructor(container, data) {
this.container = container;
this.data = data;
this.eventListeners = [];
this.setup();
}

setup() {
// Store references to event listeners
const mousemove = event => this.handleMouseMove(event);
const resize = () => this.handleResize();

this.eventListeners.push(
{ element: window, event: 'resize', handler: resize },
{ element: this.container.node(), event: 'mousemove', handler: mousemove }
);

// Add listeners
this.eventListeners.forEach(({ element, event, handler }) => {
element.addEventListener(event, handler);
});
}

destroy() {
// Remove all event listeners
this.eventListeners.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});

// Clear references
this.eventListeners = [];
this.data = null;

// Remove DOM elements
this.container.selectAll('*').remove();
}
}

// Use WeakMap for element associations
const elementData = new WeakMap();

svg.selectAll('circle').each(function (d) {
elementData.set(this, d); // Automatically garbage collected
});

Efficient Data Updates

// Reuse objects instead of creating new ones
class DataPool {
constructor() {
this.pool = [];
}

get() {
return this.pool.pop() || {};
}

release(obj) {
// Clear properties but reuse object
Object.keys(obj).forEach(key => delete obj[key]);
this.pool.push(obj);
}
}

const pool = new DataPool();

function processData(rawData) {
return rawData.map(raw => {
const processed = pool.get();
processed.x = +raw.x;
processed.y = +raw.y;
processed.value = +raw.value;
return processed;
});
}

Animation Performance

Efficient Transitions

// Use CSS transforms when possible (GPU accelerated)
svg
.selectAll('circle')
.transition()
.duration(1000)
.style('transform', d => `translate(${x(d.x)}px, ${y(d.y)}px)`);

// Avoid animating many properties simultaneously
// Instead of this:
selection
.transition()
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', d => d.r)
.attr('fill', d => d.color);

// Do this:
selection
.transition()
.attr('transform', d => `translate(${d.x}, ${d.y})`)
.transition()
.attr('r', d => d.r)
.attr('fill', d => d.color);

Animation Culling

// Only animate visible elements
function animateVisible() {
const visibleElements = svg.selectAll('circle').filter(function (d) {
const rect = this.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0;
});

visibleElements
.transition()
.duration(500)
.attr('r', d => d.newRadius);
}

// Pause animations when not visible
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
resumeAnimations();
} else {
pauseAnimations();
}
});
});

observer.observe(chartContainer.node());

Data Loading Performance

Streaming and Progressive Loading

// Stream large CSV files
async function streamLargeDataset(url, chunkSize = 1000) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();

let buffer = '';
let processedRows = 0;

while (true) {
const { done, value } = await reader.read();

if (done) break;

buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep incomplete line

// Process chunk
const chunk = lines.slice(0, chunkSize);
if (chunk.length > 0) {
const parsedChunk = chunk.map(parseCSVRow);
processChunk(parsedChunk);
processedRows += chunk.length;

// Update progress
updateProgress(processedRows);

// Yield control to prevent blocking
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}

// Web Workers for data processing
class DataWorker {
constructor() {
this.worker = new Worker('data-processor.js');
this.callbacks = new Map();
this.requestId = 0;

this.worker.onmessage = event => {
const { id, result, error } = event.data;
const callback = this.callbacks.get(id);

if (callback) {
this.callbacks.delete(id);
if (error) {
callback.reject(error);
} else {
callback.resolve(result);
}
}
};
}

processData(data) {
return new Promise((resolve, reject) => {
const id = this.requestId++;
this.callbacks.set(id, { resolve, reject });

this.worker.postMessage({
id,
command: 'process',
data,
});
});
}
}

Caching Strategies

// LRU cache for computed values
class LRUCache {
constructor(maxSize = 100) {
this.maxSize = maxSize;
this.cache = new Map();
}

get(key) {
if (this.cache.has(key)) {
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
return null;
}

set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}

// Cache expensive computations
const computationCache = new LRUCache(50);

function expensiveComputation(data) {
const key = data.map(d => d.id).join(',');
let result = computationCache.get(key);

if (!result) {
result = performComputation(data);
computationCache.set(key, result);
}

return result;
}

Monitoring and Profiling

Performance Metrics

// Performance monitoring
class PerformanceMonitor {
constructor() {
this.metrics = {};
}

startTimer(label) {
this.metrics[label] = performance.now();
}

endTimer(label) {
if (this.metrics[label]) {
const duration = performance.now() - this.metrics[label];
console.log(`${label}: ${duration.toFixed(2)}ms`);
delete this.metrics[label];
return duration;
}
}

measureFunction(fn, label) {
return (...args) => {
this.startTimer(label);
const result = fn.apply(this, args);
this.endTimer(label);
return result;
};
}

measureFrameRate() {
let frameCount = 0;
let lastTime = performance.now();

function countFrame() {
frameCount++;
const currentTime = performance.now();

if (currentTime - lastTime >= 1000) {
console.log(`FPS: ${frameCount}`);
frameCount = 0;
lastTime = currentTime;
}

requestAnimationFrame(countFrame);
}

requestAnimationFrame(countFrame);
}
}

// Usage
const monitor = new PerformanceMonitor();

const optimizedRender = monitor.measureFunction(render, 'render');
const optimizedUpdate = monitor.measureFunction(updateData, 'update');

monitor.measureFrameRate();

Memory Usage Tracking

// Track memory usage
function trackMemoryUsage() {
if (performance.memory) {
const memory = performance.memory;
console.log({
used: Math.round(memory.usedJSHeapSize / 1048576) + ' MB',
total: Math.round(memory.totalJSHeapSize / 1048576) + ' MB',
limit: Math.round(memory.jsHeapSizeLimit / 1048576) + ' MB',
});
}
}

// Monitor DOM node count
function trackDOMNodes() {
const nodeCount = document.querySelectorAll('*').length;
console.log(`DOM nodes: ${nodeCount}`);
}

// Automated monitoring
setInterval(() => {
trackMemoryUsage();
trackDOMNodes();
}, 5000);

Advanced Performance Patterns

Real-Time Data Processing

// Efficient real-time data streaming with backpressure
class RealTimeDataProcessor {
constructor(options = {}) {
this.bufferSize = options.bufferSize || 1000;
this.processInterval = options.processInterval || 16; // ~60fps
this.dataBuffer = [];
this.isProcessing = false;
this.droppedFrames = 0;
}

addData(newData) {
// Implement backpressure to prevent memory overflow
if (this.dataBuffer.length > this.bufferSize) {
this.dataBuffer = this.dataBuffer.slice(-this.bufferSize / 2);
this.droppedFrames++;
}

this.dataBuffer.push(...newData);
this.scheduleProcessing();
}

scheduleProcessing() {
if (this.isProcessing) return;

this.isProcessing = true;
requestAnimationFrame(() => {
this.processBuffer();
this.isProcessing = false;
});
}

processBuffer() {
const startTime = performance.now();
const maxProcessingTime = 10; // Max 10ms per frame

while (
this.dataBuffer.length > 0 &&
performance.now() - startTime < maxProcessingTime
) {
const batch = this.dataBuffer.splice(0, 50);
this.renderBatch(batch);
}

// Continue processing if buffer still has data
if (this.dataBuffer.length > 0) {
this.scheduleProcessing();
}
}

renderBatch(batch) {
// Efficient batch rendering using join pattern
const selection = this.container
.selectAll('.data-point')
.data(batch, d => d.id);

selection
.enter()
.append('circle')
.attr('class', 'data-point')
.attr('r', 2)
.merge(selection)
.attr('cx', d => this.xScale(d.x))
.attr('cy', d => this.yScale(d.y));

selection.exit().remove();
}
}

GPU-Accelerated Rendering with WebGL

// High-performance WebGL scatter plot for massive datasets
class WebGLScatterPlot {
constructor(container, data) {
this.container = container;
this.canvas = container.append('canvas');
this.gl = this.canvas.node().getContext('webgl');
this.data = data;
this.pointCount = data.length;

this.initWebGL();
this.createBuffers();
this.setupShaders();
}

initWebGL() {
const gl = this.gl;

// Enable blending for overlapping points
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

// Set viewport
gl.viewport(0, 0, this.canvas.attr('width'), this.canvas.attr('height'));
}

createBuffers() {
const gl = this.gl;

// Create position buffer
const positions = new Float32Array(this.pointCount * 2);
const colors = new Float32Array(this.pointCount * 4);

this.data.forEach((d, i) => {
positions[i * 2] = d.x;
positions[i * 2 + 1] = d.y;

// RGBA color
colors[i * 4] = d.color.r / 255;
colors[i * 4 + 1] = d.color.g / 255;
colors[i * 4 + 2] = d.color.b / 255;
colors[i * 4 + 3] = d.opacity || 1.0;
});

// Create and bind buffers
this.positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

this.colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
}

setupShaders() {
const gl = this.gl;

const vertexShaderSource = `
attribute vec2 a_position;
attribute vec4 a_color;
uniform vec2 u_resolution;
uniform mat3 u_transform;
varying vec4 v_color;

void main() {
vec3 position = u_transform * vec3(a_position, 1.0);
vec2 normalized = position.xy / u_resolution * 2.0 - 1.0;
gl_Position = vec4(normalized * vec2(1, -1), 0, 1);
gl_PointSize = 4.0;
v_color = a_color;
}
`;

const fragmentShaderSource = `
precision mediump float;
varying vec4 v_color;

void main() {
float distance = length(gl_PointCoord - vec2(0.5));
if (distance > 0.5) discard;
gl_FragColor = v_color;
}
`;

this.program = this.createProgram(vertexShaderSource, fragmentShaderSource);
gl.useProgram(this.program);

// Get attribute and uniform locations
this.attributes = {
position: gl.getAttribLocation(this.program, 'a_position'),
color: gl.getAttribLocation(this.program, 'a_color'),
};

this.uniforms = {
resolution: gl.getUniformLocation(this.program, 'u_resolution'),
transform: gl.getUniformLocation(this.program, 'u_transform'),
};
}

render(transform = [1, 0, 0, 0, 1, 0, 0, 0, 1]) {
const gl = this.gl;

gl.clear(gl.COLOR_BUFFER_BIT);

// Set uniforms
gl.uniform2f(
this.uniforms.resolution,
this.canvas.attr('width'),
this.canvas.attr('height')
);
gl.uniformMatrix3fv(this.uniforms.transform, false, transform);

// Bind position buffer
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
gl.enableVertexAttribArray(this.attributes.position);
gl.vertexAttribPointer(this.attributes.position, 2, gl.FLOAT, false, 0, 0);

// Bind color buffer
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
gl.enableVertexAttribArray(this.attributes.color);
gl.vertexAttribPointer(this.attributes.color, 4, gl.FLOAT, false, 0, 0);

// Draw points
gl.drawArrays(gl.POINTS, 0, this.pointCount);
}

createProgram(vertexSource, fragmentSource) {
const gl = this.gl;

const vertexShader = this.createShader(gl.VERTEX_SHADER, vertexSource);
const fragmentShader = this.createShader(
gl.FRAGMENT_SHADER,
fragmentSource
);

const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);

if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(
'Program failed to link: ' + gl.getProgramInfoLog(program)
);
}

return program;
}

createShader(type, source) {
const gl = this.gl;
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);

if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(
'Shader compilation error: ' + gl.getShaderInfoLog(shader)
);
}

return shader;
}
}

// Usage for 1M+ data points
const massiveDataset = generateData(1000000);
const webglChart = new WebGLScatterPlot(container, massiveDataset);
webglChart.render();

Advanced Memory Pool Management

// Object pooling for high-frequency updates
class AdvancedObjectPool {
constructor(createFn, resetFn, initialSize = 100) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
this.inUse = new Set();

// Pre-populate pool
for (let i = 0; i < initialSize; i++) {
this.pool.push(this.createFn());
}
}

acquire() {
let obj;

if (this.pool.length > 0) {
obj = this.pool.pop();
} else {
obj = this.createFn();
}

this.inUse.add(obj);
return obj;
}

release(obj) {
if (this.inUse.has(obj)) {
this.inUse.delete(obj);
this.resetFn(obj);
this.pool.push(obj);
}
}

releaseAll() {
this.inUse.forEach(obj => {
this.resetFn(obj);
this.pool.push(obj);
});
this.inUse.clear();
}

getStats() {
return {
poolSize: this.pool.length,
inUse: this.inUse.size,
total: this.pool.length + this.inUse.size,
};
}
}

// Usage for data point objects
const dataPointPool = new AdvancedObjectPool(
() => ({ x: 0, y: 0, value: 0, category: '', element: null }),
obj => {
obj.x = 0;
obj.y = 0;
obj.value = 0;
obj.category = '';
if (obj.element) {
obj.element.remove();
obj.element = null;
}
}
);

// Efficient data processing with pooling
function processLargeDataset(rawData) {
const processedData = [];

rawData.forEach(raw => {
const dataPoint = dataPointPool.acquire();
dataPoint.x = +raw.x;
dataPoint.y = +raw.y;
dataPoint.value = +raw.value;
dataPoint.category = raw.category;
processedData.push(dataPoint);
});

return {
data: processedData,
cleanup: () => {
processedData.forEach(point => dataPointPool.release(point));
},
};
}

Efficient Spatial Indexing for Interactions

// R-tree spatial index for fast collision detection
class SpatialIndex {
constructor(maxEntries = 16) {
this.tree = new RBush(maxEntries);
this.elementMap = new Map();
}

insert(element, bounds) {
const item = {
minX: bounds.x,
minY: bounds.y,
maxX: bounds.x + bounds.width,
maxY: bounds.y + bounds.height,
element: element,
};

this.tree.insert(item);
this.elementMap.set(element, item);
}

update(element, newBounds) {
const oldItem = this.elementMap.get(element);
if (oldItem) {
this.tree.remove(oldItem);
}

this.insert(element, newBounds);
}

search(bounds) {
const results = this.tree.search({
minX: bounds.x,
minY: bounds.y,
maxX: bounds.x + bounds.width,
maxY: bounds.y + bounds.height,
});

return results.map(item => item.element);
}

findNearestNeighbors(point, maxDistance, maxResults = 10) {
const searchBounds = {
minX: point.x - maxDistance,
minY: point.y - maxDistance,
maxX: point.x + maxDistance,
maxY: point.y + maxDistance,
};

const candidates = this.tree.search(searchBounds);

return candidates
.map(item => ({
element: item.element,
distance: Math.sqrt(
Math.pow(point.x - (item.minX + item.maxX) / 2, 2) +
Math.pow(point.y - (item.minY + item.maxY) / 2, 2)
),
}))
.filter(item => item.distance <= maxDistance)
.sort((a, b) => a.distance - b.distance)
.slice(0, maxResults)
.map(item => item.element);
}

clear() {
this.tree.clear();
this.elementMap.clear();
}
}

// Usage for fast mouse interactions
class OptimizedInteractionChart {
constructor(container, data) {
this.container = container;
this.data = data;
this.spatialIndex = new SpatialIndex();
this.setupChart();
this.setupInteractions();
}

setupChart() {
this.svg = this.container.append('svg');

const circles = this.svg
.selectAll('circle')
.data(this.data)
.join('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', 5);

// Index all elements for fast lookup
circles.each((d, i, nodes) => {
const element = nodes[i];
this.spatialIndex.insert(element, {
x: d.x - 5,
y: d.y - 5,
width: 10,
height: 10,
});
});
}

setupInteractions() {
// Use a single event listener with spatial indexing
this.svg.on('mousemove', event => {
const [x, y] = d3.pointer(event);

// Fast spatial query instead of checking all elements
const nearbyElements = this.spatialIndex.search({
x: x - 10,
y: y - 10,
width: 20,
height: 20,
});

// Precise hit testing only on nearby elements
const hitElement = nearbyElements.find(element => {
const rect = element.getBoundingClientRect();
const svgRect = this.svg.node().getBoundingClientRect();
const elementX = rect.left - svgRect.left + rect.width / 2;
const elementY = rect.top - svgRect.top + rect.height / 2;

return (
Math.sqrt(Math.pow(x - elementX, 2) + Math.pow(y - elementY, 2)) <= 5
);
});

if (hitElement) {
this.highlightElement(hitElement);
} else {
this.clearHighlight();
}
});
}

highlightElement(element) {
this.svg.selectAll('circle').classed('highlighted', false);
d3.select(element).classed('highlighted', true);
}

clearHighlight() {
this.svg.selectAll('circle').classed('highlighted', false);
}
}

Adaptive Rendering Based on Performance

// Dynamic quality adjustment based on performance
class AdaptiveQualityRenderer {
constructor(container) {
this.container = container;
this.performanceMetrics = {
frameTime: 16, // Target 60fps
lastFrameTime: 0,
averageFrameTime: 16,
qualityLevel: 1.0,
};

this.qualitySettings = {
1.0: { pointSize: 4, opacity: 1.0, antialiasing: true },
0.8: { pointSize: 3, opacity: 0.9, antialiasing: true },
0.6: { pointSize: 2, opacity: 0.8, antialiasing: false },
0.4: { pointSize: 2, opacity: 0.7, antialiasing: false },
0.2: { pointSize: 1, opacity: 0.6, antialiasing: false },
};

this.setupPerformanceMonitoring();
}

setupPerformanceMonitoring() {
let frameStart = performance.now();

const measureFrame = () => {
const frameEnd = performance.now();
const frameTime = frameEnd - frameStart;

// Exponential moving average
this.performanceMetrics.averageFrameTime =
this.performanceMetrics.averageFrameTime * 0.9 + frameTime * 0.1;

this.adjustQuality();

frameStart = performance.now();
requestAnimationFrame(measureFrame);
};

requestAnimationFrame(measureFrame);
}

adjustQuality() {
const targetFrameTime = 16; // 60fps
const currentFrameTime = this.performanceMetrics.averageFrameTime;

if (currentFrameTime > targetFrameTime * 1.5) {
// Performance is poor, reduce quality
this.performanceMetrics.qualityLevel = Math.max(
0.2,
this.performanceMetrics.qualityLevel - 0.1
);
} else if (currentFrameTime < targetFrameTime * 0.8) {
// Performance is good, increase quality
this.performanceMetrics.qualityLevel = Math.min(
1.0,
this.performanceMetrics.qualityLevel + 0.05
);
}

this.applyQualitySettings();
}

applyQualitySettings() {
const qualityLevel = this.performanceMetrics.qualityLevel;
const settings = this.getQualitySettings(qualityLevel);

this.container
.selectAll('circle')
.attr('r', settings.pointSize)
.style('opacity', settings.opacity)
.style(
'shape-rendering',
settings.antialiasing ? 'auto' : 'optimizeSpeed'
);
}

getQualitySettings(level) {
const levels = Object.keys(this.qualitySettings)
.map(Number)
.sort((a, b) => b - a);

const targetLevel =
levels.find(l => l <= level) || levels[levels.length - 1];
return this.qualitySettings[targetLevel];
}

render(data) {
const startTime = performance.now();

// Adaptive sampling based on quality level
const sampleSize = Math.floor(
data.length * this.performanceMetrics.qualityLevel
);
const sampledData =
this.performanceMetrics.qualityLevel < 1.0
? this.sampleData(data, sampleSize)
: data;

// Render with current quality settings
const settings = this.getQualitySettings(
this.performanceMetrics.qualityLevel
);

this.container
.selectAll('circle')
.data(sampledData, d => d.id)
.join('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', settings.pointSize)
.style('opacity', settings.opacity);

const renderTime = performance.now() - startTime;
this.performanceMetrics.lastFrameTime = renderTime;
}

sampleData(data, targetSize) {
if (data.length <= targetSize) return data;

const step = data.length / targetSize;
const sampled = [];

for (let i = 0; i < data.length; i += step) {
sampled.push(data[Math.floor(i)]);
}

return sampled;
}
}

These performance optimization strategies help create smooth, responsive visualizations that can handle large datasets and complex interactions while maintaining good user experience.