@pixerate/editor 0.171.0 → 0.174.0

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.
Files changed (63) hide show
  1. package/README.md +4 -0
  2. package/dist/agent/index.cjs +28 -28
  3. package/dist/agent/index.js +3 -3
  4. package/dist/animation-editor/index.cjs +9 -9
  5. package/dist/animation-editor/index.js +1 -1
  6. package/dist/canvas/index.cjs +10 -10
  7. package/dist/canvas/index.js +2 -2
  8. package/dist/chart/index.cjs +156 -68
  9. package/dist/chart/index.d.cts +1 -0
  10. package/dist/chart/index.d.ts +1 -0
  11. package/dist/chart/index.js +91 -3
  12. package/dist/chart/layout-shared.d.cts +2 -1
  13. package/dist/chart/layout-shared.d.ts +2 -1
  14. package/dist/chart/layout.d.cts +5 -0
  15. package/dist/chart/layout.d.ts +5 -0
  16. package/dist/chart/tooltip.d.cts +53 -0
  17. package/dist/chart/tooltip.d.ts +53 -0
  18. package/dist/chart/types.d.cts +14 -0
  19. package/dist/chart/types.d.ts +14 -0
  20. package/dist/{chunk-CYKWYP6D.cjs → chunk-6DUEV36F.cjs} +14 -14
  21. package/dist/{chunk-7KWJEYIK.js → chunk-6G2NLMVK.js} +36 -4
  22. package/dist/{chunk-RUZQMYXH.cjs → chunk-A3A5TI6R.cjs} +43 -11
  23. package/dist/{chunk-SBAPXFPB.cjs → chunk-FHNGNL3V.cjs} +44 -44
  24. package/dist/{chunk-2F4GAZL5.cjs → chunk-GXEC4FS6.cjs} +16 -16
  25. package/dist/{chunk-T62BRXMS.js → chunk-IMHRN4ZZ.js} +1 -1
  26. package/dist/{chunk-N3ACT6VJ.js → chunk-KPWG3PSW.js} +1 -1
  27. package/dist/chunk-NZ3CTY7K.js +260 -0
  28. package/dist/{chunk-M52UCHVZ.js → chunk-OJQVKJYD.js} +12 -1
  29. package/dist/{chunk-YN4AYYBJ.js → chunk-OZSA5YEJ.js} +1 -1
  30. package/dist/{chunk-ZUPP4NKM.js → chunk-R2VHVQVY.js} +2 -2
  31. package/dist/{chunk-GD34ICEX.cjs → chunk-RLOIEDA3.cjs} +10 -10
  32. package/dist/{chunk-WOIXNDHV.cjs → chunk-TUXKLM5W.cjs} +13 -2
  33. package/dist/chunk-VCL7CB56.cjs +260 -0
  34. package/dist/{chunk-HCC2LBCW.js → chunk-WMVEYSOA.js} +1 -1
  35. package/dist/{chunk-3OSXHLIT.cjs → chunk-Z64NRDEH.cjs} +7 -7
  36. package/dist/commands/format.d.cts +43 -0
  37. package/dist/commands/format.d.ts +43 -0
  38. package/dist/commands/index.cjs +7 -1
  39. package/dist/commands/index.d.cts +1 -0
  40. package/dist/commands/index.d.ts +1 -0
  41. package/dist/commands/index.js +6 -0
  42. package/dist/effects/helpers.d.cts +11 -2
  43. package/dist/effects/helpers.d.ts +11 -2
  44. package/dist/effects/index.cjs +40 -34
  45. package/dist/effects/index.js +8 -2
  46. package/dist/effects/types.d.cts +2 -0
  47. package/dist/effects/types.d.ts +2 -0
  48. package/dist/extensions/index.cjs +5 -5
  49. package/dist/extensions/index.js +4 -4
  50. package/dist/image-editor/index.cjs +13 -13
  51. package/dist/image-editor/index.js +1 -1
  52. package/dist/index.cjs +16 -10
  53. package/dist/index.d.cts +1 -0
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.js +10 -4
  56. package/dist/paint-editor/index.cjs +7 -7
  57. package/dist/paint-editor/index.js +2 -2
  58. package/dist/vector/index.cjs +6 -6
  59. package/dist/vector/index.js +1 -1
  60. package/dist/video-editor/index.cjs +309 -309
  61. package/dist/video-editor/index.js +3 -3
  62. package/package.json +114 -11
  63. package/skills/pixerate-chart/SKILL.md +1 -0
@@ -22,6 +22,7 @@ export * from './recommend.cjs';
22
22
  export * from './controller.cjs';
23
23
  export * from './commands.cjs';
24
24
  export * from './interaction.cjs';
25
+ export * from './tooltip.cjs';
25
26
  export * from './mermaid.cjs';
26
27
  export * from './ooxml.cjs';
27
28
  export * from './tools.cjs';
@@ -22,6 +22,7 @@ export * from './recommend.js';
22
22
  export * from './controller.js';
23
23
  export * from './commands.js';
24
24
  export * from './interaction.js';
25
+ export * from './tooltip.js';
25
26
  export * from './mermaid.js';
26
27
  export * from './ooxml.js';
27
28
  export * from './tools.js';
@@ -47,7 +47,7 @@ import {
47
47
  monochromeChartPalette,
48
48
  sparklineToSvg,
49
49
  summarizeChart
50
- } from "../chunk-N3ACT6VJ.js";
50
+ } from "../chunk-KPWG3PSW.js";
51
51
  import "../chunk-KGRZMHZB.js";
