pmx-canvas 0.1.10 → 0.1.11

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.
@@ -34,7 +34,11 @@ import {
34
34
  import {
35
35
  CHART_COLORS,
36
36
  CartesianChart,
37
+ axisTickMargin,
37
38
  axisStyle,
39
+ chartMargin,
40
+ legendMargin,
41
+ polarChartMargin,
38
42
  tooltipStyle,
39
43
  type CartesianChartProps,
40
44
  } from './components';
@@ -47,7 +51,7 @@ function ChartAreaChart({ props }: BaseComponentProps<AreaChartProps>) {
47
51
  return (
48
52
  <CartesianChart props={props} className="pmx-chart--area">
49
53
  {(data) => (
50
- <RechartsAreaChart data={data}>
54
+ <RechartsAreaChart data={data} margin={chartMargin}>
51
55
  <defs>
52
56
  <linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
53
57
  <stop offset="0%" stopColor={stroke} stopOpacity={0.45} />
@@ -55,8 +59,8 @@ function ChartAreaChart({ props }: BaseComponentProps<AreaChartProps>) {
55
59
  </linearGradient>
56
60
  </defs>
57
61
  <CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
58
- <XAxis dataKey={props.xKey} tick={axisStyle} />
59
- <YAxis tick={axisStyle} />
62
+ <XAxis dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} />
63
+ <YAxis tick={axisStyle} tickMargin={axisTickMargin} />
60
64
  <Tooltip contentStyle={tooltipStyle} />
61
65
  <Area
62
66
  type="monotone"
@@ -91,10 +95,10 @@ function ChartScatterChart({ props }: BaseComponentProps<ScatterChartProps>) {
91
95
  <div className="pmx-chart pmx-chart--scatter">
92
96
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
93
97
  <ResponsiveContainer width="100%" height={h}>
94
- <RechartsScatterChart>
98
+ <RechartsScatterChart margin={chartMargin}>
95
99
  <CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
96
- <XAxis type="number" dataKey={props.xKey} tick={axisStyle} name={props.xKey} />
97
- <YAxis type="number" dataKey={props.yKey} tick={axisStyle} name={props.yKey} />
100
+ <XAxis type="number" dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} name={props.xKey} />
101
+ <YAxis type="number" dataKey={props.yKey} tick={axisStyle} tickMargin={axisTickMargin} name={props.yKey} />
98
102
  {props.zKey && <ZAxis type="number" dataKey={props.zKey} range={[40, 400]} name={props.zKey} />}
99
103
  <Tooltip contentStyle={tooltipStyle} cursor={{ strokeDasharray: '3 3' }} />
100
104
  <Scatter data={data} fill={fill} />
@@ -110,6 +114,7 @@ interface RadarChartProps {
110
114
  axisKey: string;
111
115
  metrics: string[];
112
116
  height?: number | null;
117
+ showLegend?: boolean | null;
113
118
  }
114
119
 
115
120
  function ChartRadarChart({ props }: BaseComponentProps<RadarChartProps>) {
@@ -121,12 +126,12 @@ function ChartRadarChart({ props }: BaseComponentProps<RadarChartProps>) {
121
126
  <div className="pmx-chart pmx-chart--radar">
122
127
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
123
128
  <ResponsiveContainer width="100%" height={h}>
124
- <RechartsRadarChart data={data} outerRadius="75%">
129
+ <RechartsRadarChart data={data} outerRadius="66%" margin={polarChartMargin}>
125
130
  <PolarGrid stroke="var(--border, #e5e5e5)" />
126
131
  <PolarAngleAxis dataKey={props.axisKey} tick={axisStyle} />
127
132
  <PolarRadiusAxis tick={axisStyle} />
128
133
  <Tooltip contentStyle={tooltipStyle} />
129
- <Legend />
134
+ {props.showLegend !== false && <Legend wrapperStyle={legendMargin} />}
130
135
  {metrics.map((metric, i) => {
131
136
  const color = CHART_COLORS[i % CHART_COLORS.length];
132
137
  return (
@@ -153,6 +158,7 @@ interface StackedBarChartProps {
153
158
  series: string[];
154
159
  aggregate?: 'sum' | 'count' | 'avg' | null;
155
160
  height?: number | null;
161
+ showLegend?: boolean | null;
156
162
  }
157
163
 
158
164
  function ChartStackedBarChart({ props }: BaseComponentProps<StackedBarChartProps>) {
@@ -166,12 +172,12 @@ function ChartStackedBarChart({ props }: BaseComponentProps<StackedBarChartProps
166
172
  <div className="pmx-chart pmx-chart--stacked-bar">
167
173
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
168
174
  <ResponsiveContainer width="100%" height={h}>
169
- <RechartsBarChart data={chartData}>
175
+ <RechartsBarChart data={chartData} margin={chartMargin}>
170
176
  <CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
171
- <XAxis dataKey={props.xKey} tick={axisStyle} />
172
- <YAxis tick={axisStyle} />
177
+ <XAxis dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} />
178
+ <YAxis tick={axisStyle} tickMargin={axisTickMargin} />
173
179
  <Tooltip contentStyle={tooltipStyle} cursor={false} />
174
- <Legend />
180
+ {props.showLegend !== false && <Legend wrapperStyle={legendMargin} />}
175
181
  {series.map((key, i) => (
176
182
  <Bar
177
183
  key={key}
@@ -225,6 +231,7 @@ interface ComposedChartProps {
225
231
  barColor?: string | null;
226
232
  lineColor?: string | null;
227
233
  height?: number | null;
234
+ showLegend?: boolean | null;
228
235
  }
229
236
 
230
237
  function ChartComposedChart({ props }: BaseComponentProps<ComposedChartProps>) {
@@ -237,12 +244,12 @@ function ChartComposedChart({ props }: BaseComponentProps<ComposedChartProps>) {
237
244
  <div className="pmx-chart pmx-chart--composed">
238
245
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
239
246
  <ResponsiveContainer width="100%" height={h}>
240
- <RechartsComposedChart data={data}>
247
+ <RechartsComposedChart data={data} margin={chartMargin}>
241
248
  <CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
242
- <XAxis dataKey={props.xKey} tick={axisStyle} />
243
- <YAxis tick={axisStyle} />
249
+ <XAxis dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} />
250
+ <YAxis tick={axisStyle} tickMargin={axisTickMargin} />
244
251
  <Tooltip contentStyle={tooltipStyle} cursor={false} />
245
- <Legend />
252
+ {props.showLegend !== false && <Legend wrapperStyle={legendMargin} />}
246
253
  <Bar dataKey={props.barKey} fill={barFill} radius={[4, 4, 0, 0]} />
247
254
  <Line
248
255
  type="monotone"
@@ -73,6 +73,7 @@ export const extraChartComponentDefinitions = {
73
73
  axisKey: z.string(),
74
74
  metrics: z.array(z.string()),
75
75
  height: z.number().nullable(),
76
+ showLegend: z.boolean().optional(),
76
77
  }),
77
78
  description:
78
79
  'Radar chart for comparing multiple metrics across categories. Each metric in `metrics` is plotted as its own polygon.',
@@ -86,6 +87,7 @@ export const extraChartComponentDefinitions = {
86
87
  axisKey: 'skill',
87
88
  metrics: ['alice', 'bob'],
88
89
  height: null,
90
+ showLegend: true,
89
91
  },
90
92
  },
91
93
 
@@ -97,6 +99,7 @@ export const extraChartComponentDefinitions = {
97
99
  series: z.array(z.string()),
98
100
  aggregate: z.enum(['sum', 'count', 'avg']).nullable(),
99
101
  height: z.number().nullable(),
102
+ showLegend: z.boolean().optional(),
100
103
  }),
101
104
  description:
102
105
  'Stacked bar chart for compositional data. Each entry in `series` is plotted as its own bar segment per x value.',
@@ -111,6 +114,7 @@ export const extraChartComponentDefinitions = {
111
114
  series: ['north', 'south', 'east'],
112
115
  aggregate: null,
113
116
  height: null,
117
+ showLegend: true,
114
118
  },
115
119
  },
116
120
 
@@ -124,6 +128,7 @@ export const extraChartComponentDefinitions = {
124
128
  barColor: z.string().nullable(),
125
129
  lineColor: z.string().nullable(),
126
130
  height: z.number().nullable(),
131
+ showLegend: z.boolean().optional(),
127
132
  }),
128
133
  description:
129
134
  'Combined bar + line chart for paired metrics (e.g. counts + a derived rate) on the same axis.',
@@ -140,6 +145,7 @@ export const extraChartComponentDefinitions = {
140
145
  barColor: null,
141
146
  lineColor: null,
142
147
  height: null,
148
+ showLegend: true,
143
149
  },
144
150
  },
145
151
  } as const;
@@ -17,6 +17,7 @@ export interface JsonRenderNodeInput {
17
17
  y?: number;
18
18
  width?: number;
19
19
  height?: number;
20
+ strictSize?: boolean;
20
21
  }
21
22
 
22
23
  export interface GraphNodeInput {
@@ -38,10 +39,13 @@ export interface GraphNodeInput {
38
39
  barColor?: string;
39
40
  lineColor?: string;
40
41
  height?: number;
42
+ showLegend?: boolean;
43
+ showLabels?: boolean;
41
44
  x?: number;
42
45
  y?: number;
43
46
  width?: number;
44
47
  heightPx?: number;
48
+ strictSize?: boolean;
45
49
  }
46
50
 
47
51
  export const JSON_RENDER_NODE_SIZE = { width: 840, height: 620 };
@@ -514,6 +518,8 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
514
518
  case 'PieChart': {
515
519
  chartProps.nameKey = input.nameKey ?? 'name';
516
520
  chartProps.valueKey = input.valueKey ?? 'value';
521
+ chartProps.showLegend = input.showLegend !== false;
522
+ chartProps.showLabels = input.showLabels !== false;
517
523
  break;
518
524
  }
519
525
  case 'ScatterChart': {
@@ -533,6 +539,7 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
533
539
  }
534
540
  chartProps.axisKey = axisKey;
535
541
  chartProps.metrics = metrics;
542
+ chartProps.showLegend = input.showLegend !== false;
536
543
  break;
537
544
  }
538
545
  case 'StackedBarChart': {
@@ -546,6 +553,7 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
546
553
  chartProps.xKey = xKey;
547
554
  chartProps.series = series;
548
555
  chartProps.aggregate = input.aggregate ?? null;
556
+ chartProps.showLegend = input.showLegend !== false;
549
557
  break;
550
558
  }
551
559
  case 'ComposedChart': {
@@ -556,6 +564,7 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
556
564
  ?? 'rate';
557
565
  chartProps.barColor = input.barColor ?? null;
558
566
  chartProps.lineColor = input.lineColor ?? null;
567
+ chartProps.showLegend = input.showLegend !== false;
559
568
  break;
560
569
  }
561
570
  case 'AreaChart':
@@ -613,6 +622,8 @@ export function buildGraphConfig(input: GraphNodeInput): Record<string, unknown>
613
622
  ...(input.barColor ? { barColor: input.barColor } : {}),
614
623
  ...(input.lineColor ? { lineColor: input.lineColor } : {}),
615
624
  ...(typeof input.height === 'number' ? { height: input.height } : {}),
625
+ ...(typeof input.showLegend === 'boolean' ? { showLegend: input.showLegend } : {}),
626
+ ...(typeof input.showLabels === 'boolean' ? { showLabels: input.showLabels } : {}),
616
627
  };
617
628
  }
618
629
 
package/src/mcp/server.ts CHANGED
@@ -155,6 +155,7 @@ export async function startMcpServer(): Promise<void> {
155
155
  y: z.number().optional().describe('Y position (auto-placed if omitted)'),
156
156
  width: z.number().optional().describe('Width in pixels (default: 720)'),
157
157
  height: z.number().optional().describe('Height in pixels (default: 600)'),
158
+ strictSize: z.boolean().optional().describe('Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting'),
158
159
  },
159
160
  async (input) => {
160
161
  const c = await ensureCanvas();
@@ -173,6 +174,7 @@ export async function startMcpServer(): Promise<void> {
173
174
  ...(typeof input.y === 'number' ? { y: input.y } : {}),
174
175
  ...(typeof input.width === 'number' ? { width: input.width } : {}),
175
176
  ...(typeof input.height === 'number' ? { height: input.height } : {}),
177
+ ...(input.strictSize === true ? { strictSize: true } : {}),
176
178
  });
177
179
  return {
178
180
  content: [{ type: 'text', text: JSON.stringify(result) }],
@@ -461,6 +463,7 @@ export async function startMcpServer(): Promise<void> {
461
463
  y: z.number().optional().describe('Optional Y position'),
462
464
  width: z.number().optional().describe('Optional node width'),
463
465
  height: z.number().optional().describe('Optional node height'),
466
+ strictSize: z.boolean().optional().describe('Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting'),
464
467
  },
465
468
  async (input) => {
466
469
  const c = await ensureCanvas();
@@ -472,6 +475,7 @@ export async function startMcpServer(): Promise<void> {
472
475
  ...(typeof input.y === 'number' ? { y: input.y } : {}),
473
476
  ...(typeof input.width === 'number' ? { width: input.width } : {}),
474
477
  ...(typeof input.height === 'number' ? { height: input.height } : {}),
478
+ ...(input.strictSize === true ? { strictSize: true } : {}),
475
479
  });
476
480
  return {
477
481
  content: [{
@@ -515,10 +519,13 @@ export async function startMcpServer(): Promise<void> {
515
519
  barColor: z.string().optional().describe('Optional bar color for composed charts'),
516
520
  lineColor: z.string().optional().describe('Optional line color for composed charts'),
517
521
  height: z.number().optional().describe('Optional chart content height'),
522
+ showLegend: z.boolean().optional().describe('Show chart legend when supported; pass false for compact node layouts'),
523
+ showLabels: z.boolean().optional().describe('Show direct labels when supported, such as pie slice labels (defaults to true)'),
518
524
  x: z.number().optional().describe('Optional X position'),
519
525
  y: z.number().optional().describe('Optional Y position'),
520
526
  width: z.number().optional().describe('Optional node width'),
521
527
  nodeHeight: z.number().optional().describe('Optional node height'),
528
+ strictSize: z.boolean().optional().describe('Keep explicit node size fixed and scroll overflowing content instead of browser auto-fitting'),
522
529
  },
523
530
  async (input) => {
524
531
  const c = await ensureCanvas();
@@ -542,10 +549,13 @@ export async function startMcpServer(): Promise<void> {
542
549
  ...(typeof input.barColor === 'string' ? { barColor: input.barColor } : {}),
543
550
  ...(typeof input.lineColor === 'string' ? { lineColor: input.lineColor } : {}),
544
551
  ...(typeof input.height === 'number' ? { height: input.height } : {}),
552
+ ...(typeof input.showLegend === 'boolean' ? { showLegend: input.showLegend } : {}),
553
+ ...(typeof input.showLabels === 'boolean' ? { showLabels: input.showLabels } : {}),
545
554
  ...(typeof input.x === 'number' ? { x: input.x } : {}),
546
555
  ...(typeof input.y === 'number' ? { y: input.y } : {}),
547
556
  ...(typeof input.width === 'number' ? { width: input.width } : {}),
548
557
  ...(typeof input.nodeHeight === 'number' ? { heightPx: input.nodeHeight } : {}),
558
+ ...(input.strictSize === true ? { strictSize: true } : {}),
549
559
  });
550
560
  return {
551
561
  content: [{
@@ -67,6 +67,7 @@ export interface CanvasStructuredNodeUpdateInput extends Omit<CanvasGraphNodeUpd
67
67
  content?: unknown;
68
68
  data?: unknown;
69
69
  arrangeLocked?: unknown;
70
+ strictSize?: boolean;
70
71
  chartHeight?: unknown;
71
72
  }
72
73
 
@@ -82,6 +83,7 @@ interface CanvasAddNodeInput {
82
83
  defaultWidth?: number;
83
84
  defaultHeight?: number;
84
85
  fileMode?: 'path' | 'inline' | 'auto';
86
+ strictSize?: boolean;
85
87
  }
86
88
 
87
89
  interface CanvasCreateGroupInput {
@@ -164,6 +166,8 @@ function hasGraphUpdateFields(input: Record<string, unknown>): boolean {
164
166
  input.zKey !== undefined ||
165
167
  input.nameKey !== undefined ||
166
168
  input.valueKey !== undefined ||
169
+ input.showLegend !== undefined ||
170
+ input.showLabels !== undefined ||
167
171
  input.axisKey !== undefined ||
168
172
  input.metrics !== undefined ||
169
173
  input.series !== undefined ||
@@ -200,6 +204,7 @@ function mergeNodeDataFields(
200
204
  ...base,
201
205
  ...(isRecord(input.data) ? input.data : {}),
202
206
  ...(typeof input.arrangeLocked === 'boolean' ? { arrangeLocked: input.arrangeLocked } : {}),
207
+ ...(typeof input.strictSize === 'boolean' ? { strictSize: input.strictSize } : {}),
203
208
  };
204
209
  }
205
210
 
@@ -251,6 +256,8 @@ function graphConfigToInput(config: Record<string, unknown>, fallbackTitle: stri
251
256
  ...(pickString(config, 'zKey') ? { zKey: pickString(config, 'zKey') } : {}),
252
257
  ...(pickString(config, 'nameKey') ? { nameKey: pickString(config, 'nameKey') } : {}),
253
258
  ...(pickString(config, 'valueKey') ? { valueKey: pickString(config, 'valueKey') } : {}),
259
+ ...(typeof config.showLegend === 'boolean' ? { showLegend: config.showLegend } : {}),
260
+ ...(typeof config.showLabels === 'boolean' ? { showLabels: config.showLabels } : {}),
254
261
  ...(pickString(config, 'axisKey') ? { axisKey: pickString(config, 'axisKey') } : {}),
255
262
  ...(pickStringArray(config, 'metrics') ? { metrics: pickStringArray(config, 'metrics') } : {}),
256
263
  ...(pickStringArray(config, 'series') ? { series: pickStringArray(config, 'series') } : {}),
@@ -276,6 +283,12 @@ function mergeGraphInput(source: Record<string, unknown>, fallback: GraphNodeInp
276
283
  ...((pickString(source, 'zKey') ?? fallback?.zKey) ? { zKey: pickString(source, 'zKey') ?? fallback?.zKey } : {}),
277
284
  ...((pickString(source, 'nameKey') ?? fallback?.nameKey) ? { nameKey: pickString(source, 'nameKey') ?? fallback?.nameKey } : {}),
278
285
  ...((pickString(source, 'valueKey') ?? fallback?.valueKey) ? { valueKey: pickString(source, 'valueKey') ?? fallback?.valueKey } : {}),
286
+ ...(typeof source.showLegend === 'boolean' || typeof fallback?.showLegend === 'boolean'
287
+ ? { showLegend: typeof source.showLegend === 'boolean' ? source.showLegend : fallback?.showLegend }
288
+ : {}),
289
+ ...(typeof source.showLabels === 'boolean' || typeof fallback?.showLabels === 'boolean'
290
+ ? { showLabels: typeof source.showLabels === 'boolean' ? source.showLabels : fallback?.showLabels }
291
+ : {}),
279
292
  ...((pickString(source, 'axisKey') ?? fallback?.axisKey) ? { axisKey: pickString(source, 'axisKey') ?? fallback?.axisKey } : {}),
280
293
  ...((pickStringArray(source, 'metrics') ?? fallback?.metrics) ? { metrics: pickStringArray(source, 'metrics') ?? fallback?.metrics } : {}),
281
294
  ...((pickStringArray(source, 'series') ?? fallback?.series) ? { series: pickStringArray(source, 'series') ?? fallback?.series } : {}),
@@ -752,6 +765,7 @@ function buildNodeData(input: CanvasAddNodeInput): Record<string, unknown> {
752
765
  ...(input.data ?? {}),
753
766
  ...(input.title ? { title: input.title } : {}),
754
767
  ...(input.content ? { content: input.content } : {}),
768
+ ...(input.strictSize ? { strictSize: true } : {}),
755
769
  };
756
770
  }
757
771
 
@@ -1271,6 +1285,7 @@ export function createCanvasJsonRenderNode(
1271
1285
  dockPosition: null,
1272
1286
  data: createJsonRenderNodeData(id, input.title?.trim() || inferJsonRenderNodeTitle(spec), spec, {
1273
1287
  viewerType: 'json-render',
1288
+ ...(input.strictSize ? { strictSize: true } : {}),
1274
1289
  }),
1275
1290
  };
1276
1291
 
@@ -1302,6 +1317,7 @@ export function createCanvasGraphNode(
1302
1317
  data: createJsonRenderNodeData(id, title, spec, {
1303
1318
  viewerType: 'graph',
1304
1319
  graphConfig: buildGraphConfig(input),
1320
+ ...(input.strictSize ? { strictSize: true } : {}),
1305
1321
  }),
1306
1322
  };
1307
1323
 
@@ -1418,6 +1434,7 @@ export async function executeCanvasBatch(
1418
1434
  ...(typeof args.y === 'number' ? { y: args.y } : {}),
1419
1435
  ...(typeof args.width === 'number' ? { width: args.width } : {}),
1420
1436
  ...(typeof args.height === 'number' ? { height: args.height } : {}),
1437
+ ...(args.strictSize === true ? { strictSize: true } : {}),
1421
1438
  defaultWidth: 520,
1422
1439
  defaultHeight: 420,
1423
1440
  });
@@ -1441,6 +1458,7 @@ export async function executeCanvasBatch(
1441
1458
  ...(typeof args.y === 'number' ? { y: args.y } : {}),
1442
1459
  ...(typeof args.width === 'number' ? { width: args.width } : {}),
1443
1460
  ...(typeof args.height === 'number' ? { height: args.height } : {}),
1461
+ ...(args.strictSize === true ? { strictSize: true } : {}),
1444
1462
  defaultWidth: 360,
1445
1463
  defaultHeight: 200,
1446
1464
  fileMode: 'auto',
@@ -1471,12 +1489,13 @@ export async function executeCanvasBatch(
1471
1489
  if (args.dockPosition === null || args.dockPosition === 'left' || args.dockPosition === 'right') {
1472
1490
  patch.dockPosition = args.dockPosition;
1473
1491
  }
1474
- if (typeof args.title === 'string' || typeof args.content === 'string' || typeof args.arrangeLocked === 'boolean' || isPlainRecord(args.data)) {
1492
+ if (typeof args.title === 'string' || typeof args.content === 'string' || typeof args.arrangeLocked === 'boolean' || typeof args.strictSize === 'boolean' || isPlainRecord(args.data)) {
1475
1493
  patch.data = {
1476
1494
  ...node.data,
1477
1495
  ...(typeof args.title === 'string' ? { title: args.title } : {}),
1478
1496
  ...(typeof args.content === 'string' ? { content: args.content } : {}),
1479
1497
  ...(typeof args.arrangeLocked === 'boolean' ? { arrangeLocked: args.arrangeLocked } : {}),
1498
+ ...(typeof args.strictSize === 'boolean' ? { strictSize: args.strictSize } : {}),
1480
1499
  ...(isPlainRecord(args.data) ? args.data : {}),
1481
1500
  };
1482
1501
  }
@@ -1511,6 +1530,7 @@ export async function executeCanvasBatch(
1511
1530
  ...(typeof args.y === 'number' ? { y: args.y } : {}),
1512
1531
  ...(typeof args.width === 'number' ? { width: args.width } : {}),
1513
1532
  ...(typeof args.nodeHeight === 'number' ? { heightPx: args.nodeHeight } : {}),
1533
+ ...(args.strictSize === true ? { strictSize: true } : {}),
1514
1534
  });
1515
1535
  result = {
1516
1536
  ok: true,
@@ -72,6 +72,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
72
72
  { name: 'y', type: 'number', required: false, description: 'Optional Y position.' },
73
73
  { name: 'width', type: 'number', required: false, description: 'Optional node width.' },
74
74
  { name: 'height', type: 'number', required: false, description: 'Optional node height.' },
75
+ { name: 'strictSize', type: 'boolean', required: false, description: 'Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting.', aliases: ['strict-size', 'scroll-overflow'] },
75
76
  ],
76
77
  example: {
77
78
  type: 'markdown',
@@ -203,6 +204,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
203
204
  { name: 'y', type: 'number', required: false, description: 'Optional Y position.' },
204
205
  { name: 'width', type: 'number', required: false, description: 'Optional node width.' },
205
206
  { name: 'height', type: 'number', required: false, description: 'Optional node height.' },
207
+ { name: 'strictSize', type: 'boolean', required: false, description: 'Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting.', aliases: ['strict-size', 'scroll-overflow'] },
206
208
  ],
207
209
  example: {
208
210
  type: 'webpage',
@@ -337,8 +339,11 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
337
339
  { name: 'barColor', type: 'string', required: false, description: 'Optional bar color for composed charts.', aliases: ['bar-color'] },
338
340
  { name: 'lineColor', type: 'string', required: false, description: 'Optional line color for composed charts.', aliases: ['line-color'] },
339
341
  { name: 'height', type: 'number', required: false, description: 'Optional chart content height.', aliases: ['chart-height'] },
342
+ { name: 'showLegend', type: 'boolean', required: false, description: 'Show chart legend when supported; pass false for compact node layouts.', aliases: ['show-legend'] },
343
+ { name: 'showLabels', type: 'boolean', required: false, description: 'Show direct labels when supported, such as pie slice labels; defaults to true.', aliases: ['show-labels'] },
340
344
  { name: 'width', type: 'number', required: false, description: 'Optional node width.' },
341
345
  { name: 'nodeHeight', type: 'number', required: false, description: 'Optional node height (canvas frame). Distinct from `height`, which sets only the chart content height inside the node.', aliases: ['node-height'] },
346
+ { name: 'strictSize', type: 'boolean', required: false, description: 'Keep explicit node size fixed and scroll overflowing content instead of browser auto-fitting.', aliases: ['strict-size', 'scroll-overflow'] },
342
347
  ],
343
348
  example: {
344
349
  title: 'Deploy Trend',
@@ -115,6 +115,18 @@ function elementHasCameraUpdate(elements: Array<Record<string, unknown>>): boole
115
115
  return elements.some((element) => element.type === 'cameraUpdate');
116
116
  }
117
117
 
118
+ function hasRenderableExcalidrawElement(elements: Array<Record<string, unknown>>): boolean {
119
+ return elements.some((element) => {
120
+ if (element.isDeleted === true) return false;
121
+ if (element.type === 'cameraUpdate' || element.type === 'restoreCheckpoint' || element.type === 'delete') return false;
122
+ if (typeof element.type !== 'string' || element.type.length === 0) return false;
123
+ if (element.type === 'text') {
124
+ return typeof element.text === 'string' && element.text.trim().length > 0;
125
+ }
126
+ return finiteNumber(element.x) !== null && finiteNumber(element.y) !== null;
127
+ });
128
+ }
129
+
118
130
  function normalizeExcalidrawBoundText(elements: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
119
131
  const elementsById = new Map<string, Record<string, unknown>>();
120
132
  for (const element of elements) {
@@ -123,38 +135,58 @@ function normalizeExcalidrawBoundText(elements: Array<Record<string, unknown>>):
123
135
 
124
136
  let changed = false;
125
137
  const boundElementIdsByContainer = new Map<string, Set<string>>();
138
+ const labelByContainer = new Map<string, Record<string, unknown>>();
139
+ const textIdsConvertedToLabels = new Set<string>();
126
140
 
127
141
  for (const element of elements) {
128
142
  if (element.type !== 'text' || typeof element.id !== 'string' || typeof element.containerId !== 'string') continue;
129
- if (!elementsById.has(element.containerId)) continue;
143
+ const container = elementsById.get(element.containerId);
144
+ if (!container) continue;
130
145
  const ids = boundElementIdsByContainer.get(element.containerId) ?? new Set<string>();
131
146
  ids.add(element.id);
132
147
  boundElementIdsByContainer.set(element.containerId, ids);
148
+ const text = typeof element.text === 'string' ? element.text.trim() : '';
149
+ if (!isRecord(container.label) && text.length > 0) {
150
+ labelByContainer.set(element.containerId, {
151
+ text,
152
+ ...(typeof element.fontSize === 'number' && Number.isFinite(element.fontSize) ? { fontSize: element.fontSize } : {}),
153
+ });
154
+ textIdsConvertedToLabels.add(element.id);
155
+ }
133
156
  }
134
157
 
135
- const normalized = elements.map((element) => {
158
+ const normalized = elements.flatMap<Record<string, unknown>>((element) => {
159
+ if (typeof element.id === 'string' && textIdsConvertedToLabels.has(element.id)) {
160
+ changed = true;
161
+ return [];
162
+ }
136
163
  if (typeof element.id !== 'string') return element;
137
164
  const boundTextIds = boundElementIdsByContainer.get(element.id);
138
- if (!boundTextIds || boundTextIds.size === 0) return element;
165
+ const label = labelByContainer.get(element.id);
166
+ if ((!boundTextIds || boundTextIds.size === 0) && !label) return element;
139
167
 
140
168
  const existing = Array.isArray(element.boundElements)
141
169
  ? element.boundElements.filter(isRecord)
142
170
  : [];
171
+ const remainingExisting = existing.filter((boundElement) => {
172
+ return !(boundElement.type === 'text' && typeof boundElement.id === 'string' && textIdsConvertedToLabels.has(boundElement.id));
173
+ });
143
174
  const existingTextIds = new Set(
144
- existing
175
+ remainingExisting
145
176
  .filter((boundElement) => boundElement.type === 'text' && typeof boundElement.id === 'string')
146
177
  .map((boundElement) => boundElement.id as string),
147
178
  );
148
- const missing = [...boundTextIds].filter((id) => !existingTextIds.has(id));
149
- if (missing.length === 0) return element;
179
+ const missing = [...(boundTextIds ?? [])]
180
+ .filter((id) => !textIdsConvertedToLabels.has(id) && !existingTextIds.has(id));
181
+ if (missing.length === 0 && !label && remainingExisting.length === existing.length) return element;
150
182
 
151
183
  changed = true;
152
184
  return {
153
185
  ...element,
154
- boundElements: [
155
- ...existing,
156
- ...missing.map((id) => ({ type: 'text', id })),
157
- ],
186
+ ...(label ? { label } : {}),
187
+ ...(remainingExisting.length > 0 || missing.length > 0
188
+ ? { boundElements: [...remainingExisting, ...missing.map((id) => ({ type: 'text', id }))] }
189
+ : {}),
158
190
  };
159
191
  });
160
192
 
@@ -251,12 +283,12 @@ function withInferredCameraUpdate(
251
283
 
252
284
  export function normalizeExcalidrawElements(elements: unknown): string {
253
285
  const parsed = parseExcalidrawElements(elements);
254
- return JSON.stringify(parsed.length > 0 ? parsed : DEFAULT_EXCALIDRAW_ELEMENTS);
286
+ return JSON.stringify(hasRenderableExcalidrawElement(parsed) ? parsed : DEFAULT_EXCALIDRAW_ELEMENTS);
255
287
  }
256
288
 
257
289
  export function normalizeExcalidrawElementsForToolInput(elements: unknown): string {
258
290
  const parsed = parseExcalidrawElements(elements);
259
- const seeded = parsed.length > 0 ? parsed : [...DEFAULT_EXCALIDRAW_ELEMENTS];
291
+ const seeded = hasRenderableExcalidrawElement(parsed) ? parsed : [...DEFAULT_EXCALIDRAW_ELEMENTS];
260
292
  return JSON.stringify(withInferredCameraUpdate(normalizeExcalidrawBoundText(seeded)));
261
293
  }
262
294
 
@@ -162,6 +162,7 @@ export class PmxCanvas extends EventEmitter {
162
162
  y?: number;
163
163
  width?: number;
164
164
  height?: number;
165
+ strictSize?: boolean;
165
166
  }): string {
166
167
  if (input.type === 'webpage') {
167
168
  throw new Error('Use addWebpageNode for webpage nodes so page content is fetched and cached on the server.');
@@ -171,6 +172,7 @@ export class PmxCanvas extends EventEmitter {
171
172
  defaultWidth: 360,
172
173
  defaultHeight: 200,
173
174
  fileMode: 'path',
175
+ ...(input.strictSize ? { strictSize: true } : {}),
174
176
  });
175
177
 
176
178
  emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
@@ -191,6 +193,7 @@ export class PmxCanvas extends EventEmitter {
191
193
  y?: number;
192
194
  width?: number;
193
195
  height?: number;
196
+ strictSize?: boolean;
194
197
  }): Promise<{ ok: boolean; id: string; error?: string; fetch: { ok: boolean; error?: string } }> {
195
198
  const { id } = addCanvasNode({
196
199
  type: 'webpage',
@@ -200,6 +203,7 @@ export class PmxCanvas extends EventEmitter {
200
203
  ...(typeof input.y === 'number' ? { y: input.y } : {}),
201
204
  ...(typeof input.width === 'number' ? { width: input.width } : {}),
202
205
  ...(typeof input.height === 'number' ? { height: input.height } : {}),
206
+ ...(input.strictSize ? { strictSize: true } : {}),
203
207
  defaultWidth: 520,
204
208
  defaultHeight: 420,
205
209
  });
@@ -238,7 +242,8 @@ export class PmxCanvas extends EventEmitter {
238
242
  patch.data !== undefined ||
239
243
  patch.title !== undefined ||
240
244
  patch.content !== undefined ||
241
- typeof patch.arrangeLocked === 'boolean'
245
+ typeof patch.arrangeLocked === 'boolean' ||
246
+ typeof patch.strictSize === 'boolean'
242
247
  ) {
243
248
  resolvedPatch.data = {
244
249
  ...existing.data,
@@ -246,6 +251,7 @@ export class PmxCanvas extends EventEmitter {
246
251
  ...(typeof patch.title === 'string' ? { title: patch.title } : {}),
247
252
  ...(typeof patch.content === 'string' ? { content: patch.content } : {}),
248
253
  ...(typeof patch.arrangeLocked === 'boolean' ? { arrangeLocked: patch.arrangeLocked } : {}),
254
+ ...(typeof patch.strictSize === 'boolean' ? { strictSize: patch.strictSize } : {}),
249
255
  };
250
256
  }
251
257