/** * Fast chart data parser - NO AI needed * Extracts structured data from text using regex patterns */ export interface ChartDataPoint { label: string value: number } export interface ParsedChartData { hasData: boolean data: ChartDataPoint[] suggestedType: 'bar' | 'horizontal-bar' | 'line' | 'area' | 'pie' confidence: number } /** * Parse text for chart data using multiple regex patterns * Runs in < 50ms - NO AI calls */ export function parseChartData(text: string): ParsedChartData { const patterns = [ // Pattern 1: "Label : Value | Label2 : Value2" { regex: /([^|:|\n]+?)\s*[:|=]\s*(\d+(?:[.,]\d+)?)(?:\s*\||\n|,|$)/gi, priority: 1, }, // Pattern 2: "Label: Value, Label2: Value2" { regex: /([^,:;\n]+?)\s*[:=]\s*(\d+(?:[.,]\d+)?)(?:\s*,|;|\n|$)/gi, priority: 2, }, // Pattern 3: "Label - Value" or "Label — Value" { regex: /([^-—\n]+?)\s*[—-]\s*(\d+(?:[.,]\d+)?)(?:\s*,|\n|$)/gi, priority: 3, }, // Pattern 4: Lists with percentages "Label • 45%" { regex: /([^•%\n]+?)\s*[•·]\s*(\d+)(?:\s*%)?(?:\s*,|\n|$)/gi, priority: 4, }, ] const seen = new Set() let bestMatch: { data: ChartDataPoint[], score: number, type: ParsedChartData['suggestedType'] } | null = null for (const pattern of patterns) { const matches: ChartDataPoint[] = [] let match: RegExpExecArray | null // Reset regex state pattern.regex.lastIndex = 0 while ((match = pattern.regex.exec(text)) !== null) { const label = match[1].trim() const valueStr = match[2].trim().replace(',', '.') const value = parseFloat(valueStr) if (!isNaN(value) && value > 0 && label && !seen.has(label)) { matches.push({ label, value }) seen.add(label) } } if (matches.length >= 2) { const score = matches.length * pattern.priority if (!bestMatch || score > bestMatch.score) { bestMatch = { data: matches, score, type: suggestChartType(matches, text), } } } } if (!bestMatch) { return { hasData: false, data: [], suggestedType: 'bar', confidence: 0 } } return { hasData: true, data: bestMatch.data, suggestedType: bestMatch.type, confidence: Math.min(bestMatch.score / 10, 1), } } /** * Suggest chart type based on data and text context */ function suggestChartType( data: ChartDataPoint[], text: string ): ParsedChartData['suggestedType'] { const textLower = text.toLowerCase() // Time series indicators const timeKeywords = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec', 'janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre', 'q1', 'q2', 'q3', 'q4', 'month', 'mois', 'week', 'semaine', 'year', 'année', 'trend', 'tendance', 'growth', 'croissance'] const hasTimeLabels = data.some(d => timeKeywords.some(k => d.label.toLowerCase().includes(k))) const hasTimeInText = timeKeywords.some(k => textLower.includes(k)) // Proportion indicators const proportionKeywords = ['%', 'percent', 'part', 'share', 'répartition', 'distribution', 'segment', 'total'] const hasProportion = proportionKeywords.some(k => textLower.includes(k)) // Check if values sum to ~100 (percentage data) const sum = data.reduce((a, b) => a + b.value, 0) const isPercentage = Math.abs(sum - 100) < 10 || data.some(d => d.value > 100) // Decision tree if (hasProportion || isPercentage) { return data.length <= 5 ? 'pie' : 'bar' } if (hasTimeLabels || hasTimeInText) { return 'line' } // Long labels favor horizontal if (data.some(d => d.label.length > 15)) { return 'horizontal-bar' } return 'bar' } /** * Generate chart suggestions from parsed data * NO AI - instant generation */ export function generateChartSuggestions(data: ChartDataPoint[]): Array<{ type: 'bar' | 'horizontal-bar' | 'line' | 'area' | 'pie' title: string data: ChartDataPoint[] description: string }> { const suggestions: ReturnType = [] // Always include bar chart as fallback suggestions.push({ type: 'bar', title: 'Comparaison', data, description: 'Graphique à barres pour comparer les valeurs', }) // Add line if we have enough data points if (data.length >= 3) { suggestions.push({ type: 'line', title: 'Tendance', data, description: 'Graphique linéaire pour visualiser la progression', }) } // Add area for magnitude if (data.length >= 3) { suggestions.push({ type: 'area', title: 'Évolution', data, description: 'Graphique en aires pour souligner le volume', }) } // Add pie if data looks proportional const sum = data.reduce((a, b) => a + b.value, 0) if (data.length >= 2 && data.length <= 6 && (sum > 90 || data.some(d => d.label.includes('%')))) { suggestions.push({ type: 'pie', title: 'Répartition', data, description: 'Graphique circulaire pour montrer les proportions', }) } // Add horizontal-bar for long labels if (data.some(d => d.label.length > 12)) { suggestions.push({ type: 'horizontal-bar', title: 'Comparaison', data, description: 'Barres horizontales pour les labels longs', }) } // Return max 3 suggestions return suggestions.slice(0, 3) }