Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 72 additions & 2 deletions schemas/generated/library-v2.bundle.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -408,7 +478,7 @@
},
"allOf": [
{
"$ref": "#/$defs/chartCfg"
"$ref": "#/$defs/lineChartCfg"
},
{
"properties": {
Expand Down Expand Up @@ -436,7 +506,7 @@
},
"allOf": [
{
"$ref": "#/$defs/chartCfg"
"$ref": "#/$defs/lineChartCfg"
},
{
"properties": {
Expand Down
43 changes: 41 additions & 2 deletions schemas/query-spec-v1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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"] }
]
},
Expand All @@ -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"] }
]
},
Expand Down
62 changes: 60 additions & 2 deletions src/core/chart-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));

/**
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -304,14 +348,28 @@ 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];
if (isPie) {
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 };
});
Expand Down
Loading