Something went wrong. Try again.
svelte astro
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283import { parse, type ParseResult } from "@elekcsv/core";
/// COLUMN INDEX
/** Maps normalized column names to their index in a CSV row */export type ColumnIndex = Map<string, number>;
const normalizeHeader = (h: string): string => h.trim().toLowerCase();
/** Builds a lookup index mapping header names to their column position. * Keeps the first occurrence when there are duplicate headers. */export function buildColumnIndex(headers: string[]): ColumnIndex { const index: ColumnIndex = new Map(); headers.forEach((h, i) => { const key = normalizeHeader(h); if (!index.has(key)) index.set(key, i); }); return index;}
/** Column index for a given name, or -1 if not found */export function col(index: ColumnIndex, name: string): number { return index.get(normalizeHeader(name)) ?? -1;}
/** * Tries each candidate header name in order and returns the first one * present in this CSV */export function colAny(index: ColumnIndex, names: string[]): number { for (const name of names) { const i = col(index, name); if (i !== -1) return i; } return -1;}
/// STAR COLOR
/** Color stops for the stellar color sequence, hot blue to cool red/brown * dwarfs, expressed directly in BP-RP units. */
export type ColorStops = { stop: number; color: [number, number, number] }
/** Returns a vivid rgb hex color for a BP-RP (or B-V) color index */// utils.tsexport function getColor(val: number, colorStops: ColorStops[]): string { if (!colorStops || colorStops.length === 0) return "#000000";
const min = colorStops[0].stop; const max = colorStops[colorStops.length - 1].stop; const x = Math.min(Math.max(val, min), max);
// Buscar el segmento let lower = colorStops[0]; let upper = colorStops[colorStops.length - 1]; for (let i = 0; i < colorStops.length - 1; i++) { if (x >= colorStops[i].stop && x <= colorStops[i + 1].stop) { lower = colorStops[i]; upper = colorStops[i + 1]; break; } }
const range = upper.stop - lower.stop; const t = range === 0 ? 0 : (x - lower.stop) / range; const lerp = (a: number, b: number) => Math.round(a + (b - a) * t); const toHex = (v: number) => v.toString(16).padStart(2, "0");
return `#${toHex(lerp(lower.color[0], upper.color[0]))}${toHex(lerp(lower.color[1], upper.color[1]))}${toHex(lerp(lower.color[2], upper.color[2]))}`;}
/// COLUMN METADATA (generated from the parsed file)
/** Everything we know about a single column of a loaded CSV. */export type ColumnMeta = { /** The header exactly as it appears in the file */ name: string; /** Value type, inferred from the file's own data */ type: "number" | "string"; /** From the source file when it carries one, else a prettified `name` */ description: string; /** e.g. "mas", "deg". `undefined` when the file didn't carry one */ unit?: string;};
/** Currently supported VizieR formats store metadata with '#' */const isCommentLine = (line: string): boolean => line.startsWith("#") || line.startsWith("\\");const stripCommentPrefix = (line: string): string => line.replace(/^[#\\]\s?/, "");
/** VizieR's dashed column-width separator */function isSeparatorLine(line: string): boolean { const bare = line.trim(); return bare.length > 0 && /^[\s\-|;,\t]+$/.test(bare) && bare.includes("-");}
/** Turns "phot_bp_mean_mag" into "Phot Bp Mean Mag" */function prettifyName(name: string): string { return name .replace(/[_\-]+/g, " ") .trim() .replace(/\b\w/g, (c) => c.toUpperCase());}
/** Infers a column's type from a sample value. Long all-digit strings are * kept as strings on purpose: parsing them as floats silently loses precision. */function inferType(sample: string | undefined): "number" | "string" { if (sample === undefined || sample === "") return "string"; if (/^\d{16,}$/.test(sample)) return "string"; return Number.isNaN(parseFloat(sample)) ? "string" : "number";}
const DELIMITER_CANDIDATES = [",", ";", "\t", "|"];
/** Picks whichever candidate delimiter shows up most often in the header line */function detectDelimiter(headerLine: string): string { let best = DELIMITER_CANDIDATES[0]; let bestCount = 0; for (const d of DELIMITER_CANDIDATES) { const count = headerLine.split(d).length - 1; if (count > bestCount) { best = d; bestCount = count; } } return best;}
/** True when every non-empty cell looks like a short unit token */function looksLikeUnitsRow(cells: string[]): boolean { const nonEmpty = cells.filter((c) => c.length > 0); return nonEmpty.length > 0 && nonEmpty.every((c) => !/\s/.test(c) && c.length <= 12);}
/** Reads an optional '#'-prefixed metadata row, returning its cells only if * the line is a comment that splits into exactly `expected` cells. */function commentRowCells(line: string | undefined, delimiter: string, expected: number): string[] | null { if (line === undefined || !isCommentLine(line)) return null; const cells = stripCommentPrefix(line).split(delimiter).map((c) => c.trim()); return cells.length === expected ? cells : null;}
/** * Splits raw catalog text into what a plain CSV parser needs (`body`, * `delimiter`) plus whatever per-column metadata the file itself carried. * Transparently handles plain local CSVs, VizieR-style exports (an optional * units row, an optional description row, an optional dashed separator — * each only consumed if it actually looks like what it claims to be), and * anything in between. */function extractCatalogMeta(text: string): { meta: Map<number, ColumnMeta>; body: string; delimiter: string } { const lines = text.split(/\r\n|\n/);
let i = 0; while (i < lines.length && lines[i].trim() === "") i++; if (i >= lines.length) return { meta: new Map(), body: text, delimiter: "," };
const headerLine = stripCommentPrefix(lines[i]); const delimiter = detectDelimiter(headerLine); const headers = headerLine.split(delimiter).map((h) => h.trim()); i++;
const meta = new Map<number, ColumnMeta>(); headers.forEach((name, idx) => meta.set(idx, { name, type: "string", description: prettifyName(name) }));
const unitCells = commentRowCells(lines[i], delimiter, headers.length); if (unitCells && looksLikeUnitsRow(unitCells)) { unitCells.forEach((unit, idx) => { if (unit) meta.get(idx)!.unit = unit; }); i++; }
const descriptionCells = commentRowCells(lines[i], delimiter, headers.length); if (descriptionCells && descriptionCells.some((c) => c.length > 0)) { descriptionCells.forEach((description, idx) => { if (description) meta.get(idx)!.description = description; }); i++; }
if (i < lines.length && isSeparatorLine(lines[i])) i++;
const body = [headers.join(delimiter), ...lines.slice(i)].join("\n"); return { meta, body, delimiter };}
/// MAIN DATA
/** CSV parse result extended with a column index and per-column metadata, * generated fresh from the file, never hardcoded. */export type CSVData = ParseResult & { columnIndex: ColumnIndex; columnMeta: Map<number, ColumnMeta>, col: (names: string[]) => number };
/** * Parses raw CSV text into structured data with headers, a column index, and * per-column metadata. Understands VizieR-style exports as well as plain * local CSVs transparently — same function either way. */export function parseCSV(text: string): CSVData { const { meta, body, delimiter } = extractCatalogMeta(text); const result = parse(body, { header: true, delimiter }); const columnIndex = buildColumnIndex(result.headers ?? []); const firstRow = result.rows[0];
for (const [idx, def] of meta) { def.type = inferType(firstRow?.[idx]); }
return { ...result, columnIndex, columnMeta: meta, col: (names: string[]) => colAny(columnIndex, names) };}
function metaFor(csv: CSVData, name: string): ColumnMeta | undefined { const i = col(csv.columnIndex, name); return i !== -1 ? csv.columnMeta.get(i) : undefined;}
/** Human description for a column, from the source file's own metadata when * it carried one, otherwise a prettified version of the column name. */export function columnDescription(csv: CSVData, name: string): string { return metaFor(csv, name)?.description ?? name;}
/** Unit string for a column (e.g. "mas", "deg"), from the source file's own * metadata. `undefined` when the file didn't carry one. */export function columnUnit(csv: CSVData, name: string): string | undefined { return metaFor(csv, name)?.unit;}
/** Ready-to-display label for a column, e.g. "Parallax (mas)". Omits the * unit suffix entirely when there isn't one. */export function columnLabel(csv: CSVData, name: string): string { const description = columnDescription(csv, name); const unit = columnUnit(csv, name); return unit ? `${description} (${unit})` : description;}
/** Reads and parses a single column's value from a row, applying the type * inferred for that column when the file was parsed. */export function readValue(csv: CSVData, row: string[], name: string): number | string | null { const i = col(csv.columnIndex, name); if (i === -1 || row[i] === undefined || row[i] === "") return null; const meta = csv.columnMeta.get(i); if (meta?.type === "number") { const parsed = parseFloat(row[i]); return Number.isNaN(parsed) ? null : parsed; } return row[i];}
export type RowValues = Record<string, number | string | null>;
/** Reads every column's value for a given row, keyed by the column's real name. */export function readRow(csv: CSVData, rowIndex: number): RowValues | null { const row = csv.rows[rowIndex]; if (!row) return null;
const values: RowValues = {}; for (const meta of csv.columnMeta.values()) { values[meta.name] = readValue(csv, row, meta.name); } return values;}
/** Finds the row (star) whose values in two given columns exactly match the * target values. Returns null if either column is missing or nothing matches. */export function findStarExact( csv: CSVData, columnA: string, valueA: string | number, columnB: string, valueB: string | number,): number | null { const iA = col(csv.columnIndex, columnA); const iB = col(csv.columnIndex, columnB); if (iA === -1 || iB === -1) return null;
const rowIndex = csv.rows.findIndex((row) => row[iA] === String(valueA) && row[iB] === String(valueB)); return rowIndex === -1 ? null : rowIndex;}
/// UTILITIES
/** Picks a random tooltip string from a list of options */export function randomTooltip(tooltips: string[]): string { return tooltips[Math.floor(Math.random() * tooltips.length)];}