@miao-vision/cli 0.1.3 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/dist/cli.cjs +1445 -123
- package/dist/examples/finance-review-deck.yaml +110 -0
- package/dist/examples/finance-review.csv +7 -0
- package/dist/examples/ops-update-deck.yaml +106 -0
- package/dist/examples/ops-update.csv +7 -0
- package/dist/examples/product-metrics-deck.yaml +110 -0
- package/dist/examples/product-metrics.csv +7 -0
- package/dist/types.ts +26 -1
- package/examples/finance-review-deck.yaml +110 -0
- package/examples/finance-review.csv +7 -0
- package/examples/ops-update-deck.yaml +106 -0
- package/examples/ops-update.csv +7 -0
- package/examples/product-metrics-deck.yaml +110 -0
- package/examples/product-metrics.csv +7 -0
- package/package.json +2 -2
package/dist/cli.cjs
CHANGED
|
@@ -39673,7 +39673,7 @@ var require_xlsx = __commonJS({
|
|
|
39673
39673
|
if (DBF_SUPPORTED_VERSIONS.indexOf(n[0]) > -1 && n[2] <= 12 && n[3] <= 31) return DBF.to_workbook(d, o);
|
|
39674
39674
|
return read_prn(data, d, o, str);
|
|
39675
39675
|
}
|
|
39676
|
-
function
|
|
39676
|
+
function readFileSync4(filename, opts) {
|
|
39677
39677
|
var o = opts || {};
|
|
39678
39678
|
o.type = "file";
|
|
39679
39679
|
return readSync(filename, o);
|
|
@@ -40509,8 +40509,8 @@ var require_xlsx = __commonJS({
|
|
|
40509
40509
|
if (typeof parse_xlscfb !== "undefined") XLSX3.parse_xlscfb = parse_xlscfb;
|
|
40510
40510
|
XLSX3.parse_zip = parse_zip;
|
|
40511
40511
|
XLSX3.read = readSync;
|
|
40512
|
-
XLSX3.readFile =
|
|
40513
|
-
XLSX3.readFileSync =
|
|
40512
|
+
XLSX3.readFile = readFileSync4;
|
|
40513
|
+
XLSX3.readFileSync = readFileSync4;
|
|
40514
40514
|
XLSX3.write = writeSync;
|
|
40515
40515
|
XLSX3.writeFile = writeFileSync2;
|
|
40516
40516
|
XLSX3.writeFileSync = writeFileSync2;
|
|
@@ -40541,8 +40541,8 @@ var require_xlsx = __commonJS({
|
|
|
40541
40541
|
});
|
|
40542
40542
|
|
|
40543
40543
|
// src/cli.ts
|
|
40544
|
-
var
|
|
40545
|
-
var
|
|
40544
|
+
var import_node_fs3 = require("node:fs");
|
|
40545
|
+
var import_node_path3 = require("node:path");
|
|
40546
40546
|
var YAML = __toESM(require_dist(), 1);
|
|
40547
40547
|
|
|
40548
40548
|
// src/data-loader.ts
|
|
@@ -40685,10 +40685,20 @@ function coerceCell(value) {
|
|
|
40685
40685
|
}
|
|
40686
40686
|
|
|
40687
40687
|
// src/data-profiler.ts
|
|
40688
|
-
function
|
|
40689
|
-
const
|
|
40688
|
+
function profileSummary(dataset) {
|
|
40689
|
+
const allCols = dataset.columns.map((name) => {
|
|
40690
|
+
const values = dataset.rows.map((r) => r[name]).filter((v) => v !== null && v !== void 0 && v !== "");
|
|
40691
|
+
const type = inferColumnType(values);
|
|
40692
|
+
return { name, type };
|
|
40693
|
+
});
|
|
40694
|
+
return { file: dataset.file, rows: dataset.rows.length, sheet: dataset.sheet, columns: allCols };
|
|
40695
|
+
}
|
|
40696
|
+
function profileDataset(dataset, options = {}) {
|
|
40697
|
+
const targetCols = options.columns ? dataset.columns.filter((c) => options.columns.includes(c)) : dataset.columns;
|
|
40698
|
+
const columns = targetCols.map((col) => profileColumn(col, dataset.rows, options.reliableOnly));
|
|
40690
40699
|
const numericNames = columns.filter((c) => c.type === "number").map((c) => c.name);
|
|
40691
|
-
const
|
|
40700
|
+
const rawCorrelations = numericNames.length >= 2 ? computeCorrelations(numericNames, dataset.rows) : void 0;
|
|
40701
|
+
const correlations = options.reliableOnly ? rawCorrelations?.filter((c) => c.reliable) : rawCorrelations;
|
|
40692
40702
|
const hints = generateHints(columns, correlations ?? []);
|
|
40693
40703
|
const quality = computeDataQuality(columns, dataset.rows.length);
|
|
40694
40704
|
const insights = generateProfileInsights(columns, quality, correlations ?? [], dataset.rows.length);
|
|
@@ -40703,7 +40713,7 @@ function profileDataset(dataset) {
|
|
|
40703
40713
|
insights: insights.length > 0 ? insights : void 0
|
|
40704
40714
|
};
|
|
40705
40715
|
}
|
|
40706
|
-
function profileColumn(name, rows) {
|
|
40716
|
+
function profileColumn(name, rows, reliableOnly = false) {
|
|
40707
40717
|
const values = rows.map((row) => row[name]);
|
|
40708
40718
|
const nonNull = values.filter((v) => v !== null && v !== void 0 && v !== "");
|
|
40709
40719
|
const type = inferColumnType(nonNull);
|
|
@@ -40729,7 +40739,7 @@ function profileColumn(name, rows) {
|
|
|
40729
40739
|
const sorted = [...nums].sort((a, b) => a - b);
|
|
40730
40740
|
profile.min = sorted[0];
|
|
40731
40741
|
profile.max = sorted[sorted.length - 1];
|
|
40732
|
-
Object.assign(profile, computeNumericStats(nums, sorted));
|
|
40742
|
+
Object.assign(profile, computeNumericStats(nums, sorted, rows.length, reliableOnly));
|
|
40733
40743
|
}
|
|
40734
40744
|
}
|
|
40735
40745
|
if (type === "string") {
|
|
@@ -40752,7 +40762,7 @@ function profileColumn(name, rows) {
|
|
|
40752
40762
|
profile.role = inferColumnRole(profile);
|
|
40753
40763
|
return profile;
|
|
40754
40764
|
}
|
|
40755
|
-
function computeNumericStats(values, sorted) {
|
|
40765
|
+
function computeNumericStats(values, sorted, totalRows, reliableOnly) {
|
|
40756
40766
|
const n = values.length;
|
|
40757
40767
|
const sum = values.reduce((s, v) => s + v, 0);
|
|
40758
40768
|
const mean = sum / n;
|
|
@@ -40764,18 +40774,31 @@ function computeNumericStats(values, sorted) {
|
|
|
40764
40774
|
const skewness = stddev < 1e-10 ? 0 : round(3 * (mean - median) / stddev);
|
|
40765
40775
|
const iqr = p75 - p25;
|
|
40766
40776
|
const outlierCount = values.filter((v) => v < p25 - 1.5 * iqr || v > p75 + 1.5 * iqr).length;
|
|
40767
|
-
|
|
40777
|
+
const skewnessReliable = totalRows >= 30;
|
|
40778
|
+
const outlierReliable = totalRows >= 20;
|
|
40779
|
+
const histogramReliable = totalRows >= 20;
|
|
40780
|
+
const result = {
|
|
40768
40781
|
sum: round(sum),
|
|
40769
40782
|
mean: round(mean),
|
|
40770
40783
|
median: round(median),
|
|
40771
40784
|
p25: round(p25),
|
|
40772
40785
|
p75: round(p75),
|
|
40773
40786
|
stddev: round(stddev),
|
|
40774
|
-
|
|
40775
|
-
coefficientOfVariation: Math.abs(mean) < 1e-10 ? void 0 : round(stddev / Math.abs(mean)),
|
|
40776
|
-
outlierCount,
|
|
40777
|
-
histogram: computeHistogram(values, sorted[0], sorted[n - 1])
|
|
40787
|
+
coefficientOfVariation: Math.abs(mean) < 1e-10 ? void 0 : round(stddev / Math.abs(mean))
|
|
40778
40788
|
};
|
|
40789
|
+
if (!reliableOnly || skewnessReliable) {
|
|
40790
|
+
result.skewness = skewness;
|
|
40791
|
+
result.skewnessReliable = skewnessReliable;
|
|
40792
|
+
}
|
|
40793
|
+
if (!reliableOnly || outlierReliable) {
|
|
40794
|
+
result.outlierCount = outlierCount;
|
|
40795
|
+
result.outlierReliable = outlierReliable;
|
|
40796
|
+
}
|
|
40797
|
+
if (!reliableOnly || histogramReliable) {
|
|
40798
|
+
result.histogram = computeHistogram(values, sorted[0], sorted[n - 1]);
|
|
40799
|
+
result.histogramReliable = histogramReliable;
|
|
40800
|
+
}
|
|
40801
|
+
return result;
|
|
40779
40802
|
}
|
|
40780
40803
|
function percentile(sorted, p) {
|
|
40781
40804
|
if (sorted.length === 0) return 0;
|
|
@@ -40862,7 +40885,7 @@ function computeCorrelations(numericNames, rows) {
|
|
|
40862
40885
|
const pairs = rows.map((row) => [Number(row[a]), Number(row[b])]).filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y));
|
|
40863
40886
|
if (pairs.length < 3) continue;
|
|
40864
40887
|
const r = pearsonR(pairs);
|
|
40865
|
-
if (Math.abs(r) >= 0.3) result.push({ a, b, r: round(r) });
|
|
40888
|
+
if (Math.abs(r) >= 0.3) result.push({ a, b, r: round(r), n: pairs.length, reliable: pairs.length >= 10 });
|
|
40866
40889
|
}
|
|
40867
40890
|
}
|
|
40868
40891
|
return result.sort((x, y) => Math.abs(y.r) - Math.abs(x.r));
|
|
@@ -40953,14 +40976,6 @@ function generateProfileInsights(columns, quality, correlations, rowCount) {
|
|
|
40953
40976
|
});
|
|
40954
40977
|
}
|
|
40955
40978
|
for (const col of columns) {
|
|
40956
|
-
if (col.role === "dimension" && col.distinctCount >= 2 && col.distinctCount <= 12 && col.topSharePct && col.topSharePct >= 0.5) {
|
|
40957
|
-
insights.push({
|
|
40958
|
-
type: "suggestion",
|
|
40959
|
-
title: `Dominant category in ${col.name}`,
|
|
40960
|
-
description: `${String(col.topValue)} accounts for ${Math.round(col.topSharePct * 100)}% of non-empty values; annotate this imbalance in share or ranking charts.`,
|
|
40961
|
-
fields: [col.name]
|
|
40962
|
-
});
|
|
40963
|
-
}
|
|
40964
40979
|
if (col.role === "measure" && col.coefficientOfVariation !== void 0 && col.coefficientOfVariation >= 1) {
|
|
40965
40980
|
insights.push({
|
|
40966
40981
|
type: "trend",
|
|
@@ -41024,6 +41039,153 @@ function round(value) {
|
|
|
41024
41039
|
return Math.round(value * 1e4) / 1e4;
|
|
41025
41040
|
}
|
|
41026
41041
|
|
|
41042
|
+
// src/data-query.ts
|
|
41043
|
+
var SUPPORTED_OPS = ["sum", "count", "avg", "min", "max"];
|
|
41044
|
+
function queryDataset(rows, options) {
|
|
41045
|
+
const groupByCols = options.groupby ? options.groupby.split(",").map((c) => c.trim()).filter(Boolean) : [];
|
|
41046
|
+
const measuresResult = options.measure ? parseMeasures(options.measure) : [];
|
|
41047
|
+
if (isAgentError(measuresResult)) return measuresResult;
|
|
41048
|
+
const measures = measuresResult;
|
|
41049
|
+
let current;
|
|
41050
|
+
if (options.filter) {
|
|
41051
|
+
const filterResult = applyFilter(rows, options.filter);
|
|
41052
|
+
if (!filterResult.ok) return filterResult.error;
|
|
41053
|
+
current = filterResult.rows;
|
|
41054
|
+
} else {
|
|
41055
|
+
current = [...rows];
|
|
41056
|
+
}
|
|
41057
|
+
if (groupByCols.length > 0 || measures.length > 0) {
|
|
41058
|
+
current = aggregateQuery(current, groupByCols, measures);
|
|
41059
|
+
}
|
|
41060
|
+
if (options.orderby) {
|
|
41061
|
+
const ordered = applyOrderBy(current, options.orderby);
|
|
41062
|
+
if (isAgentError(ordered)) return ordered;
|
|
41063
|
+
current = ordered;
|
|
41064
|
+
}
|
|
41065
|
+
if (options.limit != null && options.limit > 0) {
|
|
41066
|
+
current = current.slice(0, options.limit);
|
|
41067
|
+
}
|
|
41068
|
+
return {
|
|
41069
|
+
rows: current,
|
|
41070
|
+
sql: buildSql(groupByCols, measures, options.filter, options.orderby, options.limit),
|
|
41071
|
+
rowCount: current.length
|
|
41072
|
+
};
|
|
41073
|
+
}
|
|
41074
|
+
function parseMeasures(measure) {
|
|
41075
|
+
const parts = measure.split(",").map((s) => s.trim()).filter(Boolean);
|
|
41076
|
+
const results = [];
|
|
41077
|
+
for (const part of parts) {
|
|
41078
|
+
const match = part.match(/^(\w+)\(([^)]+)\)(?:\s+as\s+(\w+))?$/i);
|
|
41079
|
+
if (!match) {
|
|
41080
|
+
return agentError(
|
|
41081
|
+
"QUERY_INVALID_MEASURE",
|
|
41082
|
+
`Cannot parse measure: "${part}". Use "fn(col) as alias". Supported functions: ${SUPPORTED_OPS.join(", ")}.`
|
|
41083
|
+
);
|
|
41084
|
+
}
|
|
41085
|
+
const [, fnRaw, rawField, aliasRaw] = match;
|
|
41086
|
+
const fn = fnRaw.toLowerCase();
|
|
41087
|
+
if (!SUPPORTED_OPS.includes(fn)) {
|
|
41088
|
+
return agentError(
|
|
41089
|
+
"QUERY_UNSUPPORTED_FUNCTION",
|
|
41090
|
+
`Unsupported aggregate function: "${fn}". Supported: ${SUPPORTED_OPS.join(", ")}.`
|
|
41091
|
+
);
|
|
41092
|
+
}
|
|
41093
|
+
const field = rawField.trim();
|
|
41094
|
+
const alias = aliasRaw ?? `${fn}_${field.replace("*", "all")}`;
|
|
41095
|
+
results.push({ fn, field, alias });
|
|
41096
|
+
}
|
|
41097
|
+
return results;
|
|
41098
|
+
}
|
|
41099
|
+
function applyFilter(rows, filter) {
|
|
41100
|
+
const match = filter.match(/^(\w+)\s*=\s*(.+)$/);
|
|
41101
|
+
if (!match) {
|
|
41102
|
+
return {
|
|
41103
|
+
ok: false,
|
|
41104
|
+
error: agentError(
|
|
41105
|
+
"QUERY_INVALID_FILTER",
|
|
41106
|
+
`Cannot parse filter: "${filter}". Use "column=value" (simple equality only).`
|
|
41107
|
+
)
|
|
41108
|
+
};
|
|
41109
|
+
}
|
|
41110
|
+
const [, col, rawVal] = match;
|
|
41111
|
+
const val = rawVal.trim();
|
|
41112
|
+
const numVal = Number(val);
|
|
41113
|
+
const filtered = rows.filter((row) => {
|
|
41114
|
+
const cell = row[col];
|
|
41115
|
+
if (Number.isFinite(numVal) && Number.isFinite(Number(cell))) {
|
|
41116
|
+
return Number(cell) === numVal;
|
|
41117
|
+
}
|
|
41118
|
+
return String(cell ?? "") === val;
|
|
41119
|
+
});
|
|
41120
|
+
return { ok: true, rows: filtered };
|
|
41121
|
+
}
|
|
41122
|
+
function aggregateQuery(rows, groupByCols, measures) {
|
|
41123
|
+
if (groupByCols.length === 0) {
|
|
41124
|
+
const out = {};
|
|
41125
|
+
for (const m of measures) {
|
|
41126
|
+
out[m.alias] = computeAggregate(rows, m);
|
|
41127
|
+
}
|
|
41128
|
+
return [out];
|
|
41129
|
+
}
|
|
41130
|
+
const groups = /* @__PURE__ */ new Map();
|
|
41131
|
+
for (const row of rows) {
|
|
41132
|
+
const key = JSON.stringify(groupByCols.map((c) => row[c]));
|
|
41133
|
+
const existing = groups.get(key) ?? [];
|
|
41134
|
+
existing.push(row);
|
|
41135
|
+
groups.set(key, existing);
|
|
41136
|
+
}
|
|
41137
|
+
return Array.from(groups.values()).map((groupRows) => {
|
|
41138
|
+
const first = groupRows[0];
|
|
41139
|
+
const out = {};
|
|
41140
|
+
for (const col of groupByCols) {
|
|
41141
|
+
out[col] = first[col];
|
|
41142
|
+
}
|
|
41143
|
+
for (const m of measures) {
|
|
41144
|
+
out[m.alias] = computeAggregate(groupRows, m);
|
|
41145
|
+
}
|
|
41146
|
+
return out;
|
|
41147
|
+
});
|
|
41148
|
+
}
|
|
41149
|
+
function computeAggregate(rows, measure) {
|
|
41150
|
+
if (measure.fn === "count") return rows.length;
|
|
41151
|
+
const values = rows.map((r) => Number(r[measure.field])).filter((v) => Number.isFinite(v));
|
|
41152
|
+
if (values.length === 0) return 0;
|
|
41153
|
+
if (measure.fn === "avg") return values.reduce((s, v) => s + v, 0) / values.length;
|
|
41154
|
+
if (measure.fn === "min") return Math.min(...values);
|
|
41155
|
+
if (measure.fn === "max") return Math.max(...values);
|
|
41156
|
+
return values.reduce((s, v) => s + v, 0);
|
|
41157
|
+
}
|
|
41158
|
+
function applyOrderBy(rows, orderby) {
|
|
41159
|
+
const parts = orderby.trim().split(/\s+/);
|
|
41160
|
+
const col = parts[0];
|
|
41161
|
+
const dir = (parts[1] ?? "desc").toLowerCase();
|
|
41162
|
+
if (dir !== "asc" && dir !== "desc") {
|
|
41163
|
+
return agentError("QUERY_INVALID_ORDERBY", `Invalid sort direction: "${dir}". Use "asc" or "desc".`);
|
|
41164
|
+
}
|
|
41165
|
+
const sign = dir === "asc" ? 1 : -1;
|
|
41166
|
+
return [...rows].sort((a, b) => {
|
|
41167
|
+
const aNum = Number(a[col]);
|
|
41168
|
+
const bNum = Number(b[col]);
|
|
41169
|
+
if (Number.isFinite(aNum) && Number.isFinite(bNum)) return (aNum - bNum) * sign;
|
|
41170
|
+
return String(a[col] ?? "").localeCompare(String(b[col] ?? "")) * sign;
|
|
41171
|
+
});
|
|
41172
|
+
}
|
|
41173
|
+
function buildSql(groupByCols, measures, filter, orderby, limit) {
|
|
41174
|
+
const selectParts = [
|
|
41175
|
+
...groupByCols,
|
|
41176
|
+
...measures.map((m) => `${m.fn.toUpperCase()}(${m.field}) AS ${m.alias}`)
|
|
41177
|
+
];
|
|
41178
|
+
let sql = `SELECT ${selectParts.length > 0 ? selectParts.join(", ") : "*"} FROM data`;
|
|
41179
|
+
if (filter) {
|
|
41180
|
+
const [col, val] = filter.split("=", 2);
|
|
41181
|
+
sql += ` WHERE ${col.trim()} = ${val.trim()}`;
|
|
41182
|
+
}
|
|
41183
|
+
if (groupByCols.length > 0) sql += ` GROUP BY ${groupByCols.join(", ")}`;
|
|
41184
|
+
if (orderby) sql += ` ORDER BY ${orderby}`;
|
|
41185
|
+
if (limit != null) sql += ` LIMIT ${limit}`;
|
|
41186
|
+
return sql;
|
|
41187
|
+
}
|
|
41188
|
+
|
|
41027
41189
|
// src/data-transform.ts
|
|
41028
41190
|
function prepareChartData(rows, chart) {
|
|
41029
41191
|
let current = [...rows];
|
|
@@ -41105,16 +41267,17 @@ var DEFAULT_SVG_THEME = {
|
|
|
41105
41267
|
axisColor: "#94a3b8",
|
|
41106
41268
|
labelColor: "#475569"
|
|
41107
41269
|
};
|
|
41108
|
-
function renderChartSvg(chart, rows, svgTheme) {
|
|
41270
|
+
function renderChartSvg(chart, rows, svgTheme, options = {}) {
|
|
41109
41271
|
const theme = svgTheme ?? DEFAULT_SVG_THEME;
|
|
41110
41272
|
const data = prepareChartData(rows, chart);
|
|
41111
|
-
if (chart.type === "line" || chart.type === "area") return renderLineChart(chart, data, theme);
|
|
41112
|
-
if (chart.type === "bar") return renderBarChart(chart, data, theme);
|
|
41113
|
-
if (chart.type === "
|
|
41273
|
+
if (chart.type === "line" || chart.type === "area") return renderLineChart(chart, data, theme, options);
|
|
41274
|
+
if (chart.type === "bar") return renderBarChart(chart, data, theme, options);
|
|
41275
|
+
if (chart.type === "pie") return renderPieChart(chart, data, theme, options);
|
|
41276
|
+
if (chart.type === "table") return renderTable(chart, data, options);
|
|
41114
41277
|
if (chart.type === "bigvalue") return renderBigValue(chart, data);
|
|
41115
41278
|
return renderUnsupported(chart);
|
|
41116
41279
|
}
|
|
41117
|
-
function renderBarChart(chart, rows, theme) {
|
|
41280
|
+
function renderBarChart(chart, rows, theme, options) {
|
|
41118
41281
|
const xField = chart.encoding.x?.field ?? "";
|
|
41119
41282
|
const yField = chart.encoding.y?.field ?? "";
|
|
41120
41283
|
const width = numberStyle(chart, "width", 720);
|
|
@@ -41133,9 +41296,11 @@ function renderBarChart(chart, rows, theme) {
|
|
|
41133
41296
|
const x = margin.left + index * (barWidth + barGap);
|
|
41134
41297
|
const y = margin.top + chartHeight - barHeight;
|
|
41135
41298
|
const color = theme.palette[index % theme.palette.length];
|
|
41299
|
+
const label = String(row[xField] ?? "");
|
|
41300
|
+
const tooltip = `${label}: ${value}`;
|
|
41136
41301
|
return `<g>
|
|
41137
|
-
<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${barWidth.toFixed(1)}" height="${barHeight.toFixed(1)}" rx="3" fill="${color}" />
|
|
41138
|
-
<text x="${(x + barWidth / 2).toFixed(1)}" y="${(margin.top + chartHeight + 18).toFixed(1)}" text-anchor="middle" fill="${theme.labelColor}" font-size="11">${escapeHtml(
|
|
41302
|
+
<rect ${markAttrs(options.chartId, xField, row[xField], index, tooltip)} x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${barWidth.toFixed(1)}" height="${barHeight.toFixed(1)}" rx="3" fill="${color}" />
|
|
41303
|
+
<text x="${(x + barWidth / 2).toFixed(1)}" y="${(margin.top + chartHeight + 18).toFixed(1)}" text-anchor="middle" fill="${theme.labelColor}" font-size="11">${escapeHtml(label)}</text>
|
|
41139
41304
|
</g>`;
|
|
41140
41305
|
}).join("");
|
|
41141
41306
|
return svgFrame(width, height, theme.background, `
|
|
@@ -41143,7 +41308,7 @@ function renderBarChart(chart, rows, theme) {
|
|
|
41143
41308
|
${bars}
|
|
41144
41309
|
`);
|
|
41145
41310
|
}
|
|
41146
|
-
function renderLineChart(chart, rows, theme) {
|
|
41311
|
+
function renderLineChart(chart, rows, theme, options) {
|
|
41147
41312
|
const xField = chart.encoding.x?.field ?? "";
|
|
41148
41313
|
const yField = chart.encoding.y?.field ?? "";
|
|
41149
41314
|
const width = numberStyle(chart, "width", 720);
|
|
@@ -41163,7 +41328,7 @@ function renderLineChart(chart, rows, theme) {
|
|
|
41163
41328
|
});
|
|
41164
41329
|
const path = points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
|
|
41165
41330
|
const dots = points.map(
|
|
41166
|
-
(p) => `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="4" fill="${lineColor}"><title>${escapeHtml(p.label)}: ${p.value}</title></circle>`
|
|
41331
|
+
(p) => `<circle ${markAttrs(options.chartId, xField, p.label, 0, `${p.label}: ${p.value}`)} cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="4" fill="${lineColor}"><title>${escapeHtml(p.label)}: ${p.value}</title></circle>`
|
|
41167
41332
|
).join("");
|
|
41168
41333
|
const labels = points.map((p, i) => {
|
|
41169
41334
|
if (i % Math.ceil(points.length / 8) !== 0) return "";
|
|
@@ -41176,11 +41341,42 @@ function renderLineChart(chart, rows, theme) {
|
|
|
41176
41341
|
${labels}
|
|
41177
41342
|
`);
|
|
41178
41343
|
}
|
|
41179
|
-
function
|
|
41344
|
+
function renderPieChart(chart, rows, theme, options) {
|
|
41345
|
+
const labelField = chart.encoding.label?.field ?? "";
|
|
41346
|
+
const valueField = chart.encoding.value?.field ?? "";
|
|
41347
|
+
const width = numberStyle(chart, "width", 720);
|
|
41348
|
+
const height = numberStyle(chart, "height", 420);
|
|
41349
|
+
const cx = width / 2 - 80;
|
|
41350
|
+
const cy = height / 2;
|
|
41351
|
+
const radius = Math.min(width, height) * 0.34;
|
|
41352
|
+
const values = rows.map((row) => Math.max(0, Number(row[valueField]) || 0));
|
|
41353
|
+
const total = values.reduce((sum, value) => sum + value, 0) || 1;
|
|
41354
|
+
let angle = -Math.PI / 2;
|
|
41355
|
+
const slices = rows.map((row, index) => {
|
|
41356
|
+
const value = values[index];
|
|
41357
|
+
const nextAngle = angle + value / total * Math.PI * 2;
|
|
41358
|
+
const path = describeArc(cx, cy, radius, angle, nextAngle);
|
|
41359
|
+
const color = theme.palette[index % theme.palette.length];
|
|
41360
|
+
const label = String(row[labelField] ?? "");
|
|
41361
|
+
const tooltip = `${label}: ${value}`;
|
|
41362
|
+
angle = nextAngle;
|
|
41363
|
+
return `<path ${markAttrs(options.chartId, labelField, row[labelField], index, tooltip)} d="${path}" fill="${color}" stroke="${theme.background}" stroke-width="2" />`;
|
|
41364
|
+
}).join("");
|
|
41365
|
+
const legend = rows.map((row, index) => {
|
|
41366
|
+
const y = 72 + index * 24;
|
|
41367
|
+
return `<g>
|
|
41368
|
+
<rect x="${width - 210}" y="${y - 10}" width="10" height="10" fill="${theme.palette[index % theme.palette.length]}" />
|
|
41369
|
+
<text x="${width - 192}" y="${y}" fill="${theme.labelColor}" font-size="12">${escapeHtml(String(row[labelField] ?? ""))}</text>
|
|
41370
|
+
</g>`;
|
|
41371
|
+
}).join("");
|
|
41372
|
+
return svgFrame(width, height, theme.background, `${slices}${legend}`);
|
|
41373
|
+
}
|
|
41374
|
+
function renderTable(chart, rows, options) {
|
|
41180
41375
|
const columns = Object.keys(rows[0] ?? {}).slice(0, 8);
|
|
41181
41376
|
const header = columns.map((c) => `<th>${escapeHtml(c)}</th>`).join("");
|
|
41377
|
+
const markField = chart.encoding.label?.field ?? chart.encoding.x?.field ?? columns[0] ?? "";
|
|
41182
41378
|
const body = rows.slice(0, 20).map(
|
|
41183
|
-
(row) => `<tr>${columns.map((c) => `<td>${escapeHtml(String(row[c] ?? ""))}</td>`).join("")}</tr>`
|
|
41379
|
+
(row) => `<tr ${markAttrs(options.chartId, markField, row[markField], 0, String(row[markField] ?? "Row"))}>${columns.map((c) => `<td>${escapeHtml(String(row[c] ?? ""))}</td>`).join("")}</tr>`
|
|
41184
41380
|
).join("");
|
|
41185
41381
|
return `<div class="miao-table-wrap"><table class="miao-table"><caption>${escapeHtml(chart.title ?? "Table")}</caption><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table></div>`;
|
|
41186
41382
|
}
|
|
@@ -41198,6 +41394,33 @@ function svgFrame(width, height, bgColor, body) {
|
|
|
41198
41394
|
${body}
|
|
41199
41395
|
</svg>`;
|
|
41200
41396
|
}
|
|
41397
|
+
function markAttrs(chartId, field, value, rowKey, tooltip) {
|
|
41398
|
+
return [
|
|
41399
|
+
'data-miao-mark="true"',
|
|
41400
|
+
chartId ? `data-chart-id="${escapeHtml(chartId)}"` : "",
|
|
41401
|
+
`data-field="${escapeHtml(field)}"`,
|
|
41402
|
+
`data-value="${escapeHtml(String(value ?? ""))}"`,
|
|
41403
|
+
`data-row-key="${escapeHtml(String(rowKey))}"`,
|
|
41404
|
+
`data-tooltip="${escapeHtml(tooltip)}"`
|
|
41405
|
+
].filter(Boolean).join(" ");
|
|
41406
|
+
}
|
|
41407
|
+
function describeArc(cx, cy, radius, startAngle, endAngle) {
|
|
41408
|
+
const start = polarToCartesian(cx, cy, radius, endAngle);
|
|
41409
|
+
const end = polarToCartesian(cx, cy, radius, startAngle);
|
|
41410
|
+
const largeArcFlag = endAngle - startAngle <= Math.PI ? "0" : "1";
|
|
41411
|
+
return [
|
|
41412
|
+
`M ${cx.toFixed(1)} ${cy.toFixed(1)}`,
|
|
41413
|
+
`L ${start.x.toFixed(1)} ${start.y.toFixed(1)}`,
|
|
41414
|
+
`A ${radius.toFixed(1)} ${radius.toFixed(1)} 0 ${largeArcFlag} 0 ${end.x.toFixed(1)} ${end.y.toFixed(1)}`,
|
|
41415
|
+
"Z"
|
|
41416
|
+
].join(" ");
|
|
41417
|
+
}
|
|
41418
|
+
function polarToCartesian(cx, cy, radius, angle) {
|
|
41419
|
+
return {
|
|
41420
|
+
x: cx + radius * Math.cos(angle),
|
|
41421
|
+
y: cy + radius * Math.sin(angle)
|
|
41422
|
+
};
|
|
41423
|
+
}
|
|
41201
41424
|
function buildAxis(margin, chartWidth, chartHeight, xLabel, yLabel, yMin, yMax, theme) {
|
|
41202
41425
|
const x0 = margin.left;
|
|
41203
41426
|
const y0 = margin.top + chartHeight;
|
|
@@ -41442,6 +41665,441 @@ function getTheme(name) {
|
|
|
41442
41665
|
return THEMES.default;
|
|
41443
41666
|
}
|
|
41444
41667
|
|
|
41668
|
+
// src/interactive-runtime-assets.ts
|
|
41669
|
+
var INTERACTIVE_CSS = `
|
|
41670
|
+
.miao-interactive-controls { display: flex; flex-wrap: wrap; gap: 12px; align-items: end; margin: 0 0 24px; padding: 12px 0; border-top: 1px solid rgba(128,128,128,0.18); border-bottom: 1px solid rgba(128,128,128,0.18); }
|
|
41671
|
+
.miao-filter { display: grid; gap: 5px; font-size: 12px; }
|
|
41672
|
+
.miao-filter label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; opacity: 0.56; }
|
|
41673
|
+
.miao-filter select, .miao-filter input { min-width: 140px; border: 1px solid rgba(128,128,128,0.28); border-radius: 4px; padding: 6px 8px; background: transparent; color: inherit; font: inherit; }
|
|
41674
|
+
.miao-filter-range { display: flex; gap: 6px; }
|
|
41675
|
+
.miao-reset { border: 1px solid rgba(128,128,128,0.28); border-radius: 4px; padding: 7px 10px; background: transparent; color: inherit; cursor: pointer; font: inherit; }
|
|
41676
|
+
.miao-reset:hover { background: rgba(128,128,128,0.08); }
|
|
41677
|
+
.miao-chart-svg [data-miao-mark] { cursor: pointer; transition: opacity 0.15s ease, stroke-width 0.15s ease; }
|
|
41678
|
+
.miao-chart-svg [data-miao-mark]:hover { opacity: 0.78; }
|
|
41679
|
+
.miao-chart-svg [data-miao-selected="true"] { stroke: currentColor; stroke-width: 2; }
|
|
41680
|
+
.miao-mark-hidden { opacity: 0.18; }
|
|
41681
|
+
.miao-detail { margin-top: 12px; overflow: auto; max-height: 320px; border: 1px solid rgba(128,128,128,0.18); border-radius: 4px; }
|
|
41682
|
+
.miao-detail-title { padding: 9px 10px; font-size: 12px; font-weight: 700; border-bottom: 1px solid rgba(128,128,128,0.18); }
|
|
41683
|
+
.miao-detail table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
41684
|
+
.miao-detail th, .miao-detail td { padding: 7px 9px; border-bottom: 1px solid rgba(128,128,128,0.12); text-align: left; white-space: nowrap; }
|
|
41685
|
+
.miao-detail th { font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.64; }
|
|
41686
|
+
.miao-tooltip { position: fixed; z-index: 9999; pointer-events: none; padding: 6px 8px; border-radius: 4px; background: rgba(20,20,19,0.92); color: #fff; font: 12px/1.35 system-ui, sans-serif; box-shadow: 0 8px 24px rgba(0,0,0,0.18); transform: translate(10px, 10px); }
|
|
41687
|
+
`;
|
|
41688
|
+
var INTERACTIVE_JS = `
|
|
41689
|
+
(function() {
|
|
41690
|
+
var specEl = document.getElementById('miao-viz-spec');
|
|
41691
|
+
var dataEl = document.getElementById('miao-viz-data');
|
|
41692
|
+
if (!specEl || !dataEl) return;
|
|
41693
|
+
|
|
41694
|
+
var spec = JSON.parse(specEl.textContent || '{}');
|
|
41695
|
+
var rows = JSON.parse(dataEl.textContent || '[]');
|
|
41696
|
+
var filters = (spec.interactions && spec.interactions.globalFilters) || [];
|
|
41697
|
+
var state = { filters: {}, selection: null };
|
|
41698
|
+
var tooltip = createTooltip();
|
|
41699
|
+
|
|
41700
|
+
function init() {
|
|
41701
|
+
renderControls();
|
|
41702
|
+
update();
|
|
41703
|
+
}
|
|
41704
|
+
|
|
41705
|
+
function renderControls() {
|
|
41706
|
+
if (!filters.length) return;
|
|
41707
|
+
var main = document.querySelector('.miao-viz-report');
|
|
41708
|
+
if (!main) return;
|
|
41709
|
+
var controls = document.createElement('section');
|
|
41710
|
+
controls.className = 'miao-interactive-controls';
|
|
41711
|
+
controls.setAttribute('aria-label', 'Interactive filters');
|
|
41712
|
+
filters.forEach(function(filter) {
|
|
41713
|
+
controls.appendChild(filter.type === 'range' ? renderRangeFilter(filter) : renderSelectFilter(filter));
|
|
41714
|
+
});
|
|
41715
|
+
var reset = document.createElement('button');
|
|
41716
|
+
reset.type = 'button';
|
|
41717
|
+
reset.className = 'miao-reset';
|
|
41718
|
+
reset.textContent = 'Reset';
|
|
41719
|
+
reset.addEventListener('click', function() {
|
|
41720
|
+
state.filters = {};
|
|
41721
|
+
state.selection = null;
|
|
41722
|
+
controls.querySelectorAll('select,input').forEach(function(input) { input.value = ''; });
|
|
41723
|
+
update();
|
|
41724
|
+
});
|
|
41725
|
+
controls.appendChild(reset);
|
|
41726
|
+
var header = main.querySelector('header');
|
|
41727
|
+
main.insertBefore(controls, header ? header.nextSibling : main.firstChild);
|
|
41728
|
+
}
|
|
41729
|
+
|
|
41730
|
+
function renderSelectFilter(filter) {
|
|
41731
|
+
var wrap = document.createElement('div');
|
|
41732
|
+
wrap.className = 'miao-filter';
|
|
41733
|
+
var label = document.createElement('label');
|
|
41734
|
+
label.textContent = filter.field;
|
|
41735
|
+
var select = document.createElement('select');
|
|
41736
|
+
select.innerHTML = '<option value="">All</option>' + uniqueValues(filter.field).map(function(value) {
|
|
41737
|
+
return '<option value="' + escapeAttr(value) + '">' + escapeHtml(value) + '</option>';
|
|
41738
|
+
}).join('');
|
|
41739
|
+
select.addEventListener('change', function() {
|
|
41740
|
+
state.filters[filter.field] = select.value;
|
|
41741
|
+
update();
|
|
41742
|
+
});
|
|
41743
|
+
wrap.appendChild(label);
|
|
41744
|
+
wrap.appendChild(select);
|
|
41745
|
+
return wrap;
|
|
41746
|
+
}
|
|
41747
|
+
|
|
41748
|
+
function renderRangeFilter(filter) {
|
|
41749
|
+
var wrap = document.createElement('div');
|
|
41750
|
+
wrap.className = 'miao-filter';
|
|
41751
|
+
var label = document.createElement('label');
|
|
41752
|
+
label.textContent = filter.field;
|
|
41753
|
+
var pair = document.createElement('div');
|
|
41754
|
+
pair.className = 'miao-filter-range';
|
|
41755
|
+
var min = document.createElement('input');
|
|
41756
|
+
var max = document.createElement('input');
|
|
41757
|
+
min.placeholder = 'Min';
|
|
41758
|
+
max.placeholder = 'Max';
|
|
41759
|
+
[min, max].forEach(function(input) {
|
|
41760
|
+
input.addEventListener('input', function() {
|
|
41761
|
+
state.filters[filter.field] = [min.value, max.value];
|
|
41762
|
+
update();
|
|
41763
|
+
});
|
|
41764
|
+
});
|
|
41765
|
+
pair.appendChild(min);
|
|
41766
|
+
pair.appendChild(max);
|
|
41767
|
+
wrap.appendChild(label);
|
|
41768
|
+
wrap.appendChild(pair);
|
|
41769
|
+
return wrap;
|
|
41770
|
+
}
|
|
41771
|
+
|
|
41772
|
+
function bindMarks() {
|
|
41773
|
+
document.querySelectorAll('[data-miao-mark]').forEach(function(mark) {
|
|
41774
|
+
var chart = chartSpec(mark.getAttribute('data-chart-id'));
|
|
41775
|
+
mark.addEventListener('mouseenter', function(event) {
|
|
41776
|
+
var text = mark.getAttribute('data-tooltip');
|
|
41777
|
+
if (!text) return;
|
|
41778
|
+
tooltip.textContent = text;
|
|
41779
|
+
tooltip.hidden = false;
|
|
41780
|
+
moveTooltip(event);
|
|
41781
|
+
});
|
|
41782
|
+
mark.addEventListener('mousemove', moveTooltip);
|
|
41783
|
+
mark.addEventListener('mouseleave', function() { tooltip.hidden = true; });
|
|
41784
|
+
mark.addEventListener('click', function() {
|
|
41785
|
+
if (!canSelect(chart)) return;
|
|
41786
|
+
var field = mark.getAttribute('data-field');
|
|
41787
|
+
var value = mark.getAttribute('data-value');
|
|
41788
|
+
if (!field) return;
|
|
41789
|
+
state.selection = state.selection && state.selection.field === field && String(state.selection.value) === String(value)
|
|
41790
|
+
? null
|
|
41791
|
+
: { field: field, value: value };
|
|
41792
|
+
update();
|
|
41793
|
+
});
|
|
41794
|
+
});
|
|
41795
|
+
}
|
|
41796
|
+
|
|
41797
|
+
function update() {
|
|
41798
|
+
var filtered = applyFilters(rows);
|
|
41799
|
+
document.querySelectorAll('[data-miao-chart]').forEach(function(container) {
|
|
41800
|
+
var chart = chartSpec(container.getAttribute('data-miao-chart'));
|
|
41801
|
+
renderChart(container, chart, filtered);
|
|
41802
|
+
});
|
|
41803
|
+
bindMarks();
|
|
41804
|
+
document.querySelectorAll('[data-miao-mark]').forEach(function(mark) {
|
|
41805
|
+
var field = mark.getAttribute('data-field');
|
|
41806
|
+
var value = mark.getAttribute('data-value');
|
|
41807
|
+
var selected = state.selection && state.selection.field === field && String(state.selection.value) === String(value);
|
|
41808
|
+
mark.setAttribute('data-miao-selected', selected ? 'true' : 'false');
|
|
41809
|
+
mark.classList.toggle('miao-mark-hidden', Boolean((state.selection && !selected) || !markMatchesFilters(field, value)));
|
|
41810
|
+
});
|
|
41811
|
+
document.querySelectorAll('[data-miao-chart]').forEach(function(chart) {
|
|
41812
|
+
renderDetail(chart, filtered);
|
|
41813
|
+
});
|
|
41814
|
+
}
|
|
41815
|
+
|
|
41816
|
+
function renderChart(container, chart, sourceRows) {
|
|
41817
|
+
if (!chart || ['bar', 'pie', 'table'].indexOf(chart.type) === -1) return;
|
|
41818
|
+
var slot = container.querySelector('.miao-render-slot');
|
|
41819
|
+
if (!slot) return;
|
|
41820
|
+
var chartRows = prepareRows(sourceRows, chart);
|
|
41821
|
+
if (chart.type === 'bar') slot.innerHTML = renderBar(chart, chartRows, container.getAttribute('data-miao-chart'));
|
|
41822
|
+
else if (chart.type === 'pie') slot.innerHTML = renderPie(chart, chartRows, container.getAttribute('data-miao-chart'));
|
|
41823
|
+
else if (chart.type === 'table') slot.innerHTML = renderTable(chart, chartRows, container.getAttribute('data-miao-chart'));
|
|
41824
|
+
}
|
|
41825
|
+
|
|
41826
|
+
function prepareRows(sourceRows, chart) {
|
|
41827
|
+
return ((chart.data && chart.data.transform) || []).reduce(function(current, transform) {
|
|
41828
|
+
if (transform.type === 'derive-month' && transform.field && transform.as) {
|
|
41829
|
+
return current.map(function(row) {
|
|
41830
|
+
var copy = Object.assign({}, row);
|
|
41831
|
+
copy[transform.as] = toMonth(row[transform.field]);
|
|
41832
|
+
return copy;
|
|
41833
|
+
});
|
|
41834
|
+
}
|
|
41835
|
+
if (transform.type === 'aggregate') return aggregateRows(current, transform);
|
|
41836
|
+
if (transform.type === 'sort' && transform.field) {
|
|
41837
|
+
var order = transform.order === 'asc' ? 1 : -1;
|
|
41838
|
+
return current.slice().sort(function(a, b) {
|
|
41839
|
+
var an = Number(a[transform.field]);
|
|
41840
|
+
var bn = Number(b[transform.field]);
|
|
41841
|
+
if (Number.isFinite(an) && Number.isFinite(bn)) return (an - bn) * order;
|
|
41842
|
+
return String(a[transform.field] || '').localeCompare(String(b[transform.field] || '')) * order;
|
|
41843
|
+
});
|
|
41844
|
+
}
|
|
41845
|
+
if (transform.type === 'limit' && typeof transform.value === 'number') return current.slice(0, transform.value);
|
|
41846
|
+
return current;
|
|
41847
|
+
}, sourceRows.slice());
|
|
41848
|
+
}
|
|
41849
|
+
|
|
41850
|
+
function aggregateRows(sourceRows, transform) {
|
|
41851
|
+
var groupBy = transform.groupBy || [];
|
|
41852
|
+
var measures = transform.measures || [];
|
|
41853
|
+
var groups = new Map();
|
|
41854
|
+
sourceRows.forEach(function(row) {
|
|
41855
|
+
var key = JSON.stringify(groupBy.map(function(field) { return row[field]; }));
|
|
41856
|
+
var existing = groups.get(key) || [];
|
|
41857
|
+
existing.push(row);
|
|
41858
|
+
groups.set(key, existing);
|
|
41859
|
+
});
|
|
41860
|
+
return Array.from(groups.values()).map(function(groupRows) {
|
|
41861
|
+
var first = groupRows[0] || {};
|
|
41862
|
+
var out = {};
|
|
41863
|
+
groupBy.forEach(function(field) { out[field] = first[field]; });
|
|
41864
|
+
measures.forEach(function(measure) { out[measure.as] = aggregateMeasure(groupRows, measure); });
|
|
41865
|
+
return out;
|
|
41866
|
+
});
|
|
41867
|
+
}
|
|
41868
|
+
|
|
41869
|
+
function aggregateMeasure(sourceRows, measure) {
|
|
41870
|
+
if (measure.op === 'count') return sourceRows.length;
|
|
41871
|
+
var values = sourceRows.map(function(row) { return Number(row[measure.field]); }).filter(Number.isFinite);
|
|
41872
|
+
if (!values.length) return 0;
|
|
41873
|
+
if (measure.op === 'avg') return values.reduce(sum, 0) / values.length;
|
|
41874
|
+
if (measure.op === 'min') return Math.min.apply(null, values);
|
|
41875
|
+
if (measure.op === 'max') return Math.max.apply(null, values);
|
|
41876
|
+
return values.reduce(sum, 0);
|
|
41877
|
+
}
|
|
41878
|
+
|
|
41879
|
+
function renderBar(chart, chartRows, chartId) {
|
|
41880
|
+
var xField = field(chart, 'x');
|
|
41881
|
+
var yField = field(chart, 'y');
|
|
41882
|
+
var width = numberStyle(chart, 'width', 720);
|
|
41883
|
+
var height = numberStyle(chart, 'height', 420);
|
|
41884
|
+
var margin = { top: 24, right: 24, bottom: 48, left: 72 };
|
|
41885
|
+
var chartWidth = width - margin.left - margin.right;
|
|
41886
|
+
var chartHeight = height - margin.top - margin.bottom;
|
|
41887
|
+
var values = chartRows.map(function(row) { return Number(row[yField]); }).filter(Number.isFinite);
|
|
41888
|
+
var yMax = Math.max.apply(null, values.concat([1]));
|
|
41889
|
+
var gap = 8;
|
|
41890
|
+
var barWidth = Math.max(8, (chartWidth - gap * Math.max(chartRows.length - 1, 0)) / Math.max(chartRows.length, 1));
|
|
41891
|
+
var body = chartRows.map(function(row, index) {
|
|
41892
|
+
var value = Number(row[yField]) || 0;
|
|
41893
|
+
var barHeight = value / yMax * chartHeight;
|
|
41894
|
+
var x = margin.left + index * (barWidth + gap);
|
|
41895
|
+
var y = margin.top + chartHeight - barHeight;
|
|
41896
|
+
var label = String(row[xField] == null ? '' : row[xField]);
|
|
41897
|
+
return '<g><rect ' + markAttrs(chartId, xField, row[xField], index, label + ': ' + value) +
|
|
41898
|
+
' x="' + fixed(x) + '" y="' + fixed(y) + '" width="' + fixed(barWidth) + '" height="' + fixed(barHeight) +
|
|
41899
|
+
'" rx="3" fill="' + color(index) + '" />' +
|
|
41900
|
+
'<text x="' + fixed(x + barWidth / 2) + '" y="' + fixed(margin.top + chartHeight + 18) +
|
|
41901
|
+
'" text-anchor="middle" fill="#475569" font-size="11">' + escapeHtml(label) + '</text></g>';
|
|
41902
|
+
}).join('');
|
|
41903
|
+
return svgFrame(width, height, body);
|
|
41904
|
+
}
|
|
41905
|
+
|
|
41906
|
+
function renderPie(chart, chartRows, chartId) {
|
|
41907
|
+
var labelField = field(chart, 'label');
|
|
41908
|
+
var valueField = field(chart, 'value');
|
|
41909
|
+
var width = numberStyle(chart, 'width', 720);
|
|
41910
|
+
var height = numberStyle(chart, 'height', 420);
|
|
41911
|
+
var cx = width / 2 - 80;
|
|
41912
|
+
var cy = height / 2;
|
|
41913
|
+
var radius = Math.min(width, height) * 0.34;
|
|
41914
|
+
var values = chartRows.map(function(row) { return Math.max(0, Number(row[valueField]) || 0); });
|
|
41915
|
+
var total = values.reduce(sum, 0) || 1;
|
|
41916
|
+
var angle = -Math.PI / 2;
|
|
41917
|
+
var slices = chartRows.map(function(row, index) {
|
|
41918
|
+
var value = values[index];
|
|
41919
|
+
var nextAngle = angle + value / total * Math.PI * 2;
|
|
41920
|
+
var path = describeArc(cx, cy, radius, angle, nextAngle);
|
|
41921
|
+
var label = String(row[labelField] == null ? '' : row[labelField]);
|
|
41922
|
+
angle = nextAngle;
|
|
41923
|
+
return '<path ' + markAttrs(chartId, labelField, row[labelField], index, label + ': ' + value) +
|
|
41924
|
+
' d="' + path + '" fill="' + color(index) + '" stroke="#fff" stroke-width="2" />';
|
|
41925
|
+
}).join('');
|
|
41926
|
+
return svgFrame(width, height, slices);
|
|
41927
|
+
}
|
|
41928
|
+
|
|
41929
|
+
function renderTable(chart, chartRows, chartId) {
|
|
41930
|
+
var columns = Object.keys(chartRows[0] || rows[0] || {}).slice(0, 8);
|
|
41931
|
+
var markField = field(chart, 'label') || field(chart, 'x') || columns[0] || '';
|
|
41932
|
+
return '<div class="miao-table-wrap"><table class="miao-table"><thead><tr>' +
|
|
41933
|
+
columns.map(function(col) { return '<th>' + escapeHtml(col) + '</th>'; }).join('') +
|
|
41934
|
+
'</tr></thead><tbody>' + chartRows.slice(0, 20).map(function(row, index) {
|
|
41935
|
+
return '<tr ' + markAttrs(chartId, markField, row[markField], index, String(row[markField] || 'Row')) + '>' +
|
|
41936
|
+
columns.map(function(col) { return '<td>' + escapeHtml(row[col] == null ? '' : row[col]) + '</td>'; }).join('') +
|
|
41937
|
+
'</tr>';
|
|
41938
|
+
}).join('') + '</tbody></table></div>';
|
|
41939
|
+
}
|
|
41940
|
+
|
|
41941
|
+
function renderDetail(chart, filtered) {
|
|
41942
|
+
var chartId = chart.getAttribute('data-miao-chart');
|
|
41943
|
+
var detail = chart.querySelector('.miao-detail');
|
|
41944
|
+
if (!detail) {
|
|
41945
|
+
detail = document.createElement('div');
|
|
41946
|
+
detail.className = 'miao-detail';
|
|
41947
|
+
chart.appendChild(detail);
|
|
41948
|
+
}
|
|
41949
|
+
if (!state.selection) {
|
|
41950
|
+
detail.innerHTML = '';
|
|
41951
|
+
detail.hidden = true;
|
|
41952
|
+
return;
|
|
41953
|
+
}
|
|
41954
|
+
var selectedRows = filtered.filter(function(row) {
|
|
41955
|
+
return String(row[state.selection.field] == null ? '' : row[state.selection.field]) === String(state.selection.value);
|
|
41956
|
+
});
|
|
41957
|
+
detail.hidden = false;
|
|
41958
|
+
detail.innerHTML = detailTable(selectedRows, chartId);
|
|
41959
|
+
}
|
|
41960
|
+
|
|
41961
|
+
function applyFilters(source) {
|
|
41962
|
+
return source.filter(function(row) {
|
|
41963
|
+
return filters.every(function(filter) {
|
|
41964
|
+
var active = state.filters[filter.field];
|
|
41965
|
+
if (active == null || active === '' || (Array.isArray(active) && !active[0] && !active[1])) return true;
|
|
41966
|
+
if (filter.type === 'select') return String(row[filter.field] == null ? '' : row[filter.field]) === String(active);
|
|
41967
|
+
var current = comparable(row[filter.field]);
|
|
41968
|
+
if (current == null) return false;
|
|
41969
|
+
var min = comparable(active[0]);
|
|
41970
|
+
var max = comparable(active[1]);
|
|
41971
|
+
if (min != null && current < min) return false;
|
|
41972
|
+
if (max != null && current > max) return false;
|
|
41973
|
+
return true;
|
|
41974
|
+
});
|
|
41975
|
+
});
|
|
41976
|
+
}
|
|
41977
|
+
|
|
41978
|
+
function markMatchesFilters(field, value) {
|
|
41979
|
+
var activeFilters = filters.filter(function(filter) { return filter.field === field; });
|
|
41980
|
+
if (!activeFilters.length) return true;
|
|
41981
|
+
return activeFilters.every(function(filter) {
|
|
41982
|
+
var active = state.filters[filter.field];
|
|
41983
|
+
if (active == null || active === '' || (Array.isArray(active) && !active[0] && !active[1])) return true;
|
|
41984
|
+
if (filter.type === 'select') return String(value == null ? '' : value) === String(active);
|
|
41985
|
+
var current = comparable(value);
|
|
41986
|
+
if (current == null) return false;
|
|
41987
|
+
var min = comparable(active[0]);
|
|
41988
|
+
var max = comparable(active[1]);
|
|
41989
|
+
if (min != null && current < min) return false;
|
|
41990
|
+
if (max != null && current > max) return false;
|
|
41991
|
+
return true;
|
|
41992
|
+
});
|
|
41993
|
+
}
|
|
41994
|
+
|
|
41995
|
+
function chartSpec(chartId) {
|
|
41996
|
+
return (spec.charts || []).find(function(chart, index) { return (chart.id || ('chart-' + (index + 1))) === chartId; }) || null;
|
|
41997
|
+
}
|
|
41998
|
+
function canSelect(chart) { return Boolean(chart && ((chart.interaction && chart.interaction.select) || chart.drilldownPreset)); }
|
|
41999
|
+
function field(chart, channel) { return chart && chart.encoding && chart.encoding[channel] ? chart.encoding[channel].field : ''; }
|
|
42000
|
+
function numberStyle(chart, key, fallback) { return chart && chart.style && typeof chart.style[key] === 'number' ? chart.style[key] : fallback; }
|
|
42001
|
+
|
|
42002
|
+
function svgFrame(width, height, body) {
|
|
42003
|
+
return '<svg class="miao-chart-svg" viewBox="0 0 ' + width + ' ' + height + '" width="100%" height="' + height +
|
|
42004
|
+
'" role="img" xmlns="http://www.w3.org/2000/svg"><rect x="0" y="0" width="' + width + '" height="' + height +
|
|
42005
|
+
'" fill="#fff" />' + body + '</svg>';
|
|
42006
|
+
}
|
|
42007
|
+
|
|
42008
|
+
function markAttrs(chartId, markField, value, rowKey, tooltipText) {
|
|
42009
|
+
return 'data-miao-mark="true" data-chart-id="' + escapeAttr(chartId || '') + '" data-field="' + escapeAttr(markField || '') +
|
|
42010
|
+
'" data-value="' + escapeAttr(value == null ? '' : value) + '" data-row-key="' + escapeAttr(rowKey) +
|
|
42011
|
+
'" data-tooltip="' + escapeAttr(tooltipText || '') + '"';
|
|
42012
|
+
}
|
|
42013
|
+
|
|
42014
|
+
function describeArc(cx, cy, radius, startAngle, endAngle) {
|
|
42015
|
+
var start = polarToCartesian(cx, cy, radius, endAngle);
|
|
42016
|
+
var end = polarToCartesian(cx, cy, radius, startAngle);
|
|
42017
|
+
var largeArc = endAngle - startAngle <= Math.PI ? '0' : '1';
|
|
42018
|
+
return 'M ' + fixed(cx) + ' ' + fixed(cy) + ' L ' + fixed(start.x) + ' ' + fixed(start.y) +
|
|
42019
|
+
' A ' + fixed(radius) + ' ' + fixed(radius) + ' 0 ' + largeArc + ' 0 ' + fixed(end.x) + ' ' + fixed(end.y) + ' Z';
|
|
42020
|
+
}
|
|
42021
|
+
|
|
42022
|
+
function polarToCartesian(cx, cy, radius, angle) { return { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) }; }
|
|
42023
|
+
function toMonth(value) {
|
|
42024
|
+
var date = new Date(String(value));
|
|
42025
|
+
if (!Number.isFinite(date.getTime())) return String(value == null ? '' : value);
|
|
42026
|
+
return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0');
|
|
42027
|
+
}
|
|
42028
|
+
function color(index) { var palette = ['#2563eb', '#16a34a', '#f97316', '#dc2626', '#7c3aed', '#0891b2']; return palette[index % palette.length]; }
|
|
42029
|
+
function fixed(value) { return Number(value).toFixed(1); }
|
|
42030
|
+
function sum(a, b) { return a + b; }
|
|
42031
|
+
|
|
42032
|
+
function detailTable(selectedRows, chartId) {
|
|
42033
|
+
var visible = selectedRows.slice(0, 100);
|
|
42034
|
+
var columns = Object.keys(visible[0] || rows[0] || {}).slice(0, 8);
|
|
42035
|
+
if (!columns.length) return '<div class="miao-detail-title">No rows</div>';
|
|
42036
|
+
return '<div class="miao-detail-title">' + escapeHtml(chartId || 'Detail') + ': ' + selectedRows.length + ' rows</div>' +
|
|
42037
|
+
'<table><thead><tr>' + columns.map(function(col) { return '<th>' + escapeHtml(col) + '</th>'; }).join('') +
|
|
42038
|
+
'</tr></thead><tbody>' + visible.map(function(row) {
|
|
42039
|
+
return '<tr>' + columns.map(function(col) { return '<td>' + escapeHtml(row[col] == null ? '' : row[col]) + '</td>'; }).join('') + '</tr>';
|
|
42040
|
+
}).join('') + '</tbody></table>';
|
|
42041
|
+
}
|
|
42042
|
+
|
|
42043
|
+
function uniqueValues(field) {
|
|
42044
|
+
var seen = new Set();
|
|
42045
|
+
rows.forEach(function(row) {
|
|
42046
|
+
if (row[field] != null) seen.add(String(row[field]));
|
|
42047
|
+
});
|
|
42048
|
+
return Array.from(seen).slice(0, 200).sort();
|
|
42049
|
+
}
|
|
42050
|
+
|
|
42051
|
+
function comparable(value) {
|
|
42052
|
+
if (value == null || value === '') return null;
|
|
42053
|
+
var number = Number(value);
|
|
42054
|
+
if (Number.isFinite(number)) return number;
|
|
42055
|
+
var date = new Date(String(value)).getTime();
|
|
42056
|
+
return Number.isFinite(date) ? date : null;
|
|
42057
|
+
}
|
|
42058
|
+
|
|
42059
|
+
function createTooltip() {
|
|
42060
|
+
var el = document.createElement('div');
|
|
42061
|
+
el.className = 'miao-tooltip';
|
|
42062
|
+
el.hidden = true;
|
|
42063
|
+
document.body.appendChild(el);
|
|
42064
|
+
return el;
|
|
42065
|
+
}
|
|
42066
|
+
|
|
42067
|
+
function moveTooltip(event) {
|
|
42068
|
+
tooltip.style.left = event.clientX + 'px';
|
|
42069
|
+
tooltip.style.top = event.clientY + 'px';
|
|
42070
|
+
}
|
|
42071
|
+
|
|
42072
|
+
function escapeHtml(value) {
|
|
42073
|
+
return String(value).replace(/[&<>"']/g, function(char) {
|
|
42074
|
+
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char];
|
|
42075
|
+
});
|
|
42076
|
+
}
|
|
42077
|
+
|
|
42078
|
+
function escapeAttr(value) {
|
|
42079
|
+
return escapeHtml(value).replace(/\\n/g, ' ');
|
|
42080
|
+
}
|
|
42081
|
+
|
|
42082
|
+
init();
|
|
42083
|
+
})();
|
|
42084
|
+
`;
|
|
42085
|
+
|
|
42086
|
+
// src/interactive-runtime.ts
|
|
42087
|
+
function shouldEnableInteractiveRuntime(spec, options = {}) {
|
|
42088
|
+
if (options.enabled === true) return true;
|
|
42089
|
+
if (options.enabled === false) return false;
|
|
42090
|
+
if ((spec.interactions?.globalFilters?.length ?? 0) > 0) return true;
|
|
42091
|
+
return spec.charts.some((chart) => Boolean(chart.interaction || chart.drilldownPreset));
|
|
42092
|
+
}
|
|
42093
|
+
function renderInteractiveAssets(rows) {
|
|
42094
|
+
return `
|
|
42095
|
+
<script type="application/json" id="miao-viz-data">${escapeScriptJson(rows)}</script>
|
|
42096
|
+
<style>${INTERACTIVE_CSS}</style>
|
|
42097
|
+
<script>${INTERACTIVE_JS}</script>`;
|
|
42098
|
+
}
|
|
42099
|
+
function escapeScriptJson(value) {
|
|
42100
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
42101
|
+
}
|
|
42102
|
+
|
|
41445
42103
|
// src/html-export.ts
|
|
41446
42104
|
var INSIGHTS_CSS = `
|
|
41447
42105
|
.report-insights { margin: 0 0 32px; padding: 16px 20px 14px; border-radius: 4px; border: 1px solid rgba(128,128,128,0.18); background: rgba(128,128,128,0.04); }
|
|
@@ -41449,9 +42107,10 @@ var INSIGHTS_CSS = `
|
|
|
41449
42107
|
.insights-list { margin: 0; padding: 0 0 0 18px; }
|
|
41450
42108
|
.insights-list li { margin: 5px 0; font-size: 13px; line-height: 1.55; opacity: 0.75; }
|
|
41451
42109
|
`;
|
|
41452
|
-
function renderStaticHtml(spec, profile, rows, themeOverride) {
|
|
42110
|
+
function renderStaticHtml(spec, profile, rows, themeOverride, interactiveOptions = {}) {
|
|
41453
42111
|
const theme = getTheme(themeOverride ?? spec.theme);
|
|
41454
42112
|
const title = spec.title ?? "Miao Vision Report";
|
|
42113
|
+
const interactive = shouldEnableInteractiveRuntime(spec, interactiveOptions);
|
|
41455
42114
|
const header = theme.layout === "editorial" ? renderEditorialHeader(title, spec.description, profile) : renderDefaultHeader(title, spec.description, profile);
|
|
41456
42115
|
const insights = spec.insights && spec.insights.length > 0 ? renderInsights(spec.insights) : "";
|
|
41457
42116
|
let charts;
|
|
@@ -41467,16 +42126,18 @@ function renderStaticHtml(spec, profile, rows, themeOverride) {
|
|
|
41467
42126
|
sections.push(renderKpiGroup(group, rows, theme));
|
|
41468
42127
|
} else {
|
|
41469
42128
|
const chart = spec.charts[i];
|
|
41470
|
-
const
|
|
41471
|
-
|
|
42129
|
+
const chartId = chartIdFor(chart, i);
|
|
42130
|
+
const svg = renderChartSvg(chart, rows, theme.svg, { chartId });
|
|
42131
|
+
sections.push(renderEditorialCard(chart, i, svg, chartId));
|
|
41472
42132
|
i++;
|
|
41473
42133
|
}
|
|
41474
42134
|
}
|
|
41475
42135
|
charts = sections.join("\n");
|
|
41476
42136
|
} else {
|
|
41477
42137
|
charts = spec.charts.map((chart, index) => {
|
|
41478
|
-
const
|
|
41479
|
-
|
|
42138
|
+
const chartId = chartIdFor(chart, index);
|
|
42139
|
+
const svg = renderChartSvg(chart, rows, theme.svg, { chartId });
|
|
42140
|
+
return renderDefaultCard(chart, index, svg, chartId);
|
|
41480
42141
|
}).join("\n");
|
|
41481
42142
|
}
|
|
41482
42143
|
return `<!doctype html>
|
|
@@ -41495,6 +42156,7 @@ function renderStaticHtml(spec, profile, rows, themeOverride) {
|
|
|
41495
42156
|
</main>
|
|
41496
42157
|
<script type="application/json" id="miao-viz-spec">${escapeHtml(JSON.stringify(spec, null, 2))}</script>
|
|
41497
42158
|
<script type="application/json" id="miao-viz-profile">${escapeHtml(JSON.stringify(profile, null, 2))}</script>
|
|
42159
|
+
${interactive ? renderInteractiveAssets(rows) : ""}
|
|
41498
42160
|
</body>
|
|
41499
42161
|
</html>`;
|
|
41500
42162
|
}
|
|
@@ -41522,11 +42184,11 @@ function renderEditorialHeader(title, description, profile) {
|
|
|
41522
42184
|
</div>
|
|
41523
42185
|
</header>`;
|
|
41524
42186
|
}
|
|
41525
|
-
function renderDefaultCard(chart, index, svg) {
|
|
42187
|
+
function renderDefaultCard(chart, index, svg, chartId) {
|
|
41526
42188
|
const chartTitle = chart.title ?? `${chart.type} chart ${index + 1}`;
|
|
41527
|
-
return `<section class="chart-block">
|
|
42189
|
+
return `<section class="chart-block" data-miao-chart="${escapeHtml(chartId)}">
|
|
41528
42190
|
<h2>${escapeHtml(chartTitle)}</h2>
|
|
41529
|
-
|
|
42191
|
+
<div class="miao-render-slot">${svg}</div>
|
|
41530
42192
|
</section>`;
|
|
41531
42193
|
}
|
|
41532
42194
|
function renderKpiGroup(charts, rows, theme) {
|
|
@@ -41536,16 +42198,19 @@ function renderKpiGroup(charts, rows, theme) {
|
|
|
41536
42198
|
<div class="kpi-grid">${items}</div>
|
|
41537
42199
|
</section>`;
|
|
41538
42200
|
}
|
|
41539
|
-
function renderEditorialCard(chart, index, svg) {
|
|
42201
|
+
function renderEditorialCard(chart, index, svg, chartId) {
|
|
41540
42202
|
const chartTitle = chart.title ?? `${chart.type} chart ${index + 1}`;
|
|
41541
42203
|
const caption = buildCaption(chart);
|
|
41542
|
-
return `<section class="chart-card">
|
|
42204
|
+
return `<section class="chart-card" data-miao-chart="${escapeHtml(chartId)}">
|
|
41543
42205
|
<div class="chart-label">${escapeHtml(chart.type.toUpperCase())} CHART</div>
|
|
41544
42206
|
<h2>${escapeHtml(chartTitle)}</h2>
|
|
41545
|
-
|
|
42207
|
+
<div class="miao-render-slot">${svg}</div>
|
|
41546
42208
|
${caption ? `<p class="chart-caption">${escapeHtml(caption)}</p>` : ""}
|
|
41547
42209
|
</section>`;
|
|
41548
42210
|
}
|
|
42211
|
+
function chartIdFor(chart, index) {
|
|
42212
|
+
return chart.id ?? `chart-${index + 1}`;
|
|
42213
|
+
}
|
|
41549
42214
|
function renderInsights(insights) {
|
|
41550
42215
|
const items = insights.map((s) => `<li>${escapeHtml(s)}</li>`).join("\n ");
|
|
41551
42216
|
return `<section class="report-insights">
|
|
@@ -56116,9 +56781,20 @@ var transformSchema = external_exports.object({
|
|
|
56116
56781
|
order: external_exports.enum(["asc", "desc"]).optional(),
|
|
56117
56782
|
value: external_exports.unknown().optional()
|
|
56118
56783
|
});
|
|
56784
|
+
var globalFilterSchema = external_exports.object({
|
|
56785
|
+
field: external_exports.string().min(1),
|
|
56786
|
+
type: external_exports.enum(["select", "range"])
|
|
56787
|
+
});
|
|
56788
|
+
var chartInteractionSchema = external_exports.object({
|
|
56789
|
+
tooltip: external_exports.boolean().optional(),
|
|
56790
|
+
select: external_exports.enum(["filter", "detail"]).optional()
|
|
56791
|
+
});
|
|
56119
56792
|
var chartSpecSchema = external_exports.object({
|
|
56793
|
+
id: external_exports.string().min(1).optional(),
|
|
56120
56794
|
type: external_exports.enum(MVP_CHART_TYPES),
|
|
56121
56795
|
title: external_exports.string().optional(),
|
|
56796
|
+
interaction: chartInteractionSchema.optional(),
|
|
56797
|
+
drilldownPreset: external_exports.enum(["category-detail"]).optional(),
|
|
56122
56798
|
data: external_exports.object({
|
|
56123
56799
|
source: external_exports.string().optional(),
|
|
56124
56800
|
transform: external_exports.array(transformSchema).optional()
|
|
@@ -56137,6 +56813,9 @@ var reportSpecSchema = external_exports.object({
|
|
|
56137
56813
|
title: external_exports.string().optional(),
|
|
56138
56814
|
description: external_exports.string().optional(),
|
|
56139
56815
|
theme: external_exports.enum(["default", "editorial", "dark", "minimal"]).optional(),
|
|
56816
|
+
interactions: external_exports.object({
|
|
56817
|
+
globalFilters: external_exports.array(globalFilterSchema).optional()
|
|
56818
|
+
}).optional(),
|
|
56140
56819
|
insights: external_exports.array(external_exports.string()).optional(),
|
|
56141
56820
|
charts: external_exports.array(chartSpecSchema).min(1)
|
|
56142
56821
|
});
|
|
@@ -56170,6 +56849,7 @@ var REQUIRED_ENCODINGS = {
|
|
|
56170
56849
|
table: [],
|
|
56171
56850
|
bigvalue: ["value"]
|
|
56172
56851
|
};
|
|
56852
|
+
var DRILLDOWN_CHART_TYPES = ["bar", "pie", "table"];
|
|
56173
56853
|
function validateReportSpec(spec, profile, formats = ["html"]) {
|
|
56174
56854
|
const parsed = reportSpecSchema.safeParse(spec);
|
|
56175
56855
|
if (!parsed.success) {
|
|
@@ -56183,11 +56863,24 @@ function validateReportSpec(spec, profile, formats = ["html"]) {
|
|
|
56183
56863
|
}
|
|
56184
56864
|
}
|
|
56185
56865
|
const availableFields = profile.columns.map((column) => column.name);
|
|
56866
|
+
const chartIds = /* @__PURE__ */ new Set();
|
|
56867
|
+
const interactionResult = validateReportInteractions(parsed.data, profile, availableFields);
|
|
56868
|
+
if (isAgentError(interactionResult)) return interactionResult;
|
|
56186
56869
|
for (const chart of parsed.data.charts) {
|
|
56870
|
+
if (chart.id) {
|
|
56871
|
+
if (chartIds.has(chart.id)) {
|
|
56872
|
+
return agentError("DUPLICATE_CHART_ID", `Chart id '${chart.id}' is used more than once.`, {
|
|
56873
|
+
chartId: chart.id
|
|
56874
|
+
});
|
|
56875
|
+
}
|
|
56876
|
+
chartIds.add(chart.id);
|
|
56877
|
+
}
|
|
56187
56878
|
const chartTypeResult = validateChartType(chart);
|
|
56188
56879
|
if (isAgentError(chartTypeResult)) return chartTypeResult;
|
|
56189
56880
|
const encodingResult = validateRequiredEncodings(chart);
|
|
56190
56881
|
if (isAgentError(encodingResult)) return encodingResult;
|
|
56882
|
+
const chartInteractionResult = validateChartInteraction(chart);
|
|
56883
|
+
if (isAgentError(chartInteractionResult)) return chartInteractionResult;
|
|
56191
56884
|
const derivedFields = collectDerivedFields(chart);
|
|
56192
56885
|
const sourceFields = collectSourceFields(chart, derivedFields);
|
|
56193
56886
|
for (const field of sourceFields) {
|
|
@@ -56230,6 +56923,46 @@ function validateRequiredEncodings(chart) {
|
|
|
56230
56923
|
}
|
|
56231
56924
|
return ok(chart);
|
|
56232
56925
|
}
|
|
56926
|
+
function validateReportInteractions(spec, profile, availableFields) {
|
|
56927
|
+
for (const filter of spec.interactions?.globalFilters ?? []) {
|
|
56928
|
+
const column = profile.columns.find((candidate) => candidate.name === filter.field);
|
|
56929
|
+
if (!column) {
|
|
56930
|
+
return agentError("INTERACTION_FIELD_NOT_FOUND", `Interactive filter field '${filter.field}' was not found in the input data.`, {
|
|
56931
|
+
field: filter.field,
|
|
56932
|
+
availableFields
|
|
56933
|
+
});
|
|
56934
|
+
}
|
|
56935
|
+
if (filter.type === "range" && column.type !== "number" && column.type !== "date") {
|
|
56936
|
+
return agentError("INTERACTION_FILTER_TYPE_MISMATCH", `Range filter '${filter.field}' requires a number or date field.`, {
|
|
56937
|
+
field: filter.field,
|
|
56938
|
+
filterType: filter.type,
|
|
56939
|
+
columnType: column.type,
|
|
56940
|
+
supportedColumnTypes: ["number", "date"]
|
|
56941
|
+
});
|
|
56942
|
+
}
|
|
56943
|
+
}
|
|
56944
|
+
return ok(spec);
|
|
56945
|
+
}
|
|
56946
|
+
function validateChartInteraction(chart) {
|
|
56947
|
+
if (chart.drilldownPreset && chart.drilldownPreset !== "category-detail") {
|
|
56948
|
+
return agentError("UNSUPPORTED_DRILLDOWN_PRESET", `Drilldown preset '${chart.drilldownPreset}' is not supported.`, {
|
|
56949
|
+
supportedPresets: ["category-detail"]
|
|
56950
|
+
});
|
|
56951
|
+
}
|
|
56952
|
+
if (chart.drilldownPreset && !DRILLDOWN_CHART_TYPES.includes(chart.type)) {
|
|
56953
|
+
return agentError("UNSUPPORTED_DRILLDOWN_CHART_TYPE", `Drilldown preset '${chart.drilldownPreset}' is not supported for chart type '${chart.type}'.`, {
|
|
56954
|
+
chartType: chart.type,
|
|
56955
|
+
supportedChartTypes: DRILLDOWN_CHART_TYPES
|
|
56956
|
+
});
|
|
56957
|
+
}
|
|
56958
|
+
if (chart.interaction?.select && !DRILLDOWN_CHART_TYPES.includes(chart.type)) {
|
|
56959
|
+
return agentError("UNSUPPORTED_INTERACTION_CHART_TYPE", `Chart interaction select '${chart.interaction.select}' is not supported for chart type '${chart.type}'.`, {
|
|
56960
|
+
chartType: chart.type,
|
|
56961
|
+
supportedChartTypes: DRILLDOWN_CHART_TYPES
|
|
56962
|
+
});
|
|
56963
|
+
}
|
|
56964
|
+
return ok(chart);
|
|
56965
|
+
}
|
|
56233
56966
|
function collectSourceFields(chart, derivedFields) {
|
|
56234
56967
|
const fields = /* @__PURE__ */ new Set();
|
|
56235
56968
|
for (const encoding of Object.values(chart.encoding)) {
|
|
@@ -56251,56 +56984,6 @@ function collectDerivedFields(chart) {
|
|
|
56251
56984
|
return fields;
|
|
56252
56985
|
}
|
|
56253
56986
|
|
|
56254
|
-
// src/deck-schema.ts
|
|
56255
|
-
var SLIDE_LAYOUTS = [
|
|
56256
|
-
"cover",
|
|
56257
|
-
"title-only",
|
|
56258
|
-
"text-points",
|
|
56259
|
-
"text-chart",
|
|
56260
|
-
"metrics-chart",
|
|
56261
|
-
"chart-full",
|
|
56262
|
-
"table-full",
|
|
56263
|
-
"ending"
|
|
56264
|
-
];
|
|
56265
|
-
var metricTransformSchema = external_exports.object({
|
|
56266
|
-
type: external_exports.enum(["derive-month", "aggregate", "sort", "limit", "filter"]),
|
|
56267
|
-
field: external_exports.string().optional(),
|
|
56268
|
-
as: external_exports.string().optional(),
|
|
56269
|
-
groupBy: external_exports.array(external_exports.string()).optional(),
|
|
56270
|
-
measures: external_exports.array(external_exports.object({
|
|
56271
|
-
field: external_exports.string(),
|
|
56272
|
-
op: external_exports.enum(["sum", "avg", "count", "min", "max"]),
|
|
56273
|
-
as: external_exports.string()
|
|
56274
|
-
})).optional(),
|
|
56275
|
-
order: external_exports.enum(["asc", "desc"]).optional(),
|
|
56276
|
-
value: external_exports.unknown().optional()
|
|
56277
|
-
});
|
|
56278
|
-
var slideMetricSchema = external_exports.object({
|
|
56279
|
-
label: external_exports.string().min(1),
|
|
56280
|
-
value: external_exports.union([external_exports.string(), external_exports.number()]).optional(),
|
|
56281
|
-
format: external_exports.string().optional(),
|
|
56282
|
-
data: external_exports.object({
|
|
56283
|
-
transform: external_exports.array(metricTransformSchema).optional()
|
|
56284
|
-
}).optional()
|
|
56285
|
-
});
|
|
56286
|
-
var slideSpecSchema = external_exports.object({
|
|
56287
|
-
layout: external_exports.enum(SLIDE_LAYOUTS),
|
|
56288
|
-
eyebrow: external_exports.string().optional(),
|
|
56289
|
-
title: external_exports.string().optional(),
|
|
56290
|
-
claim: external_exports.string().optional(),
|
|
56291
|
-
bullets: external_exports.array(external_exports.string()).optional(),
|
|
56292
|
-
callout: external_exports.string().optional(),
|
|
56293
|
-
annotation: external_exports.string().optional(),
|
|
56294
|
-
metrics: external_exports.array(slideMetricSchema).optional(),
|
|
56295
|
-
charts: external_exports.array(chartSpecSchema).optional()
|
|
56296
|
-
});
|
|
56297
|
-
var deckSpecSchema = external_exports.object({
|
|
56298
|
-
title: external_exports.string().optional(),
|
|
56299
|
-
description: external_exports.string().optional(),
|
|
56300
|
-
theme: external_exports.enum(["default", "editorial", "dark", "minimal"]).optional(),
|
|
56301
|
-
slides: external_exports.array(slideSpecSchema).min(1, "Deck must have at least one slide")
|
|
56302
|
-
});
|
|
56303
|
-
|
|
56304
56987
|
// src/deck-layouts.ts
|
|
56305
56988
|
function withSize(chart, width, height) {
|
|
56306
56989
|
return { ...chart, style: { ...chart.style, width, height } };
|
|
@@ -56489,8 +57172,8 @@ var SLIDE_CSS = `
|
|
|
56489
57172
|
|
|
56490
57173
|
/* \u2500\u2500 Shared slide elements \u2500\u2500 */
|
|
56491
57174
|
.slide-eyebrow { font-family: var(--mv-mono); font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 3px; color: var(--mv-muted); margin-bottom: 14px; }
|
|
56492
|
-
.slide-title { font-family: var(--mv-serif); font-size: 50px; font-weight: 500; line-height: 1.1; letter-spacing: -0.6px; color: var(--mv-ink); margin-bottom: 24px; }
|
|
56493
|
-
.slide-claim { font-family: var(--mv-serif); font-size: 21px; line-height: 1.45; color: var(--mv-soft); margin-bottom: 18px; max-width: 52ch; }
|
|
57175
|
+
.slide-title { font-family: var(--mv-serif); font-size: 50px; font-weight: 500; line-height: 1.1; letter-spacing: -0.6px; color: var(--mv-ink); margin-bottom: 24px; max-width: 19ch; overflow-wrap: anywhere; }
|
|
57176
|
+
.slide-claim { font-family: var(--mv-serif); font-size: 21px; line-height: 1.45; color: var(--mv-soft); margin-bottom: 18px; max-width: 52ch; overflow-wrap: anywhere; }
|
|
56494
57177
|
.slide-pts { list-style: none; counter-reset: pts; }
|
|
56495
57178
|
.slide-pts li { counter-increment: pts; font-size: 16px; line-height: 1.55; color: var(--mv-soft); padding-left: 22px; position: relative; margin-bottom: 10px; }
|
|
56496
57179
|
.slide-pts li::before { content: counter(pts) "."; position: absolute; left: 0; color: var(--mv-brand); font-weight: 500; font-family: var(--mv-mono); font-size: 13px; }
|
|
@@ -56501,7 +57184,7 @@ var SLIDE_CSS = `
|
|
|
56501
57184
|
.slide-chart-full svg, .slide-chart-full .miao-table-wrap { width: 100%; }
|
|
56502
57185
|
|
|
56503
57186
|
/* \u2500\u2500 Metrics \u2500\u2500 */
|
|
56504
|
-
.slide-metrics { display:
|
|
57187
|
+
.slide-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; padding-top: 14px; border-top: 0.5px dotted var(--mv-border); margin-bottom: 20px; }
|
|
56505
57188
|
.slide-metric { flex: 1; display: flex; flex-direction: column; gap: 5px; padding-right: 24px; }
|
|
56506
57189
|
.slide-metric .v { font-family: var(--mv-serif); font-size: 42px; font-weight: 500; color: var(--mv-brand); line-height: 1; font-variant-numeric: tabular-nums; letter-spacing: -0.5px; }
|
|
56507
57190
|
.slide-metric .l { font-family: var(--mv-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 1.5px; color: var(--mv-muted); }
|
|
@@ -56509,8 +57192,8 @@ var SLIDE_CSS = `
|
|
|
56509
57192
|
/* \u2500\u2500 Cover \u2500\u2500 */
|
|
56510
57193
|
.slide-cover { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: center; height: 100%; }
|
|
56511
57194
|
.slide-cover-left { display: flex; flex-direction: column; }
|
|
56512
|
-
.slide-cover h1 { font-family: var(--mv-serif); font-size: 56px; font-weight: 500; line-height: 1.05; letter-spacing: -1px; color: var(--mv-ink); margin-bottom: 14px; }
|
|
56513
|
-
.slide-cover .sub { font-size: 18px; color: var(--mv-soft); line-height: 1.5; margin-bottom: 22px; max-width: 34ch; }
|
|
57195
|
+
.slide-cover h1 { font-family: var(--mv-serif); font-size: 56px; font-weight: 500; line-height: 1.05; letter-spacing: -1px; color: var(--mv-ink); margin-bottom: 14px; max-width: 12ch; overflow-wrap: anywhere; }
|
|
57196
|
+
.slide-cover .sub { font-size: 18px; color: var(--mv-soft); line-height: 1.5; margin-bottom: 22px; max-width: 34ch; overflow-wrap: anywhere; }
|
|
56514
57197
|
.slide-cover .line { width: 48px; height: 2px; background: var(--mv-brand); margin-bottom: 14px; }
|
|
56515
57198
|
.slide-cover .meta { font-family: var(--mv-mono); font-size: 11px; color: var(--mv-muted); letter-spacing: 0.5px; }
|
|
56516
57199
|
.slide-cover-right { display: flex; align-items: center; justify-content: center; }
|
|
@@ -56531,6 +57214,8 @@ var SLIDE_CSS = `
|
|
|
56531
57214
|
|
|
56532
57215
|
/* \u2500\u2500 Table styles inside slide \u2500\u2500 */
|
|
56533
57216
|
.miao-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
57217
|
+
.miao-table-wrap { max-height: 430px; overflow: hidden; }
|
|
57218
|
+
.miao-table caption { caption-side: top; text-align: left; padding: 0 0 10px; color: var(--mv-muted); font-family: var(--mv-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; }
|
|
56534
57219
|
.miao-table th { border-bottom: 1px solid var(--mv-border); padding: 7px 12px; text-align: left; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--mv-muted); font-family: var(--mv-mono); }
|
|
56535
57220
|
.miao-table td { border-bottom: 1px solid var(--mv-border); padding: 8px 12px; color: var(--mv-soft); font-variant-numeric: tabular-nums; }
|
|
56536
57221
|
.miao-table tbody tr:last-child td { border-bottom: none; }
|
|
@@ -56570,8 +57255,8 @@ var SLIDE_JS = `
|
|
|
56570
57255
|
if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); goTo(current + 1); }
|
|
56571
57256
|
else if (e.key === 'ArrowLeft') { e.preventDefault(); goTo(current - 1); }
|
|
56572
57257
|
else if (e.key === 'f' || e.key === 'F') {
|
|
56573
|
-
if (!document.fullscreenElement) document.documentElement.requestFullscreen();
|
|
56574
|
-
else document.exitFullscreen();
|
|
57258
|
+
if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(function() {});
|
|
57259
|
+
else document.exitFullscreen().catch(function() {});
|
|
56575
57260
|
}
|
|
56576
57261
|
});
|
|
56577
57262
|
|
|
@@ -56582,8 +57267,8 @@ var SLIDE_JS = `
|
|
|
56582
57267
|
if (btnPrev) btnPrev.addEventListener('click', function() { goTo(current - 1); });
|
|
56583
57268
|
if (btnNext) btnNext.addEventListener('click', function() { goTo(current + 1); });
|
|
56584
57269
|
if (btnFs) btnFs.addEventListener('click', function() {
|
|
56585
|
-
if (!document.fullscreenElement) document.documentElement.requestFullscreen();
|
|
56586
|
-
else document.exitFullscreen();
|
|
57270
|
+
if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(function() {});
|
|
57271
|
+
else document.exitFullscreen().catch(function() {});
|
|
56587
57272
|
});
|
|
56588
57273
|
if (btnPrint) btnPrint.addEventListener('click', function() {
|
|
56589
57274
|
document.body.classList.remove('present-mode');
|
|
@@ -56648,7 +57333,527 @@ function renderDeckHtml(spec, rows, themeOverride) {
|
|
|
56648
57333
|
</html>`;
|
|
56649
57334
|
}
|
|
56650
57335
|
|
|
57336
|
+
// src/deck-schema.ts
|
|
57337
|
+
var SLIDE_LAYOUTS = [
|
|
57338
|
+
"cover",
|
|
57339
|
+
"title-only",
|
|
57340
|
+
"text-points",
|
|
57341
|
+
"text-chart",
|
|
57342
|
+
"metrics-chart",
|
|
57343
|
+
"chart-full",
|
|
57344
|
+
"table-full",
|
|
57345
|
+
"ending"
|
|
57346
|
+
];
|
|
57347
|
+
var metricTransformSchema = external_exports.object({
|
|
57348
|
+
type: external_exports.enum(["derive-month", "aggregate", "sort", "limit", "filter"]),
|
|
57349
|
+
field: external_exports.string().optional(),
|
|
57350
|
+
as: external_exports.string().optional(),
|
|
57351
|
+
groupBy: external_exports.array(external_exports.string()).optional(),
|
|
57352
|
+
measures: external_exports.array(external_exports.object({
|
|
57353
|
+
field: external_exports.string(),
|
|
57354
|
+
op: external_exports.enum(["sum", "avg", "count", "min", "max"]),
|
|
57355
|
+
as: external_exports.string()
|
|
57356
|
+
})).optional(),
|
|
57357
|
+
order: external_exports.enum(["asc", "desc"]).optional(),
|
|
57358
|
+
value: external_exports.unknown().optional()
|
|
57359
|
+
});
|
|
57360
|
+
var slideMetricSchema = external_exports.object({
|
|
57361
|
+
label: external_exports.string().min(1),
|
|
57362
|
+
value: external_exports.union([external_exports.string(), external_exports.number()]).optional(),
|
|
57363
|
+
format: external_exports.string().optional(),
|
|
57364
|
+
data: external_exports.object({
|
|
57365
|
+
transform: external_exports.array(metricTransformSchema).optional()
|
|
57366
|
+
}).optional()
|
|
57367
|
+
});
|
|
57368
|
+
var slideSpecSchema = external_exports.object({
|
|
57369
|
+
layout: external_exports.enum(SLIDE_LAYOUTS),
|
|
57370
|
+
eyebrow: external_exports.string().optional(),
|
|
57371
|
+
title: external_exports.string().optional(),
|
|
57372
|
+
claim: external_exports.string().optional(),
|
|
57373
|
+
bullets: external_exports.array(external_exports.string()).optional(),
|
|
57374
|
+
callout: external_exports.string().optional(),
|
|
57375
|
+
annotation: external_exports.string().optional(),
|
|
57376
|
+
metrics: external_exports.array(slideMetricSchema).optional(),
|
|
57377
|
+
charts: external_exports.array(chartSpecSchema).optional()
|
|
57378
|
+
});
|
|
57379
|
+
var deckSpecSchema = external_exports.object({
|
|
57380
|
+
title: external_exports.string().optional(),
|
|
57381
|
+
description: external_exports.string().optional(),
|
|
57382
|
+
theme: external_exports.enum(["default", "editorial", "dark", "minimal"]).optional(),
|
|
57383
|
+
slides: external_exports.array(slideSpecSchema).min(1, "Deck must have at least one slide")
|
|
57384
|
+
});
|
|
57385
|
+
|
|
57386
|
+
// src/deck-validator.ts
|
|
57387
|
+
var REQUIRED_ENCODINGS2 = {
|
|
57388
|
+
bar: ["x", "y"],
|
|
57389
|
+
line: ["x", "y"],
|
|
57390
|
+
area: ["x", "y"],
|
|
57391
|
+
pie: ["label", "value"],
|
|
57392
|
+
scatter: ["x", "y"],
|
|
57393
|
+
histogram: ["x"],
|
|
57394
|
+
heatmap: ["x", "y", "value"],
|
|
57395
|
+
table: [],
|
|
57396
|
+
bigvalue: ["value"]
|
|
57397
|
+
};
|
|
57398
|
+
function parseDeckSpec(spec) {
|
|
57399
|
+
const parsed = deckSpecSchema.safeParse(spec);
|
|
57400
|
+
if (!parsed.success) {
|
|
57401
|
+
const errors = formatDeckSpecIssues(parsed.error);
|
|
57402
|
+
return agentError("INVALID_DECK_SPEC", errors[0]?.message ?? "DeckSpec is invalid.", { errors });
|
|
57403
|
+
}
|
|
57404
|
+
const semanticErrors = validateDeckSpecSemantics(parsed.data);
|
|
57405
|
+
if (semanticErrors.length > 0) {
|
|
57406
|
+
return agentError("INVALID_DECK_SPEC", semanticErrors[0].message, { errors: semanticErrors });
|
|
57407
|
+
}
|
|
57408
|
+
return ok(parsed.data);
|
|
57409
|
+
}
|
|
57410
|
+
function validateDeckSpecSemantics(spec) {
|
|
57411
|
+
const errors = [];
|
|
57412
|
+
spec.slides.forEach((slide, index) => {
|
|
57413
|
+
if (["text-chart", "metrics-chart", "chart-full"].includes(slide.layout) && !slide.charts?.length) {
|
|
57414
|
+
const path = `slides[${index}].charts`;
|
|
57415
|
+
errors.push({
|
|
57416
|
+
path,
|
|
57417
|
+
message: `${path}: Layout '${slide.layout}' requires at least one chart.`,
|
|
57418
|
+
hint: hintForIssue(path, "requires at least one chart")
|
|
57419
|
+
});
|
|
57420
|
+
}
|
|
57421
|
+
if (slide.layout === "metrics-chart" && !slide.metrics?.length) {
|
|
57422
|
+
const path = `slides[${index}].metrics`;
|
|
57423
|
+
errors.push({
|
|
57424
|
+
path,
|
|
57425
|
+
message: `${path}: Layout 'metrics-chart' requires at least one metric.`,
|
|
57426
|
+
hint: hintForIssue(path, "requires at least one metric")
|
|
57427
|
+
});
|
|
57428
|
+
}
|
|
57429
|
+
if ((slide.metrics?.length ?? 0) > 4) {
|
|
57430
|
+
const path = `slides[${index}].metrics`;
|
|
57431
|
+
errors.push({
|
|
57432
|
+
path,
|
|
57433
|
+
message: `${path}: A slide can include at most 4 metrics.`,
|
|
57434
|
+
hint: hintForIssue(path, "at most 4 metrics")
|
|
57435
|
+
});
|
|
57436
|
+
}
|
|
57437
|
+
if (slide.layout === "table-full" && slide.charts?.[0] && slide.charts[0].type !== "table") {
|
|
57438
|
+
const path = `slides[${index}].charts[0].type`;
|
|
57439
|
+
errors.push({
|
|
57440
|
+
path,
|
|
57441
|
+
message: `${path}: Layout 'table-full' only accepts a table chart.`,
|
|
57442
|
+
hint: hintForIssue(path, "only accepts a table chart")
|
|
57443
|
+
});
|
|
57444
|
+
}
|
|
57445
|
+
});
|
|
57446
|
+
return errors;
|
|
57447
|
+
}
|
|
57448
|
+
function validateDeckFields(spec, profile) {
|
|
57449
|
+
const sourceFields = new Set(profile.columns.map((column) => column.name));
|
|
57450
|
+
for (let slideIndex = 0; slideIndex < spec.slides.length; slideIndex += 1) {
|
|
57451
|
+
const slide = spec.slides[slideIndex];
|
|
57452
|
+
for (let chartIndex = 0; chartIndex < (slide.charts ?? []).length; chartIndex += 1) {
|
|
57453
|
+
const result = validateChartFields(slide.charts[chartIndex], sourceFields, `slides[${slideIndex}].charts[${chartIndex}]`);
|
|
57454
|
+
if (isAgentError(result)) return result;
|
|
57455
|
+
}
|
|
57456
|
+
for (let metricIndex = 0; metricIndex < (slide.metrics ?? []).length; metricIndex += 1) {
|
|
57457
|
+
const metric = slide.metrics[metricIndex];
|
|
57458
|
+
const result = validateMetricFields(metric, sourceFields, `slides[${slideIndex}].metrics[${metricIndex}]`);
|
|
57459
|
+
if (isAgentError(result)) return result;
|
|
57460
|
+
}
|
|
57461
|
+
}
|
|
57462
|
+
return ok(spec);
|
|
57463
|
+
}
|
|
57464
|
+
function formatDeckSpecIssues(error51) {
|
|
57465
|
+
return error51.issues.map((issue2) => {
|
|
57466
|
+
const path = issue2.path.length ? formatPath(issue2.path) : "deck";
|
|
57467
|
+
return {
|
|
57468
|
+
path,
|
|
57469
|
+
message: `${path}: ${issue2.message}`,
|
|
57470
|
+
hint: hintForIssue(path, issue2.message)
|
|
57471
|
+
};
|
|
57472
|
+
});
|
|
57473
|
+
}
|
|
57474
|
+
function validateChartFields(chart, sourceFields, path) {
|
|
57475
|
+
if (!MVP_CHART_TYPES.includes(chart.type)) {
|
|
57476
|
+
return deckFieldError(path, chart.type, `Chart type '${chart.type}' is not supported.`);
|
|
57477
|
+
}
|
|
57478
|
+
for (const encoding of REQUIRED_ENCODINGS2[chart.type] ?? []) {
|
|
57479
|
+
if (!chart.encoding[encoding]?.field) {
|
|
57480
|
+
return deckFieldError(`${path}.encoding.${encoding}`, encoding, `Chart type '${chart.type}' requires encoding '${encoding}'.`);
|
|
57481
|
+
}
|
|
57482
|
+
}
|
|
57483
|
+
const available = applyTransforms(chart.data?.transform ?? [], sourceFields, path);
|
|
57484
|
+
if (isAgentError(available)) return available;
|
|
57485
|
+
for (const [encoding, spec] of Object.entries(chart.encoding)) {
|
|
57486
|
+
if (spec?.field && !available.value.has(spec.field)) {
|
|
57487
|
+
return deckFieldError(`${path}.encoding.${encoding}.field`, spec.field, `Field '${spec.field}' is not available for this chart encoding.`);
|
|
57488
|
+
}
|
|
57489
|
+
}
|
|
57490
|
+
return ok(chart);
|
|
57491
|
+
}
|
|
57492
|
+
function validateMetricFields(metric, sourceFields, path) {
|
|
57493
|
+
const available = applyTransforms(metric.data?.transform ?? [], sourceFields, path);
|
|
57494
|
+
if (isAgentError(available)) return available;
|
|
57495
|
+
return ok(metric);
|
|
57496
|
+
}
|
|
57497
|
+
function applyTransforms(transforms, sourceFields, path) {
|
|
57498
|
+
let available = new Set(sourceFields);
|
|
57499
|
+
for (let index = 0; index < transforms.length; index += 1) {
|
|
57500
|
+
const transform2 = transforms[index];
|
|
57501
|
+
const transformPath = `${path}.data.transform[${index}]`;
|
|
57502
|
+
if (transform2.field && !available.has(transform2.field)) {
|
|
57503
|
+
return deckFieldError(`${transformPath}.field`, transform2.field, `Field '${transform2.field}' was not found before transform '${transform2.type}'.`);
|
|
57504
|
+
}
|
|
57505
|
+
for (const field of transform2.groupBy ?? []) {
|
|
57506
|
+
if (!available.has(field)) {
|
|
57507
|
+
return deckFieldError(`${transformPath}.groupBy`, field, `Group field '${field}' was not found before transform '${transform2.type}'.`);
|
|
57508
|
+
}
|
|
57509
|
+
}
|
|
57510
|
+
for (const measure of transform2.measures ?? []) {
|
|
57511
|
+
if (!available.has(measure.field)) {
|
|
57512
|
+
return deckFieldError(`${transformPath}.measures.${measure.as}`, measure.field, `Measure field '${measure.field}' was not found before transform '${transform2.type}'.`);
|
|
57513
|
+
}
|
|
57514
|
+
}
|
|
57515
|
+
available = nextAvailableFields(available, transform2);
|
|
57516
|
+
}
|
|
57517
|
+
return ok(available);
|
|
57518
|
+
}
|
|
57519
|
+
function nextAvailableFields(current, transform2) {
|
|
57520
|
+
if (transform2.type === "derive-month" && transform2.as) {
|
|
57521
|
+
return /* @__PURE__ */ new Set([...current, transform2.as]);
|
|
57522
|
+
}
|
|
57523
|
+
if (transform2.type === "aggregate") {
|
|
57524
|
+
return /* @__PURE__ */ new Set([
|
|
57525
|
+
...transform2.groupBy ?? [],
|
|
57526
|
+
...(transform2.measures ?? []).map((measure) => measure.as)
|
|
57527
|
+
]);
|
|
57528
|
+
}
|
|
57529
|
+
return current;
|
|
57530
|
+
}
|
|
57531
|
+
function deckFieldError(path, field, message) {
|
|
57532
|
+
return agentError("DECK_FIELD_NOT_FOUND", message, {
|
|
57533
|
+
path,
|
|
57534
|
+
field,
|
|
57535
|
+
hint: `Check ${path} and use a field from the input data or a field created by an earlier transform.`
|
|
57536
|
+
});
|
|
57537
|
+
}
|
|
57538
|
+
function formatPath(path) {
|
|
57539
|
+
return path.reduce((result, item) => {
|
|
57540
|
+
if (typeof item === "number") return `${result}[${item}]`;
|
|
57541
|
+
const segment = String(item);
|
|
57542
|
+
return result ? `${result}.${segment}` : segment;
|
|
57543
|
+
}, "");
|
|
57544
|
+
}
|
|
57545
|
+
function hintForIssue(path, message) {
|
|
57546
|
+
if (message.includes("requires at least one chart")) return `Add a chart under ${path}.`;
|
|
57547
|
+
if (message.includes("requires at least one metric")) return `Add one to four metrics under ${path}.`;
|
|
57548
|
+
if (message.includes("at most 4 metrics")) return `Reduce ${path} to four metrics or split them across multiple slides.`;
|
|
57549
|
+
if (message.includes("only accepts a table chart")) return `Change ${path} to 'table' or use a chart-focused layout.`;
|
|
57550
|
+
return `Check ${path} in the DeckSpec.`;
|
|
57551
|
+
}
|
|
57552
|
+
|
|
57553
|
+
// src/article-infographic.ts
|
|
57554
|
+
var import_node_fs2 = require("node:fs");
|
|
57555
|
+
var import_node_path2 = require("node:path");
|
|
57556
|
+
var ARTICLE_STYLES = ["editorial", "executive", "minimal"];
|
|
57557
|
+
var ARTICLE_FORMATS = ["html", "json", "markdown"];
|
|
57558
|
+
var DATE_PATTERN = /\b(?:\d{4}(?:[-/]\d{1,2}(?:[-/]\d{1,2})?)?|(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{1,2},?\s+\d{4})\b/i;
|
|
57559
|
+
var NUMBER_PATTERN = /(?:[$¥€]\s?\d[\d,.]*|\b\d+(?:\.\d+)?%?\b)/;
|
|
57560
|
+
function parseArticleStyle(value) {
|
|
57561
|
+
if (!value) return "editorial";
|
|
57562
|
+
return ARTICLE_STYLES.includes(value) ? value : void 0;
|
|
57563
|
+
}
|
|
57564
|
+
function parseArticleFormat(value) {
|
|
57565
|
+
if (!value) return "html";
|
|
57566
|
+
return ARTICLE_FORMATS.includes(value) ? value : void 0;
|
|
57567
|
+
}
|
|
57568
|
+
function generateInfographicFromFile(file2, style) {
|
|
57569
|
+
const extension = (0, import_node_path2.extname)(file2).toLowerCase();
|
|
57570
|
+
if (extension && ![".md", ".markdown", ".txt"].includes(extension)) {
|
|
57571
|
+
return agentError("UNSUPPORTED_ARTICLE_INPUT", "Article input must be a Markdown or plain-text file.", {
|
|
57572
|
+
supportedExtensions: [".md", ".markdown", ".txt"]
|
|
57573
|
+
});
|
|
57574
|
+
}
|
|
57575
|
+
let raw;
|
|
57576
|
+
try {
|
|
57577
|
+
raw = (0, import_node_fs2.readFileSync)(file2, "utf8");
|
|
57578
|
+
} catch (error51) {
|
|
57579
|
+
return agentError("ARTICLE_INPUT_UNREADABLE", error51 instanceof Error ? error51.message : "Article input could not be read.", {
|
|
57580
|
+
file: file2
|
|
57581
|
+
});
|
|
57582
|
+
}
|
|
57583
|
+
const normalized = normalizeArticleText(raw);
|
|
57584
|
+
if (!normalized) {
|
|
57585
|
+
return agentError("EMPTY_ARTICLE_INPUT", "Article input is empty after normalization.", { file: file2 });
|
|
57586
|
+
}
|
|
57587
|
+
const parsed = parseArticle(normalized, file2);
|
|
57588
|
+
const spec = buildInfographicSpec(parsed, style, file2);
|
|
57589
|
+
return ok({ spec, markdown: renderInfographicMarkdown(spec) });
|
|
57590
|
+
}
|
|
57591
|
+
function normalizeArticleText(raw) {
|
|
57592
|
+
return raw.replace(/\r\n/g, "\n").replace(/\t/g, " ").split("\n").map((line) => line.replace(/[ \u00a0]+$/g, "")).join("\n").trim();
|
|
57593
|
+
}
|
|
57594
|
+
function parseArticle(text, file2) {
|
|
57595
|
+
const lines = text.split("\n");
|
|
57596
|
+
const metadata = extractMetadata(lines);
|
|
57597
|
+
const title = findTitle(lines) ?? titleFromFilename(file2);
|
|
57598
|
+
const contentLines = lines.filter((line) => {
|
|
57599
|
+
const trimmed = line.trim();
|
|
57600
|
+
return trimmed !== title && !trimmed.match(/^#\s+/) && !trimmed.match(/^(source|url|author|date):\s*/i);
|
|
57601
|
+
});
|
|
57602
|
+
const quotes = contentLines.filter((line) => line.trim().startsWith(">")).map((line) => cleanMarkdown(line.replace(/^>\s?/, ""))).filter(Boolean);
|
|
57603
|
+
const listItems = contentLines.filter((line) => line.trim().match(/^[-*+]\s+/) || line.trim().match(/^\d+\.\s+/)).map((line) => cleanMarkdown(line.replace(/^\s*(?:[-*+]|\d+\.)\s+/, ""))).filter(Boolean);
|
|
57604
|
+
const tableRows = extractTableRows(contentLines);
|
|
57605
|
+
const paragraphs = contentLines.join("\n").split(/\n{2,}/).map((block) => cleanMarkdown(block.replace(/\n/g, " "))).filter((block) => block.length > 0 && !block.startsWith("|") && !block.match(/^[-*+]\s+/));
|
|
57606
|
+
return {
|
|
57607
|
+
title,
|
|
57608
|
+
subtitle: metadata.subtitle ?? firstUsefulParagraph(paragraphs),
|
|
57609
|
+
source: metadata.source,
|
|
57610
|
+
paragraphs,
|
|
57611
|
+
listItems,
|
|
57612
|
+
quotes,
|
|
57613
|
+
tableRows
|
|
57614
|
+
};
|
|
57615
|
+
}
|
|
57616
|
+
function buildInfographicSpec(parsed, style, file2) {
|
|
57617
|
+
const evidence = [...parsed.listItems, ...sentences(parsed.paragraphs.join(" "))];
|
|
57618
|
+
const facts = collectFacts(evidence);
|
|
57619
|
+
const timeline = collectTimeline(evidence);
|
|
57620
|
+
const comparison = collectComparison(evidence, parsed.tableRows);
|
|
57621
|
+
const takeaways = collectTakeaways(evidence, facts);
|
|
57622
|
+
const summary = parsed.subtitle ?? takeaways[0]?.text ?? facts[0]?.text ?? "A concise visual summary of the source article.";
|
|
57623
|
+
const sections = [
|
|
57624
|
+
{
|
|
57625
|
+
type: "hero",
|
|
57626
|
+
title: parsed.title,
|
|
57627
|
+
emphasis: summary,
|
|
57628
|
+
items: [{ text: summary }]
|
|
57629
|
+
}
|
|
57630
|
+
];
|
|
57631
|
+
if (facts.length > 0) sections.push({ type: "facts", title: "Key Facts", items: facts.slice(0, 6) });
|
|
57632
|
+
if (timeline.length > 1) sections.push({ type: "timeline", title: "Timeline", items: timeline.slice(0, 6) });
|
|
57633
|
+
if (comparison.length > 1) sections.push({ type: "comparison", title: "Comparison", items: comparison.slice(0, 6) });
|
|
57634
|
+
if (parsed.quotes.length > 0) {
|
|
57635
|
+
sections.push({
|
|
57636
|
+
type: "quote",
|
|
57637
|
+
title: "Notable Quote",
|
|
57638
|
+
emphasis: parsed.quotes[0],
|
|
57639
|
+
items: parsed.quotes.slice(0, 3).map((text) => ({ text }))
|
|
57640
|
+
});
|
|
57641
|
+
}
|
|
57642
|
+
if (takeaways.length > 0) sections.push({ type: "takeaways", title: "Takeaways", items: takeaways.slice(0, 5) });
|
|
57643
|
+
return {
|
|
57644
|
+
title: parsed.title,
|
|
57645
|
+
subtitle: parsed.subtitle,
|
|
57646
|
+
source: parsed.source,
|
|
57647
|
+
style,
|
|
57648
|
+
summary,
|
|
57649
|
+
sections,
|
|
57650
|
+
metadata: {
|
|
57651
|
+
inputFile: file2,
|
|
57652
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
57653
|
+
wordCount: parsed.paragraphs.join(" ").split(/\s+/).filter(Boolean).length
|
|
57654
|
+
}
|
|
57655
|
+
};
|
|
57656
|
+
}
|
|
57657
|
+
function renderInfographicMarkdown(spec) {
|
|
57658
|
+
const lines = [`# ${spec.title}`, ""];
|
|
57659
|
+
if (spec.subtitle) lines.push(spec.subtitle, "");
|
|
57660
|
+
if (spec.source) lines.push(`Source: ${spec.source}`, "");
|
|
57661
|
+
lines.push(`Style: ${spec.style}`, "", `## Summary`, "", spec.summary, "");
|
|
57662
|
+
for (const section of spec.sections.filter((section2) => section2.type !== "hero")) {
|
|
57663
|
+
lines.push(`## ${section.title}`, "");
|
|
57664
|
+
for (const item of section.items) {
|
|
57665
|
+
const prefix = item.label ? `**${item.label}:** ` : "";
|
|
57666
|
+
lines.push(`- ${prefix}${item.value ? `${item.value} \u2014 ` : ""}${item.text}`);
|
|
57667
|
+
}
|
|
57668
|
+
lines.push("");
|
|
57669
|
+
}
|
|
57670
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
57671
|
+
}
|
|
57672
|
+
function extractMetadata(lines) {
|
|
57673
|
+
const sourceLine = lines.find((line) => line.match(/^(source|url):\s*/i));
|
|
57674
|
+
const subtitleLine = lines.find((line) => line.match(/^subtitle:\s*/i));
|
|
57675
|
+
return {
|
|
57676
|
+
source: sourceLine?.replace(/^(source|url):\s*/i, "").trim(),
|
|
57677
|
+
subtitle: subtitleLine?.replace(/^subtitle:\s*/i, "").trim()
|
|
57678
|
+
};
|
|
57679
|
+
}
|
|
57680
|
+
function findTitle(lines) {
|
|
57681
|
+
const heading = lines.find((line) => line.trim().match(/^#\s+\S/));
|
|
57682
|
+
if (heading) return cleanMarkdown(heading.replace(/^#\s+/, ""));
|
|
57683
|
+
const first = lines.find((line) => line.trim() && !line.match(/^(source|url|author|date):\s*/i));
|
|
57684
|
+
return first ? cleanMarkdown(first).slice(0, 120) : void 0;
|
|
57685
|
+
}
|
|
57686
|
+
function titleFromFilename(file2) {
|
|
57687
|
+
return (0, import_node_path2.basename)(file2, (0, import_node_path2.extname)(file2)).replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
57688
|
+
}
|
|
57689
|
+
function extractTableRows(lines) {
|
|
57690
|
+
return lines.filter((line) => line.includes("|") && !line.match(/^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/)).map((line) => line.split("|").map((cell) => cleanMarkdown(cell)).filter(Boolean)).filter((row) => row.length > 1);
|
|
57691
|
+
}
|
|
57692
|
+
function collectFacts(candidates) {
|
|
57693
|
+
return uniqueItems(candidates.filter((text) => NUMBER_PATTERN.test(text)).map((text) => ({
|
|
57694
|
+
value: text.match(NUMBER_PATTERN)?.[0],
|
|
57695
|
+
text: compactText(text, 150)
|
|
57696
|
+
})));
|
|
57697
|
+
}
|
|
57698
|
+
function collectTimeline(candidates) {
|
|
57699
|
+
return uniqueItems(candidates.filter((text) => DATE_PATTERN.test(text)).map((text) => ({
|
|
57700
|
+
label: text.match(DATE_PATTERN)?.[0],
|
|
57701
|
+
text: compactText(text, 150)
|
|
57702
|
+
})));
|
|
57703
|
+
}
|
|
57704
|
+
function collectComparison(candidates, tableRows) {
|
|
57705
|
+
const tableItems = tableRows.slice(1).map((row) => ({
|
|
57706
|
+
label: row[0],
|
|
57707
|
+
text: row.slice(1).join(" \u2014 ")
|
|
57708
|
+
}));
|
|
57709
|
+
const textItems = candidates.filter((text) => /\b(vs\.?|versus|compared with|compared to|whereas|while)\b/i.test(text)).map((text) => ({ text: compactText(text, 160) }));
|
|
57710
|
+
return uniqueItems([...tableItems, ...textItems]);
|
|
57711
|
+
}
|
|
57712
|
+
function collectTakeaways(candidates, facts) {
|
|
57713
|
+
const explicit = candidates.filter((text) => /\b(key|takeaway|therefore|recommend|should|must|need to|in summary|conclusion|next)\b/i.test(text)).map((text) => ({ text: compactText(text, 160) }));
|
|
57714
|
+
if (explicit.length > 0) return uniqueItems(explicit);
|
|
57715
|
+
return facts.slice(0, 3).map((item) => ({ text: item.text }));
|
|
57716
|
+
}
|
|
57717
|
+
function sentences(text) {
|
|
57718
|
+
return text.split(/(?<=[.!?。!?])\s+/).map(cleanMarkdown).filter((sentence) => sentence.length > 20);
|
|
57719
|
+
}
|
|
57720
|
+
function firstUsefulParagraph(paragraphs) {
|
|
57721
|
+
return paragraphs.find((paragraph) => paragraph.length > 40)?.slice(0, 220);
|
|
57722
|
+
}
|
|
57723
|
+
function uniqueItems(items) {
|
|
57724
|
+
const seen = /* @__PURE__ */ new Set();
|
|
57725
|
+
return items.filter((item) => {
|
|
57726
|
+
const key = item.text.toLowerCase();
|
|
57727
|
+
if (seen.has(key)) return false;
|
|
57728
|
+
seen.add(key);
|
|
57729
|
+
return true;
|
|
57730
|
+
});
|
|
57731
|
+
}
|
|
57732
|
+
function compactText(text, max) {
|
|
57733
|
+
const clean = cleanMarkdown(text);
|
|
57734
|
+
return clean.length > max ? `${clean.slice(0, max - 1).trim()}...` : clean;
|
|
57735
|
+
}
|
|
57736
|
+
function cleanMarkdown(value) {
|
|
57737
|
+
return value.replace(/`([^`]+)`/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^#+\s*/, "").replace(/\s+/g, " ").trim();
|
|
57738
|
+
}
|
|
57739
|
+
|
|
57740
|
+
// src/article-html.ts
|
|
57741
|
+
function renderInfographicHtml(spec) {
|
|
57742
|
+
return `<!doctype html>
|
|
57743
|
+
<html lang="en">
|
|
57744
|
+
<head>
|
|
57745
|
+
<meta charset="utf-8" />
|
|
57746
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
57747
|
+
<title>${escapeHtml(spec.title)}</title>
|
|
57748
|
+
<meta name="generator" content="Miao Vision article infographic" />
|
|
57749
|
+
${spec.source ? `<meta name="source" content="${escapeHtml(spec.source)}" />` : ""}
|
|
57750
|
+
<style>${buildCss(spec.style)}</style>
|
|
57751
|
+
</head>
|
|
57752
|
+
<body>
|
|
57753
|
+
<main class="mv-infographic mv-infographic-${spec.style}">
|
|
57754
|
+
${spec.sections.map(renderSection).join("\n")}
|
|
57755
|
+
</main>
|
|
57756
|
+
<script type="application/json" id="miao-infographic-spec">${escapeHtml(JSON.stringify(spec, null, 2))}</script>
|
|
57757
|
+
</body>
|
|
57758
|
+
</html>`;
|
|
57759
|
+
}
|
|
57760
|
+
function renderSection(section) {
|
|
57761
|
+
if (section.type === "hero") return renderHero(section);
|
|
57762
|
+
if (section.type === "facts") return renderFacts(section);
|
|
57763
|
+
if (section.type === "timeline") return renderTimeline(section);
|
|
57764
|
+
if (section.type === "comparison") return renderComparison(section);
|
|
57765
|
+
if (section.type === "quote") return renderQuote(section);
|
|
57766
|
+
return renderTakeaways(section);
|
|
57767
|
+
}
|
|
57768
|
+
function renderHero(section) {
|
|
57769
|
+
const lead = section.emphasis ?? section.items[0]?.text ?? "";
|
|
57770
|
+
return `<section class="mv-hero">
|
|
57771
|
+
<p class="mv-eyebrow">Miao Vision Infographic</p>
|
|
57772
|
+
<h1>${escapeHtml(section.title)}</h1>
|
|
57773
|
+
<p class="mv-lead">${escapeHtml(lead)}</p>
|
|
57774
|
+
</section>`;
|
|
57775
|
+
}
|
|
57776
|
+
function renderFacts(section) {
|
|
57777
|
+
return `<section class="mv-section mv-facts">
|
|
57778
|
+
<div class="mv-section-head"><span>01</span><h2>${escapeHtml(section.title)}</h2></div>
|
|
57779
|
+
<div class="mv-fact-grid">
|
|
57780
|
+
${section.items.map((item) => `<article class="mv-fact">
|
|
57781
|
+
${item.value ? `<strong>${escapeHtml(item.value)}</strong>` : ""}
|
|
57782
|
+
<p>${escapeHtml(item.text)}</p>
|
|
57783
|
+
</article>`).join("\n")}
|
|
57784
|
+
</div>
|
|
57785
|
+
</section>`;
|
|
57786
|
+
}
|
|
57787
|
+
function renderTimeline(section) {
|
|
57788
|
+
return `<section class="mv-section mv-timeline">
|
|
57789
|
+
<div class="mv-section-head"><span>02</span><h2>${escapeHtml(section.title)}</h2></div>
|
|
57790
|
+
<ol>
|
|
57791
|
+
${section.items.map((item) => `<li><time>${escapeHtml(item.label ?? "")}</time><p>${escapeHtml(item.text)}</p></li>`).join("\n")}
|
|
57792
|
+
</ol>
|
|
57793
|
+
</section>`;
|
|
57794
|
+
}
|
|
57795
|
+
function renderComparison(section) {
|
|
57796
|
+
return `<section class="mv-section mv-comparison">
|
|
57797
|
+
<div class="mv-section-head"><span>03</span><h2>${escapeHtml(section.title)}</h2></div>
|
|
57798
|
+
<div class="mv-comparison-grid">
|
|
57799
|
+
${section.items.map((item) => `<article>
|
|
57800
|
+
${item.label ? `<h3>${escapeHtml(item.label)}</h3>` : ""}
|
|
57801
|
+
<p>${escapeHtml(item.text)}</p>
|
|
57802
|
+
</article>`).join("\n")}
|
|
57803
|
+
</div>
|
|
57804
|
+
</section>`;
|
|
57805
|
+
}
|
|
57806
|
+
function renderQuote(section) {
|
|
57807
|
+
const quote = section.emphasis ?? section.items[0]?.text ?? "";
|
|
57808
|
+
return `<section class="mv-section mv-quote">
|
|
57809
|
+
<blockquote>${escapeHtml(quote)}</blockquote>
|
|
57810
|
+
</section>`;
|
|
57811
|
+
}
|
|
57812
|
+
function renderTakeaways(section) {
|
|
57813
|
+
return `<section class="mv-section mv-takeaways">
|
|
57814
|
+
<div class="mv-section-head"><span>04</span><h2>${escapeHtml(section.title)}</h2></div>
|
|
57815
|
+
<ul>
|
|
57816
|
+
${section.items.map((item) => `<li>${escapeHtml(item.text)}</li>`).join("\n")}
|
|
57817
|
+
</ul>
|
|
57818
|
+
</section>`;
|
|
57819
|
+
}
|
|
57820
|
+
function buildCss(style) {
|
|
57821
|
+
const palette = style === "minimal" ? { bg: "#ffffff", ink: "#161616", muted: "#666666", card: "#ffffff", accent: "#111111", line: "#d8d8d8" } : style === "executive" ? { bg: "#f4f0e8", ink: "#18212f", muted: "#667085", card: "#ffffff", accent: "#1f5d8c", line: "#d7c9b8" } : { bg: "#f7efe2", ink: "#241b16", muted: "#75695d", card: "#fffaf2", accent: "#b64f2a", line: "#dfcdb7" };
|
|
57822
|
+
return `
|
|
57823
|
+
:root { color-scheme: light; --bg:${palette.bg}; --ink:${palette.ink}; --muted:${palette.muted}; --card:${palette.card}; --accent:${palette.accent}; --line:${palette.line}; }
|
|
57824
|
+
* { box-sizing: border-box; }
|
|
57825
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
57826
|
+
.mv-infographic { width: min(1120px, calc(100% - 32px)); margin: 0 auto; padding: 48px 0 64px; }
|
|
57827
|
+
.mv-hero { min-height: 42vh; display: grid; align-content: center; border-bottom: 1px solid var(--line); padding: 28px 0 44px; }
|
|
57828
|
+
.mv-eyebrow { margin: 0 0 18px; color: var(--accent); font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.14em; }
|
|
57829
|
+
h1 { max-width: 860px; margin: 0; font-size: 58px; line-height: 1.02; letter-spacing: 0; }
|
|
57830
|
+
.mv-lead { max-width: 760px; margin: 22px 0 0; color: var(--muted); font-size: 21px; line-height: 1.55; }
|
|
57831
|
+
.mv-section { padding: 34px 0; border-bottom: 1px solid var(--line); }
|
|
57832
|
+
.mv-section-head { display: flex; align-items: baseline; gap: 14px; margin-bottom: 20px; }
|
|
57833
|
+
.mv-section-head span { color: var(--accent); font-weight: 800; font-size: 12px; }
|
|
57834
|
+
h2 { margin: 0; font-size: 28px; line-height: 1.15; letter-spacing: 0; }
|
|
57835
|
+
.mv-fact-grid, .mv-comparison-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
|
|
57836
|
+
.mv-fact, .mv-comparison article { background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 18px; }
|
|
57837
|
+
.mv-fact strong { display: block; color: var(--accent); font-size: 30px; line-height: 1; margin-bottom: 12px; }
|
|
57838
|
+
.mv-fact p, .mv-comparison p, .mv-timeline p { margin: 0; color: var(--muted); line-height: 1.5; }
|
|
57839
|
+
.mv-comparison h3 { margin: 0 0 8px; font-size: 16px; }
|
|
57840
|
+
.mv-timeline ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
|
|
57841
|
+
.mv-timeline li { display: grid; grid-template-columns: 150px 1fr; gap: 18px; align-items: start; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 16px; }
|
|
57842
|
+
.mv-timeline time { color: var(--accent); font-weight: 800; }
|
|
57843
|
+
.mv-quote blockquote { margin: 0; max-width: 900px; color: var(--ink); font-size: 34px; line-height: 1.25; font-weight: 750; }
|
|
57844
|
+
.mv-takeaways ul { margin: 0; padding-left: 22px; display: grid; gap: 10px; color: var(--muted); line-height: 1.55; }
|
|
57845
|
+
@media (max-width: 720px) {
|
|
57846
|
+
.mv-infographic { width: min(100% - 24px, 1120px); padding-top: 28px; }
|
|
57847
|
+
h1 { font-size: 38px; }
|
|
57848
|
+
.mv-lead { font-size: 18px; }
|
|
57849
|
+
.mv-timeline li { grid-template-columns: 1fr; gap: 6px; }
|
|
57850
|
+
.mv-quote blockquote { font-size: 25px; }
|
|
57851
|
+
}
|
|
57852
|
+
`;
|
|
57853
|
+
}
|
|
57854
|
+
|
|
56651
57855
|
// src/cli.ts
|
|
57856
|
+
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["h", "help", "summary", "reliable-only", "interactive", "no-interactive"]);
|
|
56652
57857
|
async function main() {
|
|
56653
57858
|
const args = parseArgs(process.argv.slice(2));
|
|
56654
57859
|
if (args.command === "--help" || args.command === "-h" || args.command === "help" || !args.command) {
|
|
@@ -56680,8 +57885,16 @@ async function main() {
|
|
|
56680
57885
|
printJson(runDeck(args));
|
|
56681
57886
|
return;
|
|
56682
57887
|
}
|
|
57888
|
+
if (args.command === "article") {
|
|
57889
|
+
printJson(runArticle(args));
|
|
57890
|
+
return;
|
|
57891
|
+
}
|
|
57892
|
+
if (args.command === "query") {
|
|
57893
|
+
printJson(runQuery(args));
|
|
57894
|
+
return;
|
|
57895
|
+
}
|
|
56683
57896
|
printJson(agentError("UNKNOWN_COMMAND", `Unknown command: ${args.command ?? "(none)"}`, {
|
|
56684
|
-
commands: ["profile", "validate", "catalog", "render", "deck"]
|
|
57897
|
+
commands: ["profile", "validate", "catalog", "render", "deck", "article", "query"]
|
|
56685
57898
|
}));
|
|
56686
57899
|
process.exitCode = 1;
|
|
56687
57900
|
} catch (error51) {
|
|
@@ -56691,13 +57904,19 @@ async function main() {
|
|
|
56691
57904
|
}
|
|
56692
57905
|
function runProfile(args) {
|
|
56693
57906
|
const file2 = args.positional[0];
|
|
56694
|
-
if (!file2) return fail(agentError("MISSING_INPUT", "Usage: miao-viz profile <file> [--sheet <name>] [--limit <rows>]"));
|
|
57907
|
+
if (!file2) return fail(agentError("MISSING_INPUT", "Usage: miao-viz profile <file> [--summary] [--columns col1,col2] [--reliable-only] [--sheet <name>] [--limit <rows>]"));
|
|
56695
57908
|
const dataset = loadDataset(file2, {
|
|
56696
57909
|
sheet: stringFlag(args, "sheet"),
|
|
56697
57910
|
limit: numberFlag(args, "limit")
|
|
56698
57911
|
});
|
|
56699
57912
|
if (isAgentError(dataset)) return fail(dataset);
|
|
56700
|
-
|
|
57913
|
+
if (args.flags["summary"] === true) {
|
|
57914
|
+
return { ok: true, value: profileSummary(dataset.value) };
|
|
57915
|
+
}
|
|
57916
|
+
const columnsFlag = stringFlag(args, "columns");
|
|
57917
|
+
const columns = columnsFlag ? columnsFlag.split(",").map((c) => c.trim()).filter(Boolean) : void 0;
|
|
57918
|
+
const reliableOnly = args.flags["reliable-only"] === true;
|
|
57919
|
+
return { ok: true, value: profileDataset(dataset.value, { columns, reliableOnly }) };
|
|
56701
57920
|
}
|
|
56702
57921
|
function runValidate(args) {
|
|
56703
57922
|
const specPath = requiredFlag(args, "spec");
|
|
@@ -56733,11 +57952,12 @@ function runRender(args) {
|
|
|
56733
57952
|
const validation = validateReportSpec(normalized, profile, formats);
|
|
56734
57953
|
if (isAgentError(validation)) return fail(validation);
|
|
56735
57954
|
const themeFlag = stringFlag(args, "theme");
|
|
57955
|
+
const interactive = args.flags["interactive"] === true ? true : args.flags["no-interactive"] === true ? false : void 0;
|
|
56736
57956
|
const written = [];
|
|
56737
57957
|
for (const format of formats) {
|
|
56738
57958
|
if (format === "html") {
|
|
56739
57959
|
const htmlPath = formatOutputPath(output, "html", formats.length > 1);
|
|
56740
|
-
writeOutput(htmlPath, renderStaticHtml(validation.value, profile, dataset.value.rows, themeFlag));
|
|
57960
|
+
writeOutput(htmlPath, renderStaticHtml(validation.value, profile, dataset.value.rows, themeFlag, { enabled: interactive }));
|
|
56741
57961
|
written.push(htmlPath);
|
|
56742
57962
|
} else if (format === "svg") {
|
|
56743
57963
|
const svgPath = formatOutputPath(output, "svg", formats.length > 1);
|
|
@@ -56767,14 +57987,71 @@ function runDeck(args) {
|
|
|
56767
57987
|
});
|
|
56768
57988
|
if (isAgentError(dataset)) return fail(dataset);
|
|
56769
57989
|
const raw = readSpec(specPath);
|
|
56770
|
-
const parsed =
|
|
56771
|
-
if (
|
|
56772
|
-
|
|
56773
|
-
|
|
57990
|
+
const parsed = parseDeckSpec(raw);
|
|
57991
|
+
if (isAgentError(parsed)) return fail(parsed);
|
|
57992
|
+
const profile = profileDataset(dataset.value);
|
|
57993
|
+
const validation = validateDeckFields(parsed.value, profile);
|
|
57994
|
+
if (isAgentError(validation)) return fail(validation);
|
|
56774
57995
|
const themeFlag = stringFlag(args, "theme");
|
|
56775
|
-
const html = renderDeckHtml(
|
|
57996
|
+
const html = renderDeckHtml(validation.value, dataset.value.rows, themeFlag);
|
|
56776
57997
|
writeOutput(output, html);
|
|
56777
|
-
return { ok: true, value: { output, slides:
|
|
57998
|
+
return { ok: true, value: { output, slides: validation.value.slides.length } };
|
|
57999
|
+
}
|
|
58000
|
+
function runQuery(args) {
|
|
58001
|
+
const file2 = args.positional[0];
|
|
58002
|
+
if (!file2) return fail(agentError("MISSING_INPUT", 'Usage: miao-viz query <file> [--groupby cols] [--measure "fn(col) as alias"] [--filter col=val] [--orderby "col desc"] [--limit n]'));
|
|
58003
|
+
const dataset = loadDataset(file2, { sheet: stringFlag(args, "sheet") });
|
|
58004
|
+
if (isAgentError(dataset)) return fail(dataset);
|
|
58005
|
+
const result = queryDataset(dataset.value.rows, {
|
|
58006
|
+
groupby: stringFlag(args, "groupby"),
|
|
58007
|
+
measure: stringFlag(args, "measure"),
|
|
58008
|
+
filter: stringFlag(args, "filter"),
|
|
58009
|
+
orderby: stringFlag(args, "orderby"),
|
|
58010
|
+
limit: numberFlag(args, "limit")
|
|
58011
|
+
});
|
|
58012
|
+
if (isAgentError(result)) return fail(result);
|
|
58013
|
+
return { ok: true, value: result };
|
|
58014
|
+
}
|
|
58015
|
+
function runArticle(args) {
|
|
58016
|
+
const file2 = args.positional[0];
|
|
58017
|
+
if (!file2) {
|
|
58018
|
+
return fail(agentError("MISSING_INPUT", "Usage: miao-viz article <file> --output <file> [--style editorial|executive|minimal] [--format html|json|markdown]"));
|
|
58019
|
+
}
|
|
58020
|
+
const output = requiredFlag(args, "output");
|
|
58021
|
+
if (isAgentError(output)) return fail(output);
|
|
58022
|
+
const styleFlag = stringFlag(args, "style");
|
|
58023
|
+
const style = parseArticleStyle(styleFlag);
|
|
58024
|
+
if (!style) {
|
|
58025
|
+
return fail(agentError("UNSUPPORTED_ARTICLE_STYLE", `Unsupported article style: ${styleFlag}`, {
|
|
58026
|
+
supportedStyles: ["editorial", "executive", "minimal"]
|
|
58027
|
+
}));
|
|
58028
|
+
}
|
|
58029
|
+
const formatFlag = stringFlag(args, "format");
|
|
58030
|
+
const format = parseArticleFormat(formatFlag);
|
|
58031
|
+
if (!format) {
|
|
58032
|
+
return fail(agentError("UNSUPPORTED_ARTICLE_FORMAT", `Unsupported article output format: ${formatFlag}`, {
|
|
58033
|
+
supportedFormats: ["html", "json", "markdown"]
|
|
58034
|
+
}));
|
|
58035
|
+
}
|
|
58036
|
+
const generated = generateInfographicFromFile(file2, style);
|
|
58037
|
+
if (isAgentError(generated)) return fail(generated);
|
|
58038
|
+
if (format === "json") {
|
|
58039
|
+
writeOutput(output, `${JSON.stringify(generated.value.spec, null, 2)}
|
|
58040
|
+
`);
|
|
58041
|
+
} else if (format === "markdown") {
|
|
58042
|
+
writeOutput(output, generated.value.markdown);
|
|
58043
|
+
} else {
|
|
58044
|
+
writeOutput(output, renderInfographicHtml(generated.value.spec));
|
|
58045
|
+
}
|
|
58046
|
+
return {
|
|
58047
|
+
ok: true,
|
|
58048
|
+
value: {
|
|
58049
|
+
output,
|
|
58050
|
+
format,
|
|
58051
|
+
style,
|
|
58052
|
+
sections: generated.value.spec.sections.map((section) => section.type)
|
|
58053
|
+
}
|
|
58054
|
+
};
|
|
56778
58055
|
}
|
|
56779
58056
|
function normalizeSpec(spec) {
|
|
56780
58057
|
const parsed = singleOrReportSpecSchema.safeParse(spec);
|
|
@@ -56791,12 +58068,12 @@ function parseFormats(value) {
|
|
|
56791
58068
|
}
|
|
56792
58069
|
}
|
|
56793
58070
|
function readSpec(file2) {
|
|
56794
|
-
const text = (0,
|
|
58071
|
+
const text = (0, import_node_fs3.readFileSync)(file2, "utf8");
|
|
56795
58072
|
if (file2.endsWith(".json")) return JSON.parse(text);
|
|
56796
58073
|
return YAML.parse(text);
|
|
56797
58074
|
}
|
|
56798
58075
|
function readJson(file2) {
|
|
56799
|
-
return JSON.parse((0,
|
|
58076
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(file2, "utf8"));
|
|
56800
58077
|
}
|
|
56801
58078
|
function readProfile(file2) {
|
|
56802
58079
|
const parsed = readJson(file2);
|
|
@@ -56813,6 +58090,10 @@ function parseArgs(argv) {
|
|
|
56813
58090
|
const value = rest[i];
|
|
56814
58091
|
if (value.startsWith("--")) {
|
|
56815
58092
|
const key = value.slice(2);
|
|
58093
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
58094
|
+
flags[key] = true;
|
|
58095
|
+
continue;
|
|
58096
|
+
}
|
|
56816
58097
|
const next = rest[i + 1];
|
|
56817
58098
|
if (!next || next.startsWith("--")) {
|
|
56818
58099
|
flags[key] = true;
|
|
@@ -56847,8 +58128,8 @@ function formatOutputPath(output, ext, multiple) {
|
|
|
56847
58128
|
return output;
|
|
56848
58129
|
}
|
|
56849
58130
|
function writeOutput(file2, content) {
|
|
56850
|
-
(0,
|
|
56851
|
-
(0,
|
|
58131
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(file2), { recursive: true });
|
|
58132
|
+
(0, import_node_fs3.writeFileSync)(file2, content, "utf8");
|
|
56852
58133
|
}
|
|
56853
58134
|
function fail(error51) {
|
|
56854
58135
|
process.exitCode = 1;
|
|
@@ -56864,11 +58145,20 @@ var COMMAND_HELP = {
|
|
|
56864
58145
|
Profile a data file and output column statistics.
|
|
56865
58146
|
|
|
56866
58147
|
Arguments:
|
|
56867
|
-
file
|
|
58148
|
+
file Path to CSV, Excel (.xlsx/.xls), or JSON file
|
|
56868
58149
|
|
|
56869
58150
|
Options:
|
|
56870
|
-
--
|
|
56871
|
-
--
|
|
58151
|
+
--summary Return only file, row count, and column names+types (~200 tokens)
|
|
58152
|
+
--columns col1,col2 Deep-profile only the specified columns (comma-separated)
|
|
58153
|
+
--reliable-only Suppress statistics where sample size is too small to be reliable
|
|
58154
|
+
--sheet <name> Sheet name (Excel only)
|
|
58155
|
+
--limit <n> Max rows to read
|
|
58156
|
+
|
|
58157
|
+
Reliability thresholds:
|
|
58158
|
+
skewness rows >= 30
|
|
58159
|
+
correlation n >= 10 (paired non-null values)
|
|
58160
|
+
outlierCount rows >= 20
|
|
58161
|
+
histogram rows >= 20
|
|
56872
58162
|
`,
|
|
56873
58163
|
validate: `Usage: miao-viz validate --spec <file> --profile <file>
|
|
56874
58164
|
|
|
@@ -56892,6 +58182,8 @@ Options:
|
|
|
56892
58182
|
--output <file> Output file path
|
|
56893
58183
|
--format <fmt> Output format: html, svg (default: html)
|
|
56894
58184
|
--theme <name> Theme: default, editorial, dark, minimal
|
|
58185
|
+
--interactive Force lightweight interactive runtime for HTML output
|
|
58186
|
+
--no-interactive Force static HTML output even when interaction spec exists
|
|
56895
58187
|
--sheet <name> Sheet name (Excel only)
|
|
56896
58188
|
--limit <n> Max rows to read
|
|
56897
58189
|
`,
|
|
@@ -56906,6 +58198,34 @@ Options:
|
|
|
56906
58198
|
--theme <name> Theme: default, editorial, dark, minimal
|
|
56907
58199
|
--sheet <name> Sheet name (Excel only)
|
|
56908
58200
|
--limit <n> Max rows to read
|
|
58201
|
+
`,
|
|
58202
|
+
article: `Usage: miao-viz article <file> --output <file> [options]
|
|
58203
|
+
|
|
58204
|
+
Convert a local Markdown or plain-text article into a static infographic artifact.
|
|
58205
|
+
URL fetching is intentionally handled by the agent/skill layer; this command only reads local files.
|
|
58206
|
+
|
|
58207
|
+
Arguments:
|
|
58208
|
+
file Path to a .md, .markdown, or .txt article file
|
|
58209
|
+
|
|
58210
|
+
Options:
|
|
58211
|
+
--output <file> Output file path
|
|
58212
|
+
--format <fmt> Output format: html, json, markdown (default: html)
|
|
58213
|
+
--style <name> Style: editorial, executive, minimal (default: editorial)
|
|
58214
|
+
`,
|
|
58215
|
+
query: `Usage: miao-viz query <file> [options]
|
|
58216
|
+
|
|
58217
|
+
Run an aggregation query against a data file and return JSON results.
|
|
58218
|
+
Use this to get real computed values before writing chart insights.
|
|
58219
|
+
|
|
58220
|
+
Supported aggregate functions: sum, count, avg, min, max
|
|
58221
|
+
|
|
58222
|
+
Options:
|
|
58223
|
+
--groupby <cols> Comma-separated column names to group by
|
|
58224
|
+
--measure <exprs> Aggregate expressions, e.g. "sum(sales) as total, count(*) as cnt"
|
|
58225
|
+
--filter <col=val> Simple equality filter (one condition only)
|
|
58226
|
+
--orderby <col dir> Sort column and direction, e.g. "total_sales desc"
|
|
58227
|
+
--limit <n> Max rows to return
|
|
58228
|
+
--sheet <name> Sheet name (Excel only)
|
|
56909
58229
|
`
|
|
56910
58230
|
};
|
|
56911
58231
|
function printHelp(command) {
|
|
@@ -56920,10 +58240,12 @@ Usage:
|
|
|
56920
58240
|
|
|
56921
58241
|
Commands:
|
|
56922
58242
|
profile Profile a data file (CSV, Excel, JSON)
|
|
58243
|
+
query Run an aggregation query to get real computed values
|
|
56923
58244
|
validate Validate a vizspec against a data profile
|
|
56924
58245
|
catalog List all available chart types
|
|
56925
58246
|
render Render a vizspec to HTML or SVG
|
|
56926
58247
|
deck Render a deck spec to HTML slides
|
|
58248
|
+
article Convert a local article to an infographic artifact
|
|
56927
58249
|
|
|
56928
58250
|
Run "miao-viz <command> --help" for command-specific options.
|
|
56929
58251
|
`);
|