Skip to content

Commit b4fbb83

Browse files
Merge pull request #74 from peterjthomson/cursor/chart-theming-and-rendering-2b1e
Chart theming and rendering
2 parents cfabc4e + d5c9cef commit b4fbb83

39 files changed

Lines changed: 8851 additions & 758 deletions

app/components/SettingsPanel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const PANEL_OPTIONS: Record<SlotType, { value: PanelType; label: string }[]> = {
2424
{ value: 'git-graph', label: 'Git Graph' },
2525
{ value: 'timeline', label: 'Timeline' },
2626
{ value: 'tech-tree', label: 'Tech Tree' },
27+
{ value: 'erd-canvas', label: 'ERD' },
28+
{ value: 'file-graph', label: 'Code Map' },
2729
],
2830
}
2931

app/components/canvas/CanvasRenderer.tsx

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { EditorSlot } from './EditorSlot'
3131
// Import panels
3232
import { PRList, BranchList, WorktreeList, StashList, CommitList, Sidebar, RepoList } from '../panels/list'
3333
import { GitGraph, ContributorChart, TechTreeChart, FileGraph } from '../panels/viz'
34+
import { ERDCanvasPanel } from '../panels/viz/erd'
3435

3536
// ========================================
3637
// Data Interface
@@ -402,6 +403,8 @@ export function CanvasRenderer({
402403
{ id: 'git-graph', label: 'Git Graph', icon: '◉' },
403404
{ id: 'timeline', label: 'Timeline', icon: '◔' },
404405
{ id: 'tech-tree', label: 'Tech Tree', icon: '⬡' },
406+
{ id: 'erd-canvas', label: 'ERD', icon: '◫' },
407+
{ id: 'file-graph', label: 'Code Map', icon: '▦' },
405408
]
406409

407410
return (
@@ -494,10 +497,10 @@ export function CanvasRenderer({
494497
case 'tech-tree':
495498
return (
496499
<div className="viz-panel tech-tree-panel">
497-
<VizHeader
500+
<VizHeader
498501
panel={column.panel}
499-
label={column.label || 'Tech Tree'}
500-
icon={column.icon || '⬡'}
502+
label={column.label || 'Tech Tree'}
503+
icon={column.icon || '⬡'}
501504
/>
502505
<div className="viz-panel-content">
503506
<TechTreeChart
@@ -511,17 +514,28 @@ export function CanvasRenderer({
511514
</div>
512515
)
513516

517+
case 'erd-canvas':
518+
return (
519+
<div className="viz-panel erd-canvas-panel">
520+
<VizHeader
521+
panel={column.panel}
522+
label={column.label || 'ERD'}
523+
icon={column.icon || '◫'}
524+
/>
525+
<div className="viz-panel-content erd-canvas-content">
526+
<ERDCanvasPanel repoPath={data.repoPath} />
527+
</div>
528+
</div>
529+
)
530+
514531
case 'file-graph':
515532
return (
516533
<div className="viz-panel file-graph-panel">
517-
<div className="column-header">
518-
<div className="column-title">
519-
<h2>
520-
<span className="column-icon">{column.icon || '▦'}</span>
521-
{column.label || 'Code Map'}
522-
</h2>
523-
</div>
524-
</div>
534+
<VizHeader
535+
panel={column.panel}
536+
label={column.label || 'Code Map'}
537+
icon={column.icon || '▦'}
538+
/>
525539
<div className="viz-panel-content file-graph-content">
526540
<FileGraph data={data.fileGraph} loading={data.fileGraphLoading} />
527541
</div>
@@ -538,6 +552,7 @@ export function CanvasRenderer({
538552
},
539553
[
540554
data.commits,
555+
data.repoPath,
541556
data.fileGraph,
542557
data.fileGraphLoading,
543558
selection.selectedCommit,

app/components/canvas/Column.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,20 @@ export function Column({
110110
data-column-id={column.id}
111111
data-width-mode={column.width === 'flex' ? 'flex' : 'fixed'}
112112
style={style}
113-
draggable={canDrag}
114-
onDragStart={canDrag ? handleDragStart : undefined}
115113
onDragOver={canDrag ? handleDragOver : undefined}
116114
onDrop={canDrag ? handleDrop : undefined}
117115
onDragEnd={canDrag ? handleDragEnd : undefined}
118116
onDragLeave={canDrag ? onDragLeave : undefined}
119117
>
120-
{/* Drag handle - only show if dragging is enabled */}
118+
{/* Drag handle - only the handle is draggable, not the entire column */}
119+
{/* This allows canvas content (like tldraw) to handle its own pointer events */}
121120
{canDrag && (
122-
<div className="canvas-column-drag-handle" title="Drag to reorder">
121+
<div
122+
className="canvas-column-drag-handle"
123+
title="Drag to reorder"
124+
draggable
125+
onDragStart={handleDragStart}
126+
>
123127
⋮⋮
124128
</div>
125129
)}

app/components/panels/viz/ContributorChart.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,8 @@ export function ContributorChart({
285285
<defs>
286286
{/* Gradient for the glow effect */}
287287
<linearGradient id="ridge-glow" x1="0%" y1="0%" x2="0%" y2="100%">
288-
<stop offset="0%" stopColor={invertedTheme ? '#fff' : 'var(--accent)'} stopOpacity="0.8" />
289-
<stop offset="100%" stopColor={invertedTheme ? '#fff' : 'var(--accent)'} stopOpacity="0" />
288+
<stop offset="0%" stopColor={invertedTheme ? 'var(--text-primary)' : 'var(--accent)'} stopOpacity="0.8" />
289+
<stop offset="100%" stopColor={invertedTheme ? 'var(--text-primary)' : 'var(--accent)'} stopOpacity="0" />
290290
</linearGradient>
291291

292292
{/* Clip path for clean edges */}
@@ -301,7 +301,7 @@ export function ContributorChart({
301301
y="0"
302302
width={chartWidth}
303303
height={height}
304-
fill={invertedTheme ? '#0a0a0a' : 'var(--bg-secondary)'}
304+
fill={invertedTheme ? 'var(--bg-primary)' : 'var(--bg-secondary)'}
305305
/>
306306

307307
{/* Ridgelines - render back to front for proper overlap */}
@@ -325,7 +325,7 @@ export function ContributorChart({
325325
{/* Fill area - solid background to occlude lines behind */}
326326
<path
327327
d={fillPath}
328-
fill={invertedTheme ? '#0a0a0a' : 'var(--bg-secondary)'}
328+
fill={invertedTheme ? 'var(--bg-primary)' : 'var(--bg-secondary)'}
329329
className="ridge-fill-bg"
330330
/>
331331

@@ -341,7 +341,7 @@ export function ContributorChart({
341341
<path
342342
d={linePath}
343343
fill="none"
344-
stroke={invertedTheme ? '#ffffff' : 'var(--accent)'}
344+
stroke={invertedTheme ? 'var(--text-primary)' : 'var(--accent)'}
345345
strokeWidth={isHovered ? 2.5 : 1.5}
346346
opacity={isHovered ? 1 : 0.85}
347347
className="ridge-line"
@@ -368,7 +368,7 @@ export function ContributorChart({
368368
y={baseY + 4}
369369
textAnchor="end"
370370
className={`author-label ${isHovered ? 'hovered' : ''}`}
371-
fill={invertedTheme ? '#ffffff' : 'var(--text-primary)'}
371+
fill="var(--text-primary)"
372372
opacity={isHovered ? 1 : 0.7}
373373
fontSize="12"
374374
fontFamily="var(--font-sans, system-ui)"
@@ -382,7 +382,7 @@ export function ContributorChart({
382382
y={baseY + 18}
383383
textAnchor="end"
384384
className="commit-count"
385-
fill={invertedTheme ? '#666' : 'var(--text-muted)'}
385+
fill="var(--text-muted)"
386386
fontSize="10"
387387
fontFamily="var(--font-mono, monospace)"
388388
>
@@ -400,7 +400,7 @@ export function ContributorChart({
400400
y1={height - bottomPadding + 10}
401401
x2={chartWidth - 20}
402402
y2={height - bottomPadding + 10}
403-
stroke={invertedTheme ? '#333' : 'var(--border)'}
403+
stroke="var(--border)"
404404
strokeWidth="1"
405405
/>
406406
{timeLabels.map((label, i) => (
@@ -409,7 +409,7 @@ export function ContributorChart({
409409
x={label.x}
410410
y={height - bottomPadding + 28}
411411
textAnchor="middle"
412-
fill={invertedTheme ? '#666' : 'var(--text-muted)'}
412+
fill="var(--text-muted)"
413413
fontSize="11"
414414
fontFamily="var(--font-sans, system-ui)"
415415
>

app/components/panels/viz/FileGraph.tsx

Lines changed: 28 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -7,151 +7,22 @@
77

88
import { useState, useMemo, useCallback, useRef, useEffect } from 'react'
99
import type { FileGraphData, FileNode } from '../../../types/electron'
10+
import { squarify } from './file-graph/layout/treemap-layout'
1011

1112
export interface FileGraphProps {
1213
data: FileGraphData | null
1314
loading?: boolean
1415
}
1516

16-
// Language colors
17-
const LANGUAGE_COLORS: Record<string, string> = {
18-
TypeScript: '#3178C6',
19-
JavaScript: '#F7DF1E',
20-
CSS: '#563D7C',
21-
SCSS: '#CC6699',
22-
HTML: '#E34C26',
23-
JSON: '#F5D800',
24-
Markdown: '#083FA1',
25-
Python: '#3776AB',
26-
Go: '#00ADD8',
27-
Rust: '#DEA584',
28-
Java: '#B07219',
29-
Ruby: '#CC342D',
30-
PHP: '#4F5D95',
31-
C: '#555555',
32-
'C++': '#F34B7D',
33-
'C#': '#178600',
34-
Swift: '#F05138',
35-
Kotlin: '#A97BFF',
36-
Shell: '#89E051',
37-
YAML: '#CB171E',
38-
TOML: '#9C4221',
39-
XML: '#0060AC',
40-
SQL: '#E38C00',
41-
GraphQL: '#E535AB',
42-
Vue: '#41B883',
43-
Svelte: '#FF3E00',
44-
Other: '#6B7280',
45-
}
46-
47-
interface TreemapRect {
48-
node: FileNode
49-
x: number
50-
y: number
51-
width: number
52-
height: number
53-
}
54-
55-
/** Squarified treemap layout */
56-
function squarify(
57-
nodes: FileNode[],
58-
x: number,
59-
y: number,
60-
width: number,
61-
height: number,
62-
totalValue: number
63-
): TreemapRect[] {
64-
if (nodes.length === 0 || totalValue === 0) return []
65-
66-
const rects: TreemapRect[] = []
67-
const sorted = [...nodes].sort((a, b) => b.lines - a.lines)
68-
69-
let currentX = x
70-
let currentY = y
71-
let remainingWidth = width
72-
let remainingHeight = height
73-
let remainingValue = totalValue
74-
75-
let row: FileNode[] = []
76-
let rowValue = 0
77-
78-
function aspectRatio(areas: number[], length: number): number {
79-
if (areas.length === 0 || length === 0) return Infinity
80-
const sum = areas.reduce((a, b) => a + b, 0)
81-
const min = Math.min(...areas)
82-
const max = Math.max(...areas)
83-
const s2 = sum * sum
84-
const l2 = length * length
85-
return Math.max((l2 * max) / s2, s2 / (l2 * min))
86-
}
87-
88-
function layoutRow(row: FileNode[], rowValue: number, isHorizontal: boolean) {
89-
if (row.length === 0) return
90-
const rowLength = isHorizontal
91-
? (rowValue / remainingValue) * remainingHeight
92-
: (rowValue / remainingValue) * remainingWidth
93-
let offset = 0
94-
for (const node of row) {
95-
const nodeRatio = node.lines / rowValue
96-
const nodeLength = isHorizontal ? nodeRatio * remainingWidth : nodeRatio * remainingHeight
97-
rects.push({
98-
node,
99-
x: isHorizontal ? currentX + offset : currentX,
100-
y: isHorizontal ? currentY : currentY + offset,
101-
width: isHorizontal ? nodeLength : rowLength,
102-
height: isHorizontal ? rowLength : nodeLength,
103-
})
104-
offset += nodeLength
105-
}
106-
if (isHorizontal) {
107-
currentY += rowLength
108-
remainingHeight -= rowLength
109-
} else {
110-
currentX += rowLength
111-
remainingWidth -= rowLength
112-
}
113-
remainingValue -= rowValue
114-
}
115-
116-
for (const node of sorted) {
117-
const isHorizontal = remainingWidth >= remainingHeight
118-
const length = isHorizontal ? remainingWidth : remainingHeight
119-
const rowAreas = row.map((n) => (n.lines / remainingValue) * length * (isHorizontal ? remainingHeight : remainingWidth))
120-
const newAreas = [...rowAreas, (node.lines / remainingValue) * length * (isHorizontal ? remainingHeight : remainingWidth)]
121-
const currentAspect = aspectRatio(rowAreas, length)
122-
const newAspect = aspectRatio(newAreas, length)
123-
124-
if (row.length === 0 || newAspect <= currentAspect) {
125-
row.push(node)
126-
rowValue += node.lines
127-
} else {
128-
layoutRow(row, rowValue, isHorizontal)
129-
row = [node]
130-
rowValue = node.lines
131-
}
132-
}
133-
134-
if (row.length > 0) {
135-
layoutRow(row, rowValue, remainingWidth >= remainingHeight)
136-
}
137-
138-
return rects
139-
}
17+
const DEFAULT_LANGUAGE_COLOR = 'var(--chart-1)'
18+
const FALLBACK_LANGUAGE_COLOR = 'var(--chart-8)'
14019

14120
function formatLines(lines: number): string {
14221
if (lines >= 1000000) return `${(lines / 1000000).toFixed(1)}M`
14322
if (lines >= 1000) return `${(lines / 1000).toFixed(1)}K`
14423
return lines.toString()
14524
}
14625

147-
function getContrastColor(hex: string): string {
148-
const r = parseInt(hex.slice(1, 3), 16)
149-
const g = parseInt(hex.slice(3, 5), 16)
150-
const b = parseInt(hex.slice(5, 7), 16)
151-
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
152-
return luminance > 0.5 ? '#000000' : '#FFFFFF'
153-
}
154-
15526
function truncateLabel(label: string, width: number): string {
15627
const maxChars = Math.floor(width / 7)
15728
if (label.length <= maxChars) return label
@@ -196,6 +67,26 @@ export function FileGraph({ data, loading }: FileGraphProps) {
19667
return node
19768
}, [data, currentPath])
19869

70+
const languageColorMap = useMemo(() => {
71+
const map = new Map<string, string>()
72+
if (!data) return map
73+
for (const language of data.languages) {
74+
map.set(language.language, language.color || DEFAULT_LANGUAGE_COLOR)
75+
}
76+
if (!map.has('Other')) {
77+
map.set('Other', FALLBACK_LANGUAGE_COLOR)
78+
}
79+
return map
80+
}, [data])
81+
82+
const legendLanguages = useMemo(() => {
83+
if (!data) return []
84+
return data.languages.slice(0, 8).map((lang) => ({
85+
...lang,
86+
color: languageColorMap.get(lang.language) || lang.color || DEFAULT_LANGUAGE_COLOR,
87+
}))
88+
}, [data, languageColorMap])
89+
19990
// Calculate treemap layout
20091
const treemapRects = useMemo(() => {
20192
if (!currentNode?.children) return []
@@ -221,9 +112,10 @@ export function FileGraph({ data, loading }: FileGraphProps) {
221112
}, [])
222113

223114
const getNodeColor = useCallback((node: FileNode): string => {
224-
if (node.isDirectory) return 'var(--bg-tertiary)'
225-
return LANGUAGE_COLORS[node.language || 'Other'] || LANGUAGE_COLORS.Other
226-
}, [])
115+
if (node.isDirectory) return 'var(--bg-hover)'
116+
const language = node.language || 'Other'
117+
return languageColorMap.get(language) || DEFAULT_LANGUAGE_COLOR
118+
}, [languageColorMap])
227119

228120
if (loading) {
229121
return (
@@ -297,7 +189,6 @@ export function FileGraph({ data, loading }: FileGraphProps) {
297189
className="file-graph-label"
298190
style={{
299191
fontSize: Math.min(12, minDim / 4),
300-
fill: rect.node.isDirectory ? 'var(--text-primary)' : getContrastColor(getNodeColor(rect.node)),
301192
}}
302193
>
303194
{truncateLabel(rect.node.name, rect.width)}
@@ -311,7 +202,7 @@ export function FileGraph({ data, loading }: FileGraphProps) {
311202

312203
{/* Legend */}
313204
<div className="file-graph-legend">
314-
{data.languages.slice(0, 8).map((lang) => (
205+
{legendLanguages.map((lang) => (
315206
<div key={lang.language} className="legend-item">
316207
<span className="legend-color" style={{ backgroundColor: lang.color }} />
317208
<span className="legend-label">{lang.language}</span>

0 commit comments

Comments
 (0)