UNPKG

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
"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