@opendata-ai/openchart-vanilla 6.3.0 → 6.4.1

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/index.js CHANGED
@@ -3255,7 +3255,7 @@ function escapeHtml(str) {
3255
3255
  }
3256
3256
 
3257
3257
  // src/mount.ts
3258
- import { isLayerSpec } from "@opendata-ai/openchart-core";
3258
+ import { elementRef, isLayerSpec } from "@opendata-ai/openchart-core";
3259
3259
  import { compileChart, compileLayer } from "@opendata-ai/openchart-engine";
3260
3260
 
3261
3261
  // src/svg-renderer.ts
@@ -3847,6 +3847,9 @@ function renderAnnotation(parent, annotation, index2) {
3847
3847
  const g = createSVGElement("g");
3848
3848
  g.setAttribute("class", `viz-annotation viz-annotation-${annotation.type}`);
3849
3849
  g.setAttribute("data-annotation-index", String(index2));
3850
+ if (annotation.id) {
3851
+ g.setAttribute("data-annotation-id", annotation.id);
3852
+ }
3850
3853
  if (annotation.rect) {
3851
3854
  const rect = createSVGElement("rect");
3852
3855
  rect.setAttribute("class", "viz-annotation-range");
@@ -4181,6 +4184,159 @@ function renderChartSVG(layout, container) {
4181
4184
  return svg;
4182
4185
  }
4183
4186
 
4187
+ // src/text-edit-overlay.ts
4188
+ function getScale(svg) {
4189
+ const viewBox = svg.viewBox?.baseVal;
4190
+ const svgRect = svg.getBoundingClientRect();
4191
+ return {
4192
+ scaleX: viewBox?.width && svgRect.width ? svgRect.width / viewBox.width : 1,
4193
+ scaleY: viewBox?.height && svgRect.height ? svgRect.height / viewBox.height : 1
4194
+ };
4195
+ }
4196
+ function getTextStyles(targetElement, scale) {
4197
+ const computed = window.getComputedStyle(targetElement);
4198
+ const svgFontSize = parseFloat(
4199
+ targetElement.getAttribute("font-size") ?? computed.fontSize ?? "14"
4200
+ );
4201
+ const pixelFontSize = svgFontSize * scale.scaleY;
4202
+ const fontFamily = targetElement.getAttribute("font-family") ?? computed.fontFamily ?? "inherit";
4203
+ const fontWeight = targetElement.getAttribute("font-weight") ?? computed.fontWeight ?? "400";
4204
+ const fill = targetElement.style.getPropertyValue("fill") || targetElement.getAttribute("fill") || computed.color || "#000";
4205
+ const textAnchor = targetElement.getAttribute("text-anchor") ?? "start";
4206
+ let textAlign = "left";
4207
+ if (textAnchor === "middle") textAlign = "center";
4208
+ else if (textAnchor === "end") textAlign = "right";
4209
+ return {
4210
+ fontFamily,
4211
+ fontSize: `${pixelFontSize}px`,
4212
+ fontWeight,
4213
+ color: fill,
4214
+ textAlign,
4215
+ lineHeight: "1.3"
4216
+ };
4217
+ }
4218
+ function computePosition2(targetElement, svg, container) {
4219
+ const bbox = targetElement.getBBox();
4220
+ const scale = getScale(svg);
4221
+ const svgRect = svg.getBoundingClientRect();
4222
+ const containerRect = container.getBoundingClientRect();
4223
+ const padding = 4;
4224
+ const left = bbox.x * scale.scaleX + (svgRect.left - containerRect.left) - padding;
4225
+ const top = bbox.y * scale.scaleY + (svgRect.top - containerRect.top) - padding;
4226
+ const width = bbox.width * scale.scaleX + padding * 2;
4227
+ const height = bbox.height * scale.scaleY + padding * 2;
4228
+ return {
4229
+ top: Math.max(0, top),
4230
+ left: Math.max(0, left),
4231
+ width: Math.max(width, 60),
4232
+ height: Math.max(height, 24)
4233
+ };
4234
+ }
4235
+ function createTextEditOverlay(config) {
4236
+ const { container, svg, targetElement, currentText, onCommit, onCancel } = config;
4237
+ let destroyed = false;
4238
+ const originalOpacity = targetElement.getAttribute("opacity");
4239
+ targetElement.setAttribute("opacity", "0");
4240
+ const scale = getScale(svg);
4241
+ const styles = getTextStyles(targetElement, scale);
4242
+ const position = computePosition2(targetElement, svg, container);
4243
+ const textarea = document.createElement("textarea");
4244
+ textarea.value = currentText;
4245
+ const containerPosition = window.getComputedStyle(container).position;
4246
+ const containerWasStatic = containerPosition === "static";
4247
+ if (containerWasStatic) {
4248
+ container.style.position = "relative";
4249
+ }
4250
+ Object.assign(textarea.style, {
4251
+ position: "absolute",
4252
+ top: `${position.top}px`,
4253
+ left: `${position.left}px`,
4254
+ width: `${position.width}px`,
4255
+ minHeight: `${position.height}px`,
4256
+ fontFamily: styles.fontFamily,
4257
+ fontSize: styles.fontSize,
4258
+ fontWeight: styles.fontWeight,
4259
+ color: styles.color,
4260
+ textAlign: styles.textAlign,
4261
+ lineHeight: styles.lineHeight,
4262
+ padding: "2px 4px",
4263
+ margin: "0",
4264
+ border: "1px solid rgba(79, 70, 229, 0.4)",
4265
+ borderRadius: "3px",
4266
+ background: "rgba(255, 255, 255, 0.95)",
4267
+ outline: "none",
4268
+ resize: "none",
4269
+ overflow: "hidden",
4270
+ boxSizing: "border-box",
4271
+ zIndex: "10000",
4272
+ // Remove default textarea appearance
4273
+ WebkitAppearance: "none",
4274
+ appearance: "none"
4275
+ });
4276
+ container.appendChild(textarea);
4277
+ textarea.focus();
4278
+ textarea.select();
4279
+ function destroy() {
4280
+ if (destroyed) return;
4281
+ destroyed = true;
4282
+ if (originalOpacity !== null) {
4283
+ targetElement.setAttribute("opacity", originalOpacity);
4284
+ } else {
4285
+ targetElement.removeAttribute("opacity");
4286
+ }
4287
+ textarea.removeEventListener("keydown", handleKeyDown);
4288
+ document.removeEventListener("mousedown", handleClickOutside);
4289
+ resizeObserver.disconnect();
4290
+ if (containerWasStatic) {
4291
+ container.style.position = "";
4292
+ }
4293
+ if (textarea.parentNode) {
4294
+ textarea.parentNode.removeChild(textarea);
4295
+ }
4296
+ }
4297
+ function commit() {
4298
+ if (destroyed) return;
4299
+ const newText = textarea.value;
4300
+ destroy();
4301
+ onCommit(newText);
4302
+ }
4303
+ function cancel() {
4304
+ if (destroyed) return;
4305
+ destroy();
4306
+ onCancel();
4307
+ }
4308
+ const handleKeyDown = (e) => {
4309
+ if (e.key === "Enter" && !e.shiftKey) {
4310
+ e.preventDefault();
4311
+ commit();
4312
+ } else if (e.key === "Escape") {
4313
+ e.preventDefault();
4314
+ cancel();
4315
+ }
4316
+ };
4317
+ textarea.addEventListener("keydown", handleKeyDown);
4318
+ const handleClickOutside = (e) => {
4319
+ if (!textarea.contains(e.target)) {
4320
+ commit();
4321
+ }
4322
+ };
4323
+ requestAnimationFrame(() => {
4324
+ if (!destroyed) {
4325
+ document.addEventListener("mousedown", handleClickOutside);
4326
+ }
4327
+ });
4328
+ const resizeObserver = new ResizeObserver(() => {
4329
+ if (destroyed) return;
4330
+ const newPosition = computePosition2(targetElement, svg, container);
4331
+ textarea.style.top = `${newPosition.top}px`;
4332
+ textarea.style.left = `${newPosition.left}px`;
4333
+ textarea.style.width = `${newPosition.width}px`;
4334
+ textarea.style.minHeight = `${newPosition.height}px`;
4335
+ });
4336
+ resizeObserver.observe(container);
4337
+ return { destroy };
4338
+ }
4339
+
4184
4340
  // src/mount.ts
4185
4341
  function resolveDarkMode2(mode) {
4186
4342
  if (mode === "force") return true;
@@ -4458,7 +4614,7 @@ function createDragHandler(config) {
4458
4614
  let activeDocTouchMove = null;
4459
4615
  let activeDocTouchEnd = null;
4460
4616
  let activeDocTouchCancel = null;
4461
- function getScale() {
4617
+ function getScale2() {
4462
4618
  const viewBox = svg.viewBox?.baseVal;
4463
4619
  const svgRect = svg.getBoundingClientRect();
4464
4620
  return {
@@ -4468,7 +4624,7 @@ function createDragHandler(config) {
4468
4624
  }
4469
4625
  function startDrag(startX, startY) {
4470
4626
  setDragging(true);
4471
- const { scaleX, scaleY } = getScale();
4627
+ const { scaleX, scaleY } = getScale2();
4472
4628
  element.style.cursor = "grabbing";
4473
4629
  svg.style.userSelect = "none";
4474
4630
  const handleMove = (clientX, clientY) => {
@@ -5125,6 +5281,167 @@ function createScreenReaderTable(layout, container) {
5125
5281
  container.appendChild(table);
5126
5282
  return table;
5127
5283
  }
5284
+ var EDITABLE_HOVER_CSS = `
5285
+ .viz-editable-hover {
5286
+ outline: 1.5px solid rgba(79, 70, 229, 0.35);
5287
+ outline-offset: 2px;
5288
+ border-radius: 2px;
5289
+ }
5290
+ `;
5291
+ function makeEditable(svg) {
5292
+ svg.setAttribute("tabindex", "0");
5293
+ svg.style.outline = "none";
5294
+ const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
5295
+ style.textContent = EDITABLE_HOVER_CSS;
5296
+ svg.insertBefore(style, svg.firstChild);
5297
+ }
5298
+ function hasEditingCallbacks(opts) {
5299
+ return !!(opts?.onEdit || opts?.onSelect || opts?.onDeselect || opts?.onTextEdit);
5300
+ }
5301
+ function findElementByRef(svg, ref) {
5302
+ switch (ref.type) {
5303
+ case "annotation": {
5304
+ if (ref.id) {
5305
+ const byId = svg.querySelector(`[data-annotation-id="${ref.id}"]`);
5306
+ if (byId) return byId;
5307
+ }
5308
+ return svg.querySelector(`[data-annotation-index="${ref.index}"]`);
5309
+ }
5310
+ case "chrome":
5311
+ return svg.querySelector(`[data-chrome-key="${ref.key}"]`);
5312
+ case "series-label":
5313
+ return svg.querySelector(`.viz-mark-label[data-series="${ref.series}"]`);
5314
+ case "legend":
5315
+ return svg.querySelector(".viz-legend");
5316
+ case "legend-entry":
5317
+ return svg.querySelector(`[data-legend-index="${ref.index}"]`);
5318
+ }
5319
+ }
5320
+ function buildElementRef(element, _specAnnotations) {
5321
+ const annotationEl = element.closest("[data-annotation-index]");
5322
+ if (annotationEl) {
5323
+ const index2 = Number(annotationEl.getAttribute("data-annotation-index"));
5324
+ const id = annotationEl.getAttribute("data-annotation-id") ?? void 0;
5325
+ return elementRef.annotation(index2, id);
5326
+ }
5327
+ const chromeEl = element.closest("[data-chrome-key]");
5328
+ if (chromeEl) {
5329
+ const key = chromeEl.getAttribute("data-chrome-key");
5330
+ if (key) return elementRef.chrome(key);
5331
+ }
5332
+ const seriesLabelEl = element.closest(".viz-mark-label[data-series]");
5333
+ if (seriesLabelEl) {
5334
+ const series = seriesLabelEl.getAttribute("data-series");
5335
+ if (series) return elementRef.seriesLabel(series);
5336
+ }
5337
+ const legendEntryEl = element.closest("[data-legend-index]");
5338
+ if (legendEntryEl) {
5339
+ const index2 = Number(legendEntryEl.getAttribute("data-legend-index"));
5340
+ const series = legendEntryEl.getAttribute("data-legend-label") ?? "";
5341
+ return elementRef.legendEntry(series, index2);
5342
+ }
5343
+ const legendEl = element.closest(".viz-legend");
5344
+ if (legendEl) return elementRef.legend();
5345
+ return null;
5346
+ }
5347
+ function getEditableElements(spec, layout) {
5348
+ const refs = [];
5349
+ const chromeKeys = ["title", "subtitle", "source", "byline", "footer"];
5350
+ for (const key of chromeKeys) {
5351
+ if (layout.chrome[key]) {
5352
+ refs.push(elementRef.chrome(key));
5353
+ }
5354
+ }
5355
+ const annotations = "annotations" in spec && Array.isArray(spec.annotations) ? spec.annotations : [];
5356
+ for (let i = 0; i < annotations.length; i++) {
5357
+ refs.push(elementRef.annotation(i, annotations[i].id));
5358
+ }
5359
+ const seriesLabels = [];
5360
+ for (const mark of layout.marks) {
5361
+ if (mark.type === "line" && mark.label?.visible && mark.seriesKey) {
5362
+ seriesLabels.push(mark.seriesKey);
5363
+ }
5364
+ }
5365
+ seriesLabels.sort();
5366
+ for (const series of seriesLabels) {
5367
+ refs.push(elementRef.seriesLabel(series));
5368
+ }
5369
+ if (layout.legend.entries.length > 0) {
5370
+ refs.push(elementRef.legend());
5371
+ }
5372
+ return refs;
5373
+ }
5374
+ function isTextEditable(ref, specAnnotations) {
5375
+ if (ref.type === "chrome") return true;
5376
+ if (ref.type === "annotation") {
5377
+ const annotation = specAnnotations[ref.index];
5378
+ return annotation?.type === "text";
5379
+ }
5380
+ return false;
5381
+ }
5382
+ function getElementText(ref, spec) {
5383
+ if (ref.type === "chrome") {
5384
+ const chromeConfig = "chrome" in spec ? spec.chrome : void 0;
5385
+ if (!chromeConfig) return null;
5386
+ const entry = chromeConfig[ref.key];
5387
+ if (typeof entry === "string") return entry;
5388
+ if (typeof entry === "object" && entry !== null && "text" in entry) {
5389
+ return entry.text;
5390
+ }
5391
+ return null;
5392
+ }
5393
+ if (ref.type === "annotation") {
5394
+ const annotations = "annotations" in spec && Array.isArray(spec.annotations) ? spec.annotations : [];
5395
+ const annotation = annotations[ref.index];
5396
+ if (annotation?.type === "text") return annotation.text ?? null;
5397
+ if (annotation?.label) return annotation.label;
5398
+ return null;
5399
+ }
5400
+ return null;
5401
+ }
5402
+ function refsEqual(a2, b) {
5403
+ if (a2 === null || b === null) return a2 === b;
5404
+ if (a2.type !== b.type) return false;
5405
+ switch (a2.type) {
5406
+ case "annotation": {
5407
+ const bAnno = b;
5408
+ if (a2.id && bAnno.id) return a2.id === bAnno.id;
5409
+ return a2.index === bAnno.index;
5410
+ }
5411
+ case "chrome":
5412
+ return a2.key === b.key;
5413
+ case "series-label":
5414
+ return a2.series === b.series;
5415
+ case "legend":
5416
+ return true;
5417
+ case "legend-entry": {
5418
+ const bEntry = b;
5419
+ return a2.index === bEntry.index && a2.series === bEntry.series;
5420
+ }
5421
+ }
5422
+ }
5423
+ function renderSelectionOverlay(svg, ref, layout) {
5424
+ const target = findElementByRef(svg, ref);
5425
+ if (!target) return null;
5426
+ const bbox = target.getBBox();
5427
+ const padding = 4;
5428
+ const accentColor = layout.theme.colors.categorical?.[0] ?? "#4f46e5";
5429
+ const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
5430
+ g.setAttribute("class", "viz-selection-overlay");
5431
+ const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
5432
+ rect.setAttribute("x", String(bbox.x - padding));
5433
+ rect.setAttribute("y", String(bbox.y - padding));
5434
+ rect.setAttribute("width", String(bbox.width + padding * 2));
5435
+ rect.setAttribute("height", String(bbox.height + padding * 2));
5436
+ rect.setAttribute("rx", "3");
5437
+ rect.setAttribute("fill", "transparent");
5438
+ rect.setAttribute("stroke", accentColor);
5439
+ rect.setAttribute("stroke-width", "1.5");
5440
+ rect.setAttribute("pointer-events", "none");
5441
+ g.appendChild(rect);
5442
+ svg.appendChild(g);
5443
+ return g;
5444
+ }
5128
5445
  function createChart(container, spec, options) {
5129
5446
  let currentSpec = spec;
5130
5447
  let currentLayout;
@@ -5138,11 +5455,17 @@ function createChart(container, spec, options) {
5138
5455
  let cleanupChartEvents = null;
5139
5456
  let cleanupAnnotationDrag = null;
5140
5457
  let cleanupEditDrags = null;
5458
+ let cleanupSelection = null;
5459
+ let cleanupKeyboardEdit = null;
5141
5460
  let srTable = null;
5142
5461
  let destroyed = false;
5143
5462
  let isDragging = false;
5144
5463
  let pendingRender = false;
5145
5464
  let resizeTimer = null;
5465
+ let selectedElement = options?.selectedElement ?? null;
5466
+ let overlayElement = null;
5467
+ let isTextEditingActive = false;
5468
+ let textEditCleanup = null;
5146
5469
  const measureText = createMeasureText();
5147
5470
  function compile() {
5148
5471
  const { width, height } = getContainerDimensions();
@@ -5166,6 +5489,198 @@ function createChart(container, spec, options) {
5166
5489
  height: Math.max(rect.height || 400, 100)
5167
5490
  };
5168
5491
  }
5492
+ function getSpecAnnotations() {
5493
+ return "annotations" in currentSpec && Array.isArray(currentSpec.annotations) ? currentSpec.annotations : [];
5494
+ }
5495
+ function selectElement(ref) {
5496
+ if (!svgElement) return;
5497
+ const target = findElementByRef(svgElement, ref);
5498
+ if (!target) return;
5499
+ if (selectedElement && !refsEqual(selectedElement, ref)) {
5500
+ deselectElement();
5501
+ }
5502
+ selectedElement = ref;
5503
+ overlayElement = renderSelectionOverlay(svgElement, ref, currentLayout);
5504
+ options?.onSelect?.(ref);
5505
+ svgElement.focus();
5506
+ }
5507
+ function deselectElement() {
5508
+ if (!selectedElement) return;
5509
+ if (isTextEditingActive && textEditCleanup) {
5510
+ textEditCleanup();
5511
+ textEditCleanup = null;
5512
+ isTextEditingActive = false;
5513
+ }
5514
+ const prev = selectedElement;
5515
+ selectedElement = null;
5516
+ if (overlayElement?.parentNode) {
5517
+ overlayElement.parentNode.removeChild(overlayElement);
5518
+ }
5519
+ overlayElement = null;
5520
+ options?.onDeselect?.(prev);
5521
+ }
5522
+ function enterTextEditing() {
5523
+ if (!svgElement || !selectedElement || isTextEditingActive) return;
5524
+ const specAnnotations = getSpecAnnotations();
5525
+ if (!isTextEditable(selectedElement, specAnnotations)) return;
5526
+ const currentText = getElementText(selectedElement, currentSpec);
5527
+ if (currentText === null) return;
5528
+ const target = findElementByRef(svgElement, selectedElement);
5529
+ if (!target) return;
5530
+ const textEl = target.tagName === "text" ? target : target.querySelector("text");
5531
+ if (!textEl) return;
5532
+ isTextEditingActive = true;
5533
+ const editRef = selectedElement;
5534
+ const overlay = createTextEditOverlay({
5535
+ container,
5536
+ svg: svgElement,
5537
+ targetElement: textEl,
5538
+ currentText,
5539
+ onCommit: (newText) => {
5540
+ isTextEditingActive = false;
5541
+ textEditCleanup = null;
5542
+ if (newText !== currentText) {
5543
+ options?.onTextEdit?.(editRef, currentText, newText);
5544
+ options?.onEdit?.({
5545
+ type: "text-edit",
5546
+ element: editRef,
5547
+ oldText: currentText,
5548
+ newText
5549
+ });
5550
+ }
5551
+ },
5552
+ onCancel: () => {
5553
+ isTextEditingActive = false;
5554
+ textEditCleanup = null;
5555
+ }
5556
+ });
5557
+ textEditCleanup = overlay.destroy;
5558
+ }
5559
+ function wireSelectionEvents() {
5560
+ if (!svgElement) return () => {
5561
+ };
5562
+ const svg = svgElement;
5563
+ const cleanups = [];
5564
+ const handleClick = (e) => {
5565
+ const mouseEvent = e;
5566
+ const target = mouseEvent.target;
5567
+ if (isTextEditingActive) return;
5568
+ const specAnnotations = getSpecAnnotations();
5569
+ const ref = buildElementRef(target, specAnnotations);
5570
+ if (ref) {
5571
+ selectElement(ref);
5572
+ } else {
5573
+ deselectElement();
5574
+ }
5575
+ };
5576
+ svg.addEventListener("click", handleClick);
5577
+ cleanups.push(() => svg.removeEventListener("click", handleClick));
5578
+ const handleMouseEnter = (e) => {
5579
+ const target = e.target.closest(
5580
+ "[data-annotation-index], [data-chrome-key], .viz-mark-label[data-series], .viz-legend, [data-legend-index]"
5581
+ );
5582
+ if (target) {
5583
+ target.classList.add("viz-editable-hover");
5584
+ }
5585
+ };
5586
+ const handleMouseLeave = (e) => {
5587
+ const target = e.target.closest(".viz-editable-hover");
5588
+ if (target) {
5589
+ target.classList.remove("viz-editable-hover");
5590
+ }
5591
+ };
5592
+ svg.addEventListener("mouseenter", handleMouseEnter, true);
5593
+ svg.addEventListener("mouseleave", handleMouseLeave, true);
5594
+ cleanups.push(() => {
5595
+ svg.removeEventListener("mouseenter", handleMouseEnter, true);
5596
+ svg.removeEventListener("mouseleave", handleMouseLeave, true);
5597
+ });
5598
+ const handleDblClick = (e) => {
5599
+ const mouseEvent = e;
5600
+ const target = mouseEvent.target;
5601
+ const specAnnotations = getSpecAnnotations();
5602
+ const ref = buildElementRef(target, specAnnotations);
5603
+ if (ref && isTextEditable(ref, specAnnotations)) {
5604
+ if (!refsEqual(selectedElement, ref)) {
5605
+ selectElement(ref);
5606
+ }
5607
+ enterTextEditing();
5608
+ }
5609
+ };
5610
+ svg.addEventListener("dblclick", handleDblClick);
5611
+ cleanups.push(() => svg.removeEventListener("dblclick", handleDblClick));
5612
+ return () => {
5613
+ for (const cleanup of cleanups) {
5614
+ cleanup();
5615
+ }
5616
+ };
5617
+ }
5618
+ function wireKeyboardEditEvents() {
5619
+ if (!svgElement) return () => {
5620
+ };
5621
+ const svg = svgElement;
5622
+ const handleKeyDown = (e) => {
5623
+ const specAnnotations = getSpecAnnotations();
5624
+ switch (e.key) {
5625
+ case "Delete":
5626
+ case "Backspace": {
5627
+ if (selectedElement && !isTextEditingActive) {
5628
+ e.preventDefault();
5629
+ options?.onEdit?.({ type: "delete", element: selectedElement });
5630
+ }
5631
+ break;
5632
+ }
5633
+ case "Escape": {
5634
+ e.preventDefault();
5635
+ if (isTextEditingActive && textEditCleanup) {
5636
+ textEditCleanup();
5637
+ textEditCleanup = null;
5638
+ isTextEditingActive = false;
5639
+ } else if (selectedElement) {
5640
+ deselectElement();
5641
+ }
5642
+ break;
5643
+ }
5644
+ case "ArrowDown":
5645
+ case "ArrowRight": {
5646
+ if (!isTextEditingActive && selectedElement) {
5647
+ e.preventDefault();
5648
+ const editables = getEditableElements(currentSpec, currentLayout);
5649
+ if (editables.length === 0) break;
5650
+ const currentIndex = editables.findIndex((r) => refsEqual(r, selectedElement));
5651
+ const nextIndex = currentIndex >= editables.length - 1 ? 0 : currentIndex + 1;
5652
+ selectElement(editables[nextIndex]);
5653
+ }
5654
+ break;
5655
+ }
5656
+ case "ArrowUp":
5657
+ case "ArrowLeft": {
5658
+ if (!isTextEditingActive && selectedElement) {
5659
+ e.preventDefault();
5660
+ const editables = getEditableElements(currentSpec, currentLayout);
5661
+ if (editables.length === 0) break;
5662
+ const currentIndex = editables.findIndex((r) => refsEqual(r, selectedElement));
5663
+ const nextIndex = currentIndex <= 0 ? editables.length - 1 : currentIndex - 1;
5664
+ selectElement(editables[nextIndex]);
5665
+ }
5666
+ break;
5667
+ }
5668
+ case "Enter": {
5669
+ if (selectedElement && !isTextEditingActive) {
5670
+ if (isTextEditable(selectedElement, specAnnotations)) {
5671
+ e.preventDefault();
5672
+ enterTextEditing();
5673
+ }
5674
+ }
5675
+ break;
5676
+ }
5677
+ }
5678
+ };
5679
+ svg.addEventListener("keydown", handleKeyDown);
5680
+ return () => {
5681
+ svg.removeEventListener("keydown", handleKeyDown);
5682
+ };
5683
+ }
5169
5684
  function render() {
5170
5685
  if (isDragging) {
5171
5686
  pendingRender = true;
@@ -5199,6 +5714,20 @@ function createChart(container, spec, options) {
5199
5714
  cleanupEditDrags();
5200
5715
  cleanupEditDrags = null;
5201
5716
  }
5717
+ if (cleanupSelection) {
5718
+ cleanupSelection();
5719
+ cleanupSelection = null;
5720
+ }
5721
+ if (cleanupKeyboardEdit) {
5722
+ cleanupKeyboardEdit();
5723
+ cleanupKeyboardEdit = null;
5724
+ }
5725
+ if (textEditCleanup) {
5726
+ textEditCleanup();
5727
+ textEditCleanup = null;
5728
+ isTextEditingActive = false;
5729
+ }
5730
+ overlayElement = null;
5202
5731
  if (svgElement?.parentNode) {
5203
5732
  svgElement.parentNode.removeChild(svgElement);
5204
5733
  }
@@ -5270,6 +5799,20 @@ function createChart(container, spec, options) {
5270
5799
  }
5271
5800
  };
5272
5801
  }
5802
+ if (hasEditingCallbacks(options)) {
5803
+ makeEditable(svgElement);
5804
+ cleanupSelection = wireSelectionEvents();
5805
+ cleanupKeyboardEdit = wireKeyboardEditEvents();
5806
+ if (selectedElement) {
5807
+ const target = findElementByRef(svgElement, selectedElement);
5808
+ if (target) {
5809
+ overlayElement = renderSelectionOverlay(svgElement, selectedElement, currentLayout);
5810
+ } else {
5811
+ selectedElement = null;
5812
+ overlayElement = null;
5813
+ }
5814
+ }
5815
+ }
5273
5816
  srTable = createScreenReaderTable(currentLayout, container);
5274
5817
  container.classList.add("viz-root");
5275
5818
  const isDark = resolveDarkMode2(options?.darkMode);
@@ -5279,9 +5822,12 @@ function createChart(container, spec, options) {
5279
5822
  container.classList.remove("viz-dark");
5280
5823
  }
5281
5824
  }
5282
- function update(newSpec) {
5825
+ function update(newSpec, updateOpts) {
5283
5826
  if (destroyed) return;
5284
5827
  currentSpec = newSpec;
5828
+ if (updateOpts && "selectedElement" in updateOpts) {
5829
+ selectedElement = updateOpts.selectedElement ?? null;
5830
+ }
5285
5831
  render();
5286
5832
  }
5287
5833
  function resize() {
@@ -5344,6 +5890,21 @@ function createChart(container, spec, options) {
5344
5890
  cleanupEditDrags();
5345
5891
  cleanupEditDrags = null;
5346
5892
  }
5893
+ if (cleanupSelection) {
5894
+ cleanupSelection();
5895
+ cleanupSelection = null;
5896
+ }
5897
+ if (cleanupKeyboardEdit) {
5898
+ cleanupKeyboardEdit();
5899
+ cleanupKeyboardEdit = null;
5900
+ }
5901
+ if (textEditCleanup) {
5902
+ textEditCleanup();
5903
+ textEditCleanup = null;
5904
+ isTextEditingActive = false;
5905
+ }
5906
+ selectedElement = null;
5907
+ overlayElement = null;
5347
5908
  if (disconnectResize) {
5348
5909
  disconnectResize();
5349
5910
  disconnectResize = null;
@@ -5380,6 +5941,20 @@ function createChart(container, spec, options) {
5380
5941
  destroy,
5381
5942
  get layout() {
5382
5943
  return currentLayout;
5944
+ },
5945
+ getSelectedElement() {
5946
+ return selectedElement;
5947
+ },
5948
+ select(ref) {
5949
+ if (destroyed) return;
5950
+ selectElement(ref);
5951
+ },
5952
+ deselect() {
5953
+ if (destroyed) return;
5954
+ deselectElement();
5955
+ },
5956
+ get isEditing() {
5957
+ return isTextEditingActive;
5383
5958
  }
5384
5959
  };
5385
5960
  }
@@ -6500,6 +7075,7 @@ export {
6500
7075
  createGraph,
6501
7076
  createSimulationWorker,
6502
7077
  createTable,
7078
+ createTextEditOverlay,
6503
7079
  createTooltipManager,
6504
7080
  exportCSV,
6505
7081
  exportJPG,