Something went wrong. Try again.
svelte astro
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503<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 { use, type ECharts, init } from "echarts/core"; import { PAINTS, proyectState, tableState, TwoPlotState, headers, filteredRows, } 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 Block from "./Block.svelte"; import CrossCursor from "./CrossCursor.svelte";
let color: string[] = $state([]); let container!: HTMLDivElement;
// 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 = proyectState.current?.data.col([ "ra", "ra_icrs", "RA", "RA_ICRS", ]); if (xColumn === null) { TwoPlotState.xColumn = 0; } else { TwoPlotState.xColumn = xColumn; } } if (!TwoPlotState.yColumn) { let yColumn = proyectState.current?.data.col([ "dec", "de_icrs", "DEC", "DE_ICRS", ]); if (yColumn === null) { TwoPlotState.yColumn = 1; } else { TwoPlotState.yColumn = yColumn; } } });
// Register the required components (enables echarts' tree-shaking API) use([ ScatterChart, TooltipComponent, VisualMapComponent, DataZoomComponent, CanvasRenderer, ]);
// Holds the single chart instance for this component's lifetime. let chart: 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() { if (!proyectState.current) return null;
const colorNum = proyectState.current?.color ?? -1; const activePaint = colorNum >= 0 ? PAINTS[colorNum] : undefined;
// Resolve the active paint's column, if any. Unlike before (when the // only option was "bp-rp"), a project may not have every paint's // column, so a missing column just means "no coloring" instead of // failing the whole plot. const colorCol = activePaint ? proyectState.current?.data.col(activePaint.columnNames) : null; const paintUsable = activePaint !== undefined && colorCol !== null && colorCol !== undefined;
if (activePaint && !paintUsable) { console.warn( `Paint "${activePaint.label}" needs column "${activePaint.columnNames}", which is missing from this dataset.`, ); }
// `idxs` are indices into `_mainData` for rows passing every active // filter (see State.svelte.ts). Reading filteredRows() here makes // this effect re-run whenever the current project or any filter's // `active` flag changes, so the plot stays in sync with filters. const idxs = filteredRows(); const data = idxs.map((i) => { const row = proyectState.current!.data._mainData[i]; const x: string = row[TwoPlotState.xColumn as number]?.trim() ?? ""; const y: string = row[TwoPlotState.yColumn as number]?.trim() ?? ""; // Dummy value when there's no usable paint; ignored since `color` // is a single-entry array in that case (see the effect below), // so every point gets the same color regardless of this value. const colorVal = paintUsable ? parseFloat(row[colorCol as number].trim()) : 0;
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; } 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}`;
// params.dataIndex is a position within `data` (i.e. // within the filtered rows), not a raw row index, so it // has to be translated through `idxs` first. TwoPlotState.hoveredIndex = idxs[params.dataIndex] - 1;
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 the active paint's column min: paintUsable ? activePaint!.colorRGB[0].stop : 0, max: paintUsable ? activePaint!.colorRGB.at(-1)?.stop : 1, 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: !paintUsable, 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, }, }, }, ], };
// Register what point is clicked on with echarts api chart?.off("click"); chart?.on("click", (params) => { TwoPlotState.clickedIndex = idxs[params.dataIndex] - 1; });
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 = init(node, null, { renderer: "canvas" });
resizeObserver = new ResizeObserver(() => chart?.resize()); resizeObserver.observe(node);
return { destroy() { resizeObserver?.disconnect(); chart?.dispose(); chart = null; }, }; }
$effect(() => { if (chart) { const options = buildOption(); if (!options) { console.log("oh oh"); return; } chart.setOption(options, { notMerge: false }); chart.dispatchAction({ type: "dataZoom", start: 0, end: 100 }); // Reset zoom scale when rebuilding plot } });
$effect(() => { const colorNum = proyectState.current?.color ?? -1; color = colorNum >= 0 && PAINTS[colorNum] ? PAINTS[colorNum].colorHex : ["#000000"]; });</script>
{#snippet starInfo(index: number)} {@const info = proyectState.current?.data.getStarRecors(index)} {#if info} <div class="star-info"> {#each Object.entries(info) as [key, value]} <span> <strong>{key}:</strong> {#if value} {value} {:else} [undefined] {/if} </span> {/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> {/if}{/snippet}
<div class="wrapper"> <div class="info"> {#if TwoPlotState.hoveredIndex} <Block title="Hover" color="var(--magenta)"> {@render starInfo(TwoPlotState.hoveredIndex)} </Block> {/if} {#if TwoPlotState.clickedIndex} <Block title="Clicked" color="var(--violet)"> {@render starInfo(TwoPlotState.clickedIndex)} </Block> {/if} </div>
<select class="axis-select y-axis-select" bind:value={TwoPlotState.yColumn}> {#each headers()! as header, i} {@const headerMeta = proyectState.current!.data.meta([header])} {#if headerMeta} {#if headerMeta.type === "number"} <option value={i} >{header} {#if headerMeta.unit} ({headerMeta.unit}) {/if} </option> {/if} {/if} {/each} </select>
<div class="plot-container" onmousemove={handlePlotMouseMove} onmouseleave={handlePlotMouseLeave} role="figure" > <CrossCursor {mouse} /> <div class="plot" bind:this={container} use:mountChart></div> </div>
<select class="axis-select x-axis-select" bind:value={TwoPlotState.xColumn}> {#each headers()! as header, i} {@const headerMeta = proyectState.current!.data.meta([header])} {#if headerMeta} {#if headerMeta.type === "number"} <option value={i} >{header} {#if headerMeta.unit} ({headerMeta.unit}) {/if} </option> {/if} {/if} {/each} </select></div>
<style> :global(.plot canvas) { cursor: none !important; }
.wrapper { grid-template-columns: max-content 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; width: max-content; max-width: 90vw; margin: 5px; }
.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>