@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/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 readFileSync3(filename, opts) {
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 = readFileSync3;
40513
- XLSX3.readFileSync = readFileSync3;
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 import_node_fs2 = require("node:fs");
40545
- var import_node_path2 = require("node:path");
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 profileDataset(dataset) {
40689
- const columns = dataset.columns.map((col) => profileColumn(col, dataset.rows));
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 correlations = numericNames.length >= 2 ? computeCorrelations(numericNames, dataset.rows) : void 0;
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
- return {
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
- skewness,
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 === "table") return renderTable(chart, data);
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(String(row[xField] ?? ""))}</text>
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 renderTable(chart, rows) {
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 ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[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 svg = renderChartSvg(chart, rows, theme.svg);
41471
- sections.push(renderEditorialCard(chart, i, svg));
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 svg = renderChartSvg(chart, rows, theme.svg);
41479
- return renderDefaultCard(chart, index, svg);
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
- ${svg}
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
- ${svg}
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: flex; gap: 0; padding-top: 14px; border-top: 0.5px dotted var(--mv-border); margin-bottom: 20px; }
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
- return { ok: true, value: profileDataset(dataset.value) };
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 = deckSpecSchema.safeParse(raw);
56771
- if (!parsed.success) {
56772
- return fail(agentError("INVALID_DECK_SPEC", parsed.error.issues.map((i) => i.message).join("; ")));
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(parsed.data, dataset.value.rows, themeFlag);
57996
+ const html = renderDeckHtml(validation.value, dataset.value.rows, themeFlag);
56776
57997
  writeOutput(output, html);
56777
- return { ok: true, value: { output, slides: parsed.data.slides.length } };
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, import_node_fs2.readFileSync)(file2, "utf8");
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, import_node_fs2.readFileSync)(file2, "utf8"));
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, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(file2), { recursive: true });
56851
- (0, import_node_fs2.writeFileSync)(file2, content, "utf8");
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 Path to CSV, Excel (.xlsx/.xls), or JSON file
58148
+ file Path to CSV, Excel (.xlsx/.xls), or JSON file
56868
58149
 
56869
58150
  Options:
56870
- --sheet <name> Sheet name (Excel only)
56871
- --limit <n> Max rows to read
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
  `);