datapilot-cli
Version:
Enterprise-grade streaming multi-format data analysis with comprehensive statistical insights and intelligent relationship detection - supports CSV, JSON, Excel, TSV, Parquet - memory-efficient, cross-platform
1,171 lines • 49.6 kB
JavaScript
"use strict";
/**
* Advanced Chart Composition Engine
*
* Sophisticated engine for multi-dimensional data encoding and aesthetic optimization:
* - Multi-dimensional visual encoding strategies
* - Perceptual optimization based on human visual cognition
* - Data-driven color theory and aesthetic decisions
* - Advanced visual hierarchy and composition principles
* - Cross-cultural and accessibility considerations
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChartComposer = void 0;
/**
* Advanced Chart Composition Engine
*/
class ChartComposer {
/**
* Generate comprehensive composition profile for a visualization
*/
static composeVisualization(chartType, dimensions, dataCharacteristics, contextualRequirements = {}) {
const visualEncoding = this.optimizeMultiDimensionalEncoding(dimensions, chartType);
const aestheticProfile = this.generateAestheticProfile(chartType, dataCharacteristics, contextualRequirements);
const perceptualOptimization = this.optimizePerception(visualEncoding, aestheticProfile);
const accessibilityCompliance = this.ensureAccessibility(visualEncoding, aestheticProfile);
const culturalAdaptation = this.adaptForCulture(contextualRequirements.culture || 'en-US');
const compositionPrinciples = this.applyCompositionPrinciples(chartType, visualEncoding);
const qualityMetrics = this.assessVisualQuality(visualEncoding, aestheticProfile, perceptualOptimization, accessibilityCompliance);
return {
visualEncoding,
aestheticProfile,
perceptualOptimization,
accessibilityCompliance,
culturalAdaptation,
compositionPrinciples,
qualityMetrics,
};
}
/**
* Optimize multi-dimensional encoding for maximum effectiveness
*/
static optimizeMultiDimensionalEncoding(dimensions, chartType) {
// Rank channels by perceptual effectiveness (Cleveland & McGill hierarchy)
const channelRanking = this.getChannelEffectivenessRanking();
// Optimize channel assignment based on data importance and perceptual accuracy
const optimizedDimensions = this.optimizeChannelAssignment(dimensions, channelRanking);
// Calculate encoding efficiency and cognitive load
const efficiency = this.calculateEncodingEfficiency(optimizedDimensions);
const cognitiveLoad = this.calculateCognitiveLoad(optimizedDimensions, chartType);
const informationDensity = this.calculateInformationDensity(optimizedDimensions);
// Add redundant encodings for accessibility and emphasis
const redundantEncodings = this.generateRedundantEncodings(optimizedDimensions);
// Create visual hierarchy
const hierarchicalStructure = this.createVisualHierarchy(optimizedDimensions, chartType);
const primaryDimensions = optimizedDimensions.filter((d) => d.encodingStrength > 0.7);
const secondaryDimensions = optimizedDimensions.filter((d) => d.encodingStrength <= 0.7);
return {
primaryDimensions,
secondaryDimensions,
encodingEfficiency: efficiency,
cognitiveLoad,
informationDensity,
redundantEncodings,
hierarchicalStructure,
};
}
/**
* Generate comprehensive aesthetic profile
*/
static generateAestheticProfile(chartType, dataCharacteristics, contextualRequirements) {
const colorHarmony = this.generateColorHarmony(dataCharacteristics, contextualRequirements);
const typographySystem = this.designTypographySystem(chartType, contextualRequirements);
const spatialRhythm = this.establishSpatialRhythm(chartType);
const visualBalance = this.calculateVisualBalance(chartType);
const proportionSystem = this.selectProportionSystem(chartType, contextualRequirements);
const styleConsistency = this.ensureStyleConsistency(colorHarmony, typographySystem, spatialRhythm);
return {
colorHarmony,
typographySystem,
spatialRhythm,
visualBalance,
proportionSystem,
styleConsistency,
};
}
/**
* Optimize for human visual perception
*/
static optimizePerception(visualEncoding, aestheticProfile) {
const gestaltPrinciples = this.applyGestaltPrinciples(visualEncoding);
const cognitiveLoad = this.analyzeCognitiveLoad(visualEncoding, aestheticProfile);
const attentionFlow = this.analyzeAttentionFlow(visualEncoding, aestheticProfile);
const memorability = this.assessMemorability(visualEncoding, aestheticProfile);
const usabilityMetrics = this.calculateUsabilityMetrics(visualEncoding, aestheticProfile);
return {
gestaltPrinciples,
cognitiveLoad,
attentionFlow,
memorability,
usabilityMetrics,
};
}
// Helper methods for channel effectiveness ranking (Cleveland & McGill)
static getChannelEffectivenessRanking() {
const ranking = new Map();
// Position channels (most effective)
ranking.set('position_x', 1.0);
ranking.set('position_y', 1.0);
// Length/size channels
ranking.set('size_length', 0.9);
ranking.set('size_area', 0.8);
// Angle and slope
ranking.set('position_angle', 0.7);
// Color channels
ranking.set('color_lightness', 0.6);
ranking.set('color_saturation', 0.5);
ranking.set('color_hue', 0.4);
// Other channels (least effective for quantitative data)
ranking.set('shape', 0.3);
ranking.set('texture', 0.2);
ranking.set('motion', 0.1);
return ranking;
}
static optimizeChannelAssignment(dimensions, channelRanking) {
// Sort dimensions by importance/variance and assign most effective channels
const sortedDimensions = [...dimensions].sort((a, b) => b.encodingStrength - a.encodingStrength);
return sortedDimensions.map((dim, index) => {
const effectiveness = channelRanking.get(dim.channel) || 0.1;
return {
...dim,
perceptualAccuracy: effectiveness,
discriminability: this.calculateDiscriminability(dim, effectiveness),
orderingPreservation: this.calculateOrderingPreservation(dim, effectiveness),
optimization: this.optimizeChannel(dim, effectiveness),
};
});
}
static calculateEncodingEfficiency(dimensions) {
const totalEncodingPower = dimensions.reduce((sum, dim) => sum + dim.encodingStrength, 0);
const redundancy = this.calculateRedundancy(dimensions);
return Math.max(0, Math.min(100, (totalEncodingPower / dimensions.length) * 100 * (1 - redundancy)));
}
static calculateCognitiveLoad(dimensions, chartType) {
const baseLoad = dimensions.length * 10; // Each dimension adds cognitive load
const interactionLoad = this.getInteractionComplexity(chartType) * 5;
const perceptualLoad = dimensions.reduce((sum, dim) => sum + (1 - dim.perceptualAccuracy) * 10, 0);
return Math.min(100, baseLoad + interactionLoad + perceptualLoad);
}
static calculateInformationDensity(dimensions) {
const informationBits = dimensions.reduce((sum, dim) => {
// Calculate information content based on data type and encoding effectiveness
let bits = 1;
if (dim.dataType === 'quantitative')
bits = 8;
else if (dim.dataType === 'ordinal')
bits = 4;
else if (dim.dataType === 'nominal')
bits = 2;
return sum + bits * dim.encodingStrength;
}, 0);
// Normalize to 0-100 scale
return Math.min(100, informationBits * 5);
}
static generateColorHarmony(dataCharacteristics, contextualRequirements) {
// Select base color based on context and data
const baseColor = this.selectBaseColor(dataCharacteristics, contextualRequirements);
// Generate harmonious color scheme
const scheme = this.selectColorScheme(dataCharacteristics);
const harmony = this.generateHarmoniousColors(baseColor, scheme.type);
// Create comprehensive palette
const palette = this.generateComprehensivePalette(harmony, dataCharacteristics);
// Assess psychological impact
const psychologicalImpact = this.assessPsychologicalImpact(harmony);
// Create semantic mappings
const semanticMapping = this.createSemanticColorMappings(dataCharacteristics);
// Calculate harmony score
const harmonyScore = this.calculateHarmonyScore(harmony, palette);
return {
scheme,
palette,
psychologicalImpact,
semanticMapping,
harmonyScore,
};
}
// Placeholder implementations for complex methods
static calculateDiscriminability(dim, effectiveness) {
return effectiveness * 0.9; // Simplified calculation
}
static calculateOrderingPreservation(dim, effectiveness) {
if (dim.dataType === 'quantitative' || dim.dataType === 'ordinal') {
return effectiveness;
}
return 0.5; // Nominal data doesn't preserve ordering
}
static optimizeChannel(dim, effectiveness) {
return {
scalingFunction: dim.dataType === 'quantitative' ? 'linear' : 'ordinal',
domainOptimization: {
zeroBehavior: 'include',
outlierHandling: 'clip',
domainPadding: 0.1,
symmetryPreservation: false,
},
rangeOptimization: {
minValue: 0,
maxValue: 100,
resolution: 1,
perceptualUniformity: true,
physicalConstraints: [],
},
perceptualCorrection: {
gammaCorrection: 2.2,
luminanceAdjustment: 1.0,
contrastEnhancement: 1.0,
colorBlindnessCompensation: {
protanopia: 1.0,
deuteranopia: 1.0,
tritanopia: 1.0,
achromatopsia: 1.0,
},
},
};
}
static calculateRedundancy(dimensions) {
// Simplified redundancy calculation
return Math.max(0, (dimensions.length - 3) * 0.1);
}
static getInteractionComplexity(chartType) {
const complexityMap = {
scatter_plot: 2,
line_chart: 1,
bar_chart: 1,
heatmap: 3,
parallel_coordinates: 4,
sankey: 4,
};
return complexityMap[chartType] || 2;
}
// Additional placeholder implementations...
static generateRedundantEncodings(dimensions) {
const redundancies = [];
// Add pattern redundancy for color-blind accessibility
dimensions.forEach((dim) => {
if (dim.channel === 'color_hue' || dim.channel === 'color_saturation') {
redundancies.push({
primaryChannel: dim.channel,
redundantChannel: 'shape',
redundancyLevel: 85,
purpose: 'accessibility',
effectiveness: 90,
});
// Add texture redundancy for important categorical data
if (dim.dataType === 'nominal' && dim.encodingStrength > 0.7) {
redundancies.push({
primaryChannel: dim.channel,
redundantChannel: 'texture',
redundancyLevel: 70,
purpose: 'emphasis',
effectiveness: 75,
});
}
}
// Add size redundancy for important quantitative data
if (dim.channel === 'position_y' &&
dim.dataType === 'quantitative' &&
dim.encodingStrength > 0.8) {
redundancies.push({
primaryChannel: dim.channel,
redundantChannel: 'size_area',
redundancyLevel: 60,
purpose: 'emphasis',
effectiveness: 80,
});
}
});
return redundancies;
}
static createVisualHierarchy(dimensions, chartType) {
// Sort dimensions by importance (encoding strength)
const sortedDimensions = [...dimensions].sort((a, b) => b.encodingStrength - a.encodingStrength);
// Create hierarchy levels based on encoding strength
const levels = [
{
level: 1,
elements: sortedDimensions.slice(0, 1).map((d) => d.dataField),
visualWeight: 100,
precedence: 1,
},
{
level: 2,
elements: sortedDimensions.slice(1, 3).map((d) => d.dataField),
visualWeight: 75,
precedence: 2,
},
{
level: 3,
elements: sortedDimensions.slice(3).map((d) => d.dataField),
visualWeight: 50,
precedence: 3,
},
].filter((level) => level.elements.length > 0);
// Create focus points for most important dimensions
const focusPoints = sortedDimensions.slice(0, 2).map((dim, index) => ({
element: dim.dataField,
attentionWeight: 100 - index * 25,
visualTechniques: index === 0
? ['primary_color', 'large_size', 'central_position']
: ['secondary_color', 'moderate_size'],
cognitiveReasoning: index === 0
? 'Primary data dimension requiring immediate attention'
: 'Secondary dimension providing context',
}));
// Create visual flow based on chart type
const visualFlow = this.generateVisualFlow(sortedDimensions, chartType);
// Create attention guides
const attentionGuides = [
{
technique: 'color_contrast',
target: sortedDimensions[0]?.dataField || 'primary',
effectiveness: 90,
subtlety: 30,
},
{
technique: 'size_progression',
target: 'hierarchy',
effectiveness: 85,
subtlety: 60,
},
];
return {
levels,
focusPoints,
visualFlow,
attentionGuides,
};
}
static selectBaseColor(dataCharacteristics, contextualRequirements) {
// Analyze data sentiment and domain context
const domain = contextualRequirements?.domain || 'general';
const sentiment = this.analyzeDataSentiment(dataCharacteristics);
const brandColors = contextualRequirements?.brandColors;
// If brand colors are provided, use primary brand color
if (brandColors && brandColors.length > 0) {
return this.hexToHSL(brandColors[0]);
}
// Domain-specific color selection
const domainColorMap = {
education: { hue: 220, saturation: 65, lightness: 55 }, // Trustworthy blue
healthcare: { hue: 160, saturation: 55, lightness: 50 }, // Calming green-blue
finance: { hue: 200, saturation: 75, lightness: 45 }, // Professional blue
marketing: { hue: 280, saturation: 70, lightness: 60 }, // Creative purple
technology: { hue: 210, saturation: 80, lightness: 50 }, // Tech blue
environment: { hue: 120, saturation: 60, lightness: 45 }, // Natural green
social: { hue: 340, saturation: 65, lightness: 55 }, // Warm red-pink
general: { hue: 220, saturation: 60, lightness: 50 }, // Neutral blue
};
const baseColor = domainColorMap[domain] || domainColorMap['general'];
// Adjust based on data sentiment
if (sentiment === 'positive') {
baseColor.saturation = Math.min(90, baseColor.saturation + 15);
baseColor.lightness = Math.min(70, baseColor.lightness + 10);
}
else if (sentiment === 'negative') {
baseColor.saturation = Math.max(30, baseColor.saturation - 10);
baseColor.lightness = Math.max(30, baseColor.lightness - 10);
}
// Adjust for data complexity
const complexity = this.calculateDataComplexity(dataCharacteristics);
if (complexity > 0.7) {
// Use more muted colors for complex data to reduce cognitive load
baseColor.saturation = Math.max(40, baseColor.saturation - 20);
}
return baseColor;
}
static selectColorScheme(dataCharacteristics) {
return {
type: 'analogous',
baseColor: { hue: 220, saturation: 70, lightness: 50 },
harmony: [],
reasoning: 'Analogous scheme provides harmony while maintaining distinction',
};
}
static generateHarmoniousColors(baseColor, schemeType) {
// Implementation would generate colors based on color theory
return [baseColor];
}
static generateComprehensivePalette(harmony, dataCharacteristics) {
return {
categorical: harmony,
sequential: harmony,
diverging: harmony,
specialPurpose: new Map(),
accessibilityScore: 85,
};
}
static assessPsychologicalImpact(harmony) {
return {
emotion: 'professional',
energy: 60,
trust: 80,
professionalism: 85,
clarity: 90,
};
}
static createSemanticColorMappings(dataCharacteristics) {
const mappings = [];
// Analyze data for semantic meaning
const fields = dataCharacteristics.fields || [];
fields.forEach((field) => {
const fieldName = field.name?.toLowerCase() || '';
const fieldType = field.type || 'unknown';
// Performance/success indicators
if (fieldName.includes('success') ||
fieldName.includes('positive') ||
fieldName.includes('good')) {
mappings.push({
concept: 'success',
color: { hue: 120, saturation: 70, lightness: 50 }, // Green
culturalRelevance: 90,
universalRecognition: 95,
});
}
// Warning/caution indicators
if (fieldName.includes('warning') ||
fieldName.includes('caution') ||
fieldName.includes('moderate')) {
mappings.push({
concept: 'warning',
color: { hue: 45, saturation: 85, lightness: 55 }, // Orange
culturalRelevance: 85,
universalRecognition: 90,
});
}
// Error/danger indicators
if (fieldName.includes('error') ||
fieldName.includes('danger') ||
fieldName.includes('bad') ||
fieldName.includes('negative')) {
mappings.push({
concept: 'danger',
color: { hue: 0, saturation: 75, lightness: 50 }, // Red
culturalRelevance: 85,
universalRecognition: 95,
});
}
// Information/neutral indicators
if (fieldName.includes('info') ||
fieldName.includes('neutral') ||
fieldName.includes('standard')) {
mappings.push({
concept: 'information',
color: { hue: 210, saturation: 60, lightness: 55 }, // Blue
culturalRelevance: 90,
universalRecognition: 85,
});
}
// Temporal mappings
if (fieldType === 'temporal' || fieldName.includes('time') || fieldName.includes('date')) {
mappings.push({
concept: 'temporal',
color: { hue: 260, saturation: 50, lightness: 60 }, // Purple
culturalRelevance: 70,
universalRecognition: 70,
});
}
// Financial mappings
if (fieldName.includes('profit') ||
fieldName.includes('revenue') ||
fieldName.includes('income')) {
mappings.push({
concept: 'financial_positive',
color: { hue: 140, saturation: 65, lightness: 45 }, // Forest green
culturalRelevance: 80,
universalRecognition: 75,
});
}
if (fieldName.includes('loss') ||
fieldName.includes('cost') ||
fieldName.includes('expense')) {
mappings.push({
concept: 'financial_negative',
color: { hue: 15, saturation: 70, lightness: 45 }, // Red-orange
culturalRelevance: 80,
universalRecognition: 75,
});
}
});
// Add default semantic mappings if none found
if (mappings.length === 0) {
mappings.push({
concept: 'primary',
color: { hue: 220, saturation: 70, lightness: 50 },
culturalRelevance: 90,
universalRecognition: 85,
}, {
concept: 'secondary',
color: { hue: 45, saturation: 60, lightness: 55 },
culturalRelevance: 80,
universalRecognition: 80,
});
}
return mappings;
}
static calculateHarmonyScore(harmony, palette) {
if (harmony.length === 0)
return 0;
let score = 0;
let factors = 0;
// Factor 1: Hue distribution (0-25 points)
const hues = harmony.map((c) => c.hue);
const hueSpread = this.calculateHueSpread(hues);
const hueScore = Math.min(25, (hueSpread / 360) * 100);
score += hueScore;
factors++;
// Factor 2: Saturation consistency (0-25 points)
const saturations = harmony.map((c) => c.saturation);
const saturationVariance = this.calculateVariance(saturations);
const saturationScore = Math.max(0, 25 - saturationVariance / 100);
score += saturationScore;
factors++;
// Factor 3: Lightness distribution (0-25 points)
const lightnesses = harmony.map((c) => c.lightness);
const lightnessRange = Math.max(...lightnesses) - Math.min(...lightnesses);
const lightnessScore = Math.min(25, (lightnessRange / 80) * 25); // Good range is 0-80
score += lightnessScore;
factors++;
// Factor 4: Color theory compliance (0-25 points)
const theoryScore = this.evaluateColorTheoryCompliance(hues, palette.harmonyType);
score += theoryScore;
factors++;
// Bonus factors
// Accessibility bonus (0-10 points)
const accessibilityBonus = this.calculateAccessibilityBonus(harmony);
score += accessibilityBonus;
// Cultural appropriateness bonus (0-5 points)
const culturalBonus = this.calculateCulturalBonus(harmony);
score += culturalBonus;
// Normalize to 0-100 scale
const baseScore = (score / factors) * (100 / 25);
const bonusPoints = accessibilityBonus + culturalBonus;
return Math.min(100, Math.max(0, baseScore + bonusPoints));
}
static designTypographySystem(chartType, contextualRequirements) {
return {
hierarchy: {
levels: [],
scaleRatio: 1.25,
baselineGrid: 16,
verticalRhythm: 1.5,
},
readability: {
contrastRatio: 4.5,
optimalReadingDistance: 60,
cognitiveLoad: 20,
scanability: 80,
},
personality: {
formality: 70,
friendliness: 60,
authority: 75,
creativity: 40,
},
technicalOptimization: {
hinting: true,
subpixelRendering: true,
optimalSizes: [12, 14, 16, 18, 24],
performanceImpact: 5,
},
};
}
static establishSpatialRhythm(chartType) {
return {
gridSystem: {
type: 'modular',
columns: 12,
gutters: 16,
margins: { top: 24, right: 24, bottom: 24, left: 24 },
breakpoints: [],
},
spacingScale: {
baseUnit: 8,
scale: [4, 8, 16, 24, 32, 48, 64],
semanticSpacing: new Map(),
opticalAdjustments: [],
},
alignmentPrinciples: [],
proximityRules: [],
};
}
static calculateVisualBalance(chartType) {
return {
type: 'asymmetrical',
weight: {
distribution: [],
center: { x: 0.5, y: 0.5 },
moments: [],
},
tension: [],
stability: 80,
};
}
static selectProportionSystem(chartType, contextualRequirements) {
return {
system: 'golden_ratio',
ratios: [1.618, 1.414, 1.333],
applications: [],
aestheticScore: 80,
};
}
static ensureStyleConsistency(colorHarmony, typographySystem, spatialRhythm) {
return {
consistencyScore: 85,
deviations: [],
unifyingElements: ['color palette', 'typography scale', 'spacing system'],
brandAlignment: 80,
};
}
static applyGestaltPrinciples(visualEncoding) {
const applications = [];
// Proximity: Group related data elements
if (visualEncoding.primaryDimensions.length > 1) {
applications.push({
principle: 'proximity',
application: 'Group related data points using spatial proximity to show relationships',
effectiveness: 85,
cognitiveSupport: 'Reduces cognitive load by naturally grouping related information',
});
}
// Similarity: Use consistent visual properties for similar data
const categoricalDimensions = visualEncoding.primaryDimensions.filter((d) => d.dataType === 'nominal');
if (categoricalDimensions.length > 0) {
applications.push({
principle: 'similarity',
application: 'Use consistent colors, shapes, or patterns for data elements in the same category',
effectiveness: 90,
cognitiveSupport: 'Enables rapid categorization and pattern recognition',
});
}
// Closure: Complete implied shapes and patterns
applications.push({
principle: 'closure',
application: 'Use implied connections and boundaries to group data without explicit lines',
effectiveness: 75,
cognitiveSupport: 'Reduces visual clutter while maintaining data relationships',
});
// Continuity: Create visual flow through data
const temporalDimensions = visualEncoding.primaryDimensions.filter((d) => d.dataType === 'temporal');
if (temporalDimensions.length > 0) {
applications.push({
principle: 'continuity',
application: 'Create smooth visual transitions and flow in temporal data visualization',
effectiveness: 80,
cognitiveSupport: 'Supports natural reading patterns and temporal understanding',
});
}
// Figure-ground: Establish clear hierarchy
applications.push({
principle: 'figure_ground',
application: 'Use contrast and visual weight to distinguish primary data from background context',
effectiveness: 95,
cognitiveSupport: 'Enables immediate focus on most important information',
});
// Common fate: Show data relationships through movement or transformation
if (visualEncoding.informationDensity > 60) {
applications.push({
principle: 'common_fate',
application: 'Use coordinated animations or transformations to show data relationships',
effectiveness: 70,
cognitiveSupport: 'Reveals hidden patterns and connections in complex datasets',
});
}
return applications;
}
static analyzeCognitiveLoad(visualEncoding, aestheticProfile) {
return {
intrinsicLoad: visualEncoding.cognitiveLoad * 0.4,
extraneousLoad: visualEncoding.cognitiveLoad * 0.3,
germaneLoad: visualEncoding.cognitiveLoad * 0.3,
totalLoad: visualEncoding.cognitiveLoad,
recommendations: [],
};
}
static analyzeAttentionFlow(visualEncoding, aestheticProfile) {
return {
entryPoints: [],
flowPath: [],
exitPoints: [],
distractions: [],
flowEfficiency: 75,
};
}
static assessMemorability(visualEncoding, aestheticProfile) {
return {
distinctiveness: 70,
meaningfulness: 80,
simplicity: 75,
emotionalImpact: 60,
overallMemorability: 71,
};
}
static calculateUsabilityMetrics(visualEncoding, aestheticProfile) {
return {
learnability: 80,
efficiency: 85,
memorability: 75,
errorPrevention: 90,
satisfaction: 80,
overall: 82,
};
}
static ensureAccessibility(visualEncoding, aestheticProfile) {
return {
wcagLevel: 'AA',
colorBlindnessSupport: {
protanopia: 85,
deuteranopia: 85,
tritanopia: 90,
achromatopsia: 80,
alternativeEncodings: ['pattern', 'texture', 'shape'],
},
contrastCompliance: {
minimumContrast: 4.5,
enhancedContrast: 7.0,
graphicalObjectContrast: 3.0,
complianceLevel: 'AA',
},
motorImpairmentSupport: {
minimumTargetSize: 44,
spacing: 8,
dragAlternatives: ['click', 'keyboard'],
keyboardNavigation: true,
},
cognitiveSupport: {
complexityReduction: ['clear labeling', 'consistent patterns'],
memoryAids: ['persistent legends', 'contextual help'],
consistentPatterns: ['uniform interactions', 'predictable behavior'],
errorPrevention: ['input validation', 'clear feedback'],
},
screenReaderCompatibility: {
ariaCompliance: true,
textAlternatives: true,
structuralMarkup: true,
focusManagement: true,
},
complianceScore: 85,
};
}
static adaptForCulture(culture) {
return {
readingDirection: 'ltr',
colorCulturalMeaning: [],
symbolismAdaptation: [],
numeralSystem: 'western',
dateFormat: 'ISO',
localizations: [],
};
}
static applyCompositionPrinciples(chartType, visualEncoding) {
return [
{
principle: 'Visual Hierarchy',
application: 'Primary data elements use stronger visual weight',
strength: 85,
visualImpact: 'Guides user attention to most important information',
reasoning: 'Establishes clear information priority',
},
];
}
static assessVisualQuality(visualEncoding, aestheticProfile, perceptualOptimization, accessibilityCompliance) {
const aestheticScore = aestheticProfile.styleConsistency.consistencyScore;
const functionalScore = visualEncoding.encodingEfficiency;
const accessibilityScore = accessibilityCompliance.complianceScore;
const usabilityScore = perceptualOptimization.usabilityMetrics.overall;
// Calculate originality score based on encoding innovation and visual uniqueness
const originalityScore = this.calculateOriginalityScore(visualEncoding, aestheticProfile);
const overallQuality = (aestheticScore + functionalScore + accessibilityScore + usabilityScore + originalityScore) /
5;
// Identify improvement areas based on scores
const improvementAreas = this.identifyImprovementAreas({
aestheticScore,
functionalScore,
accessibilityScore,
usabilityScore,
originalityScore,
});
return {
aestheticScore,
functionalScore,
accessibilityScore,
usabilityScore,
originalityScore,
overallQuality,
improvementAreas,
};
}
// Helper methods for enhanced implementations
static generateVisualFlow(dimensions, chartType) {
const flow = [];
if (dimensions.length < 2)
return flow;
// Create flow based on encoding strength
for (let i = 0; i < dimensions.length - 1; i++) {
const strengthDiff = dimensions[i].encodingStrength - dimensions[i + 1].encodingStrength;
if (strengthDiff > 0.2) {
flow.push({
from: dimensions[i].dataField,
to: dimensions[i + 1].dataField,
strength: Math.min(100, strengthDiff * 100),
technique: this.selectFlowTechnique(dimensions[i], dimensions[i + 1]),
});
}
}
return flow;
}
static selectFlowTechnique(from, to) {
if (from.channel.includes('color') || to.channel.includes('color')) {
return 'color_gradient';
}
else if (from.channel.includes('size') || to.channel.includes('size')) {
return 'size_progression';
}
else if (from.channel.includes('position') || to.channel.includes('position')) {
return 'position_flow';
}
else {
return 'line_connection';
}
}
static analyzeDataSentiment(dataCharacteristics) {
const fields = dataCharacteristics.fields || [];
let positiveCount = 0;
let negativeCount = 0;
fields.forEach((field) => {
const name = field.name?.toLowerCase() || '';
if (name.includes('positive') ||
name.includes('success') ||
name.includes('good') ||
name.includes('profit') ||
name.includes('growth') ||
name.includes('improvement')) {
positiveCount++;
}
else if (name.includes('negative') ||
name.includes('error') ||
name.includes('bad') ||
name.includes('loss') ||
name.includes('decline') ||
name.includes('problem')) {
negativeCount++;
}
});
if (positiveCount > negativeCount)
return 'positive';
if (negativeCount > positiveCount)
return 'negative';
return 'neutral';
}
static calculateDataComplexity(dataCharacteristics) {
const fieldCount = dataCharacteristics.fields?.length || 0;
const recordCount = dataCharacteristics.recordCount || 0;
const categoricalFields = dataCharacteristics.categoricalColumns || 0;
const numericalFields = dataCharacteristics.numericalColumns || 0;
// Normalize complexity factors
const fieldComplexity = Math.min(1, fieldCount / 20);
const recordComplexity = Math.min(1, recordCount / 10000);
const typeComplexity = Math.min(1, (categoricalFields + numericalFields) / 15);
return (fieldComplexity + recordComplexity + typeComplexity) / 3;
}
static hexToHSL(hex) {
// Remove # if present
hex = hex.replace('#', '');
// Parse RGB values
const r = parseInt(hex.substr(0, 2), 16) / 255;
const g = parseInt(hex.substr(2, 2), 16) / 255;
const b = parseInt(hex.substr(4, 2), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const diff = max - min;
let h = 0;
let s = 0;
const l = (max + min) / 2;
if (diff !== 0) {
s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min);
switch (max) {
case r:
h = (g - b) / diff + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / diff + 2;
break;
case b:
h = (r - g) / diff + 4;
break;
}
h /= 6;
}
return {
hue: Math.round(h * 360),
saturation: Math.round(s * 100),
lightness: Math.round(l * 100),
};
}
static calculateHueSpread(hues) {
if (hues.length < 2)
return 0;
// Sort hues and calculate maximum spread
const sortedHues = [...hues].sort((a, b) => a - b);
let maxSpread = 0;
for (let i = 0; i < sortedHues.length - 1; i++) {
const spread = sortedHues[i + 1] - sortedHues[i];
maxSpread = Math.max(maxSpread, spread);
}
// Check wrap-around spread
const wrapSpread = 360 - sortedHues[sortedHues.length - 1] + sortedHues[0];
maxSpread = Math.max(maxSpread, wrapSpread);
return maxSpread;
}
static evaluateColorTheoryCompliance(hues, harmonyType) {
if (hues.length < 2)
return 0;
switch (harmonyType) {
case 'analogous':
return this.checkAnalogousCompliance(hues);
case 'complementary':
return this.checkComplementaryCompliance(hues);
case 'triadic':
return this.checkTriadicCompliance(hues);
case 'tetradic':
return this.checkTetradicCompliance(hues);
case 'monochromatic':
return this.checkMonochromaticCompliance(hues);
default:
return 15; // Base score for unknown types
}
}
static checkAnalogousCompliance(hues) {
// Analogous colors should be within 30-60 degrees of each other
let totalCompliance = 0;
let comparisons = 0;
for (let i = 0; i < hues.length - 1; i++) {
for (let j = i + 1; j < hues.length; j++) {
const diff = Math.abs(hues[i] - hues[j]);
const circularDiff = Math.min(diff, 360 - diff);
if (circularDiff >= 15 && circularDiff <= 60) {
totalCompliance += 25;
}
else if (circularDiff <= 90) {
totalCompliance += 15;
}
else {
totalCompliance += 5;
}
comparisons++;
}
}
return comparisons > 0 ? totalCompliance / comparisons : 0;
}
static checkComplementaryCompliance(hues) {
// Look for hues approximately 180 degrees apart
for (let i = 0; i < hues.length - 1; i++) {
for (let j = i + 1; j < hues.length; j++) {
const diff = Math.abs(hues[i] - hues[j]);
const circularDiff = Math.min(diff, 360 - diff);
if (Math.abs(circularDiff - 180) <= 30) {
return 25; // Perfect complementary
}
}
}
return 10; // No complementary found
}
static checkTriadicCompliance(hues) {
if (hues.length < 3)
return 0;
// Check if any three hues form a triadic relationship (120 degrees apart)
for (let i = 0; i < hues.length - 2; i++) {
for (let j = i + 1; j < hues.length - 1; j++) {
for (let k = j + 1; k < hues.length; k++) {
const sorted = [hues[i], hues[j], hues[k]].sort((a, b) => a - b);
const diff1 = sorted[1] - sorted[0];
const diff2 = sorted[2] - sorted[1];
const diff3 = 360 - sorted[2] + sorted[0];
if (Math.abs(diff1 - 120) <= 30 &&
Math.abs(diff2 - 120) <= 30 &&
Math.abs(diff3 - 120) <= 30) {
return 25;
}
}
}
}
return 8;
}
static checkTetradicCompliance(hues) {
if (hues.length < 4)
return 0;
// Simplified: check if hues are reasonably distributed
const averageSpacing = 360 / hues.length;
const idealSpacing = 90; // For tetradic
if (Math.abs(averageSpacing - idealSpacing) <= 30) {
return 25;
}
return 12;
}
static checkMonochromaticCompliance(hues) {
// All hues should be very similar
const hueRange = Math.max(...hues) - Math.min(...hues);
if (hueRange <= 15)
return 25;
if (hueRange <= 30)
return 18;
if (hueRange <= 45)
return 10;
return 5;
}
static calculateAccessibilityBonus(harmony) {
let bonus = 0;
// Check contrast potential
const lightnesses = harmony.map((c) => c.lightness);
const lightnessRange = Math.max(...lightnesses) - Math.min(...lightnesses);
if (lightnessRange >= 50)
bonus += 5; // Good contrast potential
if (lightnessRange >= 70)
bonus += 3; // Excellent contrast potential
// Check color-blind friendliness (avoid red-green combinations with similar lightness)
const hasProblematicRedGreen = this.checkRedGreenProblems(harmony);
if (!hasProblematicRedGreen)
bonus += 2;
return Math.min(10, bonus);
}
static checkRedGreenProblems(harmony) {
for (let i = 0; i < harmony.length - 1; i++) {
for (let j = i + 1; j < harmony.length; j++) {
const color1 = harmony[i];
const color2 = harmony[j];
// Check if one is red-ish and one is green-ish with similar lightness
const isRed1 = color1.hue >= 330 || color1.hue <= 30;
const isGreen1 = color1.hue >= 90 && color1.hue <= 150;
const isRed2 = color2.hue >= 330 || color2.hue <= 30;
const isGreen2 = color2.hue >= 90 && color2.hue <= 150;
if ((isRed1 && isGreen2) || (isGreen1 && isRed2)) {
const lightnessDiff = Math.abs(color1.lightness - color2.lightness);
if (lightnessDiff < 20) {
return true; // Problematic combination
}
}
}
}
return false;
}
static calculateCulturalBonus(harmony) {
// Simplified cultural appropriateness check
// Avoid culturally sensitive color combinations
let bonus = 5; // Base cultural bonus
// Check for balance - avoid overly aggressive colors
const highSaturationCount = harmony.filter((c) => c.saturation > 85).length;
if (highSaturationCount / harmony.length < 0.5) {
bonus += 2; // Moderate saturation is generally more culturally appropriate
}
return Math.min(5, bonus);
}
static calculateOriginalityScore(visualEncoding, aestheticProfile) {
let originalityScore = 50; // Base score
// Reward innovative encoding combinations
const uniqueChannels = new Set(visualEncoding.primaryDimensions
.concat(visualEncoding.secondaryDimensions)
.map((d) => d.channel)).size;
if (uniqueChannels >= 4)
originalityScore += 20; // Diverse channel usage
if (uniqueChannels >= 6)
originalityScore += 10; // Very diverse
// Reward effective use of redundant encodings
if (visualEncoding.redundantEncodings.length > 0) {
const avgEffectiveness = visualEncoding.redundantEncodings.reduce((sum, enc) => sum + enc.effectiveness, 0) /
visualEncoding.redundantEncodings.length;
originalityScore += Math.min(15, (avgEffectiveness / 100) * 15);
}
// Reward sophisticated hierarchy
const hierarchyComplexity = visualEncoding.hierarchicalStructure.levels.length;
if (hierarchyComplexity >= 3)
originalityScore += 10;
// Penalize overly complex solutions
if (visualEncoding.cognitiveLoad > 80)
originalityScore -= 20;
if (visualEncoding.cognitiveLoad > 90)
originalityScore -= 10;
// Reward high information density without complexity
if (visualEncoding.informationDensity > 70 && visualEncoding.cognitiveLoad < 60) {
originalityScore += 15;
}
return Math.max(0, Math.min(100, originalityScore));
}
static identifyImprovementAreas(scores) {
const areas = [];
const threshold = 75; // Scores below this need improvement
if (scores.aestheticScore < threshold) {
areas.push({
area: 'Aesthetic Design',
currentScore: scores.aestheticScore,
potentialScore: Math.min(95, scores.aestheticScore + 20),
recommendations: [
'Improve color harmony and consistency',
'Enhance visual balance and proportion',
'Refine typography system',
],
priority: scores.aestheticScore < 50 ? 'critical' : scores.aestheticScore < 65 ? 'high' : 'medium',
});
}
if (scores.functionalScore < threshold) {
areas.push({
area: 'Functional Efficiency',
currentScore: scores.functionalScore,
potentialScore: Math.min(95, scores.functionalScore + 15),
recommendations: [
'Optimize encoding channel assignment',
'Reduce cognitive load while maintaining information density',
'Improve data-to-visualization mapping',
],
priority: scores.functionalScore < 50 ? 'critical' : 'high',
});
}
if (scores.accessibilityScore < threshold) {
areas.push({
area: 'Accessibility Compliance',
currentScore: scores.accessibilityScore,
potentialScore: Math.min(98, scores.accessibilityScore + 23),
recommendations: [
'Enhance color contrast and alternative encodings',
'Improve screen reader compatibility',
'Add motor impairment support features',
],
priority: scores.accessibilityScore < 60 ? 'critical' : 'high',
});
}
if (scores.usabilityScore < threshold) {
areas.push({
area: 'User Experience',
currentScore: scores.usabilityScore,
potentialScore: Math.min(92, scores.usabilityScore + 17),
recommendations: [
'Simplify interaction patterns',
'Improve learnability and memorability',
'Enhance user satisfaction through better feedback',
],
priority: scores.usabilityScore < 55 ? 'critical' : 'high',
});
}
if (scores.originalityScore < threshold) {
areas.push({
area: 'Visual Innovation',
currentScore: scores.originalityScore,
potentialScore: Math.min(90, scores.originalityScore + 15),
recommendations: [
'Explore innovative encoding combinations',
'Balance complexity with clarity',
'Implement sophisticated visual hierarchies',
],
priority: 'medium',
});
}
return areas;
}
static calculateVariance(values) {
if (values.length === 0)
return 0;
const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
const squaredDiffs = values.map((val) => Math.pow(val - mean, 2));
return squaredDiffs.reduce((sum, diff) => sum + diff, 0) / values.length;
}
}
exports.ChartComposer = ChartComposer;
//# sourceMappingURL=chart-composer.js.map