Files
Momento/memento-note/lib/chart/parser.ts
Antigravity e2672cd2c2
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped
feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 14:27:29 +00:00

197 lines
5.4 KiB
TypeScript

/**
* 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<string>()
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<typeof generateChartSuggestions> = []
// 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)
}