52
52
  import {
53
53
  CHART_DATE_STEPS,
@@ -55,6 +55,7 @@ import {
55
55
  MAX_CHART_LINE_POINTS,
56
56
  MAX_CHART_MARKERS,
57
57
  chartBoxStats,
58
+ chartCategoryAtPoint,
58
59
  chartDateLabels,
59
60
  chartDateToSerial,
60
61
  chartHistogram,
@@ -98,7 +99,7 @@ import {
98
99
  suggestChartReferences,
99
100
  summarizeChartSeries,
100
101
  trendNumberText
101
- } from "../chunk-M52UCHVZ.js";
102
+ } from "../chunk-OJQVKJYD.js";
102
103
  import {
103
104
  CHART_AXIS_KINDS,
104
105
  CHART_AXIS_SCALES,
@@ -1336,6 +1337,71 @@ function chartHitTest(scene, point, measure = (t, s, w = 400) => t.length * s *
1336
1337
  return point.x >= 0 && point.y >= 0 && point.x <= scene.width && point.y <= scene.height ? { kind: "chart" } : null;
1337
1338
  }
1338
1339
 
1340
+ // src/chart/tooltip.ts
1341
+ var VALUE_FAMILIES = /* @__PURE__ */ new Set(["cartesian", "xy", "pie", "radar", "waterfall", "funnel", "parliament", "slope"]);
1342
+ var SLICE_FAMILIES = /* @__PURE__ */ new Set(["pie", "funnel", "parliament"]);
1343
+ function valueFormat(spec, series) {
1344
+ return series.format ?? (series.axis === "secondary" ? spec.yAxis2.format : spec.yAxis.format);
1345
+ }
1346
+ var percentText = (share) => formatChartNumber(share, { style: "percent", decimals: share < 0.1 ? 1 : 0 });
1347
+ function item(spec, seriesIndex, index, share) {
1348
+ const family = chartFamily(spec);
1349
+ const series = spec.series[seriesIndex];
1350
+ const value = series.values[index] ?? null;
1351
+ const slice = SLICE_FAMILIES.has(family);
1352
+ let text = formatChartNumber(value, valueFormat(spec, series));
1353
+ if (family === "xy") {
1354
+ const x = series.x?.[index];
1355
+ if (x !== null && x !== void 0) text = `${formatChartNumber(x, spec.xAxis.format)}, ${text}`;
1356
+ }
1357
+ if (share !== void 0) text = `${text} (${percentText(share)})`;
1358
+ return {
1359
+ seriesId: series.id,
1360
+ name: slice ? spec.categories[index] ?? series.name : series.name,
1361
+ color: slice ? series.pointColors?.[index] ?? chartSliceColor(spec, index) : chartPointColor(spec, seriesIndex, index),
1362
+ value,
1363
+ ...share !== void 0 ? { share } : {},
1364
+ text
1365
+ };
1366
+ }
1367
+ function chartTooltipFor(spec, index, seriesId, anchor = { x: 0, y: 0 }) {
1368
+ const family = chartFamily(spec);
1369
+ if (!VALUE_FAMILIES.has(family)) return null;
1370
+ if (!Number.isInteger(index) || index < 0 || index >= Math.max(spec.categories.length, ...spec.series.map((s) => s.values.length))) return null;
1371
+ const visible = spec.series.map((s, i) => ({ s, i })).filter(({ s }) => !s.hidden && (!seriesId || s.id === seriesId));
1372
+ const present = visible.filter(({ s }) => s.values[index] !== null && s.values[index] !== void 0 && Number.isFinite(s.values[index]));
1373
+ if (!present.length) return null;
1374
+ const shareOf = (s) => {
1375
+ const v = s.values[index];
1376
+ if (spec.stacking === "percent" && family === "cartesian") {
1377
+ const total = spec.series.filter((t) => !t.hidden && t.axis === s.axis).reduce((sum, t) => sum + Math.abs(t.values[index] ?? 0), 0);
1378
+ return total ? Math.abs(v) / total : void 0;
1379
+ }
1380
+ if (family === "pie") {
1381
+ const total = s.values.reduce((sum, t) => sum + (t !== null && t > 0 ? t : 0), 0);
1382
+ return total && v > 0 ? v / total : void 0;
1383
+ }
1384
+ return void 0;
1385
+ };
1386
+ const items = present.map(({ s, i }) => item(spec, i, index, shareOf(s)));
1387
+ const title = family === "xy" ? spec.categories[index] || present[0].s.name : spec.categories[index] ?? "";
1388
+ return { title, index, items, x: anchor.x, y: anchor.y };
1389
+ }
1390
+ function chartTooltipAt(spec, scene, point, options = {}) {
1391
+ if (options.mode === "category" && scene.categoryScale) {
1392
+ const index = chartCategoryAtPoint(scene, point);
1393
+ if (index !== void 0) return chartTooltipFor(spec, index, void 0, point);
1394
+ }
1395
+ const target = chartHitTest(scene, point, options.measureText ?? estimateChartTextWidth);
1396
+ if (target?.kind === "point") return chartTooltipFor(spec, target.index, target.seriesId, point);
1397
+ if (target?.kind === "category" && scene.categoryScale && options.mode === "category") return chartTooltipFor(spec, target.index, void 0, point);
1398
+ return null;
1399
+ }
1400
+ function sameChartTooltip(a, b) {
1401
+ if (!a || !b) return a === b;
1402
+ return a.index === b.index && a.items.length === b.items.length && a.items.every((it, i) => it.seriesId === b.items[i].seriesId && it.text === b.items[i].text);
1403
+ }
1404
+
1339
1405
  // src/chart/presence.ts
1340
1406
  var ChartPresence = class extends PresenceStore {
1341
1407
  };
@@ -1767,6 +1833,24 @@ function createChartToolProvider(controller, options = {}) {
1767
1833
  return { trendline: line, coefficients: fit.coefficients, r2: fit.r2, equation: fit.equation, points: fit.points };
1768
1834
  }
1769
1835
  };
1836
+ const getTooltip = {
1837
+ name: "get_chart_tooltip",
1838
+ title: "Read a chart tooltip",
1839
+ description: "Returns what a viewer sees when hovering a category (or one series in it): the category label and, for each visible series with a value there, its name, colour, value, share (percent-stacked and pie charts, 0\u20131) and the value as the chart writes it. Returns null for charts without one value per category (histogram, box, treemap, sunburst, sankey, \u2026) or when no visible series has a value there.",
1840
+ inputSchema: {
1841
+ type: "object",
1842
+ properties: { category: CATEGORY_REF, series: { ...SERIES_REF, description: `${SERIES_REF.description} Omit to include every visible series in the category.` } },
1843
+ required: ["category"],
1844
+ additionalProperties: false
1845
+ },
1846
+ annotations: { title: "Read a chart tooltip", ...READ },
1847
+ handler: ({ category, series }) => {
1848
+ const index = categoryIndex(category);
1849
+ const seriesId = series === void 0 ? void 0 : seriesById(series).id;
1850
+ const tooltip = chartTooltipFor(controller.toJSON(), index, seriesId);
1851
+ return tooltip && { title: tooltip.title, index: tooltip.index, items: tooltip.items };
1852
+ }
1853
+ };
1770
1854
  const raceRanking = {
1771
1855
  name: "get_chart_race_ranking",
1772
1856
  title: "Read a bar race ranking",
@@ -2476,7 +2560,7 @@ function createChartToolProvider(controller, options = {}) {
2476
2560
  return { redone, ...redone ? { label } : {} };
2477
2561
  }
2478
2562
  };
2479
- const reads = [getChart, renderChart, checkColors, recommend, getTrendline, raceRanking, mapRegions, listPresets, exportMermaid, exportOoxml];
2563
+ const reads = [getChart, renderChart, checkColors, recommend, getTrendline, getTooltip, raceRanking, mapRegions, listPresets, exportMermaid, exportOoxml];
2480
2564
  const writes = [
2481
2565
  updateChart,
2482
2566
  setData,
@@ -3246,6 +3330,7 @@ export {
3246
3330
  blendChartScenes,
3247
3331
  canDragChartPoint,
3248
3332
  chartBoxStats,
3333
+ chartCategoryAtPoint,
3249
3334
  chartClientToScene,
3250
3335
  chartColorDistance,
3251
3336
  chartColorInputValue,
@@ -3305,6 +3390,8 @@ export {
3305
3390
  chartToMermaid,
3306
3391
  chartToOoxml,
3307
3392
  chartToSvg,
3393
+ chartTooltipAt,
3394
+ chartTooltipFor,
3308
3395
  chartTreeColumnLabels,
3309
3396
  chartTreeDepth,
3310
3397
  chartTreeFromRows,
@@ -3391,6 +3478,7 @@ export {
3391
3478
  removeChartTreeRow,
3392
3479
  resolveChartSvgPaints,
3393
3480
  sameChartSelection,
3481
+ sameChartTooltip,
3394
3482
  setChartLinkField,
3395
3483
  setChartTreeRowName,
3396
3484
  setChartTreeRowValue,
@@ -1,5 +1,5 @@
1
1
  import { type NiceScale } from './scale.cjs';
2
- import type { ChartAxis, ChartMark, ChartMarker, ChartRect, ChartScene, ChartSelection, ChartSpec, ChartTheme, ChartValueScale, ChartWarning } from './types.cjs';
2
+ import type { ChartAxis, ChartCategoryScale, ChartMark, ChartMarker, ChartRect, ChartScene, ChartSelection, ChartSpec, ChartTheme, ChartValueScale, ChartWarning } from './types.cjs';
3
3
  export type ChartTextMeasure = (text: string, size: number, weight?: number) => number;
4
4
  export interface ChartLayoutOptions {
5
5
  width: number;
@@ -73,6 +73,7 @@ export interface FamilyResult {
73
73
  valueScale?: ChartValueScale;
74
74
  valueScale2?: ChartValueScale;
75
75
  xScale?: ChartValueScale;
76
+ categoryScale?: ChartCategoryScale;
76
77
  percent?: boolean;
77
78
  }
78
79
  export type ChartSceneBase = Omit<ChartScene, 'marks' | 'warnings' | 'theme' | 'width' | 'height'>;
@@ -1,5 +1,5 @@
1
1
  import { type NiceScale } from './scale.js';
2
- import type { ChartAxis, ChartMark, ChartMarker, ChartRect, ChartScene, ChartSelection, ChartSpec, ChartTheme, ChartValueScale, ChartWarning } from './types.js';
2
+ import type { ChartAxis, ChartCategoryScale, ChartMark, ChartMarker, ChartRect, ChartScene, ChartSelection, ChartSpec, ChartTheme, ChartValueScale, ChartWarning } from './types.js';
3
3
  export type ChartTextMeasure = (text: string, size: number, weight?: number) => number;
4
4
  export interface ChartLayoutOptions {
5
5
  width: number;
@@ -73,6 +73,7 @@ export interface FamilyResult {
73
73
  valueScale?: ChartValueScale;
74
74
  valueScale2?: ChartValueScale;
75
75
  xScale?: ChartValueScale;
76
+ categoryScale?: ChartCategoryScale;
76
77
  percent?: boolean;
77
78
  }
78
79
  export type ChartSceneBase = Omit<ChartScene, 'marks' | 'warnings' | 'theme' | 'width' | 'height'>;
@@ -18,6 +18,11 @@ export declare function chartValueAtPoint(scene: ChartScene, point: {
18
18
  x: number;
19
19
  y: number;
20
20
  }, secondary?: boolean): number | undefined;
21
+ /** The category index under a scene point inside the plot of a bar, line or area chart, or `undefined`. */
22
+ export declare function chartCategoryAtPoint(scene: ChartScene, point: {
23
+ x: number;
24
+ y: number;
25
+ }): number | undefined;
21
26
  /** The x value at a scene point on a scatter or bubble chart, or `undefined` without a numeric x axis. */
22
27
  export declare function chartXAtPoint(scene: ChartScene, point: {
23
28
  x: number;
@@ -18,6 +18,11 @@ export declare function chartValueAtPoint(scene: ChartScene, point: {
18
18
  x: number;
19
19
  y: number;
20
20
  }, secondary?: boolean): number | undefined;
21
+ /** The category index under a scene point inside the plot of a bar, line or area chart, or `undefined`. */
22
+ export declare function chartCategoryAtPoint(scene: ChartScene, point: {
23
+ x: number;
24
+ y: number;
25
+ }): number | undefined;
21
26
  /** The x value at a scene point on a scatter or bubble chart, or `undefined` without a numeric x axis. */
22
27
  export declare function chartXAtPoint(scene: ChartScene, point: {
23
28
  x: number;
@@ -0,0 +1,53 @@
1
+ import { type ChartTextMeasure } from './layout-shared.cjs';
2
+ import type { ChartScene, ChartSpec, ChartValue } from './types.cjs';
3
+ /** `point`: the bar, point or slice under the pointer. `category`: every series in the category under the pointer (bar, line and area charts; other charts fall back to `point`). */
4
+ export type ChartTooltipMode = 'point' | 'category';
5
+ export interface ChartTooltipItem {
6
+ seriesId: string;
7
+ /** The series name (the category label on pie, funnel and parliament charts). */
8
+ name: string;
9
+ color: string;
10
+ value: ChartValue;
11
+ /** Share of the category (percent-stacked charts) or of the series (pie and donut charts), 0–1. */
12
+ share?: number;
13
+ /** The value as the chart writes it: the series format, else its value axis format. */
14
+ text: string;
15
+ }
16
+ export interface ChartTooltip {
17
+ /** The category label (the series name on scatter and bubble charts with x values). */
18
+ title: string;
19
+ /** The category the items belong to. */
20
+ index: number;
21
+ items: ChartTooltipItem[];
22
+ /** Where to anchor the tooltip, in scene pixels (the pointer). */
23
+ x: number;
24
+ y: number;
25
+ }
26
+ export interface ChartTooltipOptions {
27
+ /** Default `point`. */
28
+ mode?: ChartTooltipMode;
29
+ /** Text width for hit-testing labels (default: an estimate). */
30
+ measureText?: ChartTextMeasure;
31
+ }
32
+ /**
33
+ * What a tooltip shows for one category: the category label and each visible series' value
34
+ * there, written as the chart writes it. `seriesId` narrows it to one series (one bar, point
35
+ * or slice). Returns `null` for charts whose marks are not one value per category (histogram,
36
+ * box, treemap and sunburst with a tree, sankey, …), for an index out of range, or when no
37
+ * visible series has a value there.
38
+ */
39
+ export declare function chartTooltipFor(spec: ChartSpec, index: number, seriesId?: string, anchor?: {
40
+ x: number;
41
+ y: number;
42
+ }): ChartTooltip | null;
43
+ /**
44
+ * The tooltip at a scene point (for hover), or `null` when the pointer is over nothing with a
45
+ * value. In `category` mode, anywhere in a category's band of a bar, line or area chart shows
46
+ * every series there; elsewhere both modes show the one bar, point or slice under the pointer.
47
+ */
48
+ export declare function chartTooltipAt(spec: ChartSpec, scene: ChartScene, point: {
49
+ x: number;
50
+ y: number;
51
+ }, options?: ChartTooltipOptions): ChartTooltip | null;
52
+ /** Whether two tooltips show the same thing (so a host can skip re-rendering while the pointer moves within a mark). */
53
+ export declare function sameChartTooltip(a: ChartTooltip | null, b: ChartTooltip | null): boolean;
@@ -0,0 +1,53 @@
1
+ import { type ChartTextMeasure } from './layout-shared.js';
2
+ import type { ChartScene, ChartSpec, ChartValue } from './types.js';
3
+ /** `point`: the bar, point or slice under the pointer. `category`: every series in the category under the pointer (bar, line and area charts; other charts fall back to `point`). */
4
+ export type ChartTooltipMode = 'point' | 'category';
5
+ export interface ChartTooltipItem {
6
+ seriesId: string;
7
+ /** The series name (the category label on pie, funnel and parliament charts). */
8
+ name: string;
9
+ color: string;
10
+ value: ChartValue;
11
+ /** Share of the category (percent-stacked charts) or of the series (pie and donut charts), 0–1. */
12
+ share?: number;
13
+ /** The value as the chart writes it: the series format, else its value axis format. */
14
+ text: string;
15
+ }
16
+ export interface ChartTooltip {
17
+ /** The category label (the series name on scatter and bubble charts with x values). */
18
+ title: string;
19
+ /** The category the items belong to. */
20
+ index: number;
21
+ items: ChartTooltipItem[];
22
+ /** Where to anchor the tooltip, in scene pixels (the pointer). */
23
+ x: number;
24
+ y: number;
25
+ }
26
+ export interface ChartTooltipOptions {
27
+ /** Default `point`. */
28
+ mode?: ChartTooltipMode;
29
+ /** Text width for hit-testing labels (default: an estimate). */
30
+ measureText?: ChartTextMeasure;
31
+ }
32
+ /**
33
+ * What a tooltip shows for one category: the category label and each visible series' value
34
+ * there, written as the chart writes it. `seriesId` narrows it to one series (one bar, point
35
+ * or slice). Returns `null` for charts whose marks are not one value per category (histogram,
36
+ * box, treemap and sunburst with a tree, sankey, …), for an index out of range, or when no
37
+ * visible series has a value there.
38
+ */
39
+ export declare function chartTooltipFor(spec: ChartSpec, index: number, seriesId?: string, anchor?: {
40
+ x: number;
41
+ y: number;
42
+ }): ChartTooltip | null;
43
+ /**
44
+ * The tooltip at a scene point (for hover), or `null` when the pointer is over nothing with a
45
+ * value. In `category` mode, anywhere in a category's band of a bar, line or area chart shows
46
+ * every series there; elsewhere both modes show the one bar, point or slice under the pointer.
47
+ */
48
+ export declare function chartTooltipAt(spec: ChartSpec, scene: ChartScene, point: {
49
+ x: number;
50
+ y: number;
51
+ }, options?: ChartTooltipOptions): ChartTooltip | null;
52
+ /** Whether two tooltips show the same thing (so a host can skip re-rendering while the pointer moves within a mark). */
53
+ export declare function sameChartTooltip(a: ChartTooltip | null, b: ChartTooltip | null): boolean;
@@ -440,6 +440,18 @@ export interface ChartValueScale {
440
440
  /** Set when the axis is logarithmic: values map by their logarithm in this base. */
441
441
  logBase?: number;
442
442
  }
443
+ /**
444
+ * The category bands of a bar, line or area chart: `count` equal bands of the category axis,
445
+ * from `start` for `length` pixels (the last category first when `reverse` is set).
446
+ */
447
+ export interface ChartCategoryScale {
448
+ /** `x` for vertical charts, `y` for horizontal bars. */
449
+ axis: 'x' | 'y';
450
+ start: number;
451
+ length: number;
452
+ count: number;
453
+ reverse: boolean;
454
+ }
443
455
  /** Something the layout had to leave out or shorten, as a sentence (for agents and screen readers). */
444
456
  export interface ChartWarning {
445
457
  code: 'labels-hidden' | 'labels-truncated' | 'legend-truncated' | 'points-decimated' | 'values-skipped' | 'data-labels-hidden' | 'type-fallback' | 'annotations-skipped' | 'highlight-unmatched';
@@ -459,6 +471,8 @@ export interface ChartScene {
459
471
  valueScale2?: ChartValueScale;
460
472
  /** The numeric x scale of a scatter or bubble chart with x values. */
461
473
  xScale?: ChartValueScale;
474
+ /** The category bands of a bar, line or area chart. */
475
+ categoryScale?: ChartCategoryScale;
462
476
  /** Percent-stacked: values are shown as shares, so dragging is disabled. */
463
477
  percent: boolean;
464
478
  theme: ChartTheme;
@@ -440,6 +440,18 @@ export interface ChartValueScale {
440
440
  /** Set when the axis is logarithmic: values map by their logarithm in this base. */
441
441
  logBase?: number;
442
442
  }
443
+ /**
444
+ * The category bands of a bar, line or area chart: `count` equal bands of the category axis,
445
+ * from `start` for `length` pixels (the last category first when `reverse` is set).
446
+ */
447
+ export interface ChartCategoryScale {
448
+ /** `x` for vertical charts, `y` for horizontal bars. */
449
+ axis: 'x' | 'y';
450
+ start: number;
451
+ length: number;
452
+ count: number;
453
+ reverse: boolean;
454
+ }
443
455
  /** Something the layout had to leave out or shorten, as a sentence (for agents and screen readers). */
444
456
  export interface ChartWarning {
445
457
  code: 'labels-hidden' | 'labels-truncated' | 'legend-truncated' | 'points-decimated' | 'values-skipped' | 'data-labels-hidden' | 'type-fallback' | 'annotations-skipped' | 'highlight-unmatched';
@@ -459,6 +471,8 @@ export interface ChartScene {
459
471
  valueScale2?: ChartValueScale;
460
472
  /** The numeric x scale of a scatter or bubble chart with x values. */
461
473
  xScale?: ChartValueScale;
474
+ /** The category bands of a bar, line or area chart. */
475
+ categoryScale?: ChartCategoryScale;
462
476
  /** Percent-stacked: values are shown as shares, so dragging is disabled. */
463
477
  percent: boolean;
464
478
  theme: ChartTheme;
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
 
6
- var _chunkRUZQMYXHcjs = require('./chunk-RUZQMYXH.cjs');
6
+ var _chunkA3A5TI6Rcjs = require('./chunk-A3A5TI6R.cjs');
7
7
 
8
8
 
9
9
  var _chunkFJ2MSI43cjs = require('./chunk-FJ2MSI43.cjs');
@@ -26,7 +26,7 @@ function documentErrors(doc, description) {
26
26
  const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
27
27
  if (doc.values !== void 0) {
28
28
  if (!isRecord(doc.values)) errors.push({ path: "/values", message: "must be an object of control values" });
29
- else errors.push(..._chunkRUZQMYXHcjs.effectValueErrors.call(void 0, description, doc.values).map((e) => ({ ...e, path: `/values${e.path}` })));
29
+ else errors.push(..._chunkA3A5TI6Rcjs.effectValueErrors.call(void 0, description, doc.values).map((e) => ({ ...e, path: `/values${e.path}` })));
30
30
  }
31
31
  if (doc.tracks !== void 0) {
32
32
  if (!isRecord(doc.tracks)) errors.push({ path: "/tracks", message: "must be an object of keyframe lists" });
@@ -39,7 +39,7 @@ function documentErrors(doc, description) {
39
39
  kfs.forEach((kf, i) => {
40
40
  if (!isRecord(kf)) return errors.push({ path: `${at}/${i}`, message: "must be a keyframe object" });
41
41
  if (typeof kf.time !== "number" || !Number.isFinite(kf.time) || kf.time < 0) errors.push({ path: `${at}/${i}/time`, message: "must be a finite number of ms >= 0" });
42
- errors.push(..._chunkRUZQMYXHcjs.effectValueErrors.call(void 0, description, { [control]: kf.value }).map((e) => ({ ...e, path: `${at}/${i}/value` })));
42
+ errors.push(..._chunkA3A5TI6Rcjs.effectValueErrors.call(void 0, description, { [control]: kf.value }).map((e) => ({ ...e, path: `${at}/${i}/value` })));
43
43
  if (kf.easing !== void 0 && !_chunkDWJJI6RTcjs.isAnimationEasing.call(void 0, kf.easing)) errors.push({ path: `${at}/${i}/easing`, message: "is not a valid easing" });
44
44
  });
45
45
  }
@@ -49,7 +49,7 @@ function documentErrors(doc, description) {
49
49
  else
50
50
  doc.presets.forEach((p, i) => {
51
51
  if (!isRecord(p) || typeof p.name !== "string" || !p.name) return errors.push({ path: `/presets/${i}`, message: "must have a name" });
52
- errors.push(..._chunkRUZQMYXHcjs.effectValueErrors.call(void 0, description, p.values).map((e) => ({ ...e, path: `/presets/${i}/values${e.path}` })));
52
+ errors.push(..._chunkA3A5TI6Rcjs.effectValueErrors.call(void 0, description, p.values).map((e) => ({ ...e, path: `/presets/${i}/values${e.path}` })));
53
53
  });
54
54
  }
55
55
  if (doc.duration !== void 0 && (typeof doc.duration !== "number" || !Number.isFinite(doc.duration) || !(doc.duration > 0))) {
@@ -170,7 +170,7 @@ async function createEffectAuthoringSession(source, options) {
170
170
  if (isDocumentSource(source)) {
171
171
  const doc = source.document;
172
172
  if (doc.version !== 1 || !doc.effectId) {
173
- throw new (0, _chunkRUZQMYXHcjs.EffectUnsupportedError)("Invalid effect authoring document: version 1 required");
173
+ throw new (0, _chunkA3A5TI6Rcjs.EffectUnsupportedError)("Invalid effect authoring document: version 1 required");
174
174
  }
175
175
  effectId = doc.effectId;
176
176
  initialDoc = doc;
@@ -178,15 +178,15 @@ async function createEffectAuthoringSession(source, options) {
178
178
  effectId = source.effectId;
179
179
  requestedPreset = source.preset;
180
180
  } else {
181
- throw new (0, _chunkRUZQMYXHcjs.EffectUnsupportedError)("Invalid effect authoring source");
181
+ throw new (0, _chunkA3A5TI6Rcjs.EffectUnsupportedError)("Invalid effect authoring source");
182
182
  }
183
183
  const description = await provider.describe(effectId);
184
184
  if (initialDoc) {
185
185
  const problems = documentErrors(initialDoc, description);
186
- if (problems.length) throw new (0, _chunkRUZQMYXHcjs.EffectControlError)(effectId, problems);
186
+ if (problems.length) throw new (0, _chunkA3A5TI6Rcjs.EffectControlError)(effectId, problems);
187
187
  }
188
188
  if (description.kind === "motion") {
189
- throw new (0, _chunkRUZQMYXHcjs.EffectUnsupportedError)("Motion effects cannot be authored as pixel effects", effectId);
189
+ throw new (0, _chunkA3A5TI6Rcjs.EffectUnsupportedError)("Motion effects cannot be authored as pixel effects", effectId);
190
190
  }
191
191
  let disposed = false;
192
192
  let playing = false;
@@ -207,7 +207,7 @@ async function createEffectAuthoringSession(source, options) {
207
207
  const found = description.presets.find((p) => p.name === requestedPreset);
208
208
  if (!found) {
209
209
  const validNames = description.presets.map((p) => `"${p.name}"`).join(", ");
210
- throw new (0, _chunkRUZQMYXHcjs.EffectUnsupportedError)(
210
+ throw new (0, _chunkA3A5TI6Rcjs.EffectUnsupportedError)(
211
211
  `Unknown preset "${requestedPreset}" for effect "${effectId}". Valid presets are: ${validNames || "none"}`,
212
212
  effectId
213
213
  );
@@ -321,7 +321,7 @@ async function createEffectAuthoringSession(source, options) {
321
321
  if (!validNames.includes(name)) {
322
322
  return { ok: false, message: `there is no control "${name}"; the controls are ${validNames.join(", ")}` };
323
323
  }
324
- const errors = _chunkRUZQMYXHcjs.effectValueErrors.call(void 0, description, { [name]: value });
324
+ const errors = _chunkA3A5TI6Rcjs.effectValueErrors.call(void 0, description, { [name]: value });
325
325
  if (errors.length > 0) {
326
326
  return { ok: false, message: valueErrorMessage(errors) };
327
327
  }
@@ -391,7 +391,7 @@ async function createEffectAuthoringSession(source, options) {
391
391
  if (!validNames.includes(k)) {
392
392
  return { ok: false, message: `there is no control "${k}"; the controls are ${validNames.join(", ")}` };
393
393
  }
394
- const errors = _chunkRUZQMYXHcjs.effectValueErrors.call(void 0, description, { [k]: v });
394
+ const errors = _chunkA3A5TI6Rcjs.effectValueErrors.call(void 0, description, { [k]: v });
395
395
  if (errors.length > 0) {
396
396
  return { ok: false, message: valueErrorMessage(errors) };
397
397
  }
@@ -528,7 +528,7 @@ async function createEffectAuthoringSession(source, options) {
528
528
  return { ok: false, message: `invalid easing: ${String(easing)}` };
529
529
  }
530
530
  const targetValue = value !== void 0 ? value : valuesAtTime(time, baseValues, tracks)[name];
531
- const errors = _chunkRUZQMYXHcjs.effectValueErrors.call(void 0, description, { [name]: targetValue });
531
+ const errors = _chunkA3A5TI6Rcjs.effectValueErrors.call(void 0, description, { [name]: targetValue });
532
532
  if (errors.length > 0) {
533
533
  return { ok: false, message: valueErrorMessage(errors) };
534
534
  }
@@ -705,7 +705,7 @@ async function createEffectAuthoringSession(source, options) {
705
705
  },
706
706
  async createPreview(opts) {
707
707
  if (disposed) {
708
- throw new (0, _chunkRUZQMYXHcjs.EffectDisposedError)(effectId);
708
+ throw new (0, _chunkA3A5TI6Rcjs.EffectDisposedError)(effectId);
709
709
  }
710
710
  let renderer;
711
711
  try {
@@ -730,7 +730,7 @@ async function createEffectAuthoringSession(source, options) {
730
730
  height: renderer.height,
731
731
  depth: renderer.depth,
732
732
  render: async (req) => {
733
- if (disposed) throw new (0, _chunkRUZQMYXHcjs.EffectDisposedError)(effectId);
733
+ if (disposed) throw new (0, _chunkA3A5TI6Rcjs.EffectDisposedError)(effectId);
734
734
  const effectiveValues = _nullishCoalesce(req.values, () => ( valuesAtTime(req.time, baseValues, tracks)));
735
735
  const pixels = await renderer.render({
736
736
  ...req,
@@ -68,7 +68,20 @@ function resolveEffectValues(description, values) {
68
68
  if (errors.length) throw new EffectControlError(description.id, errors);
69
69
  return { ...description.defaults, ...values ?? {} };
70
70
  }
71
- var matchesFilters = (e, o) => (!o.kind || e.kind === o.kind) && (!o.category || e.category === o.category) && (!o.use || e.uses.includes(o.use));
71
+ function normaliseEffectTag(tag) {
72
+ return tag.trim().toLowerCase().replace(/[\s_-]+/g, "-");
73
+ }
74
+ function hasEffectTags(e, tags) {
75
+ if (!tags?.length) return true;
76
+ const own = new Set((e.tags ?? []).map(normaliseEffectTag));
77
+ return tags.every((t) => own.has(normaliseEffectTag(t)));
78
+ }
79
+ function countEffectTags(effects) {
80
+ const counts = /* @__PURE__ */ new Map();
81
+ for (const e of effects) for (const tag of new Set((e.tags ?? []).map(normaliseEffectTag))) if (tag) counts.set(tag, (counts.get(tag) ?? 0) + 1);
82
+ return [...counts].map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
83
+ }
84
+ var matchesFilters = (e, o) => (!o.kind || e.kind === o.kind) && (!o.category || e.category === o.category) && (!o.use || e.uses.includes(o.use)) && hasEffectTags(e, o.tags);
72
85
  function searchEffectSummaries(catalog, query = "", options = {}) {
73
86
  const words = query.toLowerCase().split(/\s+/).filter(Boolean);
74
87
  const scored = [];
@@ -174,6 +187,12 @@ var object = (properties, required = []) => ({
174
187
  additionalProperties: false
175
188
  });
176
189
  var ENUM_LIMIT = 60;
190
+ var TAG_LIMIT = 40;
191
+ function nearestTags(tag, known) {
192
+ const t = normaliseEffectTag(tag);
193
+ const stem = t.slice(0, 4);
194
+ return known.filter((k) => k.tag.includes(t) || t.includes(k.tag) || stem.length === 4 && k.tag.startsWith(stem)).slice(0, 5).map((k) => k.tag);
195
+ }
177
196
  var summaryOut = (e) => ({
178
197
  id: e.id,
179
198
  name: e.name,
@@ -181,6 +200,7 @@ var summaryOut = (e) => ({
181
200
  category: e.category,
182
201
  uses: e.uses,
183
202
  ...e.description ? { description: e.description } : {},
203
+ ...e.tags?.length ? { tags: e.tags } : {},
184
204
  transparent: e.transparent,
185
205
  loop: e.loop,
186
206
  ...e.duration !== void 0 ? { duration: e.duration } : {}
@@ -241,21 +261,30 @@ function createEffectToolProvider(host) {
241
261
  {
242
262
  name: "search_effects",
243
263
  title: "Search effects",
244
- description: `Searches the effects provider's catalog. Every word of query must appear in an effect's id, name, category, tags or description; filter by kind (shader, vector, motion), category or use (${EFFECT_USES.join(", ")}). For this editor, apply_effect takes uses ${host.applyUses.join(" or ")}. Returns { provider, effects: [{ id, name, kind, category, uses, description, transparent, loop, duration (ms) }], total }. With no provider registered it returns available: false and the reason. Call describe_effect next for an effect's controls.`,
264
+ description: `Searches the effects provider's catalog. Every word of query must appear in an effect's id, name, category, tags or description; filter by kind (shader, vector, motion), category, use (${EFFECT_USES.join(", ")}) or tags (effects carrying every one). For this editor, apply_effect takes uses ${host.applyUses.join(" or ")}. Returns { provider, effects: [{ id, name, kind, category, uses, description, tags, transparent, loop, duration (ms) }], total, tags: [{ tag, count }] }: tags lists the tags of every match, the most used first, so a broad search (or none) shows the tags to narrow by. A tag the catalog does not have comes back in unknownTags with the nearest ones. With no provider registered it returns available: false and the reason. Call describe_effect next for an effect's controls.`,
245
265
  inputSchema: object({
246
266
  query: { type: "string", description: "Words to match (default: every effect)." },
247
267
  kind: { type: "string", enum: [...EFFECT_KINDS], description: "shader (pixels), vector (animated SVG) or motion (animates vector art)." },
248
268
  category: { type: "string", minLength: 1, description: `The provider's category, e.g. "gradient", "light", "transition".` },
249
269
  use: { type: "string", enum: [...EFFECT_USES], description: "What the effect is for: filter changes a picture, transition blends two, generator draws a background, overlay draws over content, title draws a title, motion animates vector art, interactive reacts to input." },
270
+ tags: { type: "array", items: { type: "string", minLength: 1 }, description: "Only effects carrying every one of these tags, as search_effects lists them (any case; spaces count as hyphens)." },
250
271
  limit: { type: "integer", minimum: 1, maximum: 200, description: "Most effects to return (default 20)." }
251
272
  }),
252
273
  annotations: { title: "Search effects", ...ANNOTATIONS.read },
253
274
  handler: async (args) => {
254
275
  const p = host.provider();
255
276
  if (!p) return { available: false, reason: NO_PROVIDER, effects: [], total: 0 };
256
- const found = await explain(async () => p.search(args.query ?? "", { kind: args.kind, category: args.category, use: args.use }));
277
+ const tags = args.tags;
278
+ const found = (await explain(async () => p.search(args.query ?? "", { kind: args.kind, category: args.category, use: args.use, tags }))).filter((e) => hasEffectTags(e, tags));
257
279
  const limit = args.limit ?? 20;
258
- return { available: true, provider: { id: p.id, name: p.name, capabilities: p.capabilities }, effects: found.slice(0, limit).map(summaryOut), total: found.length };
280
+ const out = { available: true, provider: { id: p.id, name: p.name, capabilities: p.capabilities }, effects: found.slice(0, limit).map(summaryOut), total: found.length, tags: countEffectTags(found).slice(0, TAG_LIMIT) };
281
+ if (tags?.length && !found.length) {
282
+ const known = countEffectTags(await explain(async () => p.list()));
283
+ const have = new Set(known.map((k) => k.tag));
284
+ const unknown = tags.filter((t) => !have.has(normaliseEffectTag(t)));
285
+ if (unknown.length) out.unknownTags = unknown.map((tag) => ({ tag, nearest: nearestTags(tag, known) }));
286
+ }
287
+ return out;
259
288
  }
260
289
  },
261
290
  {
@@ -374,6 +403,9 @@ export {
374
403
  isEffectError,
375
404
  effectValueErrors,
376
405
  resolveEffectValues,
406
+ normaliseEffectTag,
407
+ hasEffectTags,
408
+ countEffectTags,
377
409
  searchEffectSummaries,
378
410
  createEffectPixels,
379
411
  isEffectPixels,