Something went wrong. Try again.
[PRE-ALPHA] an open source web-based 2D/3D graphing calculator cartesium.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297<script lang="ts"> import type { Viewport } from '#lib/engine/types.js'; import type { GraphAppearance } from '#lib/graph/appearance.js';
type BoundKey = 'x_min' | 'x_max' | 'y_min' | 'y_max';
let { viewport, equalAxisScale, complexMode, appearance, onboundschange, onappearancechange, oncomplexmodechange, onresetscale, onscalechange }: { viewport: Viewport; equalAxisScale: boolean; complexMode: boolean; appearance: GraphAppearance; onboundschange: (viewport: Viewport, changed: BoundKey) => void; onappearancechange: (appearance: GraphAppearance) => void; oncomplexmodechange: (active: boolean) => void; onresetscale: () => void; onscalechange: (active: boolean) => void; } = $props();
let drafts = $state<Record<BoundKey, string>>({ x_min: '', x_max: '', y_min: '', y_max: '' });
let focused = $state<BoundKey | null>(null);
let xInvalid = $derived( !Number.isFinite(Number(drafts.x_min)) || !Number.isFinite(Number(drafts.x_max)) || Number(drafts.x_min) >= Number(drafts.x_max) );
let yInvalid = $derived( !Number.isFinite(Number(drafts.y_min)) || !Number.isFinite(Number(drafts.y_max)) || Number(drafts.y_min) >= Number(drafts.y_max) );
function updateBound(key: BoundKey, event: Event) { drafts[key] = (event.currentTarget as HTMLInputElement).value; const xMin = Number(drafts.x_min); const xMax = Number(drafts.x_max); const yMin = Number(drafts.y_min); const yMax = Number(drafts.y_max);
if ( ![xMin, xMax, yMin, yMax].every(Number.isFinite) || xMin >= xMax || yMin >= yMax ) return; onboundschange({ ...viewport, x_min: xMin, x_max: xMax, y_min: yMin, y_max: yMax }, key); }
$effect(() => { const next = { ...drafts };
if (focused !== 'x_min') next.x_min = String(viewport.x_min);
if (focused !== 'x_max') next.x_max = String(viewport.x_max);
if (focused !== 'y_min') next.y_min = String(viewport.y_min);
if (focused !== 'y_max') next.y_max = String(viewport.y_max);
if (Object.keys(next).some((key) => next[key as BoundKey] !== drafts[key as BoundKey])) { drafts = next; } });</script>
<details class="graph-settings"> <summary>Graph settings</summary> <div class="bounds-grid"> <label> <span>x min</span> <input type="number" value={drafts.x_min} oninput={(event) => updateBound('x_min', event)} onfocus={() => (focused = 'x_min')} onblur={() => (focused = null)} aria-invalid={xInvalid} /> </label> <label> <span>x max</span> <input type="number" value={drafts.x_max} oninput={(event) => updateBound('x_max', event)} onfocus={() => (focused = 'x_max')} onblur={() => (focused = null)} aria-invalid={xInvalid} /> </label> <label> <span>y min</span> <input type="number" value={drafts.y_min} oninput={(event) => updateBound('y_min', event)} onfocus={() => (focused = 'y_min')} onblur={() => (focused = null)} aria-invalid={yInvalid} /> </label> <label> <span>y max</span> <input type="number" value={drafts.y_max} oninput={(event) => updateBound('y_max', event)} onfocus={() => (focused = 'y_max')} onblur={() => (focused = null)} aria-invalid={yInvalid} /> </label> </div> {#if xInvalid || yInvalid} <p class="validation">Each minimum must be a finite number below its maximum.</p> {/if} <div class="settings-actions"> <label class="equal-scale"> <input type="checkbox" checked={equalAxisScale} onchange={(event) => onscalechange(event.currentTarget.checked)} /> <span>Equal axis scale</span> </label> <label class="complex-mode"> <input type="checkbox" checked={complexMode} onchange={(event) => oncomplexmodechange(event.currentTarget.checked)} /> <span>Complex mode</span> </label> <button type="button" onclick={onresetscale}>Reset viewport</button> </div> <div class="appearance-settings"> <label class="theme-picker"> <span>Theme</span> <select value={appearance.theme} onchange={(event) => onappearancechange({ ...appearance, theme: event.currentTarget.value as GraphAppearance['theme'] })}> <option value="light">Light</option> <option value="dark">Dark</option> </select> </label> <label><input type="checkbox" checked={appearance.showGrid} onchange={(event) => onappearancechange({ ...appearance, showGrid: event.currentTarget.checked })} /> Grid</label> <label><input type="checkbox" checked={appearance.showMinorGrid} disabled={!appearance.showGrid} onchange={(event) => onappearancechange({ ...appearance, showMinorGrid: event.currentTarget.checked })} /> Minor grid</label> <label><input type="checkbox" checked={appearance.showXAxis} onchange={(event) => onappearancechange({ ...appearance, showXAxis: event.currentTarget.checked })} /> x-axis</label> <label><input type="checkbox" checked={appearance.showYAxis} onchange={(event) => onappearancechange({ ...appearance, showYAxis: event.currentTarget.checked })} /> y-axis</label> <label><input type="checkbox" checked={appearance.showAxisNumbers} onchange={(event) => onappearancechange({ ...appearance, showAxisNumbers: event.currentTarget.checked })} /> Axis numbers</label> </div> <div class="axis-labels"> <label><span>x label</span><input value={appearance.xAxisLabel} maxlength="24" oninput={(event) => onappearancechange({ ...appearance, xAxisLabel: event.currentTarget.value })} /></label> <label><span>y label</span><input value={appearance.yAxisLabel} maxlength="24" oninput={(event) => onappearancechange({ ...appearance, yAxisLabel: event.currentTarget.value })} /></label> </div></details>
<style> .graph-settings { padding: 8px 10px; border-bottom: 1px solid #cbd5e1; background: #f8fafc; color: #64748b; font-size: 12px; }
summary { width: max-content; cursor: pointer; font-weight: 650; user-select: none; }
.bounds-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; margin-top: 8px; }
.bounds-grid label { display: grid; gap: 3px; }
.bounds-grid input { box-sizing: border-box; width: 100%; min-width: 0; padding: 4px 5px; border: 1px solid #cbd5e1; border-radius: 4px; font: 12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; color: #0f172a; }
.bounds-grid input[aria-invalid='true'] { border-color: #dc2626; background: #fff7f7; }
.validation { margin: 6px 0 0; color: #b91c1c; }
.settings-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 9px; }
.equal-scale { display: flex; align-items: center; gap: 5px; color: #334155; }
.appearance-settings { display: flex; flex-wrap: wrap; gap: 7px 12px; margin-top: 10px; padding-top: 9px; border-top: 1px solid #e2e8f0; color: #334155; }
.appearance-settings label { display: flex; align-items: center; gap: 4px; }
.appearance-settings .theme-picker { width: 100%; }
.theme-picker select { padding: 3px 6px; border: 1px solid #94a3b8; border-radius: 4px; background: white; color: #334155; }
.axis-labels { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 8px; }
.axis-labels label { display: flex; align-items: center; gap: 5px; }
.axis-labels input { min-width: 0; width: 100%; padding: 4px 5px; border: 1px solid #cbd5e1; border-radius: 4px; }
.settings-actions button { padding: 5px 8px; border: 1px solid #94a3b8; border-radius: 5px; background: white; color: #334155; font-weight: 650; cursor: pointer; }</style>