Squashed 'vendor/ruvector/' content from commit b64c2172

git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
commit d803bfe2b1
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
import { scaleLinear } from 'd3-scale';
import { select } from 'd3-selection';
import { line } from 'd3-shape';
import { axisBottom, axisLeft } from 'd3-axis';
export interface SpectrumPoint {
wavelength: number;
flux: number;
}
export interface SpectrumBand {
name: string;
start: number;
end: number;
color: string;
}
export class SpectrumChart {
private container: HTMLElement;
private svg: SVGSVGElement | null = null;
private wrapper: HTMLElement | null = null;
private margin = { top: 16, right: 16, bottom: 32, left: 48 };
constructor(container: HTMLElement) {
this.container = container;
this.createSvg();
}
private createSvg(): void {
this.wrapper = document.createElement('div');
this.wrapper.className = 'chart-container';
this.container.appendChild(this.wrapper);
this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
this.wrapper.appendChild(this.svg);
}
update(data: SpectrumPoint[], bands?: SpectrumBand[]): void {
if (!this.svg || !this.wrapper || data.length === 0) return;
const rect = this.wrapper.getBoundingClientRect();
const width = rect.width || 400;
const height = rect.height || 200;
this.svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
const m = this.margin;
const innerW = width - m.left - m.right;
const innerH = height - m.top - m.bottom;
// Use loop to avoid stack overflow with large datasets
let xMin = data[0].wavelength, xMax = data[0].wavelength;
let yMin = data[0].flux, yMax = data[0].flux;
for (let i = 1; i < data.length; i++) {
if (data[i].wavelength < xMin) xMin = data[i].wavelength;
if (data[i].wavelength > xMax) xMax = data[i].wavelength;
if (data[i].flux < yMin) yMin = data[i].flux;
if (data[i].flux > yMax) yMax = data[i].flux;
}
const xExtent = [xMin, xMax];
const yExtent = [yMin, yMax];
const yPad = (yExtent[1] - yExtent[0]) * 0.1 || 0.001;
const xScale = scaleLinear().domain(xExtent).range([0, innerW]);
const yScale = scaleLinear()
.domain([yExtent[0] - yPad, yExtent[1] + yPad])
.range([innerH, 0]);
const sel = select(this.svg);
sel.selectAll('*').remove();
const g = sel
.append('g')
.attr('transform', `translate(${m.left},${m.top})`);
// Molecule absorption bands
if (bands) {
for (const b of bands) {
g.append('rect')
.attr('class', 'band-rect')
.attr('x', xScale(b.start))
.attr('y', 0)
.attr('width', Math.max(1, xScale(b.end) - xScale(b.start)))
.attr('height', innerH)
.attr('fill', b.color);
g.append('text')
.attr('x', xScale((b.start + b.end) / 2))
.attr('y', 10)
.attr('text-anchor', 'middle')
.attr('fill', b.color)
.attr('font-size', '9px')
.text(b.name);
}
}
// Axes
g.append('g')
.attr('class', 'axis')
.attr('transform', `translate(0,${innerH})`)
.call(axisBottom(xScale).ticks(6));
g.append('g').attr('class', 'axis').call(axisLeft(yScale).ticks(5));
// Spectrum line
const lineFn = line<SpectrumPoint>()
.x((d) => xScale(d.wavelength))
.y((d) => yScale(d.flux));
g.append('path')
.datum(data)
.attr('class', 'chart-line')
.attr('d', lineFn)
.attr('stroke', '#2ECC71');
}
destroy(): void {
if (this.wrapper) {
this.wrapper.remove();
}
this.svg = null;
this.wrapper = null;
}
}