Something went wrong. Try again.
svelte astro
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497<script lang="ts"> // NOTE: Most of the comments about echarts come from the official echarts docs: // https://echarts.apache.org/en/api.html#echarts
// Import the echarts core module, which provides the necessary interfaces for using echarts. import * as echarts from "echarts/core"; import { PAINTS, proyectState, tableState, TwoPlotState, } from "../State.svelte"; import { TooltipComponent, DataZoomComponent, VisualMapComponent, } from "echarts/components"; import { ScatterChart } from "echarts/charts";
// Import the Canvas renderer. Importing either CanvasRenderer or SVGRenderer is required. import { CanvasRenderer } from "echarts/renderers"; import { readRow, type CSVData, type RowValues } from "../utils";
import Block from "./Block.svelte"; import CrossCursor1 from "./CrossCursor1.svelte"; import CrossCursor2 from "./CrossCursor2.svelte"; import CrossCursor3 from "./CrossCursor3.svelte"; import CrossCursor4 from "./CrossCursor4.svelte";
// Keys mostradas en el panel de info (hover/click). Se filtran del objeto // completo para no saturar la UI con todas las columnas del CSV. const mainKeys = ["Source", "RA_ICRS", "DE_ICRS", "Plx", "PM", "Gmag"];
// Component state managed by Svelte let { csv }: { csv: CSVData } = $props(); let pinned: { info: RowValues; index: number } | null = $state(null); let hovered: { info: RowValues; index: number } | null = $state(null); let color: string[] = $state([]); let container!: HTMLDivElement;
// Temporary cursor selector let selectedCursor: 1 | 2 | 3 | 4 = $state(1);
// Mouse coords inside plot-container (.plot) for crosscursor let mouse: { x: number; y: number; over: boolean } = $state({ x: 0, y: 0, over: false, });
// Get mouse coords when over plot-container function handlePlotMouseMove(event: MouseEvent) { const rect = ( event.currentTarget as HTMLElement ).getBoundingClientRect(); mouse = { x: event.clientX - rect.left, y: event.clientY - rect.top, over: true, }; }
function handlePlotMouseLeave() { mouse = { ...mouse, over: false }; }
$effect(() => { if (!TwoPlotState.xColumn) { let xColumn = csv.col(["ra", "ra_icrs"]); if (xColumn < 0) { TwoPlotState.xColumn = 0; } else { TwoPlotState.xColumn = xColumn; } } if (!TwoPlotState.yColumn) { let yColumn = csv.col(["dec", "de_icrs"]); if (yColumn < 0) { TwoPlotState.yColumn = 0; } else { TwoPlotState.yColumn = yColumn; } } });
// Register the required components (enables echarts' tree-shaking API) echarts.use([ ScatterChart, TooltipComponent, VisualMapComponent, DataZoomComponent, CanvasRenderer, ]);
// Holds the single chart instance for this component's lifetime. let chart: echarts.ECharts | null;
// Watches the container for size changes and tells echarts to resize itself. // Preferred over a global "resize" window listener: it's scoped to this // element only and fires precisely when the element's box actually changes. let resizeObserver: ResizeObserver | null;
// Builds the option object passed to chart.setOption() function buildOption() { const colorNum = proyectState.current?.color ?? -1; const activePaint = colorNum >= 0 ? PAINTS[colorNum] : undefined;
const colorColName = activePaint ? activePaint.columnName : "bp-rp"; const bvCol = csv.col([colorColName]);
const stops = activePaint?.colorRGB; const minVal = stops ? stops[0].stop : -2; const maxVal = stops ? stops[stops.length - 1].stop : 5;
const data = csv.rows.map((row) => { const x: string = row[TwoPlotState.xColumn as number]?.trim() ?? ""; const y: string = row[TwoPlotState.yColumn as number]?.trim() ?? ""; const colorVal = bvCol !== -1 && row[bvCol] ? parseFloat(row[bvCol].trim()) : NaN;
return [parseFloat(x), parseFloat(y), colorVal]; }); // Min and Max for every axis so zooming doesnt recalculate the min and max values let xMin = Infinity, xMax = -Infinity; let yMin = Infinity, yMax = -Infinity; for (const [x, y, _] of data) { if ((x as number) < xMin) xMin = x as number; if ((x as number) > xMax) xMax = x as number; if ((y as number) < yMin) yMin = y as number; if ((y as number) > yMax) yMax = y as number; }
const headers = csv.headers as string[];
let options = { // For this tooltip the component TooltipComponent needs to be imported in echarts.use([...]) tooltip: { show: true, borderWidth: 3, borderColor: "#a8326f", trigger: "item", formatter: (params: any) => { // im using the tooltip trigger to change the hover info data const xLabel = String( headers[TwoPlotState.xColumn as number], ); const yLabel = String( headers[TwoPlotState.yColumn as number], ); const [x, y, _] = params.value;
const plotData = `${xLabel}: ${x}<br/>${yLabel}: ${y}`;
const starRowIndex = params.dataIndex;
const info = readRow(csv, starRowIndex); if (info != null) { hovered = { info, index: starRowIndex }; }
return plotData; }, }, xAxis: { type: "value", name: "", // Disable axis name because later a <select> tag is used a axis label min: xMin, max: xMax, scale: true, // Enable zoom (i think... i didn't really understand de docs): https://echarts.apache.org/en/option.html#xAxis.scale }, yAxis: { type: "value", name: "", // Disable axis name because later a <select> tag is used a axis label min: yMin, max: yMax, scale: true, // Enable zoom (see: xAxis.scale) }, dataZoom: [ { type: "inside", // Mouse Driven Zoom xAxisIndex: 0, // Indicates that this dataZoom component controls the xAxis filterMode: "filter", // Data that outside the window will be filtered out }, { type: "inside", yAxisIndex: 0, // Indicates that this dataZoom component controls the yAxis filterMode: "filter", // Data that outside the window will be filtered out }, ], visualMap: { show: false, // Hide the color scale indicator dimension: 2, // Third entry in data corresponds to B-v min: minVal, max: maxVal, inRange: { color: color, }, }, series: [ { type: "scatter", cursor: "none", symbolSize: 5, data, selectedMode: "single", // Necesary for Clicked point to show selected // "large" mode switches echarts to an optimized rendering path // for series with many points.
// TODO: I had to togle large off because the visualmap for // star color doesnt behave correctly with large:true. large: proyectState.current!.color < 0, itemStyle: { opacity: 1, color: "#000000", }, emphasis: { // Gives hover a clear, consistent visual response, // independent of whatever is currently pinned. scale: 4, itemStyle: { color: "#a8326f" }, }, select: { itemStyle: { color: "#6a35a8", // Little trick to make Clicked apear larger, borderColor: "#6a35a8", borderWidth: 15, }, }, }, ], }; return options; }
// Svelte action: runs once when `node` is mounted, and its returned // destroy() runs once when the node is unmounted. function mountChart(node: HTMLDivElement) { chart = echarts.init(node, null, { renderer: "canvas" });
resizeObserver = new ResizeObserver(() => chart?.resize()); resizeObserver.observe(node);
return { destroy() { resizeObserver?.disconnect(); chart?.dispose(); chart = null; }, }; }
// The $effect rune re-runs the callback whenever any state it reads changes. // When Svelte runs an effect function, it tracks which pieces of state are // accessed and re-runs the function when that state later changes. // Docs: https://svelte.dev/docs/svelte/$effect // // Here, the dependencies are read indirectly inside buildOption() // (PlotState.xColumn/yColumn, tooltip, csv.rows, csv.headers), so Svelte // tracks those and re-runs this effect whenever any of them changes. $effect(() => { if (chart) { chart.setOption(buildOption(), { notMerge: false }); chart.dispatchAction({ type: "dataZoom", start: 0, end: 100 }); // Reset zoom scale when rebuilding plot chart.on("click", (params) => { const info = readRow(csv, params.dataIndex); if (!info) return; pinned = { info, index: params.dataIndex, }; }); } });
$effect(() => { let colorNum = proyectState.current!.color; if (colorNum >= 0) { color = PAINTS[colorNum].colorHex; } else { color = ["#000000"]; } });</script>
{#snippet starInfo(info: RowValues, index: number)} <div class="star-info"> {#each mainKeys as key} {#if key in info} <span><strong>{key}:</strong> {info[key]}</span> {/if} {/each}
<div class="star-buttons"> <!-- This link to simbad needs to have correct naming convention https://cds.unistra.fr/cgi-bin/Dic-Simbad?Gaia%20DR3%2065020105442238720 --> <a target="_" href={"https://simbad.cds.unistra.fr/simbad/sim-id?Ident=Gaia+DR3+" + info.Source + "&NbIdent=1&Radius=2&Radius.unit=arcmin&submit=submit+id"} >Simbad</a > <button onclick={() => { tableState.currentRow = index; proyectState.current!.currentTab = "table"; }}>Table {index}</button > </div> </div>{/snippet}
<div class="wrapper"> <div class="info"> <div class="cursor-picker"> Cursors: {#each [1, 2, 3, 4] as n} <label> {n} <input type="radio" name="cursor-picker" value={n} bind:group={selectedCursor} /> </label> {/each} </div> {#if hovered} <Block title="Hover" color="var(--magenta)"> {@render starInfo(hovered.info, hovered.index)} </Block> {/if} {#if pinned} <Block title="Clicked" color="var(--violet)"> {@render starInfo(pinned.info, pinned.index)} </Block> {/if} </div>
<select class="axis-select y-axis-select" bind:value={TwoPlotState.yColumn}> {#each csv.headers as col, i}<option value={i}>{col}</option>{/each} </select>
<div class="plot-container" onmousemove={handlePlotMouseMove} onmouseleave={handlePlotMouseLeave} role="figure" > {#if selectedCursor === 1} <CrossCursor1 {mouse} /> {:else if selectedCursor === 2} <CrossCursor2 {mouse} /> {:else if selectedCursor === 3} <CrossCursor3 {mouse} /> {:else} <CrossCursor4 {mouse} /> {/if} <div class="plot" bind:this={container} use:mountChart></div> </div>
<select class="axis-select x-axis-select" bind:value={TwoPlotState.xColumn}> {#each csv.headers as col, i}<option value={i}>{col}</option>{/each} </select></div>
<style> :global(.plot canvas) { cursor: none !important; }
.wrapper { grid-template-columns: 250px max-content min(50vw, 85vh); grid-template-rows: min(50vw, 85vh) max-content;
display: grid; justify-content: center; align-content: center; height: 100%; width: 100%; }
.info { grid-column: 1; grid-row: 1 / -1; display: flex; flex-direction: column; gap: 1em; }
.star-info { display: flex; flex-direction: column; font-size: 0.9em; min-width: 800px; margin: 5px; }
.cursor-picker { display: flex; align-items: center; gap: 0.6em; font-size: 0.85em; }
.cursor-picker label { display: flex; align-items: center; gap: 0.2em; cursor: pointer; }
.y-axis-select { grid-column: 2; grid-row: 1; align-self: center; z-index: 99; }
.plot-container { grid-column: 3; grid-row: 1; position: relative; overflow: hidden; width: 100%; height: 100%; }
.plot { width: 100%; height: 100%; }
.x-axis-select { grid-column: 3; grid-row: 2; justify-self: center; }
.axis-select { font-family: "JetBrains Mono", monospace; font-size: 0.85em; color: var(--text); cursor: pointer;
appearance: none; -webkit-appearance: none; -moz-appearance: none; border: none; outline: none; background: transparent;
transition: transform 0.2s ease, font-weight 0.2s ease; }
.axis-select:hover, .axis-select:focus { transform: scale(1.1); font-weight: bold; }
@media (max-width: 1100px) { .wrapper { grid-template-columns: max-content 1fr; grid-template-rows: auto 1fr auto; } .info { grid-column: 1 / -1; grid-row: 1; flex-direction: row; flex-wrap: wrap; justify-content: center; } .y-axis-select { grid-column: 1; grid-row: 2; } .plot-container { grid-column: 2; grid-row: 2; } .x-axis-select { grid-column: 2; grid-row: 3; } }</style>