UNPKG

d3-charts-viz-library

Version:

A comprehensive D3-based chart library with customizable charts and graphs

1,932 lines (1,630 loc) 240 kB
'use strict'; var d3 = require('d3'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var d3__namespace = /*#__PURE__*/_interopNamespaceDefault(d3); /** * Base Chart class that provides common functionality for all chart types */ class BaseChart { constructor(container, options = {}) { this.container = container; this.options = { width: 800, height: 400, margin: { top: 20, right: 30, bottom: 40, left: 40 }, backgroundColor: '#ffffff', ...options }; this.data = null; this.svg = null; this.chartGroup = null; this.init(); } /** * Initialize the SVG container and chart group */ init() { // Clear existing content d3__namespace.select(this.container).selectAll('*').remove(); // Create SVG this.svg = d3__namespace.select(this.container) .append('svg') .attr('width', this.options.width) .attr('height', this.options.height) .style('background-color', this.options.backgroundColor); // Create chart group with margins this.chartGroup = this.svg.append('g') .attr('transform', `translate(${this.options.margin.left}, ${this.options.margin.top})`); // Calculate inner dimensions this.innerWidth = this.options.width - this.options.margin.left - this.options.margin.right; this.innerHeight = this.options.height - this.options.margin.top - this.options.margin.bottom; } /** * Set data for the chart */ setData(data) { this.data = data; return this; } /** * Update chart options */ updateOptions(newOptions) { this.options = { ...this.options, ...newOptions }; this.init(); return this; } /** * Get chart dimensions */ getDimensions() { return { width: this.innerWidth, height: this.innerHeight, margin: this.options.margin }; } /** * Add title to the chart */ addTitle(title, options = {}) { const titleOptions = { fontSize: '16px', fontWeight: 'bold', textAnchor: 'middle', fill: '#333', ...options }; this.svg.append('text') .attr('x', this.options.width / 2) .attr('y', titleOptions.fontSize === '16px' ? 16 : parseInt(titleOptions.fontSize)) .style('font-size', titleOptions.fontSize) .style('font-weight', titleOptions.fontWeight) .style('text-anchor', titleOptions.textAnchor) .style('fill', titleOptions.fill) .text(title); return this; } /** * Add legend to the chart */ addLegend(items, options = {}) { const legendOptions = { x: this.options.width - 100, y: 30, itemHeight: 20, fontSize: '12px', ...options }; const legend = this.svg.append('g') .attr('class', 'legend') .attr('transform', `translate(${legendOptions.x}, ${legendOptions.y})`); const legendItems = legend.selectAll('.legend-item') .data(items) .enter() .append('g') .attr('class', 'legend-item') .attr('transform', (d, i) => `translate(0, ${i * legendOptions.itemHeight})`); legendItems.append('rect') .attr('width', 12) .attr('height', 12) .attr('fill', d => d.color); legendItems.append('text') .attr('x', 16) .attr('y', 9) .style('font-size', legendOptions.fontSize) .style('alignment-baseline', 'middle') .text(d => d.label); return this; } /** * Add tooltip functionality */ addTooltip() { this.tooltip = d3__namespace.select('body') .append('div') .attr('class', 'd3-tooltip') .style('position', 'absolute') .style('visibility', 'hidden') .style('background-color', 'rgba(0, 0, 0, 0.8)') .style('color', 'white') .style('padding', '8px') .style('border-radius', '4px') .style('font-size', '12px') .style('pointer-events', 'none') .style('z-index', '1000'); return this; } /** * Show tooltip */ showTooltip(content, event) { if (this.tooltip) { this.tooltip .style('visibility', 'visible') .html(content) .style('left', (event.pageX + 10) + 'px') .style('top', (event.pageY - 10) + 'px'); } } /** * Hide tooltip */ hideTooltip() { if (this.tooltip) { this.tooltip.style('visibility', 'hidden'); } } /** * Render method to be implemented by subclasses */ render() { throw new Error('render() method must be implemented by subclasses'); } /** * Destroy the chart and clean up */ destroy() { if (this.tooltip) { this.tooltip.remove(); } d3__namespace.select(this.container).selectAll('*').remove(); } } /** * Bar Chart implementation */ class BarChart extends BaseChart { constructor(container, options = {}) { const defaultOptions = { barPadding: 0.1, barColor: '#3498db', hoverColor: '#2980b9', showValues: false, orientation: 'vertical', // 'vertical' or 'horizontal' ...options }; super(container, defaultOptions); this.addTooltip(); } /** * Render the bar chart */ render() { if (!this.data || this.data.length === 0) { console.warn('No data provided for BarChart'); return this; } // Clear previous chart this.chartGroup.selectAll('*').remove(); if (this.options.orientation === 'vertical') { this.renderVerticalBars(); } else { this.renderHorizontalBars(); } return this; } /** * Render vertical bars */ renderVerticalBars() { // Create scales const xScale = d3__namespace.scaleBand() .domain(this.data.map(d => d.label)) .range([0, this.innerWidth]) .padding(this.options.barPadding); const yScale = d3__namespace.scaleLinear() .domain([0, d3__namespace.max(this.data, d => d.value)]) .range([this.innerHeight, 0]); // Create axes const xAxis = d3__namespace.axisBottom(xScale); const yAxis = d3__namespace.axisLeft(yScale); // Add X axis this.chartGroup.append('g') .attr('class', 'x-axis') .attr('transform', `translate(0, ${this.innerHeight})`) .call(xAxis); // Add Y axis this.chartGroup.append('g') .attr('class', 'y-axis') .call(yAxis); // Create bars const bars = this.chartGroup.selectAll('.bar') .data(this.data) .enter() .append('rect') .attr('class', 'bar') .attr('x', d => xScale(d.label)) .attr('width', xScale.bandwidth()) .attr('y', this.innerHeight) .attr('height', 0) .attr('fill', this.options.barColor) .style('cursor', 'pointer'); // Animate bars bars.transition() .duration(800) .attr('y', d => yScale(d.value)) .attr('height', d => this.innerHeight - yScale(d.value)); // Add interactivity this.addBarInteractivity(bars); // Add value labels if requested if (this.options.showValues) { this.addValueLabels(xScale, yScale); } } /** * Render horizontal bars */ renderHorizontalBars() { // Create scales const yScale = d3__namespace.scaleBand() .domain(this.data.map(d => d.label)) .range([0, this.innerHeight]) .padding(this.options.barPadding); const xScale = d3__namespace.scaleLinear() .domain([0, d3__namespace.max(this.data, d => d.value)]) .range([0, this.innerWidth]); // Create axes const xAxis = d3__namespace.axisBottom(xScale); const yAxis = d3__namespace.axisLeft(yScale); // Add X axis this.chartGroup.append('g') .attr('class', 'x-axis') .attr('transform', `translate(0, ${this.innerHeight})`) .call(xAxis); // Add Y axis this.chartGroup.append('g') .attr('class', 'y-axis') .call(yAxis); // Create bars const bars = this.chartGroup.selectAll('.bar') .data(this.data) .enter() .append('rect') .attr('class', 'bar') .attr('y', d => yScale(d.label)) .attr('height', yScale.bandwidth()) .attr('x', 0) .attr('width', 0) .attr('fill', this.options.barColor) .style('cursor', 'pointer'); // Animate bars bars.transition() .duration(800) .attr('width', d => xScale(d.value)); // Add interactivity this.addBarInteractivity(bars); } /** * Add interactivity to bars */ addBarInteractivity(bars) { const self = this; bars .on('mouseover', function(event, d) { d3__namespace.select(this).attr('fill', self.options.hoverColor); self.showTooltip(`${d.label}: ${d.value}`, event); }) .on('mouseout', function() { d3__namespace.select(this).attr('fill', self.options.barColor); self.hideTooltip(); }) .on('click', function(event, d) { if (self.options.onClick) { self.options.onClick(d, event); } }); } /** * Add value labels on bars */ addValueLabels(xScale, yScale) { if (this.options.orientation === 'vertical') { this.chartGroup.selectAll('.value-label') .data(this.data) .enter() .append('text') .attr('class', 'value-label') .attr('x', d => xScale(d.label) + xScale.bandwidth() / 2) .attr('y', d => yScale(d.value) - 5) .attr('text-anchor', 'middle') .style('font-size', '12px') .style('fill', '#333') .text(d => d.value); } else { this.chartGroup.selectAll('.value-label') .data(this.data) .enter() .append('text') .attr('class', 'value-label') .attr('x', d => xScale(d.value) + 5) .attr('y', d => yScale(d.label) + yScale.bandwidth() / 2) .attr('text-anchor', 'start') .attr('alignment-baseline', 'middle') .style('font-size', '12px') .style('fill', '#333') .text(d => d.value); } } /** * Update chart with new data */ updateData(newData) { this.setData(newData); this.render(); return this; } } /** * Line Chart implementation */ class LineChart extends BaseChart { constructor(container, options = {}) { const defaultOptions = { lineColor: '#3498db', lineWidth: 2, pointRadius: 4, pointColor: '#3498db', pointHoverRadius: 6, showPoints: true, showArea: false, areaColor: 'rgba(52, 152, 219, 0.3)', curve: d3__namespace.curveLinear, ...options }; super(container, defaultOptions); this.addTooltip(); } /** * Render the line chart */ render() { if (!this.data || this.data.length === 0) { console.warn('No data provided for LineChart'); return this; } // Clear previous chart this.chartGroup.selectAll('*').remove(); // Create scales const xScale = d3__namespace.scaleLinear() .domain(d3__namespace.extent(this.data, d => d.x)) .range([0, this.innerWidth]); const yScale = d3__namespace.scaleLinear() .domain(d3__namespace.extent(this.data, d => d.y)) .range([this.innerHeight, 0]); // Create line generator const line = d3__namespace.line() .x(d => xScale(d.x)) .y(d => yScale(d.y)) .curve(this.options.curve); // Create area generator (if needed) let area; if (this.options.showArea) { area = d3__namespace.area() .x(d => xScale(d.x)) .y0(this.innerHeight) .y1(d => yScale(d.y)) .curve(this.options.curve); } // Create axes const xAxis = d3__namespace.axisBottom(xScale); const yAxis = d3__namespace.axisLeft(yScale); // Add X axis this.chartGroup.append('g') .attr('class', 'x-axis') .attr('transform', `translate(0, ${this.innerHeight})`) .call(xAxis); // Add Y axis this.chartGroup.append('g') .attr('class', 'y-axis') .call(yAxis); // Add area if requested if (this.options.showArea && area) { this.chartGroup.append('path') .datum(this.data) .attr('class', 'area') .attr('fill', this.options.areaColor) .attr('d', area); } // Add line const path = this.chartGroup.append('path') .datum(this.data) .attr('class', 'line') .attr('fill', 'none') .attr('stroke', this.options.lineColor) .attr('stroke-width', this.options.lineWidth) .attr('d', line); // Animate line drawing const totalLength = path.node().getTotalLength(); path .attr('stroke-dasharray', totalLength + ' ' + totalLength) .attr('stroke-dashoffset', totalLength) .transition() .duration(1000) .attr('stroke-dashoffset', 0); // Add points if requested if (this.options.showPoints) { this.addPoints(xScale, yScale); } return this; } /** * Add interactive points to the line */ addPoints(xScale, yScale) { const self = this; const points = this.chartGroup.selectAll('.point') .data(this.data) .enter() .append('circle') .attr('class', 'point') .attr('cx', d => xScale(d.x)) .attr('cy', d => yScale(d.y)) .attr('r', 0) .attr('fill', this.options.pointColor) .style('cursor', 'pointer'); // Animate points points.transition() .delay((d, i) => i * 50) .duration(300) .attr('r', this.options.pointRadius); // Add interactivity points .on('mouseover', function(event, d) { d3__namespace.select(this) .transition() .duration(150) .attr('r', self.options.pointHoverRadius); self.showTooltip(`(${d.x}, ${d.y})`, event); }) .on('mouseout', function() { d3__namespace.select(this) .transition() .duration(150) .attr('r', self.options.pointRadius); self.hideTooltip(); }) .on('click', function(event, d) { if (self.options.onClick) { self.options.onClick(d, event); } }); } /** * Add multiple lines for multi-series data */ renderMultiSeries(seriesData) { if (!seriesData || seriesData.length === 0) { console.warn('No series data provided for LineChart'); return this; } // Clear previous chart this.chartGroup.selectAll('*').remove(); // Get all data points for scaling const allData = seriesData.flatMap(series => series.data); // Create scales const xScale = d3__namespace.scaleLinear() .domain(d3__namespace.extent(allData, d => d.x)) .range([0, this.innerWidth]); const yScale = d3__namespace.scaleLinear() .domain(d3__namespace.extent(allData, d => d.y)) .range([this.innerHeight, 0]); // Color scale for different series const colorScale = d3__namespace.scaleOrdinal(d3__namespace.schemeCategory10); // Create line generator const line = d3__namespace.line() .x(d => xScale(d.x)) .y(d => yScale(d.y)) .curve(this.options.curve); // Create axes const xAxis = d3__namespace.axisBottom(xScale); const yAxis = d3__namespace.axisLeft(yScale); // Add axes this.chartGroup.append('g') .attr('class', 'x-axis') .attr('transform', `translate(0, ${this.innerHeight})`) .call(xAxis); this.chartGroup.append('g') .attr('class', 'y-axis') .call(yAxis); // Add lines for each series seriesData.forEach((series, index) => { const color = series.color || colorScale(index); // Add line const path = this.chartGroup.append('path') .datum(series.data) .attr('class', `line series-${index}`) .attr('fill', 'none') .attr('stroke', color) .attr('stroke-width', this.options.lineWidth) .attr('d', line); // Animate line drawing const totalLength = path.node().getTotalLength(); path .attr('stroke-dasharray', totalLength + ' ' + totalLength) .attr('stroke-dashoffset', totalLength) .transition() .delay(index * 200) .duration(1000) .attr('stroke-dashoffset', 0); // Add points if requested if (this.options.showPoints) { this.addSeriesPoints(series.data, xScale, yScale, color, series.name); } }); // Add legend const legendItems = seriesData.map((series, index) => ({ label: series.name || `Series ${index + 1}`, color: series.color || colorScale(index) })); this.addLegend(legendItems); return this; } /** * Add points for a specific series */ addSeriesPoints(data, xScale, yScale, color, seriesName) { const self = this; const points = this.chartGroup.selectAll(`.point-${seriesName}`) .data(data) .enter() .append('circle') .attr('class', `point point-${seriesName}`) .attr('cx', d => xScale(d.x)) .attr('cy', d => yScale(d.y)) .attr('r', 0) .attr('fill', color) .style('cursor', 'pointer'); // Animate points points.transition() .delay((d, i) => i * 30) .duration(300) .attr('r', this.options.pointRadius); // Add interactivity points .on('mouseover', function(event, d) { d3__namespace.select(this) .transition() .duration(150) .attr('r', self.options.pointHoverRadius); self.showTooltip(`${seriesName}: (${d.x}, ${d.y})`, event); }) .on('mouseout', function() { d3__namespace.select(this) .transition() .duration(150) .attr('r', self.options.pointRadius); self.hideTooltip(); }); } /** * Update chart with new data */ updateData(newData) { this.setData(newData); this.render(); return this; } } /** * Pie Chart implementation */ class PieChart extends BaseChart { constructor(container, options = {}) { const defaultOptions = { innerRadius: 0, outerRadius: null, // Will be calculated based on chart size padAngle: 0.02, cornerRadius: 0, colors: d3__namespace.schemeCategory10, showLabels: true, labelOffset: 20, showPercentages: true, ...options }; super(container, defaultOptions); this.addTooltip(); } /** * Render the pie chart */ render() { if (!this.data || this.data.length === 0) { console.warn('No data provided for PieChart'); return this; } // Clear previous chart this.chartGroup.selectAll('*').remove(); // Calculate radius if not provided const radius = this.options.outerRadius || Math.min(this.innerWidth, this.innerHeight) / 2 - 10; // Center the chart const centerX = this.innerWidth / 2; const centerY = this.innerHeight / 2; const chartCenter = this.chartGroup.append('g') .attr('transform', `translate(${centerX}, ${centerY})`); // Create pie layout const pie = d3__namespace.pie() .value(d => d.value) .sort(null) .padAngle(this.options.padAngle); // Create arc generator const arc = d3__namespace.arc() .innerRadius(this.options.innerRadius) .outerRadius(radius) .cornerRadius(this.options.cornerRadius); // Create arc generator for labels const labelArc = d3__namespace.arc() .innerRadius(radius + this.options.labelOffset) .outerRadius(radius + this.options.labelOffset); // Color scale const colorScale = d3__namespace.scaleOrdinal(this.options.colors); // Create pie slices const slices = chartCenter.selectAll('.slice') .data(pie(this.data)) .enter() .append('g') .attr('class', 'slice'); // Add paths const paths = slices.append('path') .attr('fill', (d, i) => colorScale(i)) .attr('stroke', '#fff') .attr('stroke-width', 2) .style('cursor', 'pointer') .each(function(d) { this._current = { startAngle: 0, endAngle: 0 }; }); // Animate slices paths.transition() .duration(1000) .attrTween('d', function(d) { const interpolate = d3__namespace.interpolate(this._current, d); this._current = interpolate(0); return function(t) { return arc(interpolate(t)); }; }); // Add interactivity this.addSliceInteractivity(paths, arc); // Add labels if requested if (this.options.showLabels) { this.addLabels(slices, pie(this.data), labelArc); } return this; } /** * Add interactivity to pie slices */ addSliceInteractivity(paths, arc) { const self = this; const hoverArc = d3__namespace.arc() .innerRadius(this.options.innerRadius) .outerRadius(arc.outerRadius()() + 10) .cornerRadius(this.options.cornerRadius); paths .on('mouseover', function(event, d) { // Expand slice d3__namespace.select(this) .transition() .duration(200) .attr('d', hoverArc); // Show tooltip const percentage = ((d.endAngle - d.startAngle) / (2 * Math.PI) * 100).toFixed(1); const tooltipContent = `${d.data.label}: ${d.data.value} (${percentage}%)`; self.showTooltip(tooltipContent, event); }) .on('mouseout', function(event, d) { // Return to normal size d3__namespace.select(this) .transition() .duration(200) .attr('d', arc); self.hideTooltip(); }) .on('click', function(event, d) { if (self.options.onClick) { self.options.onClick(d.data, event); } }); } /** * Add labels to pie slices */ addLabels(slices, pieData, labelArc) { const labels = slices.append('text') .attr('transform', d => `translate(${labelArc.centroid(d)})`) .attr('text-anchor', 'middle') .attr('alignment-baseline', 'middle') .style('font-size', '12px') .style('fill', '#333') .style('opacity', 0); // Add label text labels.text(d => { if (this.options.showPercentages) { const percentage = ((d.endAngle - d.startAngle) / (2 * Math.PI) * 100).toFixed(1); return `${d.data.label} (${percentage}%)`; } return d.data.label; }); // Animate labels labels.transition() .delay(500) .duration(500) .style('opacity', 1); // Add lines connecting labels to slices this.addLabelLines(slices, pieData, labelArc); } /** * Add lines connecting labels to slices */ addLabelLines(slices, pieData, labelArc) { const arc = d3__namespace.arc() .innerRadius(this.options.innerRadius) .outerRadius(this.options.outerRadius || Math.min(this.innerWidth, this.innerHeight) / 2 - 10); const lines = slices.append('polyline') .attr('fill', 'none') .attr('stroke', '#999') .attr('stroke-width', 1) .style('opacity', 0); lines.attr('points', d => { const pos = labelArc.centroid(d); const midPos = arc.centroid(d); return [midPos, pos]; }); // Animate lines lines.transition() .delay(500) .duration(500) .style('opacity', 0.7); } /** * Create a donut chart (pie chart with inner radius) */ createDonut(innerRadiusRatio = 0.5) { const radius = this.options.outerRadius || Math.min(this.innerWidth, this.innerHeight) / 2 - 10; this.options.innerRadius = radius * innerRadiusRatio; return this.render(); } /** * Update chart with new data */ updateData(newData) { this.setData(newData); this.render(); return this; } /** * Animate slice explosion */ explodeSlice(index, distance = 20) { const slice = this.chartGroup.select(`.slice:nth-child(${index + 1})`); const pieData = d3__namespace.pie().value(d => d.value)(this.data); const centroid = d3__namespace.arc() .innerRadius(this.options.innerRadius) .outerRadius(this.options.outerRadius || Math.min(this.innerWidth, this.innerHeight) / 2 - 10) .centroid(pieData[index]); const x = centroid[0] * distance / 100; const y = centroid[1] * distance / 100; slice.transition() .duration(300) .attr('transform', `translate(${x}, ${y})`); return this; } /** * Reset all slice positions */ resetSlices() { this.chartGroup.selectAll('.slice') .transition() .duration(300) .attr('transform', 'translate(0, 0)'); return this; } } /** * Scatter Plot implementation */ class ScatterPlot extends BaseChart { constructor(container, options = {}) { const defaultOptions = { pointRadius: 4, pointColor: '#3498db', pointOpacity: 0.7, hoverRadius: 6, hoverOpacity: 1, showTrendLine: false, trendLineColor: '#e74c3c', trendLineWidth: 2, ...options }; super(container, defaultOptions); this.addTooltip(); } /** * Render the scatter plot */ render() { if (!this.data || this.data.length === 0) { console.warn('No data provided for ScatterPlot'); return this; } // Clear previous chart this.chartGroup.selectAll('*').remove(); // Create scales const xScale = d3__namespace.scaleLinear() .domain(d3__namespace.extent(this.data, d => d.x)) .range([0, this.innerWidth]) .nice(); const yScale = d3__namespace.scaleLinear() .domain(d3__namespace.extent(this.data, d => d.y)) .range([this.innerHeight, 0]) .nice(); // Create axes const xAxis = d3__namespace.axisBottom(xScale); const yAxis = d3__namespace.axisLeft(yScale); // Add X axis this.chartGroup.append('g') .attr('class', 'x-axis') .attr('transform', `translate(0, ${this.innerHeight})`) .call(xAxis); // Add Y axis this.chartGroup.append('g') .attr('class', 'y-axis') .call(yAxis); // Add axis labels this.addAxisLabels(); // Add trend line if requested if (this.options.showTrendLine) { this.addTrendLine(xScale, yScale); } // Create points this.addPoints(xScale, yScale); return this; } /** * Add scatter plot points */ addPoints(xScale, yScale) { const self = this; const points = this.chartGroup.selectAll('.point') .data(this.data) .enter() .append('circle') .attr('class', 'point') .attr('cx', d => xScale(d.x)) .attr('cy', d => yScale(d.y)) .attr('r', 0) .attr('fill', d => d.color || this.options.pointColor) .attr('opacity', this.options.pointOpacity) .style('cursor', 'pointer'); // Animate points points.transition() .delay((d, i) => i * 20) .duration(500) .attr('r', d => d.radius || this.options.pointRadius); // Add interactivity points .on('mouseover', function(event, d) { d3__namespace.select(this) .transition() .duration(150) .attr('r', self.options.hoverRadius) .attr('opacity', self.options.hoverOpacity); const tooltipContent = self.formatTooltip(d); self.showTooltip(tooltipContent, event); }) .on('mouseout', function(event, d) { d3__namespace.select(this) .transition() .duration(150) .attr('r', d.radius || self.options.pointRadius) .attr('opacity', self.options.pointOpacity); self.hideTooltip(); }) .on('click', function(event, d) { if (self.options.onClick) { self.options.onClick(d, event); } }); } /** * Format tooltip content */ formatTooltip(d) { let content = `X: ${d.x}<br>Y: ${d.y}`; if (d.label) { content = `${d.label}<br>${content}`; } if (d.category) { content += `<br>Category: ${d.category}`; } return content; } /** * Add trend line using linear regression */ addTrendLine(xScale, yScale) { const regression = this.calculateLinearRegression(); if (!regression) return; const xDomain = xScale.domain(); const trendData = [ { x: xDomain[0], y: regression.slope * xDomain[0] + regression.intercept }, { x: xDomain[1], y: regression.slope * xDomain[1] + regression.intercept } ]; const line = d3__namespace.line() .x(d => xScale(d.x)) .y(d => yScale(d.y)); this.chartGroup.append('path') .datum(trendData) .attr('class', 'trend-line') .attr('fill', 'none') .attr('stroke', this.options.trendLineColor) .attr('stroke-width', this.options.trendLineWidth) .attr('stroke-dasharray', '5,5') .attr('d', line); // Add R² value this.chartGroup.append('text') .attr('x', this.innerWidth - 10) .attr('y', 20) .attr('text-anchor', 'end') .style('font-size', '12px') .style('fill', this.options.trendLineColor) .text(`R² = ${regression.rSquared.toFixed(3)}`); } /** * Calculate linear regression */ calculateLinearRegression() { if (this.data.length < 2) return null; const n = this.data.length; const sumX = d3__namespace.sum(this.data, d => d.x); const sumY = d3__namespace.sum(this.data, d => d.y); const sumXY = d3__namespace.sum(this.data, d => d.x * d.y); const sumXX = d3__namespace.sum(this.data, d => d.x * d.x); d3__namespace.sum(this.data, d => d.y * d.y); const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX); const intercept = (sumY - slope * sumX) / n; // Calculate R² const yMean = sumY / n; const ssRes = d3__namespace.sum(this.data, d => Math.pow(d.y - (slope * d.x + intercept), 2)); const ssTot = d3__namespace.sum(this.data, d => Math.pow(d.y - yMean, 2)); const rSquared = 1 - (ssRes / ssTot); return { slope, intercept, rSquared }; } /** * Add axis labels */ addAxisLabels() { // X axis label if (this.options.xLabel) { this.chartGroup.append('text') .attr('x', this.innerWidth / 2) .attr('y', this.innerHeight + 35) .attr('text-anchor', 'middle') .style('font-size', '14px') .style('fill', '#333') .text(this.options.xLabel); } // Y axis label if (this.options.yLabel) { this.chartGroup.append('text') .attr('transform', 'rotate(-90)') .attr('x', -this.innerHeight / 2) .attr('y', -35) .attr('text-anchor', 'middle') .style('font-size', '14px') .style('fill', '#333') .text(this.options.yLabel); } } /** * Render scatter plot with categories (different colors) */ renderWithCategories(data, categoryField = 'category') { this.setData(data); // Get unique categories const categories = [...new Set(data.map(d => d[categoryField]))]; const colorScale = d3__namespace.scaleOrdinal(d3__namespace.schemeCategory10) .domain(categories); // Assign colors based on category this.data.forEach(d => { d.color = colorScale(d[categoryField]); }); this.render(); // Add legend const legendItems = categories.map(category => ({ label: category, color: colorScale(category) })); this.addLegend(legendItems); return this; } /** * Render bubble chart (scatter plot with varying point sizes) */ renderBubbleChart(sizeField = 'size') { if (!this.data || this.data.length === 0) { console.warn('No data provided for BubbleChart'); return this; } // Create size scale const sizeExtent = d3__namespace.extent(this.data, d => d[sizeField]); const sizeScale = d3__namespace.scaleSqrt() .domain(sizeExtent) .range([3, 20]); // Assign radius based on size field this.data.forEach(d => { d.radius = sizeScale(d[sizeField]); }); this.render(); return this; } /** * Update chart with new data */ updateData(newData) { this.setData(newData); this.render(); return this; } /** * Highlight points based on condition */ highlightPoints(condition, highlightColor = '#e74c3c') { this.chartGroup.selectAll('.point') .attr('fill', d => condition(d) ? highlightColor : (d.color || this.options.pointColor)); return this; } /** * Reset point colors */ resetHighlight() { this.chartGroup.selectAll('.point') .attr('fill', d => d.color || this.options.pointColor); return this; } } /** * Area Chart implementation */ class AreaChart extends BaseChart { constructor(container, options = {}) { const defaultOptions = { areaColor: 'rgba(52, 152, 219, 0.6)', lineColor: '#3498db', lineWidth: 2, curve: d3__namespace.curveLinear, showLine: true, showPoints: false, pointRadius: 3, ...options }; super(container, defaultOptions); this.addTooltip(); } /** * Render the area chart */ render() { if (!this.data || this.data.length === 0) { console.warn('No data provided for AreaChart'); return this; } // Clear previous chart this.chartGroup.selectAll('*').remove(); // Create scales const xScale = d3__namespace.scaleLinear() .domain(d3__namespace.extent(this.data, d => d.x)) .range([0, this.innerWidth]); const yScale = d3__namespace.scaleLinear() .domain([0, d3__namespace.max(this.data, d => d.y)]) .range([this.innerHeight, 0]); // Create area generator const area = d3__namespace.area() .x(d => xScale(d.x)) .y0(this.innerHeight) .y1(d => yScale(d.y)) .curve(this.options.curve); // Create line generator const line = d3__namespace.line() .x(d => xScale(d.x)) .y(d => yScale(d.y)) .curve(this.options.curve); // Create axes const xAxis = d3__namespace.axisBottom(xScale); const yAxis = d3__namespace.axisLeft(yScale); // Add X axis this.chartGroup.append('g') .attr('class', 'x-axis') .attr('transform', `translate(0, ${this.innerHeight})`) .call(xAxis); // Add Y axis this.chartGroup.append('g') .attr('class', 'y-axis') .call(yAxis); // Add area const areaPath = this.chartGroup.append('path') .datum(this.data) .attr('class', 'area') .attr('fill', this.options.areaColor) .attr('d', area); // Animate area this.animateArea(areaPath); // Add line if requested if (this.options.showLine) { const linePath = this.chartGroup.append('path') .datum(this.data) .attr('class', 'line') .attr('fill', 'none') .attr('stroke', this.options.lineColor) .attr('stroke-width', this.options.lineWidth) .attr('d', line); // Animate line this.animateLine(linePath); } // Add points if requested if (this.options.showPoints) { this.addPoints(xScale, yScale); } // Add interaction overlay this.addInteractionOverlay(xScale, yScale); return this; } /** * Animate area drawing */ animateArea(areaPath) { const totalLength = areaPath.node().getTotalLength(); areaPath .attr('stroke-dasharray', totalLength + ' ' + totalLength) .attr('stroke-dashoffset', totalLength) .attr('stroke', this.options.areaColor) .attr('stroke-width', 1) .transition() .duration(1500) .attr('stroke-dashoffset', 0) .on('end', function() { d3__namespace.select(this).attr('stroke', 'none'); }); } /** * Animate line drawing */ animateLine(linePath) { const totalLength = linePath.node().getTotalLength(); linePath .attr('stroke-dasharray', totalLength + ' ' + totalLength) .attr('stroke-dashoffset', totalLength) .transition() .duration(1500) .attr('stroke-dashoffset', 0); } /** * Add interactive points */ addPoints(xScale, yScale) { const self = this; const points = this.chartGroup.selectAll('.point') .data(this.data) .enter() .append('circle') .attr('class', 'point') .attr('cx', d => xScale(d.x)) .attr('cy', d => yScale(d.y)) .attr('r', 0) .attr('fill', this.options.lineColor) .style('cursor', 'pointer'); // Animate points points.transition() .delay((d, i) => i * 50) .duration(300) .attr('r', this.options.pointRadius); // Add interactivity points .on('mouseover', function(event, d) { d3__namespace.select(this) .transition() .duration(150) .attr('r', self.options.pointRadius * 1.5); self.showTooltip(`(${d.x}, ${d.y})`, event); }) .on('mouseout', function() { d3__namespace.select(this) .transition() .duration(150) .attr('r', self.options.pointRadius); self.hideTooltip(); }); } /** * Add interaction overlay for hover effects */ addInteractionOverlay(xScale, yScale) { const self = this; // Create bisector for finding closest data point const bisect = d3__namespace.bisector(d => d.x).left; // Add invisible overlay for mouse tracking const overlay = this.chartGroup.append('rect') .attr('class', 'overlay') .attr('width', this.innerWidth) .attr('height', this.innerHeight) .attr('fill', 'none') .attr('pointer-events', 'all') .style('cursor', 'crosshair'); // Add focus elements const focus = this.chartGroup.append('g') .attr('class', 'focus') .style('display', 'none'); focus.append('circle') .attr('r', 4) .attr('fill', this.options.lineColor) .attr('stroke', '#fff') .attr('stroke-width', 2); focus.append('line') .attr('class', 'x-hover-line') .attr('stroke', '#999') .attr('stroke-width', 1) .attr('stroke-dasharray', '3,3'); focus.append('line') .attr('class', 'y-hover-line') .attr('stroke', '#999') .attr('stroke-width', 1) .attr('stroke-dasharray', '3,3'); // Mouse events overlay .on('mouseover', () => focus.style('display', null)) .on('mouseout', () => { focus.style('display', 'none'); self.hideTooltip(); }) .on('mousemove', function(event) { const [mouseX] = d3__namespace.pointer(event, this); const x0 = xScale.invert(mouseX); const i = bisect(self.data, x0, 1); if (i >= self.data.length) return; const d0 = self.data[i - 1]; const d1 = self.data[i]; const d = x0 - d0.x > d1.x - x0 ? d1 : d0; focus.attr('transform', `translate(${xScale(d.x)}, ${yScale(d.y)})`); focus.select('.x-hover-line') .attr('y1', -yScale(d.y)) .attr('y2', self.innerHeight - yScale(d.y)); focus.select('.y-hover-line') .attr('x1', -xScale(d.x)) .attr('x2', self.innerWidth - xScale(d.x)); self.showTooltip(`(${d.x}, ${d.y})`, event); }); } /** * Render stacked area chart */ renderStacked(seriesData) { if (!seriesData || seriesData.length === 0) { console.warn('No series data provided for stacked AreaChart'); return this; } // Clear previous chart this.chartGroup.selectAll('*').remove(); // Prepare data for stacking const keys = seriesData.map(d => d.name); const stackData = this.prepareStackData(seriesData); // Create stack generator const stack = d3__namespace.stack() .keys(keys) .order(d3__namespace.stackOrderNone) .offset(d3__namespace.stackOffsetNone); const stackedData = stack(stackData); // Create scales const xScale = d3__namespace.scaleLinear() .domain(d3__namespace.extent(stackData, d => d.x)) .range([0, this.innerWidth]); const yScale = d3__namespace.scaleLinear() .domain([0, d3__namespace.max(stackedData, d => d3__namespace.max(d, d => d[1]))]) .range([this.innerHeight, 0]); // Color scale const colorScale = d3__namespace.scaleOrdinal(d3__namespace.schemeCategory10); // Create area generator const area = d3__namespace.area() .x(d => xScale(d.data.x)) .y0(d => yScale(d[0])) .y1(d => yScale(d[1])) .curve(this.options.curve); // Create axes const xAxis = d3__namespace.axisBottom(xScale); const yAxis = d3__namespace.axisLeft(yScale); // Add axes this.chartGroup.append('g') .attr('class', 'x-axis') .attr('transform', `translate(0, ${this.innerHeight})`) .call(xAxis); this.chartGroup.append('g') .attr('class', 'y-axis') .call(yAxis); // Add areas this.chartGroup.selectAll('.area') .data(stackedData) .enter() .append('path') .attr('class', 'area') .attr('fill', (d, i) => colorScale(i)) .attr('d', area) .style('opacity', 0.8); // Add legend const legendItems = keys.map((key, i) => ({ label: key, color: colorScale(i) })); this.addLegend(legendItems); return this; } /** * Prepare data for stacking */ prepareStackData(seriesData) { // Get all unique x values const allXValues = [...new Set( seriesData.flatMap(series => series.data.map(d => d.x)) )].sort((a, b) => a - b); // Create combined data structure return allXValues.map(x => { const dataPoint = { x }; seriesData.forEach(series => { const point = series.data.find(d => d.x === x); dataPoint[series.name] = point ? point.y : 0; }); return dataPoint; }); } /** * Update chart with new data */ updateData(newData) { this.setData(newData); this.render(); return this; } } /** * Donut Chart implementation (extends PieChart) */ class DonutChart extends PieChart { constructor(container, options = {}) { const defaultOptions = { innerRadius: 0.5, // Ratio of outer radius showCenterText: true, centerText: '', centerTextSize: '24px', centerTextColor: '#333', ...options }; super(container, defaultOptions); } /** * Render the donut chart */ render() { if (!this.data || this.data.length === 0) { console.warn('No data provided for DonutChart'); return this; } // Calculate radius if not provided const outerRadius = this.options.outerRadius || Math.min(this.innerWidth, this.innerHeight) / 2 - 10; // Set inner radius based on ratio if (typeof this.options.innerRadius === 'number' && this.options.innerRadius < 1) { this.options.innerRadius = outerRadius * this.options.innerRadius; } // Call parent render method super.render(); // Add center text if requested if (this.options.showCenterText) { this.addCenterText(); } return this; } /** * Add text in the center of the donut */ addCenterText() { this.innerWidth / 2; this.innerHeight / 2; const centerGroup = this.chartGroup.select('g') .append('g') .attr('class', 'center-text'); // Main center text const centerText = this.options.centerText || this.calculateTotal(); centerGroup.append('text') .attr('class', 'center-main-text') .attr('text-anchor', 'middle') .attr('alignment-baseline', 'middle') .style('font-size', this.options.centerTextSize) .style('font-weight', 'bold') .style('fill', this.options.centerTextColor) .text(centerText); // Optional subtitle if (this.options.centerSubtext) { centerGroup.append('text') .attr('class', 'center-sub-text') .attr('text-anchor', 'middle') .attr('alignment-baseline', 'middle') .attr('dy', '1.5em') .style('font-size', '14px') .style('fill', '#666') .text(this.options.centerSubtext); } } /** * Calculate total value for center display */ calculateTotal() { return d3__namespace.sum(this.data, d => d.value); } /** * Update center text */ updateCenterText(text, subtext = null) { const centerGroup = this.chartGroup.select('.center-text'); if (!centerGroup.empty()) { centerGroup.select('.center-main-text').text(text); if (subtext !== null) { let subtextElement = centerGroup.select('.center-sub-text'); if (subtextElement.empty()) { subtextElement = centerGroup.append('text') .attr('class', 'center-sub-text') .attr('text-anchor', 'middle') .attr('alignment-baseline', 'middle') .attr('dy', '1.5em') .style('font-size', '14px') .style('fill', '#666'); } subtextElement.text(subtext); } } return this; } /** * Create a progress donut (single value with remaining) */ renderProgress(value, total, options = {}) { const progressOptions = { progressColor: '#3498db', remainingColor: '#ecf0f1', showPercentage: true, ...options }; const percentage = (value / total) * 100; const remaining = total - value; const progressData = [ { label: 'Progress', value: value, color: progressOptions.progressColor }, { label: 'Remaining', value: remaining, color: progressOptions.remainingColor } ]; this.setData(progressData); this.options.colors = [progressOptions.progressColor, progressOptions.remainingColor]; this.options.showLabels = false; this.render(); // Update center text with percentage if (progressOptions.showPercentage) { this.updateCenterText(`${percentage.toFixed(1)}%`, 'Complete'); } return this; } /** * Create animated progress donut */ animateProgress(targetValue, total, duration = 2000, options = {}) { const progressOptions = { progressColor: '#3498db', remainingColor: '#ecf0f1', ...options }; // Start with 0 progress this.renderProgress(0, total, progressOptions); // Animate to target value const self = this; const startTime = Date.now(); function animate() { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); const currentValue = targetValue * progress; self.renderProgress(currentValue, total, progressOptions); if (progress < 1) { requestAnimationFrame(animate); } } requestAnimationFrame(animate); return this; } /** * Add interactive hover effects for donut segments */ addDonutInteractivity() { const self = this; const paths = this.chartGroup.selectAll('.slice path'); paths .on('mouseover', function(event, d) { // Highlight segment d3__namespace.select(this) .transition() .duration(200) .style('opacity', 0.8); // Update center text with segment info const percentage = ((d.endAngle - d.startAngle) / (2 * Math.PI) * 100).toFixed(1); self.updateCenterText(d.data.value, `${d.data.label} (${percentage}%)`); // Show tooltip const tooltipContent = `${d.data.label}: ${d.data.value} (${percentage}%)`; self.showTooltip(tooltipContent, event); }) .on('mouseout', function(event, d) { // Reset segment d3__namespace.select(this) .transition() .duration(200) .style('opacity', 1); // Reset center text const centerText = self.options.centerText || self.calculateTotal(); self.updateCenterText(centerText, self.options.centerSubtext); self.hideTooltip(); }); return this; } /** * Create multi-level donut chart */ renderMultiLevel(innerData, outerData) { // Clear previous chart this.chartGroup.selectAll('*').remove(); const centerX = this.innerWidth / 2; const centerY = this.innerHeight / 2; const maxRadius = Math.min(this.innerWidth, this.innerHeight) / 2 - 10; const chartCenter = this.chartGroup.append('g') .attr('transform', `translate(${centerX}, ${centerY})`); // Inner donut this.renderDonutLevel(chartCenter, innerData, maxRadius * 0.3, maxRadius * 0.6, 'inner'); // Outer donut this.renderDonutLevel(chartCenter, outerData, maxRadius * 0.7, maxRadius, 'outer'); return this; } /** * Render a single level of multi-level donut */ renderDonutLevel(container, data, innerRadius, outerRadius, className) { const pie = d3__namespace.pie() .value(d => d.value) .sort(null); const arc = d3__namespace.arc() .innerRadius(innerRadius) .outerRadius(outerRadius); const colorScale = d3__namespace.scaleOrdinal(d3__namespace.schemeCategory10); const slices = container.selectAll(`.${className}-slice`) .data(pie(data)) .enter() .append('g') .attr('class', `${className}-slice`); slices.append('path') .attr('fill', (d, i) => colorScale(i)) .attr('stroke', '#fff') .attr('stroke-width', 2) .attr('d', arc) .each(function(d) { this._current = { startAngle: 0, endAngle: 0 }; }) .transition() .duration(1000) .attrTween('d', function(d) { const interpolate = d3__namespace.interpolate(this._current, d); this._current = interpolate(0); return function(t