-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add visualization support with charts and mermaid diagrams #89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| import { useMemo } from 'react'; | ||
| import Papa from 'papaparse'; | ||
| import { | ||
| LineChart, | ||
| BarChart, | ||
| PieChart, | ||
| AreaChart, | ||
| Line, | ||
| Bar, | ||
| Pie, | ||
| Area, | ||
| XAxis, | ||
| YAxis, | ||
| CartesianGrid, | ||
| Tooltip, | ||
| Legend, | ||
| ResponsiveContainer, | ||
| Cell, | ||
| } from 'recharts'; | ||
|
|
||
| export type ChartType = 'line' | 'bar' | 'pie' | 'area'; | ||
|
|
||
| interface ChartViewerProps { | ||
| csvData: string; | ||
| chartType?: ChartType; | ||
| title?: string; | ||
| } | ||
|
|
||
| const COLORS = [ | ||
| 'hsl(var(--primary))', | ||
| 'hsl(var(--secondary))', | ||
| 'hsl(var(--accent))', | ||
| '#8884d8', | ||
| '#82ca9d', | ||
| '#ffc658', | ||
| '#ff7c7c', | ||
| '#a28dff', | ||
| ]; | ||
|
|
||
| function parseCSV(csv: string): Array<Record<string, any>> { | ||
| // Use papaparse for proper CSV parsing (handles quotes, escapes, etc.) | ||
| const result = Papa.parse(csv.trim(), { | ||
| header: true, | ||
| skipEmptyLines: true, | ||
| dynamicTyping: true, // Auto-converts numbers | ||
| transform: (value) => { | ||
| // Handle empty strings - keep as string instead of converting to 0 | ||
| const trimmed = value.trim(); | ||
| if (trimmed === '') return trimmed; | ||
|
|
||
| // Try to parse as number, otherwise keep as string | ||
| const num = Number(trimmed); | ||
| return trimmed !== '' && !isNaN(num) ? num : trimmed; | ||
| }, | ||
| }); | ||
|
|
||
| if (result.errors.length > 0) { | ||
| console.error('CSV parsing errors:', result.errors); | ||
| } | ||
|
|
||
| return result.data as Array<Record<string, any>>; | ||
| } | ||
|
|
||
| function detectChartType(data: Array<Record<string, any>>): ChartType { | ||
| if (data.length === 0) return 'bar'; | ||
|
|
||
| const keys = Object.keys(data[0]); | ||
| const numericKeys = keys.filter((key) => typeof data[0][key] === 'number'); | ||
|
|
||
| // If only one numeric column and few rows, suggest pie chart | ||
| if (numericKeys.length === 1 && data.length <= 10) { | ||
| return 'pie'; | ||
| } | ||
|
|
||
| // Default to line chart for time series data | ||
| return 'line'; | ||
| } | ||
|
|
||
| export function ChartViewer({ csvData, chartType, title }: ChartViewerProps) { | ||
| const data = useMemo(() => parseCSV(csvData), [csvData]); | ||
|
|
||
| const detectedChartType = chartType || detectChartType(data); | ||
|
|
||
| if (data.length === 0) { | ||
| return ( | ||
| <div className="flex items-center justify-center h-64 border rounded-md bg-muted/10"> | ||
| <p className="text-muted-foreground">No data to display</p> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| const keys = Object.keys(data[0]); | ||
| const xKey = keys[0]; // First column is X-axis | ||
| const dataKeys = keys.slice(1).filter((key) => typeof data[0][key] === 'number'); | ||
|
|
||
| const renderChart = () => { | ||
| switch (detectedChartType) { | ||
| case 'line': | ||
| return ( | ||
| <LineChart data={data}> | ||
| <CartesianGrid strokeDasharray="3 3" /> | ||
| <XAxis dataKey={xKey} /> | ||
| <YAxis /> | ||
| <Tooltip /> | ||
| <Legend /> | ||
| {dataKeys.map((key, index) => ( | ||
| <Line | ||
| key={key} | ||
| type="monotone" | ||
| dataKey={key} | ||
| stroke={COLORS[index % COLORS.length]} | ||
| strokeWidth={2} | ||
| /> | ||
| ))} | ||
| </LineChart> | ||
| ); | ||
|
|
||
| case 'bar': | ||
| return ( | ||
| <BarChart data={data}> | ||
| <CartesianGrid strokeDasharray="3 3" /> | ||
| <XAxis dataKey={xKey} /> | ||
| <YAxis /> | ||
| <Tooltip /> | ||
| <Legend /> | ||
| {dataKeys.map((key, index) => ( | ||
| <Bar key={key} dataKey={key} fill={COLORS[index % COLORS.length]} /> | ||
| ))} | ||
| </BarChart> | ||
| ); | ||
|
|
||
| case 'area': | ||
| return ( | ||
| <AreaChart data={data}> | ||
| <CartesianGrid strokeDasharray="3 3" /> | ||
| <XAxis dataKey={xKey} /> | ||
| <YAxis /> | ||
| <Tooltip /> | ||
| <Legend /> | ||
| {dataKeys.map((key, index) => ( | ||
| <Area | ||
| key={key} | ||
| type="monotone" | ||
| dataKey={key} | ||
| fill={COLORS[index % COLORS.length]} | ||
| stroke={COLORS[index % COLORS.length]} | ||
| /> | ||
| ))} | ||
| </AreaChart> | ||
| ); | ||
|
|
||
| case 'pie': | ||
| // For pie charts, use the first numeric column | ||
| const pieDataKey = dataKeys[0]; | ||
| return ( | ||
| <PieChart> | ||
| <Pie | ||
| data={data} | ||
| dataKey={pieDataKey} | ||
| nameKey={xKey} | ||
| cx="50%" | ||
| cy="50%" | ||
| outerRadius={80} | ||
| label | ||
| > | ||
| {data.map((_entry, index) => ( | ||
| <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} /> | ||
| ))} | ||
| </Pie> | ||
| <Tooltip /> | ||
| <Legend /> | ||
| </PieChart> | ||
| ); | ||
|
|
||
| default: | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="w-full my-4"> | ||
| {title && <h3 className="text-lg font-semibold mb-2">{title}</h3>} | ||
| <ResponsiveContainer width="100%" height={400}> | ||
| {renderChart()} | ||
| </ResponsiveContainer> | ||
| </div> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { useEffect, useRef, useState } from 'react'; | ||
| import mermaid from 'mermaid'; | ||
|
|
||
| interface MermaidDiagramProps { | ||
| chart: string; | ||
| } | ||
|
|
||
| // Initialize mermaid once | ||
| mermaid.initialize({ | ||
| startOnLoad: false, | ||
| theme: 'default', | ||
| securityLevel: 'strict', | ||
| fontFamily: 'inherit', | ||
| }); | ||
|
|
||
| export function MermaidDiagram({ chart }: MermaidDiagramProps) { | ||
| const containerRef = useRef<HTMLDivElement>(null); | ||
| const [error, setError] = useState<string | null>(null); | ||
| const [id] = useState(() => `mermaid-${Math.random().toString(36).substring(2, 11)}`); | ||
|
|
||
| useEffect(() => { | ||
| const renderDiagram = async () => { | ||
| if (!containerRef.current || !chart.trim()) return; | ||
|
|
||
| try { | ||
| setError(null); | ||
| // Clear previous content | ||
| containerRef.current.innerHTML = ''; | ||
|
|
||
| // Render the diagram | ||
| const { svg } = await mermaid.render(id, chart.trim()); | ||
| containerRef.current.innerHTML = svg; | ||
| } catch (err) { | ||
| console.error('Mermaid rendering error:', err); | ||
| setError(err instanceof Error ? err.message : 'Failed to render diagram'); | ||
| } | ||
| }; | ||
|
|
||
| renderDiagram(); | ||
| }, [chart, id]); | ||
|
|
||
| if (error) { | ||
| return ( | ||
| <div className="border border-destructive rounded-md p-4 my-4 bg-destructive/10"> | ||
| <p className="text-sm text-destructive"> | ||
| <strong>Mermaid Error:</strong> {error} | ||
| </p> | ||
| <pre className="mt-2 text-xs overflow-x-auto">{chart}</pre> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| ref={containerRef} | ||
| className="mermaid-container flex justify-center items-center my-4 p-4 bg-muted/10 rounded-md overflow-x-auto" | ||
| /> | ||
| ); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security Concern: CSV Parsing Vulnerability
The current CSV parser is overly simplistic and vulnerable to CSV injection attacks. It only splits by commas without proper escaping or quoted field handling.
Issues:
"City, State"would be split incorrectly)Recommendation: Use a proper CSV parsing library like
papaparseorcsv-parsethat handles:Example fix: