mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Eng-council sweep: export hygiene, dead code, helper dedup, declare vega-expression
This commit is contained in:
Generated
+1
@@ -16,6 +16,7 @@
|
|||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
"vega": "^6.2.0",
|
"vega": "^6.2.0",
|
||||||
"vega-embed": "^7.1.0",
|
"vega-embed": "^7.1.0",
|
||||||
|
"vega-expression": "^6.1.0",
|
||||||
"vega-lite": "^6.4.2",
|
"vega-lite": "^6.4.2",
|
||||||
"vega-themes": "3.0.0",
|
"vega-themes": "3.0.0",
|
||||||
"zustand": "^5.0.14"
|
"zustand": "^5.0.14"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
"vega": "^6.2.0",
|
"vega": "^6.2.0",
|
||||||
"vega-embed": "^7.1.0",
|
"vega-embed": "^7.1.0",
|
||||||
|
"vega-expression": "^6.1.0",
|
||||||
"vega-lite": "^6.4.2",
|
"vega-lite": "^6.4.2",
|
||||||
"vega-themes": "3.0.0",
|
"vega-themes": "3.0.0",
|
||||||
"zustand": "^5.0.14"
|
"zustand": "^5.0.14"
|
||||||
|
|||||||
@@ -14,9 +14,16 @@
|
|||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { datasetReference, tabularRows, type DataSource, type Dataset } from '@core/dataset';
|
import {
|
||||||
|
cellText,
|
||||||
|
datasetReference,
|
||||||
|
tabularRows,
|
||||||
|
type DataSource,
|
||||||
|
type Dataset,
|
||||||
|
} from '@core/dataset';
|
||||||
import { detectFormat, detectFormatFromUrl, type DataFormat } from '@core/format-detection';
|
import { detectFormat, detectFormatFromUrl, type DataFormat } from '@core/format-detection';
|
||||||
import { datasetUsageCounts, snippetsReferencingDataset } from '@core/relationships';
|
import { datasetUsageCounts, snippetsReferencingDataset } from '@core/relationships';
|
||||||
|
import { humanizeBytes } from '@core/storage-estimate';
|
||||||
import { closeModal, openModal, resnapshot } from '../modals/ModalCoordinator';
|
import { closeModal, openModal, resnapshot } from '../modals/ModalCoordinator';
|
||||||
import { fetchRemoteData } from '../infrastructure/remote-data';
|
import { fetchRemoteData } from '../infrastructure/remote-data';
|
||||||
import { remoteFetchErrorMessage } from '../services/remote-data-errors';
|
import { remoteFetchErrorMessage } from '../services/remote-data-errors';
|
||||||
@@ -38,15 +45,6 @@ function formatLabel(format: DataFormat): string {
|
|||||||
return format === 'topojson' ? 'TopoJSON' : format.toUpperCase();
|
return format === 'topojson' ? 'TopoJSON' : format.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Human-readable byte size (B / KB / MB). */
|
|
||||||
function humanBytes(bytes: number): string {
|
|
||||||
if (bytes < 1024) return `${bytes} B`;
|
|
||||||
const kb = bytes / 1024;
|
|
||||||
if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`;
|
|
||||||
const mb = kb / 1024;
|
|
||||||
return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SOURCE_OPTIONS: ReadonlyArray<SegmentedOption<DataSource>> = [
|
const SOURCE_OPTIONS: ReadonlyArray<SegmentedOption<DataSource>> = [
|
||||||
{ value: 'inline', label: 'Inline' },
|
{ value: 'inline', label: 'Inline' },
|
||||||
{ value: 'url', label: 'URL' },
|
{ value: 'url', label: 'URL' },
|
||||||
@@ -55,14 +53,6 @@ const SOURCE_OPTIONS: ReadonlyArray<SegmentedOption<DataSource>> = [
|
|||||||
/** Rows shown in the tabular preview before truncating (spec §05 → Detail Panel). */
|
/** Rows shown in the tabular preview before truncating (spec §05 → Detail Panel). */
|
||||||
const PREVIEW_ROW_LIMIT = 50;
|
const PREVIEW_ROW_LIMIT = 50;
|
||||||
|
|
||||||
/** One table cell's text: blank for empty, the string as-is, else JSON (numbers,
|
|
||||||
* booleans, nested values). Avoids `String()` on objects ("[object Object]"). */
|
|
||||||
function cellText(value: unknown): string {
|
|
||||||
if (value == null) return '';
|
|
||||||
if (typeof value === 'string') return value;
|
|
||||||
return JSON.stringify(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DatasetsModal() {
|
export function DatasetsModal() {
|
||||||
const datasets = useDatasetStore(useShallow((s) => s.datasets));
|
const datasets = useDatasetStore(useShallow((s) => s.datasets));
|
||||||
const view = useDatasetStore((s) => s.view);
|
const view = useDatasetStore((s) => s.view);
|
||||||
@@ -137,7 +127,7 @@ function DatasetListItem({
|
|||||||
if (unfetched) parts.push('not fetched');
|
if (unfetched) parts.push('not fetched');
|
||||||
else if (dataset.rowCount !== null) parts.push(`${dataset.rowCount} rows`);
|
else if (dataset.rowCount !== null) parts.push(`${dataset.rowCount} rows`);
|
||||||
parts.push(formatLabel(dataset.format));
|
parts.push(formatLabel(dataset.format));
|
||||||
if (!unfetched) parts.push(humanBytes(dataset.size));
|
if (!unfetched) parts.push(humanizeBytes(dataset.size));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={`${styles.item} ${active ? styles.itemActive : ''}`}>
|
<li className={`${styles.item} ${active ? styles.itemActive : ''}`}>
|
||||||
@@ -324,7 +314,7 @@ function DatasetDetail({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Size</dt>
|
<dt>Size</dt>
|
||||||
<dd>{dataset.data == null ? 'N/A' : humanBytes(dataset.size)}</dd>
|
<dd>{dataset.data == null ? 'N/A' : humanizeBytes(dataset.size)}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export type IconName =
|
|||||||
| 'status-info'; // Carbon InformationFilled
|
| 'status-info'; // Carbon InformationFilled
|
||||||
|
|
||||||
/** Carbon icon scale (arch 09 §5.3). 16px (sm) is the default, paired to 14px body. */
|
/** Carbon icon scale (arch 09 §5.3). 16px (sm) is the default, paired to 14px body. */
|
||||||
export type IconSize = 'sm' | 'md' | 'lg' | 'xl';
|
type IconSize = 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
|
||||||
/** Inner SVG geometry per glyph, on Carbon's 0 0 32 32 grid. fill comes from CSS. */
|
/** Inner SVG geometry per glyph, on Carbon's 0 0 32 32 grid. fill comes from CSS. */
|
||||||
const GLYPHS: Record<IconName, ReactNode> = {
|
const GLYPHS: Record<IconName, ReactNode> = {
|
||||||
|
|||||||
@@ -99,9 +99,6 @@ function tx<T>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const get = <T>(store: string, key: IDBValidKey): Promise<T | undefined> =>
|
|
||||||
tx<T | undefined>(store, 'readonly', (s) => s.get(key) as IDBRequest<T | undefined>);
|
|
||||||
|
|
||||||
export const getAll = <T>(store: string): Promise<T[]> =>
|
export const getAll = <T>(store: string): Promise<T[]> =>
|
||||||
tx<T[]>(store, 'readonly', (s) => s.getAll() as IDBRequest<T[]>);
|
tx<T[]>(store, 'readonly', (s) => s.getAll() as IDBRequest<T[]>);
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ export function readView(): ViewState {
|
|||||||
return parseHash(window.location.hash);
|
return parseHash(window.location.hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The current raw hash, for comparing a serialized view against the URL. */
|
||||||
|
export function currentHash(): string {
|
||||||
|
return window.location.hash;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the next URL for `view`. The hash is the app's only view-state channel,
|
* Build the next URL for `view`. The hash is the app's only view-state channel,
|
||||||
* so we normalize to a hash-only canonical URL: any stray query string (a shared
|
* so we normalize to a hash-only canonical URL: any stray query string (a shared
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function resnapshot(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** True when the active modal's editable state differs from its baseline. */
|
/** True when the active modal's editable state differs from its baseline. */
|
||||||
export function hasUnsavedChanges(): boolean {
|
function hasUnsavedChanges(): boolean {
|
||||||
const name = useAppStore.getState().activeModal;
|
const name = useAppStore.getState().activeModal;
|
||||||
if (!name) return false;
|
if (!name) return false;
|
||||||
const current = snapshotOf(name);
|
const current = snapshotOf(name);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { useChartBuilderStore } from '../stores/ChartBuilderStore';
|
|||||||
import { useDatasetStore } from '../stores/DatasetStore';
|
import { useDatasetStore } from '../stores/DatasetStore';
|
||||||
import { useSnippetStore } from '../stores/SnippetStore';
|
import { useSnippetStore } from '../stores/SnippetStore';
|
||||||
import {
|
import {
|
||||||
|
currentHash,
|
||||||
pushView,
|
pushView,
|
||||||
readView,
|
readView,
|
||||||
replaceView,
|
replaceView,
|
||||||
@@ -72,7 +73,7 @@ export function deriveViewState(): ViewState {
|
|||||||
|
|
||||||
/** True when `view` already matches the current URL hash. */
|
/** True when `view` already matches the current URL hash. */
|
||||||
function isCurrent(view: ViewState): boolean {
|
function isCurrent(view: ViewState): boolean {
|
||||||
return serializeHash(view) === window.location.hash;
|
return serializeHash(view) === currentHash();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,12 +175,6 @@ export function startRouting(): void {
|
|||||||
useChartBuilderStore.subscribe(syncUrlFromState);
|
useChartBuilderStore.subscribe(syncUrlFromState);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Test seam: reset the module's start/applying flags between cases. */
|
|
||||||
export function resetRoutingForTest(): void {
|
|
||||||
started = false;
|
|
||||||
applying = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Coordinator seam (unchanged call sites) -------------------------------
|
// --- Coordinator seam (unchanged call sites) -------------------------------
|
||||||
// The coordinator calls these on modal open/close; both now just re-derive the
|
// The coordinator calls these on modal open/close; both now just re-derive the
|
||||||
// hash from state. Kept as named exports so ModalCoordinator needs no edit.
|
// hash from state. Kept as named exports so ModalCoordinator needs no edit.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { usePanesStore } from '../stores/PanesStore';
|
import { usePanesStore } from '../stores/PanesStore';
|
||||||
|
|
||||||
/** Delay before a settled resize is persisted. */
|
/** Delay before a settled resize is persisted. */
|
||||||
export const PANES_PERSIST_DEBOUNCE_MS = 300;
|
const PANES_PERSIST_DEBOUNCE_MS = 300;
|
||||||
|
|
||||||
/** Hydrate persisted pane widths + visibility into the store. Call before render. */
|
/** Hydrate persisted pane widths + visibility into the store. Call before render. */
|
||||||
export function initPanes(): void {
|
export function initPanes(): void {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { VisualizationSpec } from 'vega-embed';
|
|||||||
import type { Config } from 'vega-lite';
|
import type { Config } from 'vega-lite';
|
||||||
|
|
||||||
/** Options for `RenderHandle.toImageURL` (spec §08 → Per-chart export). */
|
/** Options for `RenderHandle.toImageURL` (spec §08 → Per-chart export). */
|
||||||
export interface ImageExportOptions {
|
interface ImageExportOptions {
|
||||||
/**
|
/**
|
||||||
* Pixel-density multiplier for the **PNG** raster, **relative to the device**.
|
* Pixel-density multiplier for the **PNG** raster, **relative to the device**.
|
||||||
* The image is drawn at `scale × devicePixelRatio` logical-pixel density, so
|
* The image is drawn at `scale × devicePixelRatio` logical-pixel density, so
|
||||||
@@ -80,7 +80,7 @@ export interface RenderOptions {
|
|||||||
* size against this (÷ dpr, since the backing store is dpr× the CSS size) and
|
* size against this (÷ dpr, since the backing store is dpr× the CSS size) and
|
||||||
* throws `ChartTooLargeError` rather than handing back a blank canvas.
|
* throws `ChartTooLargeError` rather than handing back a blank canvas.
|
||||||
*/
|
*/
|
||||||
export const MAX_CANVAS_PX = 32767;
|
const MAX_CANVAS_PX = 32767;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thrown by `renderSpec` when a **canvas**-backed chart resolves to a height larger
|
* Thrown by `renderSpec` when a **canvas**-backed chart resolves to a height larger
|
||||||
|
|||||||
@@ -110,12 +110,20 @@ export function runMergeChartTheme(editor: monaco.editor.IStandaloneCodeEditor):
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove the draft's `config` block, copying it to the clipboard first. */
|
/**
|
||||||
export async function runExtractConfig(editor: monaco.editor.IStandaloneCodeEditor): Promise<void> {
|
* Shared prologue of the two extract actions: the model, the spec parsed off it,
|
||||||
|
* and the config split out — or null (after toasting) when any step has nothing
|
||||||
|
* to work with.
|
||||||
|
*/
|
||||||
|
function extractableConfig(editor: monaco.editor.IStandaloneCodeEditor): {
|
||||||
|
model: monaco.editor.ITextModel;
|
||||||
|
rest: Record<string, unknown>;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
} | null {
|
||||||
const model = editor.getModel();
|
const model = editor.getModel();
|
||||||
if (!model) return;
|
if (!model) return null;
|
||||||
const spec = parseSpecObject(model);
|
const spec = parseSpecObject(model);
|
||||||
if (!spec) return;
|
if (!spec) return null;
|
||||||
|
|
||||||
const { spec: rest, config } = extractConfigFromSpec(spec);
|
const { spec: rest, config } = extractConfigFromSpec(spec);
|
||||||
if (config === null) {
|
if (config === null) {
|
||||||
@@ -124,8 +132,16 @@ export async function runExtractConfig(editor: monaco.editor.IStandaloneCodeEdit
|
|||||||
title: 'No config to extract',
|
title: 'No config to extract',
|
||||||
message: 'This spec has no config block.',
|
message: 'This spec has no config block.',
|
||||||
});
|
});
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
return { model, rest, config };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove the draft's `config` block, copying it to the clipboard first. */
|
||||||
|
export async function runExtractConfig(editor: monaco.editor.IStandaloneCodeEditor): Promise<void> {
|
||||||
|
const extracted = extractableConfig(editor);
|
||||||
|
if (!extracted) return;
|
||||||
|
const { model, rest, config } = extracted;
|
||||||
|
|
||||||
// Copy before removing — if the clipboard write fails, the spec keeps its
|
// Copy before removing — if the clipboard write fails, the spec keeps its
|
||||||
// config and nothing is lost.
|
// config and nothing is lost.
|
||||||
@@ -157,20 +173,9 @@ export async function runExtractConfig(editor: monaco.editor.IStandaloneCodeEdit
|
|||||||
* non-overlapping keys stop applying once the new theme replaces it.)
|
* non-overlapping keys stop applying once the new theme replaces it.)
|
||||||
*/
|
*/
|
||||||
export function runExtractConfigToTheme(editor: monaco.editor.IStandaloneCodeEditor): void {
|
export function runExtractConfigToTheme(editor: monaco.editor.IStandaloneCodeEditor): void {
|
||||||
const model = editor.getModel();
|
const extracted = extractableConfig(editor);
|
||||||
if (!model) return;
|
if (!extracted) return;
|
||||||
const spec = parseSpecObject(model);
|
const { model, rest, config } = extracted;
|
||||||
if (!spec) return;
|
|
||||||
|
|
||||||
const { spec: rest, config } = extractConfigFromSpec(spec);
|
|
||||||
if (config === null) {
|
|
||||||
notify({
|
|
||||||
kind: 'info',
|
|
||||||
title: 'No config to extract',
|
|
||||||
message: 'This spec has no config block.',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const snippet = selectActiveSnippet(useSnippetStore.getState());
|
const snippet = selectActiveSnippet(useSnippetStore.getState());
|
||||||
const theme = useCustomThemeStore
|
const theme = useCustomThemeStore
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
reassignCollidingSnippetIds,
|
reassignCollidingSnippetIds,
|
||||||
} from '@core/import-normalize';
|
} from '@core/import-normalize';
|
||||||
import { snippetSizeBytes } from '@core/snippet';
|
import { snippetSizeBytes } from '@core/snippet';
|
||||||
|
import { humanizeBytes } from '@core/storage-estimate';
|
||||||
import { downloadJson, readTextFile } from '../infrastructure/file-transfer';
|
import { downloadJson, readTextFile } from '../infrastructure/file-transfer';
|
||||||
import { deleteSnippet, saveSnippet, StorageQuotaError } from '../infrastructure/snippet-store';
|
import { deleteSnippet, saveSnippet, StorageQuotaError } from '../infrastructure/snippet-store';
|
||||||
import { notify } from '../stores/NotificationStore';
|
import { notify } from '../stores/NotificationStore';
|
||||||
@@ -36,13 +37,6 @@ import { useSnippetStore } from '../stores/SnippetStore';
|
|||||||
/** Practical snippet-storage budget (spec §02 storage monitor / §08). */
|
/** Practical snippet-storage budget (spec §02 storage monitor / §08). */
|
||||||
const SNIPPET_BUDGET_BYTES = 5 * 1024 * 1024;
|
const SNIPPET_BUDGET_BYTES = 5 * 1024 * 1024;
|
||||||
|
|
||||||
/** Round bytes to a short KB/MB string for the overage warning. */
|
|
||||||
function formatBytes(bytes: number): string {
|
|
||||||
const kb = bytes / 1024;
|
|
||||||
if (kb < 1024) return `${Math.max(1, Math.round(kb))} KB`;
|
|
||||||
return `${Math.round(kb / 1024)} MB`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export the whole workspace to a downloaded JSON file (spec §08 → Export).
|
* Export the whole workspace to a downloaded JSON file (spec §08 → Export).
|
||||||
* Flushes the live editor buffer first so in-progress draft edits are included.
|
* Flushes the live editor buffer first so in-progress draft edits are included.
|
||||||
@@ -209,7 +203,7 @@ export async function importWorkspace(file: File): Promise<void> {
|
|||||||
}
|
}
|
||||||
if (overage > 0) {
|
if (overage > 0) {
|
||||||
clauses.push(
|
clauses.push(
|
||||||
`This puts snippet storage about ${formatBytes(overage)} over the ~5 MB budget; ` +
|
`This puts snippet storage about ${humanizeBytes(overage)} over the ~5 MB budget; ` +
|
||||||
`consider deleting some snippets.`,
|
`consider deleting some snippets.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,6 @@ import type { ModalName } from '../modals/types';
|
|||||||
* docs/architecture/01-state-and-stores.md.
|
* docs/architecture/01-state-and-stores.md.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type { UiTheme };
|
|
||||||
// The modal union is defined once in the modal system (docs/architecture/03) and
|
|
||||||
// re-exported here for the many callers that reach it through the app store.
|
|
||||||
export type { ModalName };
|
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
/** Active UI theme; mirrored onto <html data-theme> by a subscriber. */
|
/** Active UI theme; mirrored onto <html data-theme> by a subscriber. */
|
||||||
uiTheme: UiTheme;
|
uiTheme: UiTheme;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { customThemeSelection } from '@core/vega-themes';
|
|||||||
import { useAppStore } from './AppStore';
|
import { useAppStore } from './AppStore';
|
||||||
|
|
||||||
/** The Theme Builder's in-progress edit of the selected theme. */
|
/** The Theme Builder's in-progress edit of the selected theme. */
|
||||||
export interface ThemeDraft {
|
interface ThemeDraft {
|
||||||
name: string;
|
name: string;
|
||||||
/** The config as editable JSON text (pretty-printed on load). */
|
/** The config as editable JSON text (pretty-printed on load). */
|
||||||
configText: string;
|
configText: string;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* Two layers of action live here:
|
* Two layers of action live here:
|
||||||
* - Low-level mutators (`add`/`update`/`remove`) are the single place the
|
* - Low-level mutators (`add`/`update`/`remove`) are the single place the
|
||||||
* `datasets` array changes; the persistence subscriber writes them through to
|
* `datasets` array changes; the persistence subscriber writes them through to
|
||||||
* IndexedDB, and services (RelationshipService, future import) reuse them.
|
* IndexedDB, and services (e.g. the import flow in services/transfer) reuse them.
|
||||||
* - Form orchestration (`save`) validates the form (name required + unique,
|
* - Form orchestration (`save`) validates the form (name required + unique,
|
||||||
* data detectable) and turns it into an `add`/`update`, keeping the modal
|
* data detectable) and turns it into an `add`/`update`, keeping the modal
|
||||||
* component thin.
|
* component thin.
|
||||||
@@ -31,7 +31,7 @@ import { isNameTaken } from '@core/naming';
|
|||||||
import { useSnippetStore } from './SnippetStore';
|
import { useSnippetStore } from './SnippetStore';
|
||||||
|
|
||||||
/** Which pane the Datasets manager is showing (spec §05 → Layout). */
|
/** Which pane the Datasets manager is showing (spec §05 → Layout). */
|
||||||
export type DatasetView = 'list' | 'detail' | 'new' | 'edit';
|
type DatasetView = 'list' | 'detail' | 'new' | 'edit';
|
||||||
|
|
||||||
/** The in-progress create/edit form. `input` is the paste area (inline) or URL field. */
|
/** The in-progress create/edit form. `input` is the paste area (inline) or URL field. */
|
||||||
export interface DatasetForm {
|
export interface DatasetForm {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ interface InlineData {
|
|||||||
* none (no snippet, unparseable, or no `data.values`). The format comes from an
|
* none (no snippet, unparseable, or no `data.values`). The format comes from an
|
||||||
* explicit `data.format.type` when present (raw CSV/TSV strings), else JSON.
|
* explicit `data.format.type` when present (raw CSV/TSV strings), else JSON.
|
||||||
*/
|
*/
|
||||||
export function readInlineData(draftText: string): InlineData | null {
|
function readInlineData(draftText: string): InlineData | null {
|
||||||
let parsed: unknown;
|
let parsed: unknown;
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(draftText);
|
parsed = JSON.parse(draftText);
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export function shownSideWidths(
|
|||||||
|
|
||||||
/** Per-pane visibility (spec §01A). Widths are kept *independently* of visibility,
|
/** Per-pane visibility (spec §01A). Widths are kept *independently* of visibility,
|
||||||
* so a hidden pane keeps its remembered width and re-shows at it (not a default). */
|
* so a hidden pane keeps its remembered width and re-shows at it (not a default). */
|
||||||
export interface PaneVisibility {
|
interface PaneVisibility {
|
||||||
library?: boolean;
|
library?: boolean;
|
||||||
editor?: boolean;
|
editor?: boolean;
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ export const MARK_TYPES = ['bar', 'line', 'point', 'area', 'circle'] as const;
|
|||||||
export type MarkType = (typeof MARK_TYPES)[number];
|
export type MarkType = (typeof MARK_TYPES)[number];
|
||||||
|
|
||||||
/** The four Vega-Lite field types a channel may carry, in override-menu order. */
|
/** The four Vega-Lite field types a channel may carry, in override-menu order. */
|
||||||
export const FIELD_TYPES = ['quantitative', 'nominal', 'ordinal', 'temporal'] as const;
|
export type FieldType = 'quantitative' | 'nominal' | 'ordinal' | 'temporal';
|
||||||
export type FieldType = (typeof FIELD_TYPES)[number];
|
|
||||||
|
|
||||||
/** The four encoding channels the builder offers, in display order (spec §06). */
|
/** The four encoding channels the builder offers, in display order (spec §06). */
|
||||||
export const CHANNELS = ['x', 'y', 'color', 'size'] as const;
|
export const CHANNELS = ['x', 'y', 'color', 'size'] as const;
|
||||||
@@ -49,8 +48,7 @@ export type ChannelName = (typeof CHANNELS)[number];
|
|||||||
* reduce a quantitative field; min/max also order a temporal or ordinal one. See
|
* reduce a quantitative field; min/max also order a temporal or ordinal one. See
|
||||||
* `validAggregateOps` for the per-type menu.
|
* `validAggregateOps` for the per-type menu.
|
||||||
*/
|
*/
|
||||||
export const AGGREGATE_OPS = ['count', 'distinct', 'sum', 'mean', 'median', 'min', 'max'] as const;
|
export type AggregateOp = 'count' | 'distinct' | 'sum' | 'mean' | 'median' | 'min' | 'max';
|
||||||
export type AggregateOp = (typeof AGGREGATE_OPS)[number];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporal granularities (Vega-Lite `timeUnit`), coarse → fine, with the combined
|
* Temporal granularities (Vega-Lite `timeUnit`), coarse → fine, with the combined
|
||||||
@@ -71,12 +69,10 @@ export const TIME_UNITS = [
|
|||||||
export type TimeUnit = (typeof TIME_UNITS)[number];
|
export type TimeUnit = (typeof TIME_UNITS)[number];
|
||||||
|
|
||||||
/** Sort the categorical axis by its measure (spec §06 → Ranking). */
|
/** Sort the categorical axis by its measure (spec §06 → Ranking). */
|
||||||
export const SORT_ORDERS = ['ascending', 'descending'] as const;
|
export type SortOrder = 'ascending' | 'descending';
|
||||||
export type SortOrder = (typeof SORT_ORDERS)[number];
|
|
||||||
|
|
||||||
/** Part-to-whole stacking for bar/area + a colour series: absolute vs 100%. */
|
/** Part-to-whole stacking for bar/area + a colour series: absolute vs 100%. */
|
||||||
export const STACK_MODES = ['zero', 'normalize'] as const;
|
export type StackMode = 'zero' | 'normalize';
|
||||||
export type StackMode = (typeof STACK_MODES)[number];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comparison operators a guarded filter predicate offers (Vega-Lite field
|
* Comparison operators a guarded filter predicate offers (Vega-Lite field
|
||||||
@@ -85,17 +81,7 @@ export type StackMode = (typeof STACK_MODES)[number];
|
|||||||
* `range`; a category offers membership (`oneOf`). `notEqual` is expressed as a
|
* `range`; a category offers membership (`oneOf`). `notEqual` is expressed as a
|
||||||
* `{ not: { …equal } }` logical wrapper (Vega-Lite has no bare `!=` predicate).
|
* `{ not: { …equal } }` logical wrapper (Vega-Lite has no bare `!=` predicate).
|
||||||
*/
|
*/
|
||||||
export const FILTER_OPS = [
|
export type FilterOp = 'equal' | 'notEqual' | 'lt' | 'lte' | 'gt' | 'gte' | 'range' | 'oneOf';
|
||||||
'equal',
|
|
||||||
'notEqual',
|
|
||||||
'lt',
|
|
||||||
'lte',
|
|
||||||
'gt',
|
|
||||||
'gte',
|
|
||||||
'range',
|
|
||||||
'oneOf',
|
|
||||||
] as const;
|
|
||||||
export type FilterOp = (typeof FILTER_OPS)[number];
|
|
||||||
|
|
||||||
/** How a filter expresses its predicate: a guarded shelf, or a raw expression. */
|
/** How a filter expresses its predicate: a guarded shelf, or a raw expression. */
|
||||||
export type FilterMode = 'predicate' | 'expression';
|
export type FilterMode = 'predicate' | 'expression';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
import {
|
import {
|
||||||
|
cellText,
|
||||||
computeDatasetProfile,
|
computeDatasetProfile,
|
||||||
CURRENT_DATASET_VERSION,
|
CURRENT_DATASET_VERSION,
|
||||||
createDataset,
|
createDataset,
|
||||||
@@ -153,6 +154,17 @@ describe('tabularRows', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('cellText', () => {
|
||||||
|
test('strings as-is, null/undefined blank, others as JSON (no "[object Object]")', () => {
|
||||||
|
expect(cellText('plain')).toBe('plain');
|
||||||
|
expect(cellText(null)).toBe('');
|
||||||
|
expect(cellText(undefined)).toBe('');
|
||||||
|
expect(cellText(42)).toBe('42');
|
||||||
|
expect(cellText(false)).toBe('false');
|
||||||
|
expect(cellText({ a: 1 })).toBe('{"a":1}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('snapshotFromText', () => {
|
describe('snapshotFromText', () => {
|
||||||
test('JSON content is parsed and typed json (content beats extension)', () => {
|
test('JSON content is parsed and typed json (content beats extension)', () => {
|
||||||
const { data, format } = snapshotFromText('[{"a":1}]', 'https://x/data.txt');
|
const { data, format } = snapshotFromText('[{"a":1}]', 'https://x/data.txt');
|
||||||
|
|||||||
@@ -235,6 +235,16 @@ export function tabularRows(
|
|||||||
return limit != null && limit >= 0 && rows.length > limit ? rows.slice(0, limit) : rows;
|
return limit != null && limit >= 0 && rows.length > limit ? rows.slice(0, limit) : rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a `tabularRows` cell for a preview table: strings as-is, null/undefined
|
||||||
|
* empty, anything else (numbers, booleans, nested values) as compact JSON.
|
||||||
|
*/
|
||||||
|
export function cellText(value: unknown): string {
|
||||||
|
if (value == null) return '';
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Orchestrate profiling for a dataset payload: compute `size` (always), decide
|
* Orchestrate profiling for a dataset payload: compute `size` (always), decide
|
||||||
* tabular vs N/A by format, and delegate to `profileData`. Identical for inline
|
* tabular vs N/A by format, and delegate to `profileData`. Identical for inline
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
* grammar. It also extracts the `datum.<field>` references so the UI can flag a typo
|
* grammar. It also extracts the `datum.<field>` references so the UI can flag a typo
|
||||||
* against the dataset's actual columns before the chart silently renders empty.
|
* against the dataset's actual columns before the chart silently renders empty.
|
||||||
*
|
*
|
||||||
* `vega-expression` is a pure dependency already in the bundle (Vega pulls it in for
|
* `vega-expression` is declared as a direct dependency but adds no bundle weight —
|
||||||
* rendering), so importing it here adds nothing and keeps this module browser-free.
|
* Vega already ships it for rendering; this import reuses the same copy.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { parseExpression } from 'vega-expression';
|
import { parseExpression } from 'vega-expression';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type DataFormat = 'json' | 'csv' | 'tsv' | 'topojson';
|
export type DataFormat = 'json' | 'csv' | 'tsv' | 'topojson';
|
||||||
export type DetectionConfidence = 'high' | 'medium' | 'low';
|
type DetectionConfidence = 'high' | 'medium' | 'low';
|
||||||
|
|
||||||
export interface FormatDetection {
|
export interface FormatDetection {
|
||||||
format: DataFormat | null;
|
format: DataFormat | null;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
import stringify from 'json-stringify-pretty-compact';
|
import stringify from 'json-stringify-pretty-compact';
|
||||||
|
|
||||||
/** Width budget before an array/object wraps onto multiple lines. */
|
/** Width budget before an array/object wraps onto multiple lines. */
|
||||||
export const DEFAULT_MAX_LINE = 80;
|
const DEFAULT_MAX_LINE = 80;
|
||||||
|
|
||||||
/** Default indent (spaces) — matches the editor's default tab size (spec §07). */
|
/** Default indent (spaces) — matches the editor's default tab size (spec §07). */
|
||||||
const DEFAULT_INDENT = 2;
|
const DEFAULT_INDENT = 2;
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import { inferColumnType, isEmpty, isNumeric, type ColumnType } from './type-inference';
|
import { inferColumnType, isEmpty, isNumeric, type ColumnType } from './type-inference';
|
||||||
|
|
||||||
export interface ColumnTypeInfo {
|
interface ColumnTypeInfo {
|
||||||
/** The column name. */
|
/** The column name. */
|
||||||
name: string;
|
name: string;
|
||||||
/** The inferred display type for the column. */
|
/** The inferred display type for the column. */
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/** A category of stored data and its measured size in bytes. */
|
/** A category of stored data and its measured size in bytes. */
|
||||||
export interface StorageSegment {
|
interface StorageSegment {
|
||||||
key: 'snippets' | 'datasets' | 'app';
|
key: 'snippets' | 'datasets' | 'app';
|
||||||
/** User-facing label, centralized here so the component stays presentational. */
|
/** User-facing label, centralized here so the component stays presentational. */
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ const PRESET_IDS = [
|
|||||||
'dark',
|
'dark',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ChartThemePresetId = (typeof PRESET_IDS)[number];
|
type ChartThemePresetId = (typeof PRESET_IDS)[number];
|
||||||
|
|
||||||
/** Empty config — the stock sentinel resolves to "inject nothing". */
|
/** Empty config — the stock sentinel resolves to "inject nothing". */
|
||||||
const STOCK_CONFIG: Config = {};
|
const STOCK_CONFIG: Config = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user