chore: import upstream snapshot with attribution
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
/* !include https://cdn.jsdelivr.net/npm/apexcharts@5.13.0/dist/apexcharts.min.js */
|
||||
|
||||
sqlpage_chart = (() => {
|
||||
function sqlpage_chart() {
|
||||
for (const c of document.querySelectorAll("[data-pre-init=chart]")) {
|
||||
try {
|
||||
build_sqlpage_chart(c);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tblrColors = [
|
||||
["blue", "#1c7ed6", "#339af0"],
|
||||
["red", "#f03e3e", "#ff6b6b"],
|
||||
["green", "#37b24d", "#51cf66"],
|
||||
["pink", "#d6336c", "#f06595"],
|
||||
["purple", "#ae3ec9", "#cc5de8"],
|
||||
["orange", "#f76707", "#ff922b"],
|
||||
["cyan", "#1098ad", "#22b8cf"],
|
||||
["teal", "#0ca678", "#20c997"],
|
||||
["yellow", "#f59f00", "#fcc419"],
|
||||
["indigo", "#4263eb", "#5c7cfa"],
|
||||
["lime", "#74b816", "#94d82d"],
|
||||
["azure", "#339af0", "#339af0"],
|
||||
["gray", "#495057", "#adb5bd"],
|
||||
["black", "#000000", "#000000"],
|
||||
["white", "#ffffff", "#f8f9fa"],
|
||||
];
|
||||
const colorNames = Object.fromEntries(
|
||||
tblrColors.flatMap(([name, dark, light]) => [
|
||||
[name, dark],
|
||||
[`${name}-lt`, light],
|
||||
]),
|
||||
);
|
||||
const isDarkTheme = document.body?.dataset?.bsTheme === "dark";
|
||||
|
||||
/** @typedef { { [name:string]: {data:{x:number|string|Date,y:number}[], name:string} } } Series */
|
||||
|
||||
/**
|
||||
* Aligns series data points by their x-axis categories, ensuring all series have data points
|
||||
* for each unique category. Missing values are filled with zeros.
|
||||
* Categories are ordered by their name.
|
||||
*
|
||||
* @example
|
||||
* // Input series:
|
||||
* const series = [
|
||||
* { name: "A", data: [{x: "X2", y: 10}, {x: "X3", y: 30}] },
|
||||
* { name: "B", data: [{x: "X1", y: 25}, {x: "X2", y: 20}] }
|
||||
* ];
|
||||
*
|
||||
* // Output after align_categories (orderedCategories will be ["X1","X2", "X3"]):
|
||||
* // [
|
||||
* // { name: "A", data: [{x: "X1", y: 0}, {x: "X2", y: 10}, {x: "X3", y: 30}] },
|
||||
* // { name: "B", data: [{x: "X1", y: 25}, {x: "X2", y: 20}, {x: "X3", y: 0}] }
|
||||
* // ]
|
||||
*
|
||||
* @param {(Series[string])[]} series - Array of series objects, each containing name and data points
|
||||
* @returns {Series[string][]} Aligned series with consistent categories across all series
|
||||
*/
|
||||
function align_categories(series) {
|
||||
const categoriesSet = new Set();
|
||||
const pointers = series.map((_) => 0); // Index of current data point in each series
|
||||
const x_at = (series_idx) =>
|
||||
series[series_idx].data[pointers[series_idx]].x;
|
||||
const series_idxs = series.flatMap((s, i) => (s.data.length ? i : []));
|
||||
while (series_idxs.length > 0) {
|
||||
let idx_of_xmin = series_idxs[0];
|
||||
for (const series_idx of series_idxs) {
|
||||
if (x_at(series_idx) < x_at(idx_of_xmin)) idx_of_xmin = series_idx;
|
||||
}
|
||||
|
||||
const new_category = x_at(idx_of_xmin);
|
||||
if (!categoriesSet.has(new_category)) categoriesSet.add(new_category);
|
||||
pointers[idx_of_xmin]++;
|
||||
if (pointers[idx_of_xmin] >= series[idx_of_xmin].data.length) {
|
||||
series_idxs.splice(series_idxs.indexOf(idx_of_xmin), 1);
|
||||
}
|
||||
}
|
||||
// Create a map of category -> value for each series and rebuild
|
||||
return series.map((s) => {
|
||||
const valueMap = new Map(s.data.map((point) => [point.x, point.y]));
|
||||
return {
|
||||
name: s.name,
|
||||
data: Array.from(categoriesSet, (category) => ({
|
||||
x: category,
|
||||
y: valueMap.get(category) || 0,
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {HTMLElement} c */
|
||||
function build_sqlpage_chart(c) {
|
||||
const [data_element] = c.getElementsByTagName("data");
|
||||
const data = JSON.parse(data_element.textContent);
|
||||
const chartContainer = c.querySelector(".chart");
|
||||
chartContainer.innerHTML = "";
|
||||
const is_timeseries = !!data.time;
|
||||
/** @type { Series } */
|
||||
const series_map = {};
|
||||
for (const [name, old_x, old_y, z] of data.points) {
|
||||
series_map[name] = series_map[name] || { name, data: [] };
|
||||
let x = old_x;
|
||||
let y = old_y;
|
||||
if (is_timeseries) {
|
||||
if (typeof x === "number") x = new Date(x * 1000);
|
||||
else if (data.type === "rangeBar" && Array.isArray(y))
|
||||
y = y.map((y) => new Date(y).getTime());
|
||||
else x = new Date(x);
|
||||
}
|
||||
series_map[name].data.push({ x, y, z });
|
||||
}
|
||||
if (data.xmin == null) data.xmin = undefined;
|
||||
if (data.xmax == null) data.xmax = undefined;
|
||||
if (data.ymin == null) data.ymin = undefined;
|
||||
if (data.ymax == null) data.ymax = undefined;
|
||||
|
||||
const colors = [
|
||||
...data.colors.filter((c) => c).map((c) => colorNames[c]),
|
||||
...tblrColors.map(([_, dark, light]) => (isDarkTheme ? dark : light)),
|
||||
...tblrColors.map(([_, dark, light]) => (isDarkTheme ? light : dark)),
|
||||
];
|
||||
|
||||
let series = Object.values(series_map);
|
||||
|
||||
let labels;
|
||||
const categories =
|
||||
series.length > 0 && typeof series[0].data[0].x === "string";
|
||||
if (data.type === "pie") {
|
||||
labels = data.points.map(([name, x, _y]) => x || name);
|
||||
series = data.points.map(([_name, _x, y]) => Number.parseFloat(y));
|
||||
} else if (categories && data.type === "bar" && series.length > 1)
|
||||
series = align_categories(series);
|
||||
|
||||
const chart_type = data.type || "line";
|
||||
const options = {
|
||||
chart: {
|
||||
type: chart_type,
|
||||
fontFamily: "inherit",
|
||||
background: "transparent",
|
||||
parentHeightOffset: 0,
|
||||
height: chartContainer.style.height,
|
||||
stacked: !!data.stacked,
|
||||
toolbar: {
|
||||
show: !!data.toolbar,
|
||||
},
|
||||
animations: {
|
||||
enabled: false,
|
||||
},
|
||||
zoom: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
mode: isDarkTheme ? "dark" : "light",
|
||||
palette: "palette4",
|
||||
},
|
||||
legend: {
|
||||
show: data.show_legend === null || !!data.show_legend,
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: !!data.labels,
|
||||
dropShadow: {
|
||||
enabled: true,
|
||||
color: "var(--tblr-primary-bg-subtle)",
|
||||
},
|
||||
formatter:
|
||||
data.type === "rangeBar"
|
||||
? (_val, { seriesIndex, w }) => w.config.series[seriesIndex].name
|
||||
: data.type === "pie"
|
||||
? (value, { seriesIndex, w }) =>
|
||||
`${w.config.labels[seriesIndex]}: ${value.toFixed()}%`
|
||||
: (value) => value?.toLocaleString?.() || value,
|
||||
},
|
||||
fill: {
|
||||
type: data.type === "area" ? "gradient" : "solid",
|
||||
},
|
||||
stroke: {
|
||||
width:
|
||||
{
|
||||
area: 3,
|
||||
line: 2,
|
||||
}[chart_type] || 0,
|
||||
lineCap: "round",
|
||||
curve: "smooth",
|
||||
},
|
||||
xaxis: {
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
min: data.xmin,
|
||||
max: data.xmax,
|
||||
title: {
|
||||
text: data.xtitle || undefined,
|
||||
},
|
||||
type: is_timeseries ? "datetime" : categories ? "category" : undefined,
|
||||
labels: {
|
||||
datetimeUTC: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
logarithmic: !!data.logarithmic,
|
||||
min: data.ymin,
|
||||
max: data.ymax,
|
||||
stepSize: data.ystep,
|
||||
tickAmount: data.yticks,
|
||||
title: {
|
||||
text: data.ytitle || undefined,
|
||||
},
|
||||
},
|
||||
zaxis: {
|
||||
title: {
|
||||
text: data.ztitle || undefined,
|
||||
},
|
||||
},
|
||||
markers: {
|
||||
size: data.marker || 0,
|
||||
strokeWidth: 0,
|
||||
hover: {
|
||||
sizeOffset: 5,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
fillSeriesColor: false,
|
||||
custom:
|
||||
data.type === "bubble" || data.type === "scatter"
|
||||
? bubbleTooltip
|
||||
: undefined,
|
||||
y: {
|
||||
formatter: (value) => {
|
||||
if (value == null) return "";
|
||||
if (is_timeseries && data.type === "rangeBar") {
|
||||
const d = new Date(value);
|
||||
if (d.getHours() === 0 && d.getMinutes() === 0)
|
||||
return d.toLocaleDateString();
|
||||
return d.toLocaleString();
|
||||
}
|
||||
const str_val = value.toLocaleString();
|
||||
if (str_val.length > 10 && Number.isNaN(value))
|
||||
return value.toFixed(2);
|
||||
return str_val;
|
||||
},
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: !!data.horizontal || data.type === "rangeBar",
|
||||
borderRadius: 5,
|
||||
},
|
||||
bubble: { minBubbleRadius: 5 },
|
||||
},
|
||||
colors,
|
||||
series,
|
||||
};
|
||||
if (labels) options.labels = labels;
|
||||
// tickamount is the number of intervals, not the number of ticks
|
||||
if (data.xticks) options.xaxis.tickAmount = data.xticks;
|
||||
console.log("Rendering chart", options);
|
||||
const chart = new ApexCharts(chartContainer, options);
|
||||
chart.render();
|
||||
if (window.charts) window.charts.push(chart);
|
||||
else window.charts = [chart];
|
||||
c.removeAttribute("data-pre-init");
|
||||
}
|
||||
|
||||
function bubbleTooltip({ seriesIndex, dataPointIndex, w }) {
|
||||
const { name, data } = w.config.series[seriesIndex];
|
||||
const point = data[dataPointIndex];
|
||||
|
||||
const tooltip = document.createElement("div");
|
||||
tooltip.className = "apexcharts-tooltip-text";
|
||||
tooltip.style.fontFamily = "inherit";
|
||||
|
||||
const seriesName = document.createElement("div");
|
||||
seriesName.className = "apexcharts-tooltip-y-group";
|
||||
seriesName.style.fontWeight = "bold";
|
||||
seriesName.innerText = name;
|
||||
tooltip.appendChild(seriesName);
|
||||
|
||||
for (const axis of ["x", "y", "z"]) {
|
||||
const value = point[axis];
|
||||
if (value == null) continue;
|
||||
const axisValue = document.createElement("div");
|
||||
axisValue.className = "apexcharts-tooltip-y-group";
|
||||
let axis_conf = w.config[`${axis}axis`];
|
||||
if (axis_conf.length) axis_conf = axis_conf[0];
|
||||
const title = axis_conf.title.text || axis;
|
||||
const labelSpan = document.createElement("span");
|
||||
labelSpan.className = "apexcharts-tooltip-text-y-label";
|
||||
labelSpan.innerText = `${title}: `;
|
||||
axisValue.appendChild(labelSpan);
|
||||
const valueSpan = document.createElement("span");
|
||||
valueSpan.className = "apexcharts-tooltip-text-y-value";
|
||||
valueSpan.innerText = value;
|
||||
axisValue.appendChild(valueSpan);
|
||||
tooltip.appendChild(axisValue);
|
||||
}
|
||||
return tooltip.outerHTML;
|
||||
}
|
||||
|
||||
return sqlpage_chart;
|
||||
})();
|
||||
|
||||
add_init_fn(sqlpage_chart);
|
||||
Reference in New Issue
Block a user