feat(viz): backtracking, greedy, intervals, matrix
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user