Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# CHANGELOG.md

## v0.47.0 (unreleased)
- `xticks` now represents the requested number of tick positions on numeric x-axes, rather than the number of intervals between them.
- **Mac users:** the downloadable `sqlpage-macos.tgz` now runs natively on Apple silicon (M-series Macs) and no longer runs on Intel Macs. Homebrew remains the recommended and easiest installation method. On an Intel Mac, [install Homebrew](https://brew.sh/) if needed, then run `brew install sqlpage` (or `brew update` followed by `brew upgrade sqlpage` if you already installed it with Homebrew). Open Terminal in your existing website folder and run `sqlpage` instead of `./sqlpage.bin`; keep your SQL files, database, and `sqlpage` configuration folder in place. Intel installations may build from source and take longer; see the [macOS installation guide](https://sql-page.com/your-first-sql-website/?os=macos#download) for setup and older macOS requirements.
- Updated sqlx-oldapi to v0.6.57 to fix SQL Server fallback expressions such as `ISNULL($missing, 'default')` truncating defaults or failing for date values when the bound variable is `NULL`.
- Fixed MSSQL `JSON_OBJECT('key': value)` expressions being rejected by SQLPage's parser, including when used in `SET` statements or nested in `sqlpage.*` function calls.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S
('xtitle', 'Title of the x axis, displayed below it.', 'TEXT', TRUE, TRUE),
('ytitle', 'Title of the y axis, displayed to its left.', 'TEXT', TRUE, TRUE),
('ztitle', 'Title of the z axis, displayed in tooltips.', 'TEXT', TRUE, TRUE),
('xticks', 'Number of intervals used to generate a numeric x-axis, normally producing one more tick position. On category and time axes, this is a target for label density, so the visible label count may differ.', 'INTEGER', TRUE, TRUE),
('xticks', 'Number of tick positions on a numeric x-axis. For example, 5 requests five evenly spaced positions, including the two ends. On category and time axes, it asks ApexCharts to show up to that many labels; it may show fewer to keep labels from overlapping.', 'INTEGER', TRUE, TRUE),
('yticks', 'Number of ticks on the y axis.', 'INTEGER', TRUE, TRUE),
('ystep', 'Step between ticks on the y axis.', 'REAL', TRUE, TRUE),
('marker', 'Marker size', 'REAL', TRUE, TRUE),
Expand Down
17 changes: 9 additions & 8 deletions frontend/src/apexcharts.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import ApexCharts from "apexcharts";
import { align_series_for, xaxis_type_for } from "./chart_series.js";
import {
align_series_for,
xaxis_tick_amount,
xaxis_type_for,
} from "./chart_series.js";
import { add_init_fn } from "./init.js";

/**
Expand Down Expand Up @@ -138,12 +142,11 @@ const sqlpage_chart = (() => {
let colors = palette;

let series = [...series_map.values()];
const xaxis_type = xaxis_type_for(
series,
const xaxis_type = xaxis_type_for(series, {
chart_type,
is_timeseries,
!!data.horizontal,
);
is_horizontal: !!data.horizontal,
});

let labels;
if (chart_type === "pie") {
Expand Down Expand Up @@ -299,9 +302,7 @@ const sqlpage_chart = (() => {
series,
};
if (labels) options.labels = labels;
// Numeric axes count intervals; category and time axes use tickAmount as a
// target for label density.
if (data.xticks) options.xaxis.tickAmount = data.xticks;
options.xaxis.tickAmount = xaxis_tick_amount(data.xticks, xaxis_type);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the docs say this is tick interval not the number of ticks

Number of Tick Intervals to show. Note: tickAmount doesn't affect datetime xaxis types. For numeric axes, use tickAmount: 'dataPoints' to match dataPoint counts.

https://apexcharts.com/docs/options/xaxis/

const chart = new ApexCharts(
chartContainer,
/** @type {import("apexcharts").ApexOptions} */ (options),
Expand Down
24 changes: 20 additions & 4 deletions frontend/src/chart_series.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@ const x_key = (x) => (x instanceof Date ? x.getTime() : x);
/** @param {ChartSeries[]} series */
const x_is_text = (series) => typeof series[0]?.data?.[0]?.x === "string";

/** @param {ChartSeries[]} series @param {string} chart_type */
/**
* Numeric x values need an explicit axis type to retain their proportional
* spacing; otherwise ApexCharts treats them as evenly spaced categories.
*
* @param {ChartSeries[]} series
* @param {{chart_type:string, is_timeseries:boolean, is_horizontal:boolean}} options
*/
export function xaxis_type_for(
series,
chart_type,
is_timeseries,
is_horizontal,
{ chart_type, is_timeseries, is_horizontal },
) {
if (is_timeseries) return "datetime";
if (x_is_text(series)) return "category";
Expand All @@ -37,6 +41,18 @@ export function xaxis_type_for(
return "numeric";
}

/**
* ApexCharts expects intervals for numeric axes, while SQLPage exposes the
* more intuitive number of tick positions to users.
*
* @param {number|undefined} xticks
* @param {string|undefined} xaxis_type
*/
export function xaxis_tick_amount(xticks, xaxis_type) {
if (!xticks) return;
return xaxis_type === "numeric" ? Math.max(1, xticks - 1) : xticks;
}

/**
* @param {ChartSeries[]} series
* @returns {XValue[]} every x the series hold, in their own order where they
Expand Down
4 changes: 2 additions & 2 deletions tests/end-to-end/fixtures/chart/numeric-axis-xticks.sql
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
SELECT
'chart' AS component,
'test-chart' AS id,
'Explicit numeric x intervals' AS title,
'Explicit numeric x tick count' AS title,
'bar' AS type,
2 AS xticks;
3 AS xticks;

SELECT 'A' AS series, 1 AS x, 1 AS y
UNION ALL SELECT 'A', 4, 4
Expand Down
3 changes: 2 additions & 1 deletion tests/end-to-end/fixtures/chart/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,12 @@ test("keeps irregular numeric x values proportionately spaced", async ({
);
});

test("keeps an explicit x interval count", async ({ page }) => {
test("renders the requested number of numeric x ticks", async ({ page }) => {
const chart = await renderChart(page, "numeric-axis-xticks");

expect(chart.failures).toEqual([]);
expect(chart.xaxis).toEqual({ type: "numeric", tickAmount: 2 });
expect(chart.axisLabels).toHaveLength(3);
});

test("keeps text x values as categories", async ({ page }) => {
Expand Down
39 changes: 34 additions & 5 deletions tests/js/chart_series.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,31 @@ test("uses a continuous axis for numeric Cartesian x values", () => {
const numeric = [series("a", { x: 1, y: 1 }, { x: 12, y: 12 })];

for (const type of ["line", "area", "bar", "scatter", "bubble"])
assert.equal(xaxis_type_for(numeric, type, false, false), "numeric");
assert.equal(
xaxis_type_for(numeric, {
chart_type: type,
is_timeseries: false,
is_horizontal: false,
}),
"numeric",
);
});

test("keeps text and time x values on their respective axes", () => {
assert.equal(
xaxis_type_for([series("a", { x: "Q1", y: 1 })], "bar", false, false),
xaxis_type_for([series("a", { x: "Q1", y: 1 })], {
chart_type: "bar",
is_timeseries: false,
is_horizontal: false,
}),
"category",
);
assert.equal(
xaxis_type_for([series("a", { x: 1, y: 1 })], "bar", true, false),
xaxis_type_for([series("a", { x: 1, y: 1 })], {
chart_type: "bar",
is_timeseries: true,
is_horizontal: false,
}),
"datetime",
);
});
Expand All @@ -46,8 +61,22 @@ test("does not turn category-oriented charts into numeric axes", () => {
const numeric = [series("a", { x: 1, y: 1 })];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding a test for [series("a", { x: "a", y: 1 })] and checking assert.equal(value_axis_labels(3, axis), 3); instead of undefined may have caught the horizontal edge case


for (const type of ["heatmap", "rangeBar", "pie", "treemap"])
assert.equal(xaxis_type_for(numeric, type, false, false), undefined);
assert.equal(xaxis_type_for(numeric, "bar", false, true), undefined);
assert.equal(
xaxis_type_for(numeric, {
chart_type: type,
is_timeseries: false,
is_horizontal: false,
}),
undefined,
);
assert.equal(
xaxis_type_for(numeric, {
chart_type: "bar",
is_timeseries: false,
is_horizontal: true,
}),
undefined,
);
});

test("merged_x_values keeps the order the series agree on", () => {
Expand Down
Loading