diff --git a/CHANGELOG.md b/CHANGELOG.md index 16070a95..fdf54a0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ auto-generated per-PR notes; this file is the curated, human-readable history. ## [Unreleased] +### Added +- **Line and Area panels now have compact style presets for dense time series** + (#252). Clean, Smooth, Stepped, and Points controls map to explicit Chart.js + curve and point settings, with automatic point visibility based on the final + rendered label and dataset counts. Style metadata persists across panel type + switches, shares, OAuth handoff, and Library import/export, while unsupported + future string values remain lossless and render with safe defaults. + ## [0.5.0] - 2026-07-15 ### Removed diff --git a/schemas/generated/library-v2.bundle.schema.json b/schemas/generated/library-v2.bundle.schema.json index fb16a168..89d71e41 100644 --- a/schemas/generated/library-v2.bundle.schema.json +++ b/schemas/generated/library-v2.bundle.schema.json @@ -271,6 +271,54 @@ "fieldConfig" ] }, + "chartStyle": { + "title": "Line and Area style", + "description": "Renderer-independent line presentation. Unknown fields and future string values remain storable.", + "type": "object", + "properties": { + "curve": { + "title": "Line curve", + "description": "linear draws straight segments, smooth uses monotone interpolation, and stepped draws step-after segments.", + "anyOf": [ + { + "type": "string", + "enum": [ + "linear", + "smooth", + "stepped" + ] + }, + { + "type": "string" + } + ], + "default": "linear" + }, + "points": { + "title": "Point markers", + "description": "auto shows markers only for sparse results, show always displays them, and hide retains hover targets without visible markers.", + "anyOf": [ + { + "type": "string", + "enum": [ + "auto", + "show", + "hide" + ] + }, + { + "type": "string" + } + ], + "default": "auto" + } + }, + "additionalProperties": true, + "x-altinity-order": [ + "curve", + "points" + ] + }, "chartCfg": { "type": "object", "properties": { @@ -317,6 +365,28 @@ "series" ] }, + "lineChartCfg": { + "allOf": [ + { + "$ref": "#/$defs/chartCfg" + }, + { + "type": "object", + "properties": { + "style": { + "$ref": "#/$defs/chartStyle" + } + } + } + ], + "x-altinity-order": [ + "type", + "style", + "x", + "y", + "series" + ] + }, "panelCfg": { "title": "Panel type configuration", "description": "Discriminated visualization configuration. Unknown types remain storable for forward compatibility.", @@ -408,7 +478,7 @@ }, "allOf": [ { - "$ref": "#/$defs/chartCfg" + "$ref": "#/$defs/lineChartCfg" }, { "properties": { @@ -436,7 +506,7 @@ }, "allOf": [ { - "$ref": "#/$defs/chartCfg" + "$ref": "#/$defs/lineChartCfg" }, { "properties": { diff --git a/schemas/query-spec-v1.schema.json b/schemas/query-spec-v1.schema.json index 04379f2a..5f16cd21 100644 --- a/schemas/query-spec-v1.schema.json +++ b/schemas/query-spec-v1.schema.json @@ -146,6 +146,33 @@ "additionalProperties": true, "x-altinity-order": ["cfg", "key", "fieldConfig"] }, + "chartStyle": { + "title": "Line and Area style", + "description": "Renderer-independent line presentation. Unknown fields and future string values remain storable.", + "type": "object", + "properties": { + "curve": { + "title": "Line curve", + "description": "linear draws straight segments, smooth uses monotone interpolation, and stepped draws step-after segments.", + "anyOf": [ + { "type": "string", "enum": ["linear", "smooth", "stepped"] }, + { "type": "string" } + ], + "default": "linear" + }, + "points": { + "title": "Point markers", + "description": "auto shows markers only for sparse results, show always displays them, and hide retains hover targets without visible markers.", + "anyOf": [ + { "type": "string", "enum": ["auto", "show", "hide"] }, + { "type": "string" } + ], + "default": "auto" + } + }, + "additionalProperties": true, + "x-altinity-order": ["curve", "points"] + }, "chartCfg": { "type": "object", "properties": { @@ -173,6 +200,18 @@ "additionalProperties": true, "x-altinity-order": ["type", "x", "y", "series"] }, + "lineChartCfg": { + "allOf": [ + { "$ref": "#/$defs/chartCfg" }, + { + "type": "object", + "properties": { + "style": { "$ref": "#/$defs/chartStyle" } + } + } + ], + "x-altinity-order": ["type", "style", "x", "y", "series"] + }, "panelCfg": { "title": "Panel type configuration", "description": "Discriminated visualization configuration. Unknown types remain storable for forward compatibility.", @@ -216,7 +255,7 @@ "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "line", "x": 0, "y": [1], "series": null }, "allOf": [ - { "$ref": "#/$defs/chartCfg" }, + { "$ref": "#/$defs/lineChartCfg" }, { "properties": { "type": { "const": "line" } }, "required": ["type"] } ] }, @@ -226,7 +265,7 @@ "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "area", "x": 0, "y": [1], "series": null }, "allOf": [ - { "$ref": "#/$defs/chartCfg" }, + { "$ref": "#/$defs/lineChartCfg" }, { "properties": { "type": { "const": "area" } }, "required": ["type"] } ] }, diff --git a/src/core/chart-data.js b/src/core/chart-data.js index 5e629860..27d15175 100644 --- a/src/core/chart-data.js +++ b/src/core/chart-data.js @@ -83,6 +83,45 @@ export const CHART_TYPES = [ { value: 'pie', label: 'Pie' }, ]; +export const CHART_STYLE_PRESETS = [ + { value: 'clean', label: 'Clean', style: { curve: 'linear', points: 'auto' } }, + { value: 'smooth', label: 'Smooth', style: { curve: 'smooth', points: 'auto' } }, + { value: 'stepped', label: 'Stepped', style: { curve: 'stepped', points: 'auto' } }, + { value: 'points', label: 'Points', style: { curve: 'linear', points: 'show' } }, +]; + +const CHART_CURVES = new Set(['linear', 'smooth', 'stepped']); +const CHART_POINTS = new Set(['auto', 'show', 'hide']); + +/** Resolve renderer-independent Line/Area style without mutating imported data. */ +export function normalizeChartStyle(style) { + const value = style && typeof style === 'object' && !Array.isArray(style) ? style : {}; + return { + curve: CHART_CURVES.has(value.curve) ? value.curve : 'linear', + points: CHART_POINTS.has(value.points) ? value.points : 'auto', + }; +} + +/** Pick the closest compact UI preset without rewriting unusual stored combinations. */ +export function chartStylePreset(style) { + const value = normalizeChartStyle(style); + if (value.curve === 'smooth') return 'smooth'; + if (value.curve === 'stepped') return 'stepped'; + return value.points === 'show' ? 'points' : 'clean'; +} + +/** Apply one UI preset while retaining unknown style extensions. */ +export function applyChartStylePreset(style, preset) { + const base = style && typeof style === 'object' && !Array.isArray(style) ? style : {}; + const picked = CHART_STYLE_PRESETS.find((item) => item.value === preset) || CHART_STYLE_PRESETS[0]; + return { ...base, ...picked.style }; +} + +/** Deterministic marker density rule over the final rendered labels/datasets. */ +export function shouldShowChartPoints(labels, datasets) { + return (labels || []).length <= 60 && (datasets || []).length <= 4; +} + const CHART_TYPE_SET = new Set(CHART_TYPES.map((t) => t.value)); /** @@ -91,7 +130,12 @@ const CHART_TYPE_SET = new Set(CHART_TYPES.map((t) => t.value)); * restored chart must not mutate the saved entry. null → null. */ export function cloneChartCfg(cfg) { - return cfg ? { type: cfg.type, x: cfg.x, y: [...(cfg.y || [])], series: cfg.series ?? null } : null; + if (!cfg) return null; + const out = { ...cfg, y: [...(cfg.y || [])], series: cfg.series ?? null }; + if (cfg.style && typeof cfg.style === 'object') { + out.style = JSON.parse(JSON.stringify(cfg.style)); + } + return out; } /** @@ -304,6 +348,17 @@ export function chartJsConfig(columns, rows, cfg, colors, opts = {}) { const isArea = cfg.type === 'area'; const isLine = cfg.type === 'line' || isArea; const chartType = horizontal || cfg.type === 'bar' ? 'bar' : isLine ? 'line' : 'pie'; + const style = normalizeChartStyle(cfg.style); + const pointsVisible = style.points === 'show' + || (style.points === 'auto' && shouldShowChartPoints(labels, datasets)); + const curveStyle = style.curve === 'smooth' + ? { tension: 0, stepped: false, cubicInterpolationMode: 'monotone' } + : style.curve === 'stepped' + ? { tension: 0, stepped: 'after' } + : { tension: 0, stepped: false, cubicInterpolationMode: 'default' }; + const pointStyle = pointsVisible + ? { pointRadius: 2, pointHoverRadius: 4, pointHitRadius: 8 } + : { pointRadius: 0, pointHoverRadius: 3, pointHitRadius: 8 }; const styled = datasets.map((ds, i) => { const color = pal[i % pal.length]; @@ -311,7 +366,10 @@ export function chartJsConfig(columns, rows, cfg, colors, opts = {}) { return { ...ds, backgroundColor: ds.data.map((_, j) => pal[j % pal.length]), borderColor: colors.bgModal, borderWidth: 1.5 }; } if (isLine) { - return { ...ds, borderColor: color, backgroundColor: isArea ? withAlpha(color, 0.14) : color, fill: isArea, tension: 0.25, pointRadius: 2, borderWidth: 2 }; + return { + ...ds, borderColor: color, backgroundColor: isArea ? withAlpha(color, 0.14) : color, + fill: isArea, borderWidth: 1.5, ...curveStyle, ...pointStyle, + }; } return { ...ds, backgroundColor: color, borderRadius: 2, borderWidth: 0 }; }); diff --git a/src/generated/json-schema-validators.js b/src/generated/json-schema-validators.js index 27725be2..5be260c7 100644 --- a/src/generated/json-schema-validators.js +++ b/src/generated/json-schema-validators.js @@ -285,8 +285,8 @@ var require_formats = __commonJS({ // json-schema-standalone.js var validateQuerySpecV1 = validate20; -var schema31 = { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://altinity.com/schemas/altinity-sql-browser/query-spec-v1.schema.json", "title": "Altinity SQL Browser saved-query Spec v1", "description": "The user-authored query.spec document. Saved-query envelope fields are intentionally outside this schema.", "x-altinity-kind": "query-spec", "x-altinity-version": 1, "type": "object", "properties": { "name": { "title": "Name", "description": "Panel, tile, and Library title.", "type": "string", "minLength": 1, "pattern": "\\S", "examples": ["Revenue by country"] }, "description": { "title": "Description", "description": "Optional authoring note shown with the saved query.", "type": "string" }, "favorite": { "title": "Favorite", "description": "Whether the query is included in favorite-driven surfaces.", "type": "boolean", "default": false }, "view": { "title": "Preferred result view", "description": "The result representation restored when the saved query opens.", "type": "string", "enum": ["table", "json", "panel"], "default": "table" }, "panel": { "$ref": "#/$defs/panel" }, "dashboard": { "$ref": "#/$defs/dashboard" } }, "additionalProperties": true, "x-altinity-order": ["name", "description", "favorite", "view", "panel", "dashboard"], "$defs": { "columnName": { "title": "Result column", "description": "Exact top-level ClickHouse result-column name.", "type": "string", "minLength": 1, "x-altinity-completion": { "source": "resultColumns" } }, "resultColumnIndex": { "title": "Result column index", "description": "Zero-based index of a ClickHouse result column.", "type": "integer", "minimum": 0, "x-altinity-completion": { "source": "resultColumnIndexes" } }, "deltaPresentation": { "title": "Delta presentation", "description": "Display metadata for a runtime KPI delta value.", "type": "object", "properties": { "displayName": { "title": "Delta label", "description": "Optional visible label for the delta.", "type": "string" }, "unit": { "title": "Delta unit", "description": "Display-only suffix appended to the delta.", "type": "string" }, "decimals": { "title": "Delta decimal places", "description": "Requested display rounding for the delta.", "type": "integer", "minimum": 0, "maximum": 20, "default": 0, "examples": [1] }, "positiveIsGood": { "title": "Positive is good", "description": "Whether a positive runtime delta has good semantics.", "type": "boolean" }, "show": { "title": "Show delta", "description": "Whether a present runtime delta is rendered.", "type": "boolean", "default": true } }, "additionalProperties": true, "x-altinity-order": ["displayName", "unit", "decimals", "positiveIsGood", "show"] }, "fieldConfigValue": { "title": "Field presentation metadata", "description": "Known presentation metadata for one result column. Unknown renderer extensions are retained.", "type": "object", "properties": { "displayName": { "title": "Display name", "description": "Rendered label for the field.", "type": "string" }, "decimals": { "title": "Decimal places", "description": "Requested number of decimal places for numeric display.", "type": "integer", "minimum": 0, "maximum": 20, "default": 0, "examples": [2] }, "description": { "title": "Description", "description": "Supporting display text for the field.", "type": "string" }, "unit": { "title": "Unit", "description": "Display-only suffix appended to the value.", "type": "string", "examples": ["%"] }, "color": { "title": "Color", "description": "Theme token or CSS color hint interpreted by the renderer.", "type": "string" }, "noValue": { "title": "No-value text", "description": "Text shown for NULL or unavailable values.", "type": "string", "default": "\u2014" }, "hidden": { "title": "Hidden", "description": "Suppress this otherwise eligible result field.", "type": "boolean", "default": false }, "delta": { "$ref": "#/$defs/deltaPresentation" } }, "additionalProperties": true, "x-altinity-order": ["displayName", "description", "unit", "decimals", "color", "noValue", "hidden", "delta"] }, "fieldConfig": { "title": "Panel field configuration", "description": "Default and per-column display metadata.", "type": "object", "properties": { "defaults": { "$ref": "#/$defs/fieldConfigValue" }, "columns": { "title": "Column overrides", "description": "Display metadata keyed by exact result-column name.", "type": "object", "additionalProperties": { "$ref": "#/$defs/fieldConfigValue" }, "x-altinity-key-completion": { "source": "resultColumns" } } }, "additionalProperties": true, "x-altinity-order": ["defaults", "columns"] }, "dashboard": { "title": "Dashboard configuration", "description": "Dashboard participation metadata. Feature-specific extensions remain forward compatible.", "type": "object", "properties": { "role": { "title": "Dashboard role (Panel, Filter, or Setup)", "description": "How the saved query participates in a dashboard. panel (the default) creates a visualization tile. filter returns exactly one row whose supported top-level Array, named Tuple Array, or Map columns provide option lists for parameters with the same exact names; it creates no tile and its SQL cannot declare parameters. setup is reserved for serialized Dashboard setup execution.", "type": "string", "enum": ["panel", "filter", "setup"], "default": "panel", "examples": ["filter"] } }, "additionalProperties": true, "x-altinity-order": ["role"] }, "panel": { "title": "Panel configuration", "description": "Visualization and field metadata for the saved query.", "type": "object", "properties": { "cfg": { "$ref": "#/$defs/panelCfg" }, "key": { "title": "Result schema key", "description": "Saved result-column signature used to detect stale positional roles.", "type": ["string", "null"] }, "fieldConfig": { "$ref": "#/$defs/fieldConfig" } }, "additionalProperties": true, "x-altinity-order": ["cfg", "key", "fieldConfig"] }, "chartCfg": { "type": "object", "properties": { "x": { "$ref": "#/$defs/resultColumnIndex", "default": 0 }, "y": { "title": "Measure columns", "description": "One or more zero-based result-column indexes used as measures.", "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/resultColumnIndex" } }, "series": { "title": "Series column", "description": "Optional zero-based result-column index used to split series.", "oneOf": [{ "$ref": "#/$defs/resultColumnIndex" }, { "type": "null" }], "default": null, "x-altinity-completion": { "source": "resultColumnIndexes" } } }, "required": ["x", "y"], "additionalProperties": true, "x-altinity-order": ["type", "x", "y", "series"] }, "panelCfg": { "title": "Panel type configuration", "description": "Discriminated visualization configuration. Unknown types remain storable for forward compatibility.", "type": "object", "required": ["type"], "properties": { "type": { "title": "Panel type", "description": "Visualization renderer identifier.", "type": "string", "minLength": 1 } }, "additionalProperties": true, "x-altinity-discriminator": "type", "x-altinity-order": ["type"], "oneOf": [{ "title": "Column chart", "description": "Vertical columns using positional X and measure roles.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "bar", "x": 0, "y": [1], "series": null }, "allOf": [{ "$ref": "#/$defs/chartCfg" }, { "properties": { "type": { "const": "bar" } }, "required": ["type"] }] }, { "title": "Horizontal bar chart", "description": "Horizontal bars using positional X and measure roles.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "hbar", "x": 0, "y": [1], "series": null }, "allOf": [{ "$ref": "#/$defs/chartCfg" }, { "properties": { "type": { "const": "hbar" } }, "required": ["type"] }] }, { "title": "Line chart", "description": "Line series using positional X and measure roles.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "line", "x": 0, "y": [1], "series": null }, "allOf": [{ "$ref": "#/$defs/chartCfg" }, { "properties": { "type": { "const": "line" } }, "required": ["type"] }] }, { "title": "Area chart", "description": "Filled line series using positional X and measure roles.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "area", "x": 0, "y": [1], "series": null }, "allOf": [{ "$ref": "#/$defs/chartCfg" }, { "properties": { "type": { "const": "area" } }, "required": ["type"] }] }, { "title": "Pie chart", "description": "Pie slices using one positional measure role.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "pie", "x": 0, "y": [1], "series": null }, "allOf": [{ "$ref": "#/$defs/chartCfg" }, { "properties": { "type": { "const": "pie" }, "y": { "type": "array", "maxItems": 1 } }, "required": ["type"] }] }, { "title": "KPI", "description": "One-row scalar and named-tuple KPI cards.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "kpi" }, "properties": { "type": { "const": "kpi" } }, "required": ["type"], "additionalProperties": true, "x-altinity-order": ["type"] }, { "title": "Table", "description": "Tabular result rendering with no required panel-specific fields.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "table" }, "properties": { "type": { "const": "table" } }, "required": ["type"], "additionalProperties": true }, { "title": "Logs", "description": "Timestamped log messages with optional explicit result-column roles.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "logs", "time": "event_time", "msg": "message", "level": "level" }, "properties": { "type": { "const": "logs" }, "time": { "$ref": "#/$defs/columnName" }, "msg": { "$ref": "#/$defs/columnName" }, "level": { "$ref": "#/$defs/columnName" } }, "required": ["type"], "additionalProperties": true, "x-altinity-order": ["type", "time", "msg", "level"] }, { "title": "Markdown text", "description": "Safe Markdown content that does not require a SQL result.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "text", "content": "# Heading\n\nMarkdown content." }, "properties": { "type": { "const": "text" }, "content": { "title": "Markdown content", "description": "Source text for the safe Markdown renderer.", "type": "string", "default": "" } }, "required": ["type"], "additionalProperties": true, "x-altinity-order": ["type", "content"] }, { "title": "Future panel type", "description": "Forward-compatible storage branch for a type implemented by a newer build.", "x-altinity-status": "planned", "x-altinity-snippet": { "type": "future-panel" }, "properties": { "type": { "type": "string", "minLength": 1, "not": { "enum": ["bar", "hbar", "line", "area", "pie", "kpi", "table", "logs", "text"] } } }, "required": ["type"], "additionalProperties": true }] } } }; -var schema44 = { "title": "Dashboard configuration", "description": "Dashboard participation metadata. Feature-specific extensions remain forward compatible.", "type": "object", "properties": { "role": { "title": "Dashboard role (Panel, Filter, or Setup)", "description": "How the saved query participates in a dashboard. panel (the default) creates a visualization tile. filter returns exactly one row whose supported top-level Array, named Tuple Array, or Map columns provide option lists for parameters with the same exact names; it creates no tile and its SQL cannot declare parameters. setup is reserved for serialized Dashboard setup execution.", "type": "string", "enum": ["panel", "filter", "setup"], "default": "panel", "examples": ["filter"] } }, "additionalProperties": true, "x-altinity-order": ["role"] }; +var schema31 = { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://altinity.com/schemas/altinity-sql-browser/query-spec-v1.schema.json", "title": "Altinity SQL Browser saved-query Spec v1", "description": "The user-authored query.spec document. Saved-query envelope fields are intentionally outside this schema.", "x-altinity-kind": "query-spec", "x-altinity-version": 1, "type": "object", "properties": { "name": { "title": "Name", "description": "Panel, tile, and Library title.", "type": "string", "minLength": 1, "pattern": "\\S", "examples": ["Revenue by country"] }, "description": { "title": "Description", "description": "Optional authoring note shown with the saved query.", "type": "string" }, "favorite": { "title": "Favorite", "description": "Whether the query is included in favorite-driven surfaces.", "type": "boolean", "default": false }, "view": { "title": "Preferred result view", "description": "The result representation restored when the saved query opens.", "type": "string", "enum": ["table", "json", "panel"], "default": "table" }, "panel": { "$ref": "#/$defs/panel" }, "dashboard": { "$ref": "#/$defs/dashboard" } }, "additionalProperties": true, "x-altinity-order": ["name", "description", "favorite", "view", "panel", "dashboard"], "$defs": { "columnName": { "title": "Result column", "description": "Exact top-level ClickHouse result-column name.", "type": "string", "minLength": 1, "x-altinity-completion": { "source": "resultColumns" } }, "resultColumnIndex": { "title": "Result column index", "description": "Zero-based index of a ClickHouse result column.", "type": "integer", "minimum": 0, "x-altinity-completion": { "source": "resultColumnIndexes" } }, "deltaPresentation": { "title": "Delta presentation", "description": "Display metadata for a runtime KPI delta value.", "type": "object", "properties": { "displayName": { "title": "Delta label", "description": "Optional visible label for the delta.", "type": "string" }, "unit": { "title": "Delta unit", "description": "Display-only suffix appended to the delta.", "type": "string" }, "decimals": { "title": "Delta decimal places", "description": "Requested display rounding for the delta.", "type": "integer", "minimum": 0, "maximum": 20, "default": 0, "examples": [1] }, "positiveIsGood": { "title": "Positive is good", "description": "Whether a positive runtime delta has good semantics.", "type": "boolean" }, "show": { "title": "Show delta", "description": "Whether a present runtime delta is rendered.", "type": "boolean", "default": true } }, "additionalProperties": true, "x-altinity-order": ["displayName", "unit", "decimals", "positiveIsGood", "show"] }, "fieldConfigValue": { "title": "Field presentation metadata", "description": "Known presentation metadata for one result column. Unknown renderer extensions are retained.", "type": "object", "properties": { "displayName": { "title": "Display name", "description": "Rendered label for the field.", "type": "string" }, "decimals": { "title": "Decimal places", "description": "Requested number of decimal places for numeric display.", "type": "integer", "minimum": 0, "maximum": 20, "default": 0, "examples": [2] }, "description": { "title": "Description", "description": "Supporting display text for the field.", "type": "string" }, "unit": { "title": "Unit", "description": "Display-only suffix appended to the value.", "type": "string", "examples": ["%"] }, "color": { "title": "Color", "description": "Theme token or CSS color hint interpreted by the renderer.", "type": "string" }, "noValue": { "title": "No-value text", "description": "Text shown for NULL or unavailable values.", "type": "string", "default": "\u2014" }, "hidden": { "title": "Hidden", "description": "Suppress this otherwise eligible result field.", "type": "boolean", "default": false }, "delta": { "$ref": "#/$defs/deltaPresentation" } }, "additionalProperties": true, "x-altinity-order": ["displayName", "description", "unit", "decimals", "color", "noValue", "hidden", "delta"] }, "fieldConfig": { "title": "Panel field configuration", "description": "Default and per-column display metadata.", "type": "object", "properties": { "defaults": { "$ref": "#/$defs/fieldConfigValue" }, "columns": { "title": "Column overrides", "description": "Display metadata keyed by exact result-column name.", "type": "object", "additionalProperties": { "$ref": "#/$defs/fieldConfigValue" }, "x-altinity-key-completion": { "source": "resultColumns" } } }, "additionalProperties": true, "x-altinity-order": ["defaults", "columns"] }, "dashboard": { "title": "Dashboard configuration", "description": "Dashboard participation metadata. Feature-specific extensions remain forward compatible.", "type": "object", "properties": { "role": { "title": "Dashboard role (Panel, Filter, or Setup)", "description": "How the saved query participates in a dashboard. panel (the default) creates a visualization tile. filter returns exactly one row whose supported top-level Array, named Tuple Array, or Map columns provide option lists for parameters with the same exact names; it creates no tile and its SQL cannot declare parameters. setup is reserved for serialized Dashboard setup execution.", "type": "string", "enum": ["panel", "filter", "setup"], "default": "panel", "examples": ["filter"] } }, "additionalProperties": true, "x-altinity-order": ["role"] }, "panel": { "title": "Panel configuration", "description": "Visualization and field metadata for the saved query.", "type": "object", "properties": { "cfg": { "$ref": "#/$defs/panelCfg" }, "key": { "title": "Result schema key", "description": "Saved result-column signature used to detect stale positional roles.", "type": ["string", "null"] }, "fieldConfig": { "$ref": "#/$defs/fieldConfig" } }, "additionalProperties": true, "x-altinity-order": ["cfg", "key", "fieldConfig"] }, "chartStyle": { "title": "Line and Area style", "description": "Renderer-independent line presentation. Unknown fields and future string values remain storable.", "type": "object", "properties": { "curve": { "title": "Line curve", "description": "linear draws straight segments, smooth uses monotone interpolation, and stepped draws step-after segments.", "anyOf": [{ "type": "string", "enum": ["linear", "smooth", "stepped"] }, { "type": "string" }], "default": "linear" }, "points": { "title": "Point markers", "description": "auto shows markers only for sparse results, show always displays them, and hide retains hover targets without visible markers.", "anyOf": [{ "type": "string", "enum": ["auto", "show", "hide"] }, { "type": "string" }], "default": "auto" } }, "additionalProperties": true, "x-altinity-order": ["curve", "points"] }, "chartCfg": { "type": "object", "properties": { "x": { "$ref": "#/$defs/resultColumnIndex", "default": 0 }, "y": { "title": "Measure columns", "description": "One or more zero-based result-column indexes used as measures.", "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/resultColumnIndex" } }, "series": { "title": "Series column", "description": "Optional zero-based result-column index used to split series.", "oneOf": [{ "$ref": "#/$defs/resultColumnIndex" }, { "type": "null" }], "default": null, "x-altinity-completion": { "source": "resultColumnIndexes" } } }, "required": ["x", "y"], "additionalProperties": true, "x-altinity-order": ["type", "x", "y", "series"] }, "lineChartCfg": { "allOf": [{ "$ref": "#/$defs/chartCfg" }, { "type": "object", "properties": { "style": { "$ref": "#/$defs/chartStyle" } } }], "x-altinity-order": ["type", "style", "x", "y", "series"] }, "panelCfg": { "title": "Panel type configuration", "description": "Discriminated visualization configuration. Unknown types remain storable for forward compatibility.", "type": "object", "required": ["type"], "properties": { "type": { "title": "Panel type", "description": "Visualization renderer identifier.", "type": "string", "minLength": 1 } }, "additionalProperties": true, "x-altinity-discriminator": "type", "x-altinity-order": ["type"], "oneOf": [{ "title": "Column chart", "description": "Vertical columns using positional X and measure roles.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "bar", "x": 0, "y": [1], "series": null }, "allOf": [{ "$ref": "#/$defs/chartCfg" }, { "properties": { "type": { "const": "bar" } }, "required": ["type"] }] }, { "title": "Horizontal bar chart", "description": "Horizontal bars using positional X and measure roles.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "hbar", "x": 0, "y": [1], "series": null }, "allOf": [{ "$ref": "#/$defs/chartCfg" }, { "properties": { "type": { "const": "hbar" } }, "required": ["type"] }] }, { "title": "Line chart", "description": "Line series using positional X and measure roles.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "line", "x": 0, "y": [1], "series": null }, "allOf": [{ "$ref": "#/$defs/lineChartCfg" }, { "properties": { "type": { "const": "line" } }, "required": ["type"] }] }, { "title": "Area chart", "description": "Filled line series using positional X and measure roles.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "area", "x": 0, "y": [1], "series": null }, "allOf": [{ "$ref": "#/$defs/lineChartCfg" }, { "properties": { "type": { "const": "area" } }, "required": ["type"] }] }, { "title": "Pie chart", "description": "Pie slices using one positional measure role.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "pie", "x": 0, "y": [1], "series": null }, "allOf": [{ "$ref": "#/$defs/chartCfg" }, { "properties": { "type": { "const": "pie" }, "y": { "type": "array", "maxItems": 1 } }, "required": ["type"] }] }, { "title": "KPI", "description": "One-row scalar and named-tuple KPI cards.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "kpi" }, "properties": { "type": { "const": "kpi" } }, "required": ["type"], "additionalProperties": true, "x-altinity-order": ["type"] }, { "title": "Table", "description": "Tabular result rendering with no required panel-specific fields.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "table" }, "properties": { "type": { "const": "table" } }, "required": ["type"], "additionalProperties": true }, { "title": "Logs", "description": "Timestamped log messages with optional explicit result-column roles.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "logs", "time": "event_time", "msg": "message", "level": "level" }, "properties": { "type": { "const": "logs" }, "time": { "$ref": "#/$defs/columnName" }, "msg": { "$ref": "#/$defs/columnName" }, "level": { "$ref": "#/$defs/columnName" } }, "required": ["type"], "additionalProperties": true, "x-altinity-order": ["type", "time", "msg", "level"] }, { "title": "Markdown text", "description": "Safe Markdown content that does not require a SQL result.", "x-altinity-status": "implemented", "x-altinity-snippet": { "type": "text", "content": "# Heading\n\nMarkdown content." }, "properties": { "type": { "const": "text" }, "content": { "title": "Markdown content", "description": "Source text for the safe Markdown renderer.", "type": "string", "default": "" } }, "required": ["type"], "additionalProperties": true, "x-altinity-order": ["type", "content"] }, { "title": "Future panel type", "description": "Forward-compatible storage branch for a type implemented by a newer build.", "x-altinity-status": "planned", "x-altinity-snippet": { "type": "future-panel" }, "properties": { "type": { "type": "string", "minLength": 1, "not": { "enum": ["bar", "hbar", "line", "area", "pie", "kpi", "table", "logs", "text"] } } }, "required": ["type"], "additionalProperties": true }] } } }; +var schema46 = { "title": "Dashboard configuration", "description": "Dashboard participation metadata. Feature-specific extensions remain forward compatible.", "type": "object", "properties": { "role": { "title": "Dashboard role (Panel, Filter, or Setup)", "description": "How the saved query participates in a dashboard. panel (the default) creates a visualization tile. filter returns exactly one row whose supported top-level Array, named Tuple Array, or Map columns provide option lists for parameters with the same exact names; it creates no tile and its SQL cannot declare parameters. setup is reserved for serialized Dashboard setup execution.", "type": "string", "enum": ["panel", "filter", "setup"], "default": "panel", "examples": ["filter"] } }, "additionalProperties": true, "x-altinity-order": ["role"] }; var func1 = require_ucs2length().default; var pattern4 = new RegExp("\\S", "u"); var schema32 = { "title": "Panel configuration", "description": "Visualization and field metadata for the saved query.", "type": "object", "properties": { "cfg": { "$ref": "#/$defs/panelCfg" }, "key": { "title": "Result schema key", "description": "Saved result-column signature used to detect stale positional roles.", "type": ["string", "null"] }, "fieldConfig": { "$ref": "#/$defs/fieldConfig" } }, "additionalProperties": true, "x-altinity-order": ["cfg", "key", "fieldConfig"] }; @@ -490,6 +490,160 @@ function validate23(data, { instancePath = "", parentData, parentDataProperty, r return errors === 0; } validate23.evaluated = { "props": true, "dynamicProps": false, "dynamicItems": false }; +var schema39 = { "title": "Line and Area style", "description": "Renderer-independent line presentation. Unknown fields and future string values remain storable.", "type": "object", "properties": { "curve": { "title": "Line curve", "description": "linear draws straight segments, smooth uses monotone interpolation, and stepped draws step-after segments.", "anyOf": [{ "type": "string", "enum": ["linear", "smooth", "stepped"] }, { "type": "string" }], "default": "linear" }, "points": { "title": "Point markers", "description": "auto shows markers only for sparse results, show always displays them, and hide retains hover targets without visible markers.", "anyOf": [{ "type": "string", "enum": ["auto", "show", "hide"] }, { "type": "string" }], "default": "auto" } }, "additionalProperties": true, "x-altinity-order": ["curve", "points"] }; +function validate26(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) { + let vErrors = null; + let errors = 0; + const evaluated0 = validate26.evaluated; + if (evaluated0.dynamicProps) { + evaluated0.props = void 0; + } + if (evaluated0.dynamicItems) { + evaluated0.items = void 0; + } + if (!validate23(data, { instancePath, parentData, parentDataProperty, rootData, dynamicAnchors })) { + vErrors = vErrors === null ? validate23.errors : vErrors.concat(validate23.errors); + errors = vErrors.length; + } + if (data && typeof data == "object" && !Array.isArray(data)) { + if (data.style !== void 0) { + let data0 = data.style; + if (data0 && typeof data0 == "object" && !Array.isArray(data0)) { + if (data0.curve !== void 0) { + let data1 = data0.curve; + const _errs8 = errors; + let valid4 = false; + const _errs9 = errors; + if (typeof data1 !== "string") { + const err0 = { instancePath: instancePath + "/style/curve", schemaPath: "#/$defs/chartStyle/properties/curve/anyOf/0/type", keyword: "type", params: { type: "string" }, message: "must be string" }; + if (vErrors === null) { + vErrors = [err0]; + } else { + vErrors.push(err0); + } + errors++; + } + if (!(data1 === "linear" || data1 === "smooth" || data1 === "stepped")) { + const err1 = { instancePath: instancePath + "/style/curve", schemaPath: "#/$defs/chartStyle/properties/curve/anyOf/0/enum", keyword: "enum", params: { allowedValues: schema39.properties.curve.anyOf[0].enum }, message: "must be equal to one of the allowed values" }; + if (vErrors === null) { + vErrors = [err1]; + } else { + vErrors.push(err1); + } + errors++; + } + var _valid0 = _errs9 === errors; + valid4 = valid4 || _valid0; + const _errs11 = errors; + if (typeof data1 !== "string") { + const err2 = { instancePath: instancePath + "/style/curve", schemaPath: "#/$defs/chartStyle/properties/curve/anyOf/1/type", keyword: "type", params: { type: "string" }, message: "must be string" }; + if (vErrors === null) { + vErrors = [err2]; + } else { + vErrors.push(err2); + } + errors++; + } + var _valid0 = _errs11 === errors; + valid4 = valid4 || _valid0; + if (!valid4) { + const err3 = { instancePath: instancePath + "/style/curve", schemaPath: "#/$defs/chartStyle/properties/curve/anyOf", keyword: "anyOf", params: {}, message: "must match a schema in anyOf" }; + if (vErrors === null) { + vErrors = [err3]; + } else { + vErrors.push(err3); + } + errors++; + } else { + errors = _errs8; + if (vErrors !== null) { + if (_errs8) { + vErrors.length = _errs8; + } else { + vErrors = null; + } + } + } + } + if (data0.points !== void 0) { + let data2 = data0.points; + const _errs14 = errors; + let valid5 = false; + const _errs15 = errors; + if (typeof data2 !== "string") { + const err4 = { instancePath: instancePath + "/style/points", schemaPath: "#/$defs/chartStyle/properties/points/anyOf/0/type", keyword: "type", params: { type: "string" }, message: "must be string" }; + if (vErrors === null) { + vErrors = [err4]; + } else { + vErrors.push(err4); + } + errors++; + } + if (!(data2 === "auto" || data2 === "show" || data2 === "hide")) { + const err5 = { instancePath: instancePath + "/style/points", schemaPath: "#/$defs/chartStyle/properties/points/anyOf/0/enum", keyword: "enum", params: { allowedValues: schema39.properties.points.anyOf[0].enum }, message: "must be equal to one of the allowed values" }; + if (vErrors === null) { + vErrors = [err5]; + } else { + vErrors.push(err5); + } + errors++; + } + var _valid1 = _errs15 === errors; + valid5 = valid5 || _valid1; + const _errs17 = errors; + if (typeof data2 !== "string") { + const err6 = { instancePath: instancePath + "/style/points", schemaPath: "#/$defs/chartStyle/properties/points/anyOf/1/type", keyword: "type", params: { type: "string" }, message: "must be string" }; + if (vErrors === null) { + vErrors = [err6]; + } else { + vErrors.push(err6); + } + errors++; + } + var _valid1 = _errs17 === errors; + valid5 = valid5 || _valid1; + if (!valid5) { + const err7 = { instancePath: instancePath + "/style/points", schemaPath: "#/$defs/chartStyle/properties/points/anyOf", keyword: "anyOf", params: {}, message: "must match a schema in anyOf" }; + if (vErrors === null) { + vErrors = [err7]; + } else { + vErrors.push(err7); + } + errors++; + } else { + errors = _errs14; + if (vErrors !== null) { + if (_errs14) { + vErrors.length = _errs14; + } else { + vErrors = null; + } + } + } + } + } else { + const err8 = { instancePath: instancePath + "/style", schemaPath: "#/$defs/chartStyle/type", keyword: "type", params: { type: "object" }, message: "must be object" }; + if (vErrors === null) { + vErrors = [err8]; + } else { + vErrors.push(err8); + } + errors++; + } + } + } else { + const err9 = { instancePath, schemaPath: "#/allOf/1/type", keyword: "type", params: { type: "object" }, message: "must be object" }; + if (vErrors === null) { + vErrors = [err9]; + } else { + vErrors.push(err9); + } + errors++; + } + validate26.errors = vErrors; + return errors === 0; +} +validate26.evaluated = { "props": true, "dynamicProps": false, "dynamicItems": false }; function validate22(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) { let vErrors = null; let errors = 0; @@ -576,8 +730,8 @@ function validate22(data, { instancePath = "", parentData, parentDataProperty, r } } const _errs10 = errors; - if (!validate23(data, { instancePath, parentData, parentDataProperty, rootData, dynamicAnchors })) { - vErrors = vErrors === null ? validate23.errors : vErrors.concat(validate23.errors); + if (!validate26(data, { instancePath, parentData, parentDataProperty, rootData, dynamicAnchors })) { + vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); errors = vErrors.length; } if (data && typeof data == "object" && !Array.isArray(data)) { @@ -615,8 +769,8 @@ function validate22(data, { instancePath = "", parentData, parentDataProperty, r } } const _errs14 = errors; - if (!validate23(data, { instancePath, parentData, parentDataProperty, rootData, dynamicAnchors })) { - vErrors = vErrors === null ? validate23.errors : vErrors.concat(validate23.errors); + if (!validate26(data, { instancePath, parentData, parentDataProperty, rootData, dynamicAnchors })) { + vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors); errors = vErrors.length; } if (data && typeof data == "object" && !Array.isArray(data)) { @@ -1079,10 +1233,10 @@ function validate22(data, { instancePath = "", parentData, parentDataProperty, r return errors === 0; } validate22.evaluated = { "props": true, "dynamicProps": false, "dynamicItems": false }; -function validate31(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) { +function validate33(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) { let vErrors = null; let errors = 0; - const evaluated0 = validate31.evaluated; + const evaluated0 = validate33.evaluated; if (evaluated0.dynamicProps) { evaluated0.props = void 0; } @@ -1286,14 +1440,14 @@ function validate31(data, { instancePath = "", parentData, parentDataProperty, r } errors++; } - validate31.errors = vErrors; + validate33.errors = vErrors; return errors === 0; } -validate31.evaluated = { "props": true, "dynamicProps": false, "dynamicItems": false }; -function validate30(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) { +validate33.evaluated = { "props": true, "dynamicProps": false, "dynamicItems": false }; +function validate32(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) { let vErrors = null; let errors = 0; - const evaluated0 = validate30.evaluated; + const evaluated0 = validate32.evaluated; if (evaluated0.dynamicProps) { evaluated0.props = void 0; } @@ -1302,8 +1456,8 @@ function validate30(data, { instancePath = "", parentData, parentDataProperty, r } if (data && typeof data == "object" && !Array.isArray(data)) { if (data.defaults !== void 0) { - if (!validate31(data.defaults, { instancePath: instancePath + "/defaults", parentData: data, parentDataProperty: "defaults", rootData, dynamicAnchors })) { - vErrors = vErrors === null ? validate31.errors : vErrors.concat(validate31.errors); + if (!validate33(data.defaults, { instancePath: instancePath + "/defaults", parentData: data, parentDataProperty: "defaults", rootData, dynamicAnchors })) { + vErrors = vErrors === null ? validate33.errors : vErrors.concat(validate33.errors); errors = vErrors.length; } } @@ -1311,8 +1465,8 @@ function validate30(data, { instancePath = "", parentData, parentDataProperty, r let data1 = data.columns; if (data1 && typeof data1 == "object" && !Array.isArray(data1)) { for (const key0 in data1) { - if (!validate31(data1[key0], { instancePath: instancePath + "/columns/" + key0.replace(/~/g, "~0").replace(/\//g, "~1"), parentData: data1, parentDataProperty: key0, rootData, dynamicAnchors })) { - vErrors = vErrors === null ? validate31.errors : vErrors.concat(validate31.errors); + if (!validate33(data1[key0], { instancePath: instancePath + "/columns/" + key0.replace(/~/g, "~0").replace(/\//g, "~1"), parentData: data1, parentDataProperty: key0, rootData, dynamicAnchors })) { + vErrors = vErrors === null ? validate33.errors : vErrors.concat(validate33.errors); errors = vErrors.length; } } @@ -1335,10 +1489,10 @@ function validate30(data, { instancePath = "", parentData, parentDataProperty, r } errors++; } - validate30.errors = vErrors; + validate32.errors = vErrors; return errors === 0; } -validate30.evaluated = { "props": true, "dynamicProps": false, "dynamicItems": false }; +validate32.evaluated = { "props": true, "dynamicProps": false, "dynamicItems": false }; function validate21(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) { let vErrors = null; let errors = 0; @@ -1369,8 +1523,8 @@ function validate21(data, { instancePath = "", parentData, parentDataProperty, r } } if (data.fieldConfig !== void 0) { - if (!validate30(data.fieldConfig, { instancePath: instancePath + "/fieldConfig", parentData: data, parentDataProperty: "fieldConfig", rootData, dynamicAnchors })) { - vErrors = vErrors === null ? validate30.errors : vErrors.concat(validate30.errors); + if (!validate32(data.fieldConfig, { instancePath: instancePath + "/fieldConfig", parentData: data, parentDataProperty: "fieldConfig", rootData, dynamicAnchors })) { + vErrors = vErrors === null ? validate32.errors : vErrors.concat(validate32.errors); errors = vErrors.length; } } @@ -1494,7 +1648,7 @@ function validate20(data, { instancePath = "", parentData, parentDataProperty, r errors++; } if (!(data6 === "panel" || data6 === "filter" || data6 === "setup")) { - const err8 = { instancePath: instancePath + "/dashboard/role", schemaPath: "#/$defs/dashboard/properties/role/enum", keyword: "enum", params: { allowedValues: schema44.properties.role.enum }, message: "must be equal to one of the allowed values" }; + const err8 = { instancePath: instancePath + "/dashboard/role", schemaPath: "#/$defs/dashboard/properties/role/enum", keyword: "enum", params: { allowedValues: schema46.properties.role.enum }, message: "must be equal to one of the allowed values" }; if (vErrors === null) { vErrors = [err8]; } else { @@ -1526,12 +1680,12 @@ function validate20(data, { instancePath = "", parentData, parentDataProperty, r return errors === 0; } validate20.evaluated = { "props": true, "dynamicProps": false, "dynamicItems": false }; -var validateSavedQueryV2 = validate36; -function validate36(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) { +var validateSavedQueryV2 = validate38; +function validate38(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) { ; let vErrors = null; let errors = 0; - const evaluated0 = validate36.evaluated; + const evaluated0 = validate38.evaluated; if (evaluated0.dynamicProps) { evaluated0.props = void 0; } @@ -1737,17 +1891,17 @@ function validate36(data, { instancePath = "", parentData, parentDataProperty, r } errors++; } - validate36.errors = vErrors; + validate38.errors = vErrors; return errors === 0; } -validate36.evaluated = { "props": true, "dynamicProps": false, "dynamicItems": false }; -var validateLibraryV2 = validate38; +validate38.evaluated = { "props": true, "dynamicProps": false, "dynamicItems": false }; +var validateLibraryV2 = validate40; var formats0 = require_formats().fullFormats["date-time"]; -function validate38(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) { +function validate40(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) { ; let vErrors = null; let errors = 0; - const evaluated0 = validate38.evaluated; + const evaluated0 = validate40.evaluated; if (evaluated0.dynamicProps) { evaluated0.props = void 0; } @@ -1872,8 +2026,8 @@ function validate38(data, { instancePath = "", parentData, parentDataProperty, r } const len0 = data4.length; for (let i0 = 0; i0 < len0; i0++) { - if (!validate36(data4[i0], { instancePath: instancePath + "/queries/" + i0, parentData: data4, parentDataProperty: i0, rootData, dynamicAnchors })) { - vErrors = vErrors === null ? validate36.errors : vErrors.concat(validate36.errors); + if (!validate38(data4[i0], { instancePath: instancePath + "/queries/" + i0, parentData: data4, parentDataProperty: i0, rootData, dynamicAnchors })) { + vErrors = vErrors === null ? validate38.errors : vErrors.concat(validate38.errors); errors = vErrors.length; } } @@ -1896,10 +2050,10 @@ function validate38(data, { instancePath = "", parentData, parentDataProperty, r } errors++; } - validate38.errors = vErrors; + validate40.errors = vErrors; return errors === 0; } -validate38.evaluated = { "props": true, "dynamicProps": false, "dynamicItems": false }; +validate40.evaluated = { "props": true, "dynamicProps": false, "dynamicItems": false }; export { validateLibraryV2, validateQuerySpecV1, diff --git a/src/generated/json-schemas.js b/src/generated/json-schemas.js index a51ecdf2..6f56d3ce 100644 --- a/src/generated/json-schemas.js +++ b/src/generated/json-schemas.js @@ -267,6 +267,54 @@ export const querySpecV1Schema = { "fieldConfig" ] }, + "chartStyle": { + "title": "Line and Area style", + "description": "Renderer-independent line presentation. Unknown fields and future string values remain storable.", + "type": "object", + "properties": { + "curve": { + "title": "Line curve", + "description": "linear draws straight segments, smooth uses monotone interpolation, and stepped draws step-after segments.", + "anyOf": [ + { + "type": "string", + "enum": [ + "linear", + "smooth", + "stepped" + ] + }, + { + "type": "string" + } + ], + "default": "linear" + }, + "points": { + "title": "Point markers", + "description": "auto shows markers only for sparse results, show always displays them, and hide retains hover targets without visible markers.", + "anyOf": [ + { + "type": "string", + "enum": [ + "auto", + "show", + "hide" + ] + }, + { + "type": "string" + } + ], + "default": "auto" + } + }, + "additionalProperties": true, + "x-altinity-order": [ + "curve", + "points" + ] + }, "chartCfg": { "type": "object", "properties": { @@ -313,6 +361,28 @@ export const querySpecV1Schema = { "series" ] }, + "lineChartCfg": { + "allOf": [ + { + "$ref": "#/$defs/chartCfg" + }, + { + "type": "object", + "properties": { + "style": { + "$ref": "#/$defs/chartStyle" + } + } + } + ], + "x-altinity-order": [ + "type", + "style", + "x", + "y", + "series" + ] + }, "panelCfg": { "title": "Panel type configuration", "description": "Discriminated visualization configuration. Unknown types remain storable for forward compatibility.", @@ -404,7 +474,7 @@ export const querySpecV1Schema = { }, "allOf": [ { - "$ref": "#/$defs/chartCfg" + "$ref": "#/$defs/lineChartCfg" }, { "properties": { @@ -432,7 +502,7 @@ export const querySpecV1Schema = { }, "allOf": [ { - "$ref": "#/$defs/chartCfg" + "$ref": "#/$defs/lineChartCfg" }, { "properties": { diff --git a/src/ui/chart-render.js b/src/ui/chart-render.js index 09993bd5..9605da94 100644 --- a/src/ui/chart-render.js +++ b/src/ui/chart-render.js @@ -9,7 +9,10 @@ import { h } from './dom.js'; import { Icon } from './icons.js'; import { formatRows } from '../core/format.js'; -import { chartFieldOptions, chartColors, chartJsConfig, chartCfgValid, normalizeChartCfg, chartRowCap } from '../core/chart-data.js'; +import { + chartFieldOptions, chartColors, chartJsConfig, chartCfgValid, normalizeChartCfg, chartRowCap, + CHART_STYLE_PRESETS, chartStylePreset, applyChartStylePreset, +} from '../core/chart-data.js'; /** A labelled