d3-charts-viz-library
Version:
A comprehensive D3-based chart library with customizable charts and graphs
422 lines (359 loc) • 11.9 kB
JavaScript
import * as d3 from 'd3';
import BaseChart from './BaseChart.js';
import { ColorUtils, AnimationUtils } from '../utils/index.js';
/**
* ChordDiagramChart - Creates a chord diagram for visualizing relationships between trust components
* Extends BaseChart to inherit common functionality
*/
class ChordDiagramChart extends BaseChart {
constructor(container, options = {}) {
const defaultOptions = {
width: 600,
height: 600,
margin: { top: 60, right: 60, bottom: 60, left: 60 },
outerRadius: null, // Will be calculated
innerRadius: null, // Will be calculated
padAngle: 0.05,
animationDuration: 1200,
groupAnimationDelay: 200,
chordAnimationDelay: 1000,
primaryColor: '#10b981',
secondaryColor: '#3b82f6',
accentColor: '#8b5cf6',
warningColor: '#f59e0b',
mutedColor: '#6b7280',
showTooltip: true,
showCenterInfo: true,
title: 'Trust Relationships'
};
super(container, { ...defaultOptions, ...options });
this.chordData = [];
this.matrix = [];
this.labels = ['Charitable\nDeduction', 'Income\nStream', 'Annual\nDistributions', 'Trust\nRemainder'];
this.colors = [
this.options.primaryColor,
this.options.secondaryColor,
this.options.accentColor,
this.options.warningColor
];
this.id = Math.random().toString(36).substr(2, 9);
}
/**
* Process economic data into chord matrix
*/
processData(data) {
if (!data) {
console.warn('ChordDiagramChart: No data provided');
return { matrix: [], labels: this.labels };
}
// Handle different data formats
let economicData = data;
if (data.data) {
economicData = data.data;
}
// Extract values from economic data
const totalTrust = economicData.economicSchedule?.[0]?.remainder ||
economicData.economicSchedule?.[0]?.beginningPrincipal ||
1000000;
const totalDistribution = economicData.economicSchedule?.reduce((sum, d) =>
sum + (d.distribution || 0), 0) || totalTrust * 0.05;
const charitableDeduction = economicData.charitDeduction ||
economicData.charitableDeduction ||
totalTrust * 0.3;
const remainderValue = economicData.economicSchedule?.[economicData.economicSchedule.length - 1]?.remainder ||
totalTrust * 0.7;
// Create matrix for chord diagram - represents relationships between components
const matrix = [
[0, charitableDeduction * 0.3, totalDistribution * 0.2, remainderValue * 0.1], // Charitable Deduction
[charitableDeduction * 0.3, 0, totalDistribution * 0.4, remainderValue * 0.3], // Income Stream
[totalDistribution * 0.2, totalDistribution * 0.4, 0, remainderValue * 0.2], // Distributions
[remainderValue * 0.1, remainderValue * 0.3, remainderValue * 0.2, 0] // Remainder
];
return {
matrix,
labels: this.labels,
totalFlow: totalDistribution + charitableDeduction,
totalTrust,
charitableDeduction,
totalDistribution,
remainderValue
};
}
/**
* Create gradients for chord visualization
*/
createGradients() {
const defs = this.svg.select('defs').empty() ? this.svg.append('defs') : this.svg.select('defs');
this.colors.forEach((color, i) => {
const gradient = defs.append('radialGradient')
.attr('id', `chord-gradient-${this.id}-${i}`)
.attr('cx', '50%')
.attr('cy', '50%')
.attr('r', '50%');
gradient.append('stop')
.attr('offset', '0%')
.attr('stop-color', ColorUtils.lighten(color, 0.2))
.attr('stop-opacity', 1);
gradient.append('stop')
.attr('offset', '100%')
.attr('stop-color', color)
.attr('stop-opacity', 0.8);
});
// Glow filter
const filter = defs.append('filter')
.attr('id', `chord-glow-${this.id}`)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
filter.append('feGaussianBlur')
.attr('stdDeviation', '2')
.attr('result', 'coloredBlur');
const feMerge = filter.append('feMerge');
feMerge.append('feMergeNode').attr('in', 'coloredBlur');
feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
}
/**
* Render chord groups (outer arcs)
*/
renderGroups(chords) {
const arc = d3.arc()
.innerRadius(this.innerRadius)
.outerRadius(this.outerRadius);
const groups = this.chartGroup.selectAll('.chord-group')
.data(chords.groups)
.enter()
.append('g')
.attr('class', 'chord-group');
// Group arcs
groups.append('path')
.attr('fill', (d, i) => `url(#chord-gradient-${this.id}-${i})`)
.attr('stroke', (d, i) => this.colors[i])
.attr('stroke-width', 2)
.attr('d', arc)
.style('opacity', 0)
.style('filter', `url(#chord-glow-${this.id})`)
.transition()
.delay((d, i) => i * this.options.groupAnimationDelay)
.duration(800)
.style('opacity', 0.8);
// Group labels
groups.append('text')
.attr('transform', (d) => {
const angle = (d.startAngle + d.endAngle) / 2;
const radius = this.outerRadius + 20;
return `translate(${Math.cos(angle - Math.PI / 2) * radius},${Math.sin(angle - Math.PI / 2) * radius})`;
})
.attr('text-anchor', (d) => {
const angle = (d.startAngle + d.endAngle) / 2;
return angle > Math.PI ? 'end' : 'start';
})
.attr('font-size', '11px')
.attr('font-weight', 'bold')
.attr('fill', '#1e293b')
.style('opacity', 0)
.text((d, i) => this.processedData.labels[i])
.transition()
.delay((d, i) => i * this.options.groupAnimationDelay + 400)
.duration(600)
.style('opacity', 1);
return groups;
}
/**
* Render chord connections
*/
renderChords(chords) {
const ribbon = d3.ribbon()
.radius(this.innerRadius);
const chordPaths = this.chartGroup.selectAll('.chord-path')
.data(chords)
.enter()
.append('path')
.attr('class', 'chord-path')
.attr('fill', (d) => this.colors[d.source.index])
.attr('stroke', (d) => this.colors[d.source.index])
.attr('stroke-width', 1)
.attr('opacity', 0.6)
.attr('d', ribbon)
.style('opacity', 0)
.style('filter', 'drop-shadow(0 2px 4px rgba(0,0,0,0.2))');
// Animate chords
chordPaths
.transition()
.delay(this.options.chordAnimationDelay)
.duration(this.options.animationDuration)
.style('opacity', 0.6);
return chordPaths;
}
/**
* Add hover interactions
*/
addInteractions(groups, chordPaths) {
if (!this.options.showTooltip) return;
const self = this;
groups
.on('mouseover', function(event, d) {
const groupIndex = d.index;
// Highlight related chords
chordPaths
.style('opacity', (chord) => {
return (chord.source.index === groupIndex || chord.target.index === groupIndex) ? 0.8 : 0.1;
});
self.showTooltip(event, d);
})
.on('mouseout', function() {
chordPaths.style('opacity', 0.6);
self.hideTooltip();
});
}
/**
* Show tooltip for group
*/
showTooltip(event, d) {
const tooltip = this.chartGroup.append('g')
.attr('class', 'chord-tooltip');
const value = d.value;
const text = this.formatCurrency(value);
const bbox = this.getTextBBox(text, '11px');
tooltip.append('rect')
.attr('x', -bbox.width / 2 - 5)
.attr('y', -bbox.height / 2 - 2)
.attr('width', bbox.width + 10)
.attr('height', bbox.height + 4)
.attr('fill', 'rgba(0,0,0,0.8)')
.attr('rx', 4);
tooltip.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '0.35em')
.attr('fill', 'white')
.attr('font-size', '11px')
.text(text);
}
/**
* Hide tooltip
*/
hideTooltip() {
this.chartGroup.selectAll('.chord-tooltip').remove();
}
/**
* Render center information
*/
renderCenterInfo() {
if (!this.options.showCenterInfo) return;
const centerGroup = this.chartGroup.append('g')
.attr('class', 'center-info');
// Title
centerGroup.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '-0.5em')
.attr('font-size', '14px')
.attr('font-weight', 'bold')
.attr('fill', '#1e293b')
.text(this.options.title);
centerGroup.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '1em')
.attr('font-size', '10px')
.attr('fill', this.options.mutedColor)
.text('Component Interactions');
// Value summary
const summaryGroup = this.chartGroup.append('g')
.attr('class', 'summary-info')
.attr('transform', `translate(0, ${this.outerRadius + 60})`);
summaryGroup.append('text')
.attr('text-anchor', 'middle')
.attr('font-size', '12px')
.attr('font-weight', '600')
.attr('fill', this.options.primaryColor)
.text(`Total Flow: ${this.formatCurrency(this.processedData.totalFlow)}`);
}
/**
* Format currency values
*/
formatCurrency(value) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(value);
}
/**
* Get text bounding box
*/
getTextBBox(text, fontSize) {
const tempText = this.svg.append('text')
.attr('font-size', fontSize)
.text(text)
.style('opacity', 0);
const bbox = tempText.node().getBBox();
tempText.remove();
return bbox;
}
/**
* Clear the chart content
*/
clear() {
if (this.svg) {
this.svg.selectAll('.chord-diagram-chart').remove();
}
}
/**
* Main render method
*/
render() {
if (!this.data) {
console.warn('ChordDiagramChart: No data to render');
return this;
}
// Clear previous render
this.clear();
// Process data
this.processedData = this.processData(this.data);
if (!this.processedData.matrix || this.processedData.matrix.length === 0) {
console.warn('ChordDiagramChart: No valid matrix data found');
return this;
}
// Calculate dimensions
this.outerRadius = this.options.outerRadius || (Math.min(this.innerWidth, this.innerHeight) * 0.4);
this.innerRadius = this.options.innerRadius || (this.outerRadius - 30);
// Create chart group centered
this.chartGroup = this.svg.append('g')
.attr('class', 'chord-diagram-chart')
.attr('transform', `translate(${this.options.margin.left + this.innerWidth / 2}, ${this.options.margin.top + this.innerHeight / 2})`);
// Create gradients and filters
this.createGradients();
// Create chord layout
const chord = d3.chord()
.padAngle(this.options.padAngle)
.sortSubgroups(d3.descending);
const chords = chord(this.processedData.matrix);
// Render components
const groups = this.renderGroups(chords);
const chordPaths = this.renderChords(chords);
this.addInteractions(groups, chordPaths);
this.renderCenterInfo();
return this;
}
/**
* Update chart with new data
*/
updateData(newData) {
this.setData(newData);
this.render();
return this;
}
/**
* Static method to create from economic data
*/
static fromEconomicData(economicData) {
if (!economicData || !economicData.economicSchedule) {
console.warn('ChordDiagramChart: Invalid economic data format');
return {};
}
return {
data: economicData
};
}
}
export default ChordDiagramChart;