/* !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);