feat(viz): backtracking, greedy, intervals, matrix

This commit is contained in:
2025-09-03 21:47:33 +01:00
parent 31c74f177b
commit 67c8932c14
15 changed files with 3063 additions and 1 deletions

View File

@@ -0,0 +1,115 @@
'use client';
import { cn } from '@/lib/utils';
import type { IntervalListState } from '@/lib/visualizations/types';
import { IntervalBar } from '../primitives/interval-bar';
import { AnimatePresence } from 'framer-motion';
interface IntervalViewProps {
intervalList: IntervalListState;
className?: string;
}
const PIXELS_PER_UNIT = 20;
const BAR_HEIGHT = 28;
const ROW_GAP = 8;
export function IntervalView({
intervalList,
className,
}: IntervalViewProps) {
// Calculate timeline range
const minValue = intervalList.minValue ?? 0;
const maxValue = intervalList.maxValue ?? Math.max(
...intervalList.intervals.map((i) => i.end),
20
);
const timelineWidth = (maxValue - minValue) * PIXELS_PER_UNIT;
// Generate tick marks for the number line
const tickStep = maxValue <= 10 ? 1 : maxValue <= 20 ? 2 : 5;
const ticks: number[] = [];
for (let i = minValue; i <= maxValue; i += tickStep) {
ticks.push(i);
}
// Calculate row assignments to prevent overlapping intervals
const rows: IntervalListState['intervals'][] = [];
for (const interval of intervalList.intervals) {
let placed = false;
for (const row of rows) {
const overlaps = row.some(
(existing) => !(interval.end <= existing.start || interval.start >= existing.end)
);
if (!overlaps) {
row.push(interval);
placed = true;
break;
}
}
if (!placed) {
rows.push([interval]);
}
}
const intervalsHeight = rows.length * (BAR_HEIGHT + ROW_GAP) - ROW_GAP;
return (
<div className={cn('flex flex-col items-center', className)}>
{intervalList.label && (
<span className="mb-2 text-sm font-medium text-[var(--muted-foreground)]">
{intervalList.label}
</span>
)}
<div className="relative" style={{ width: `${timelineWidth}px` }}>
{/* Intervals area */}
<div
className="relative mb-2"
style={{ height: `${Math.max(intervalsHeight, BAR_HEIGHT)}px` }}
>
<AnimatePresence mode="popLayout">
{rows.map((row, rowIndex) =>
row.map((interval) => (
<div
key={interval.id}
style={{ position: 'absolute', top: `${rowIndex * (BAR_HEIGHT + ROW_GAP)}px` }}
>
<IntervalBar
interval={interval}
pixelsPerUnit={PIXELS_PER_UNIT}
minValue={minValue}
height={BAR_HEIGHT}
/>
</div>
))
)}
</AnimatePresence>
</div>
{/* Number line */}
<div className="relative h-6">
{/* Horizontal line */}
<div
className="absolute top-0 h-px bg-[var(--border)]"
style={{ width: `${timelineWidth}px` }}
/>
{/* Tick marks and labels */}
{ticks.map((tick) => {
const x = (tick - minValue) * PIXELS_PER_UNIT;
return (
<div key={tick} className="absolute" style={{ left: `${x}px` }}>
<div className="h-2 w-px bg-[var(--border)]" />
<span className="absolute -translate-x-1/2 mt-1 text-xs text-[var(--muted-foreground)]">
{tick}
</span>
</div>
);
})}
</div>
</div>
</div>
);
}