Embedded Analytics Developer Guide
Version 0.4.3 | Last Updated: April 2026
QuantumLayers exposes its chart data, statistical analysis, and AI insights through standard WordPress AJAX endpoints secured with Bearer token authentication. This guide explains how to pull live data from a running QuantumLayers installation into any page — without rebuilding the data pipeline.
Table of Contents
- Prerequisites
- Authentication
- Chart.js Setup
- Endpoint Reference
- Dataset Management
- Complete Code Example
- Chart Type Quick Reference
Prerequisites
You need access to a running QuantumLayers installation. The third-party page does not need to install or host QL itself — it only needs network access to the QL host.
QL JavaScript Libraries
Both files are served directly from the QL host and can be loaded with a <script> tag:
- assets/auth.js — exposes the
QLAuthobject: token storage, the_ajax()wrapper, and sign-in helpers. - assets/analytics.js — exposes the
QLAnalyticsobject: chart loading, rendering, and saved-chart management.
<script src="https://quantumlayers.com/wp-content/plugins/quantumlayers/assets/auth.js"></script>
<script src="https://quantumlayers.com/wp-content/plugins/quantumlayers/assets/analytics.js"></script>
Chart.js Setup
QuantumLayers uses Chart.js 4.4.0 and three plugins. Load all four CDN scripts before your own code:
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@sgratzl/chartjs-chart-boxplot@4.2.5/build/index.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-chart-matrix@2.0.1/dist/chartjs-chart-matrix.min.js"></script>
Authentication
Every AJAX request must carry a Bearer token in the Authorization header. Two approaches are available.
Option A — QL Session Token (Same-Origin)
After the user signs in through QL, their token is in localStorage under the key ql_session_token. Read it with QLAuth.getSessionToken():
$.ajax({
url: 'https://quantumlayers.com/wp-admin/admin-ajax.php',
method: 'POST',
headers: { 'Authorization': 'Bearer ' + QLAuth.getSessionToken() },
data: { action: 'ql_get_chart_data', dataset_id: 42, chart_type: 'bar' }
});
Option B — Third-Party Provider Registration (Shadow Users)
For cross-origin embeds, the QL operator registers your application in the ql_auth_providers table with three fields:
- id — unique slug (e.g.
"acme-corp"); becomes thekidin the JWT header. - name — human-readable label.
- jwt_secret — shared HMAC-SHA256 secret (HS256).
Your backend mints a signed HS256 JWT with the following structure, then POSTs it to ql_third_party_signin. sub and email are required; first_name and last_name are optional but kept in sync on every login.
// Header — kid must match the id column in ql_auth_providers
{ "alg": "HS256", "typ": "JWT", "kid": "<provider_id>" }
// Payload
{ "sub": "<stable_user_id>", "email": "jane@example.com",
"first_name": "Jane", "last_name": "Doe",
"exp": <unix timestamp> }
Minting the JWT — PHP
With firebase/php-jwt (composer require firebase/php-jwt):
use Firebase\JWT\JWT;
$provider_id = 'acme-corp'; // matches ql_auth_providers.id
$jwt_secret = 'your-shared-secret'; // matches ql_auth_providers.jwt_secret
$payload = [
'sub' => 'user-123', // stable unique ID in your system
'email' => 'jane@example.com',
'first_name' => 'Jane',
'last_name' => 'Doe',
'exp' => time() + 3600, // 1-hour expiry
];
// Fourth argument sets the kid header field
$jwt = JWT::encode($payload, $jwt_secret, 'HS256', $provider_id);
Without an external library (PHP built-ins only):
function base64url_encode(string $data): string {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
$provider_id = 'acme-corp';
$jwt_secret = 'your-shared-secret';
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT', 'kid' => $provider_id]));
$payload = base64url_encode(json_encode([
'sub' => 'user-123',
'email' => 'jane@example.com',
'first_name' => 'Jane',
'last_name' => 'Doe',
'exp' => time() + 3600,
]));
$signature = base64url_encode(hash_hmac('sha256', "$header.$payload", $jwt_secret, true));
$jwt = "$header.$payload.$signature";
Minting the JWT — Node.js
With jsonwebtoken (npm install jsonwebtoken):
const jwt = require('jsonwebtoken');
const PROVIDER_ID = 'acme-corp'; // matches ql_auth_providers.id
const JWT_SECRET = 'your-shared-secret'; // matches ql_auth_providers.jwt_secret
const token = jwt.sign(
{
sub: 'user-123', // stable unique ID in your system
email: 'jane@example.com',
first_name: 'Jane',
last_name: 'Doe',
},
JWT_SECRET,
{
algorithm: 'HS256',
keyid: PROVIDER_ID, // sets the kid header field
expiresIn: '1h',
}
);
Without an external library (Node.js built-in crypto):
const crypto = require('crypto');
const PROVIDER_ID = 'acme-corp';
const JWT_SECRET = 'your-shared-secret';
function base64url(str) {
return Buffer.from(str).toString('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
const header = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT', kid: PROVIDER_ID }));
const payload = base64url(JSON.stringify({
sub: 'user-123',
email: 'jane@example.com',
first_name: 'Jane',
last_name: 'Doe',
exp: Math.floor(Date.now() / 1000) + 3600,
}));
const sig = crypto.createHmac('sha256', JWT_SECRET)
.update(`${header}.${payload}`)
.digest('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
const token = `${header}.${payload}.${sig}`;
Once you have the signed token, POST it to QL:
$.post(QL_AJAX, { action: 'ql_third_party_signin', jwt: SIGNED_JWT }, function(res) {
if (res.success) QLAuth.setSessionToken(res.data.session_token);
});
QL verifies the signature, then finds or creates a shadow user identified by provider_id + provider_sub. Email and name are kept in sync on every login. The response contains a standard session_token for all subsequent requests.
Note: ql_try_auth() allows unauthenticated access for public datasets; ql_verify_auth() requires a valid token and is used by all write and analysis endpoints.
Endpoint Reference
All endpoints are reached via POST to https://<QL_HOST>/wp-admin/admin-ajax.php with the action field set as shown.
Dataset Metadata
ql_get_dataset_detail
Queries the ql_datasets table for the dataset record and ql_dataset_columns for its column schema. Access is checked via ql_user_can_read_dataset(), which allows access to datasets owned by the user or shared with their organisation. The column rows include pre-computed statistics (min, max, mean, stddev, distinct count, null count) calculated at upload time — these are the cheapest way to understand a dataset without loading raw rows.
Auth: ql_verify_auth() — valid Bearer token required.
POST params:
dataset_id(int, required)
Response:
{
"dataset": {
"id": 42,
"name": "sales_2024",
"row_count": 15000,
"created_at": "2024-01-15 10:30:00"
// ... full ql_datasets row
},
"columns": [
{
"col_name": "product_category",
"inferred_type": "string", // string | integer | float | date | datetime
"col_index": 0,
"distinct_count": 8,
"null_count": 0,
"min_val": null,
"max_val": null,
"mean_val": null,
"stddev_val": null
}
// ... one object per column, ordered by col_index
]
}
Chart Data
ql_get_chart_data
Routes to a private method based on chart_type, then returns a complete Chart.js config object (type + data + options) ready to pass to new Chart(ctx, data). All data is loaded from the raw dataset rows up to the plan row limit. Filters are applied in PHP before aggregation. Below is what each chart type actually does internally:
- histogram — validates the column is numeric, loads raw values, auto-computes 20 equal-width bins using Sturges-style bin width (
(max−min)/20), returns abarchart config with bin ranges as labels and frequency counts as data. - time_series / stacked_area — parses every value in
time_columnviastrtotime(), sorts points chronologically, then optionally aggregates into calendar buckets (interval: day/week/month/year) using sum/avg/count/min/max. Ifcumulative=1, a running total is applied per series after aggregation. Gaps (null Y values) are preserved; Chart.js is instructed not to interpolate across them (spanGaps: false). Returns alinechart config;stacked_areasetsfill: trueon each dataset. - bar / horizontal_bar — validates that
category_columnis either a string column or a numeric column with fewer distinct values than 10% of row count (i.e. categorical). Groups all rows by category, applies the chosen aggregation (sum/avg/count/min/max) across eachvalue_columnsentry, and returns abarconfig (indexAxis: 'y'for horizontal). - pie / doughnut — groups by
category_column, aggregatesvalue_column, sorts descending, and trims tolimitcategories (default 10). Returns apieordoughnutconfig with auto-assigned colours. - scatter — loads raw
x_columnandy_columnvalues as{x, y}point pairs. Ifcolor_columnis provided, points are split into one dataset per distinct category value. Ifsize_columnis provided, the point radius is scaled proportionally to the column value. - bubble — identical to scatter but always requires
size_column; the radius (r) of each bubble is normalised to a 5–30 px range across the column’s min/max. - box_plot / violin — groups raw
y_columnvalues by the categories inx_column. Returns data in the format expected by@sgratzl/chartjs-chart-boxplot: arrays of raw values per group, from which the library computes quartiles, whiskers, and outlier dots client-side. - heatmap — aggregates
z_columnfor every unique(x_column, y_column)coordinate pair and returns a dataset in the matrix format required bychartjs-chart-matrix: an array of{x, y, v}objects. - regression — delegates to
analytics.performLinearRegression()(OLS, supports multiple predictors). Returns a bar chart of coefficients with error bars (±1 std error), colour-coded by p-value significance.
Auth: ql_try_auth() — token optional for public datasets, required for private.
Common POST params (all chart types):
dataset_id(int, required)chart_type(string, required) — see table belowfilter_category_column(string, optional) — column to filter rows by exact valuefilter_category_value(string, optional) — value to match infilter_category_columnfilter_date_column(string, optional) — date column to range-filterfilter_date_from/filter_date_to(string, optional) — ISO date bounds for the date filter
Type-specific params:
| chart_type | Additional POST params | Defaults |
|---|---|---|
bar | category_column, value_columns[], aggregation | aggregation: sum |
horizontal_bar | category_column, value_columns[], aggregation | aggregation: sum |
pie / doughnut | category_column, value_column, aggregation, limit | aggregation: sum, limit: 10 |
scatter | x_column, y_column, color_column (opt), size_column (opt) | — |
bubble | x_column, y_column, size_column, color_column (opt) | — |
box_plot / violin | x_column (categories), y_column (values) | — |
heatmap | x_column, y_column, z_column, aggregation | aggregation: average |
time_series | time_column, value_columns[], aggregation, interval, cumulative | aggregation: sum, interval: day, cumulative: 0 |
stacked_area | time_column, value_columns[], aggregation, interval | aggregation: none, interval: day |
histogram | column_name | — |
regression | y_column, x_columns[] | — |
Response: A Chart.js config object you can pass directly to new Chart(ctx, data). On error: { success: false, data: { error: "..." } }.
ql_get_recommended_charts
Runs the full rule-based insight pipeline (same as ql_get_insights but without the AI call) and converts each insight into a chart candidate. Candidates are scored by insight_score (0–100), the top max_charts are kept, and actual Chart.js data is generated for each one via generateChartData(). Any candidate whose chart generation returns an error is silently dropped. The result is an opinionated, ranked list of charts that best reveal patterns in the data — useful for auto-populating a dashboard without any user configuration.
Auth: ql_verify_auth() — valid Bearer token required.
POST params:
dataset_id(int, required)max_charts(int, optional, default: 20)selected_columns[](array of strings, optional) — restrict analysis to these columns- Filter params: same
filter_category_column/valueandfilter_date_*asql_get_chart_data
Response:
{
"charts": [
{
"chart_type": "scatter",
"chart_data": { /* Chart.js config — pass directly to new Chart() */ },
"insight_score": 87, // 0–100 relevance ranking
"reason": "Strong correlation (r=0.91) between price and revenue",
"params": { "x_column": "price", "y_column": "revenue" },
"metadata": {}
}
// ... up to max_charts entries, sorted by insight_score desc
],
"analysis_summary": {
"total_columns": 12,
"numeric_columns": 7,
"categorical_columns": 4,
"datetime_columns": 1,
"candidates_evaluated": 43,
"filter_applied": false
}
}
ql_save_chart
Recursively sanitises the chart_params array and serialises it to JSON, then inserts a row into ql_saved_charts (columns: dataset_id, user_id, chart_name, chart_type, chart_params, created_at). The chart_params value is the exact set of type-specific params you would pass to ql_get_chart_data — store the params from a chart you already rendered and you can reconstruct it later without re-running analysis.
Auth: ql_verify_auth() — valid Bearer token required.
POST params:
dataset_id(int, required)chart_type(string, required)chart_params(array, required) — the same type-specific params used inql_get_chart_data, stored as JSONchart_name(string, optional) — human-readable label
Response: { "message": "Chart saved successfully", "chart_id": 17 }
ql_get_saved_charts
Selects all rows from ql_saved_charts where dataset_id and user_id match, ordered newest-first. The chart_params field is a JSON string — parse it with JSON.parse() and pass the result directly to ql_get_chart_data to re-render the chart on demand.
Auth: ql_verify_auth() — valid Bearer token required.
POST params: dataset_id (int, required)
Response:
{
"charts": [
{
"id": 17,
"dataset_id": 42,
"user_id": 5,
"chart_name": "Revenue by Category",
"chart_type": "bar",
"chart_params": "{\"category_column\":\"product_category\",\"value_columns\":[\"revenue\"]}",
"created_at": "2024-03-01 14:22:00"
}
]
}
Note: chart_params is a JSON string. Parse it and pass to ql_get_chart_data to re-render the chart.
ql_delete_saved_chart
Fetches the ql_saved_charts row by chart_id and user_id before deleting — a chart belonging to a different user returns a 404-style error even if the chart_id exists. This ownership check is the only authorisation gate; no dataset access check is performed.
Auth: ql_verify_auth() — valid Bearer token required.
POST params: chart_id (int, required)
Response: { "message": "Chart deleted successfully" }
Statistical Analysis
ql_get_statistical_summary
Iterates every numeric column (inferred_type of integer or float) whose distinct_count exceeds 10% of the dataset’s row count — this threshold filters out effectively categorical numerics like status codes or boolean flags. For each qualifying column, min/max/mean/std are read directly from the pre-computed ql_dataset_columns row (no raw-data load needed). The raw column values are then loaded once to compute median, Q1, Q3, skewness (third standardised moment), and kurtosis (fourth standardised moment minus 3 — i.e. excess kurtosis).
Auth: ql_verify_auth() — valid Bearer token required.
POST params: dataset_id (int, required)
Response: array of per-column objects:
[
{
"column": "revenue",
"type": "float",
"count": 14850, // non-null rows
"missing": 150, // null count
"min": 0.99,
"max": 9999.0,
"mean": 312.45,
"std": 198.72,
"median": 265.10,
"q1": 124.00,
"q3": 478.50,
"skewness": 1.34,
"kurtosis": 2.87
}
]
ql_get_correlation_matrix
Selects all continuous numeric columns (same 10%-distinct threshold as the statistical summary), loads their raw values, then computes the full Pearson r matrix in PHP. The result is cached as a WordPress transient for one hour — the cache key includes the dataset ID, so a second call within the hour returns instantly without reloading data. Requires at least two qualifying columns; returns an error otherwise. The cache is per-dataset (not per-user), so all users sharing a dataset see the same cached matrix.
Auth: ql_verify_auth() — valid Bearer token required.
POST params: dataset_id (int, required)
Response:
{
"columns": ["price", "quantity", "revenue"],
"matrix": [
[1.0, -0.12, 0.91],
[-0.12, 1.0, 0.43],
[0.91, 0.43, 1.0]
],
"method": "pearson"
}
matrix[i][j] is the Pearson r between columns[i] and columns[j]. Values range from −1 to 1.
ql_get_distribution_analysis
Loads all non-null numeric values for the requested column, sorts them, and computes: mean, variance, std, range, IQR, full percentile set (p5/Q1/median/Q3/p95), skewness (third standardised moment), and excess kurtosis. Outliers are detected using the 1.5×IQR fence rule: values below Q1 − 1.5×IQR or above Q3 + 1.5×IQR are flagged. Up to 50 outlier values are returned in the response. Normality is classified as approximately normal (|skewness| < 0.5 and |kurtosis| < 0.5), non-normal (either exceeds 1), or moderately skewed otherwise. The column must be integer or float; string and date columns return an error.
Auth: ql_verify_auth() — valid Bearer token required.
POST params: dataset_id (int, required), column_name (string, required)
Response:
{
"column_name": "revenue",
"count": 14850,
"mean": 312.45,
"std": 198.72,
"variance": 39489.48,
"range": 9998.01,
"iqr": 354.50,
"percentiles": {
"min": 0.99, "p5": 12.50, "q1": 124.00,
"median": 265.10, "q3": 478.50, "p95": 720.00, "max": 9999.00
},
"skewness": 1.34,
"kurtosis": 2.87,
"normality": "non-normal", // "approximately normal" | "moderately skewed" | "non-normal"
"outliers": {
"count": 312,
"percentage": 2.10,
"lower_fence": -407.75,
"upper_fence": 1010.25,
"values": [1200.00, 1350.00] // up to 50 outlier values
}
}
ql_get_pca_analysis
Selects continuous numeric columns (10%-distinct threshold), loads raw values, z-score standardises each column (subtract mean, divide by std), then builds the covariance matrix. Eigenvalues and eigenvectors are computed using the Power Iteration method implemented in PHP. Components are sorted by eigenvalue descending. The transformed data (PC scores) is capped at the first 1,000 rows in the response to limit payload size. Results are cached for one hour keyed by dataset_id + n_components. If n_components is less than 2 or omitted, all components are returned. Requires at least 2 qualifying columns and 10 rows.
Auth: ql_verify_auth() — valid Bearer token required.
POST params:
dataset_id(int, required)n_components(int, optional) — number of components to return; if omitted or less than 2, defaults to the total number of eligible columns
Response:
{
"n_components": 3,
"feature_names": ["price", "quantity", "revenue"],
"explained_variance": [4.21, 1.83, 0.96],
"explained_variance_ratio": [0.60, 0.26, 0.14],
"cumulative_variance": [0.60, 0.86, 1.00],
"total_variance_explained": 1.00,
"components": [[...], [...], [...]], // eigenvectors (loadings)
"transformed_data": [ // PC scores, first 1000 rows
[2.31, -0.84, 0.12],
...
]
}
ql_get_anova_analysis
For every pair of (categorical column × continuous numeric column), runs a one-way ANOVA: groups numeric values by category label, computes Sum of Squares Between (SSB) and Within (SSW), derives the F-statistic (MSB/MSW), and approximates the p-value using an incomplete beta function (no external statistics library). Additionally computes Cohen’s d between the two groups with the largest pairwise mean difference, as a standardised effect-size measure. eta_squared = SSB / (SSB + SSW) gives the proportion of variance explained by the grouping. The significance label is derived from the p-value: not_significant (p ≥ 0.05), moderately_significant (p < 0.05), highly_significant (p < 0.01), or insufficient_data (fewer than 3 rows or fewer than 2 groups).
Auth: ql_verify_auth() — valid Bearer token required.
POST params: dataset_id (int, required)
Response:
{
"categorical_columns": ["region", "product_category"],
"numerical_columns": ["revenue", "quantity"],
"sample_size": 14850,
"anova_matrix": {
"region": {
"revenue": {
"f_statistic": 18.43,
"p_value": 0.000021,
"confidence_level": 0.99998,
"significance": "highly_significant",
// possible values: "insufficient_data" | "not_significant"
// | "moderately_significant" | "highly_significant"
"df_between": 4,
"df_within": 14845,
"groups": 5,
"sample_size": 14850,
"ssb": 12450.3,
"ssw": 98231.7,
"msb": 3112.6,
"msw": 6.62,
"eta_squared": 0.112, // effect size (0–1)
"cohens_d": 0.84, // largest pairwise effect size
"cohens_d_groups": ["North", "South"]
}
}
}
}
AI Insights
ql_get_insights
Two-stage pipeline. Stage 1 — rule-based insights (always runs): nine parallel analyses each produce typed insight objects, which are merged into one list:
- Correlation — flags strong Pearson r relationships (|r| > threshold) between numeric columns.
- ANOVA — flags categorical columns that significantly explain variance in a numeric column (F-test p < 0.05).
- Distribution — flags high skewness, excess kurtosis, and columns with a high outlier rate.
- Temporal — detects monotonic trends and seasonality in date/time columns.
- Regression — runs OLS on numeric column pairs and surfaces strong predictive relationships.
- Categorical — measures distribution entropy per categorical column; flags highly imbalanced or highly uniform distributions.
- Categorical relationships — runs chi-square tests between pairs of categorical columns to detect non-independence.
- Cross-correlation — detects lagged relationships between time series; first runs an ADF stationarity test and differences non-stationary series before computing cross-correlation.
- Multicollinearity (VIF) — computes Variance Inflation Factor for each numeric column; flags predictors with VIF above threshold as redundant.
All insights that carry a p-value in their metadata are then subjected to Benjamini-Hochberg FDR correction at α = 0.05, removing insights that become non-significant after accounting for multiple comparisons. The surviving insights are sorted by importance descending and truncated to max_insights.
Stage 2 — AI holistic analysis (runs only if an Anthropic or OpenAI key is configured and the user’s monthly token budget is not exhausted): compiles a dataset context object (column stats, top rule-based insights, top 5 chart candidates) and sends it to ql_call_llm() with a 4,000-token budget and the system prompt “You are an expert data analyst who provides clear, actionable insights for business users.” If a user_prompt is supplied, it is appended to steer the analysis. The AI response is prepended to the insight array as a holistic_analysis type object with importance: 100. If the token budget is exceeded, a warning insight is prepended instead and the AI call is skipped.
Auth: ql_verify_auth() — valid Bearer token required.
POST params:
dataset_id(int, required)selected_columns[](array, optional) — restrict insights to these columnsmax_insights(int, optional) — capped at the plan limit; defaults to plan maximumuser_prompt(string, optional) — freeform question or instruction passed to the AI for the holistic analysis- Filter params:
filter_category_column,filter_category_value,filter_date_column,filter_date_from,filter_date_to
Response:
{
"insights": [
{
"type": "holistic_analysis", // or "correlation" | "distribution" | "outlier" | etc.
"severity": "info", // "info" | "warning" | "critical"
"importance": 95, // 0–100 sort key
"title": "Holistic Dataset Analysis",
"message": "Revenue is strongly driven by region...",
"ai_analysis": "## Key Findings\n...", // markdown; null for rule-based insights
"recommendation": "Focus acquisition spend on the North region.",
"chart_suggestion": null // chart_type string or null
}
],
"generated_at": "2024-04-25 12:00:00",
"count": 14
}
Note: The first element is the AI holistic insight (if AI is configured and the token budget is not exhausted). Remaining elements are rule-based insights sorted by importance descending.
Dataset Management
These endpoints create, update, sync, and delete datasets. Every source type (CSV upload, SQL, REST API, SFTP, Google Sheets, and merged) has its own set of handlers. All write operations use ql_verify_auth() and check dataset ownership via ql_user_can_write_dataset() before acting. The visibility parameter is accepted by all create/update handlers and must be one of private, public, org, or group.
CSV Upload
ql_upload_dataset
Creates a new dataset from a CSV file upload. The file is validated against an allowlist of MIME types (text/csv, text/plain, application/csv, application/vnd.ms-excel) using finfo — not just the file extension. File size is checked against the plan limit before the file is moved. The file is saved to wp-uploads/quantumlayers/<dataset_id>_<timestamp>.csv and its SHA-256 checksum stored in ql_dataset_files. The dataset row is created with status = 'processing' and then ql_process_csv_file() is called synchronously: it reads the CSV header to detect column names, samples rows to infer types (integer/float/string/date/datetime), and computes per-column statistics (min, max, mean, stddev, distinct count, null count) stored in ql_dataset_columns. The status is set to ready on success.
Auth: ql_verify_auth(). Request type: multipart/form-data.
csv_file(file, required) — the CSV filedataset_name(string, required)visibility(string, optional, default:private)
Response: { "message": "Upload successful!", "dataset_id": 42, "dataset_name": "sales_2024" }
ql_update_upload_dataset
Updates metadata and/or replaces the CSV data for an existing source_type = 'upload' dataset. Rejects datasets of other source types with an explicit error. If a new csv_file is included it is processed via the shared ql_process_uploaded_csv() helper, which replaces the stored file, re-runs column analysis, and updates ql_dataset_columns and row_count. Metadata changes (dataset_name, visibility) and CSV replacement can be combined in a single call — the response message describes which operations occurred.
Auth: ql_verify_auth() + ownership check. Request type: multipart/form-data.
dataset_id(int, required)dataset_name(string, optional)visibility(string, optional)csv_file(file, optional) — if provided, replaces the dataset’s data
Response: { "message": "...", "row_count": 15000 } (row_count present only when a CSV was processed).
ql_resync_upload_dataset
Replaces a CSV upload dataset’s data with a newly uploaded file, without changing any metadata. Equivalent to ql_update_upload_dataset with only a csv_file and no metadata fields. A convenience endpoint for sync-only workflows.
Auth: ql_verify_auth() + ownership check. Request type: multipart/form-data.
dataset_id(int, required)csv_file(file, required)
Response: { "success": true, "message": "...", "row_count": 15000 }
SQL Database
ql_connect_sql_database
Creates a live SQL-backed dataset. Supported db_type values are mysql and postgresql. The connection is tested via ql_test_database_connection() before any database rows are written — a failed test returns an error immediately. On success: a ql_datasets row is created with source_type = 'db', the password is encrypted with ql_encrypt_credential() and stored in ql_sql_connections, then ql_sync_sql_connection() executes sql_query against the remote database, streams the result set into QL’s internal storage, and runs full column analysis. The default port is 3306 for MySQL and 5432 for PostgreSQL if omitted.
Auth: ql_verify_auth().
dataset_name(string, required)db_type(string, required) —mysqlorpostgresqlhost(string, required)port(int, optional) — defaults to 3306 / 5432database(string, required) — database/schema nameusername(string, required)password(string, required)sql_query(string, required) — the SELECT query whose result set becomes the datasetvisibility(string, optional)
Response: { "message": "Database connected successfully", "dataset_id": 42, "connection_id": 7, "rows_fetched": 5000 }. If the initial sync fails but the connection was stored, a "warning": true flag is added and rows_fetched is omitted.
ql_update_sql_connection
Updates credentials, query, or metadata for an existing SQL connection. Identified by connection_id. Any field can be omitted to leave it unchanged; if a new password is supplied it is re-encrypted. Does not re-sync data — call ql_resync_connection after updating to pull fresh data.
Auth: ql_verify_auth() + ownership check on connection_id.
connection_id(int, required)dataset_name,db_type,host,port,database_name,username,password,sql_query,visibility(all optional)
Response: { "message": "Connection updated successfully" }
REST API
ql_connect_api
Creates a dataset backed by a REST API endpoint. The URL is validated with a scheme check (http:// or https://) rather than FILTER_VALIDATE_URL to support URLs with colons in path segments (e.g. GA4’s /properties/123:runReport). A live connection test is performed before any rows are written. The API key is encrypted with ql_encrypt_credential(). Supports OAuth2: if a refresh_token is provided, QL stores it and automatically refreshes the access token when it is within 5 minutes of expiry at sync time. Custom request_headers are stored as a JSON string and merged with the default Authorization and Accept: application/json headers on every sync. response_path is a dot-notation path into the JSON response used to locate the array of records (e.g. data.items).
Auth: ql_verify_auth().
dataset_name(string, required)api_url(string, required)http_method(string, optional) —GET(default) orPOSTapi_key(string, optional) — sent asAuthorization: Bearer <key>request_headers(JSON string or array, optional) — extra headers merged at sync timerequest_body(string, optional) — raw body for POST requests; setsContent-Type: application/jsonresponse_path(string, optional) — dot-notation path to the records array in the JSON responserefresh_token(string, optional) — OAuth2 refresh tokenoauth_provider_name(string, optional)oauth_scope(string, optional)visibility(string, optional)
Response: { "message": "API connected successfully", "dataset_id": 42, "connection_id": 9, "rows_fetched": 1200 }
ql_update_api_connection
Updates credentials, headers, URL, or metadata for an existing API connection identified by connection_id. Any field can be omitted to leave it unchanged. Does not re-sync — call ql_resync_connection afterwards.
Auth: ql_verify_auth() + ownership check. POST params: connection_id (required) + any subset of the fields accepted by ql_connect_api.
SFTP
ql_create_sftp_connection
Creates a dataset backed by a file on an SFTP server. Supports password auth or private key auth — at least one must be supplied. Both credentials are encrypted with ql_encrypt_credential() before storage. A live connection test (ql_sftp_test_connection()) is performed first; if it fails no rows are written. On success, ql_sftp_sync_dataset() downloads the file at remote_path/filename, processes it as a CSV, and runs full column analysis. The dataset is created with source_type = 'sftp'.
Auth: ql_verify_auth().
dataset_name(string, required)host(string, required)port(int, optional, default: 22)username(string, required)password(string, optional) — required ifprivate_keynot providedprivate_key(string, optional) — PEM private key; required ifpasswordnot providedremote_path(string, required) — directory path on the SFTP serverfilename(string, required) — filename withinremote_pathvisibility(string, optional)
Response: { "dataset_id": 42, "connection_id": 3, "rows_fetched": 8000 }
ql_update_sftp_connection
Updates host, credentials, remote path, filename, or metadata for an existing SFTP connection. Any field can be omitted. Does not re-sync — call ql_sync_sftp_dataset after updating.
Auth: ql_verify_auth() + ownership check. POST params: connection_id (required) + any subset of ql_create_sftp_connection fields.
ql_sync_sftp_dataset
Triggers an immediate re-download and re-import of the file from the SFTP server for an existing SFTP connection. Decrypts stored credentials, opens the SSH connection, downloads the file, replaces the stored CSV data, and re-runs column analysis.
Auth: ql_verify_auth() + ownership check. POST params: connection_id (int, required).
Response: { "message": "Dataset synced successfully", "row_count": 8500 }
Google Sheets
ql_connect_google_sheet
Creates a dataset backed by a Google Sheet. Requires an OAuth2 access_token obtained via the Google Picker flow (scope: drive.file). Because drive.file tokens are rejected by the Sheets API, QL uses the Drive API export endpoint (/export?mimeType=text/csv) to download the sheet as CSV. A connection test is performed before writing any rows. The access token is stored in plaintext in ql_google_sheet_connections and a 1-hour expiry is calculated; at sync time, if the token is within 1 hour of expiry, ql_refresh_google_token() is called automatically using the stored refresh_token. If range_notation is omitted, the entire sheet is imported.
Auth: ql_verify_auth().
spreadsheet_id(string, required) — Google Sheets spreadsheet IDaccess_token(string, required) — current OAuth2 access tokenconnection_name(string, optional, default:"Google Sheet Connection") — becomes the dataset namesheet_name(string, optional, default:"Sheet1")range_notation(string, optional) — A1 notation range, e.g.A1:F500refresh_token(string, optional)visibility(string, optional)
Response: { "message": "Google Sheets connected successfully", "dataset_id": 42, "connection_id": 5 }
ql_update_google_sheet_connection
Updates the spreadsheet ID, sheet name, range, tokens, or dataset metadata for an existing Google Sheet connection. Any field can be omitted. Does not re-sync — call ql_resync_connection after updating.
Auth: ql_verify_auth() + ownership check. POST params: connection_id (required) + any subset of ql_connect_google_sheet fields.
Merged Datasets
ql_create_merged_dataset
Joins two or more existing datasets on shared columns and materialises the result as a new dataset with source_type = 'merged'. Requires at least 2 datasets. The first dataset in the array is the base; each subsequent entry specifies a join column (which must exist in that dataset) and a join type. Joins are chained left-to-right: dataset 2 joins onto dataset 1, dataset 3 joins onto the result of that join, and so on. Join records are stored in ql_dataset_joins. After the join records are created, ql_build_merged_columns() reads the column schemas of all source datasets and writes the merged column schema. The actual row data is assembled at query time from the source datasets — no copy of the rows is stored. Always created as visibility = 'private'.
Auth: ql_verify_auth().
// POST body
{
"name": "Sales + Customers",
"datasets": [
{ "dataset_id": 10 }, // base (no join needed)
{ "dataset_id": 11, "join_column": "customer_id", "join_type": "inner" },
{ "dataset_id": 12, "join_column": "customer_id", "join_type": "left" }
]
}
// join_type: "inner" | "left" | "right" | "outer"
Response: { "success": true, "dataset_id": 55 }
ql_update_merged_dataset
Replaces the name and entire join configuration of an existing merged dataset. All existing ql_dataset_joins rows for the dataset are deleted and re-created from the new datasets array using the same chaining logic as ql_create_merged_dataset. The underlying source data is not touched.
Auth: ql_verify_auth() + ownership check.
dataset_id(int, required) — must be asource_type = 'merged'datasetname(string, required)datasets(array, required) — same structure asql_create_merged_dataset
Response: { "message": "Merged dataset updated successfully" }
Common Operations
ql_resync_connection
Re-fetches data from the remote source for any connected dataset (source_type of db, api, or google_sheet). Looks up the connection record for the dataset’s source type, then delegates to the appropriate sync function: ql_sync_sql_connection(), ql_sync_api_connection(), or ql_sync_google_sheet_connection(). For Google Sheets, optional fresh access_token / refresh_token values can be passed in the same request to update tokens before syncing.
Auth: ql_verify_auth() + ownership check.
dataset_id(int, required)access_token(string, optional) — Google Sheets onlyrefresh_token(string, optional) — Google Sheets only
Response: { "message": "Data synced successfully", "row_count": 5200 }
ql_delete_dataset
Permanently deletes a dataset and all associated data. Access is checked via ql_user_can_write_dataset(). Deletion cascades through every related table in order: physical CSV files are deleted from wp-uploads/quantumlayers/ first, then rows are removed from ql_dataset_files, ql_dataset_columns, ql_dataset_joins, ql_google_sheet_connections, ql_api_connections, ql_sql_connections, ql_sftp_connections, and finally ql_datasets. Saved charts and insights for the dataset are also removed. This operation is irreversible.
Auth: ql_verify_auth() + ql_user_can_write_dataset().
dataset_id(int, required)
Response: { "message": "Dataset deleted successfully" }
ql_set_dataset_visibility
Changes the visibility of any dataset. Also registered as ql_toggle_dataset_privacy for backward compatibility. Accepts the modern visibility string parameter or the legacy is_public boolean (0 = private, 1 = public); if both are sent, visibility takes precedence. Access is checked via ql_user_can_write_dataset().
Auth: ql_verify_auth() + ql_user_can_write_dataset().
dataset_id(int, required)visibility(string) —private|public|org|groupis_public(int, legacy) —0= private,1= public; ignored ifvisibilityis present
Response: { "message": "Visibility updated to public" }
Complete Code Example
End-to-end: authenticate via a third-party JWT, fetch a bar chart for dataset 42, and render it with Chart.js.
<!-- Load libraries -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://quantumlayers.com/wp-content/plugins/quantumlayers/assets/auth.js"></script>
<!-- Canvas target -->
<canvas id="my-chart" width="800" height="400"></canvas>
<script>
const QL_AJAX = 'https://quantumlayers.com/wp-admin/admin-ajax.php';
const SIGNED_JWT = '<jwt-minted-by-your-backend>';
// Step 1: exchange JWT for QL session token
$.post(QL_AJAX, { action: 'ql_third_party_signin', jwt: SIGNED_JWT }, function(res) {
if (!res.success) return console.error('Sign-in failed', res);
QLAuth.setSessionToken(res.data.session_token);
// Step 2: fetch chart data
$.ajax({
url: QL_AJAX,
method: 'POST',
headers: { 'Authorization': 'Bearer ' + QLAuth.getSessionToken() },
data: {
action: 'ql_get_chart_data',
dataset_id: 42,
chart_type: 'bar',
x_column: 'product_category',
y_column: 'total_amount',
aggregation: 'sum'
},
success: function(chartRes) {
if (!chartRes.success) return console.error('Chart error', chartRes);
// Step 3: render
const ctx = document.getElementById('my-chart').getContext('2d');
new Chart(ctx, chartRes.data);
}
});
});
</script>
Chart Type Quick Reference
Pass one of the following values as chart_type to ql_get_chart_data.
| chart_type | Required params | Chart.js type rendered |
|---|---|---|
bar | x_column, y_column, aggregation | bar |
horizontal_bar | x_column, y_column, aggregation | bar (indexAxis: ‘y’) |
line | x_column, y_column, aggregation | line |
scatter | x_column, y_column | scatter |
pie | category_column, value_column, aggregation | pie |
doughnut | category_column, value_column, aggregation | doughnut |
bubble | x_column, y_column, size_column | bubble |
box_plot | category_column, value_column | boxplot (chartjs-chart-boxplot) |
heatmap | x_column, y_column, value_column | matrix (chartjs-chart-matrix) |
time_series | date_column, value_column, aggregation | line (time scale) |
histogram | column_name, optional bins | bar (computed bins) |
area | x_column, y_column, aggregation | line (fill: true) |