@opendata-ai/openchart-engine 8.0.0-rc.4 → 8.0.0-rc.6

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
@@ -11758,12 +11758,31 @@ function validateMapSpec(spec, errors) {
11758
11758
  }
11759
11759
  } else if (typeof focus === "object") {
11760
11760
  const focusObj = focus;
11761
- if (!("features" in focusObj) || focusObj.features == null) {
11761
+ if ("points" in focusObj) {
11762
+ const pts = focusObj.points;
11763
+ const isSubset = typeof pts === "object" && pts !== null && typeof pts.field === "string" && "value" in pts;
11764
+ if (pts !== true && !isSubset) {
11765
+ errors.push({
11766
+ message: "Spec error: geo.focus.points must be true or { field: string, value: string | number }",
11767
+ path: "geo.focus.points",
11768
+ code: "INVALID_TYPE",
11769
+ suggestion: 'Use { points: true } to fit all points, or { points: { field: "rating", value: "F" } } to fit a subset.'
11770
+ });
11771
+ }
11772
+ if (focusObj.padding !== void 0 && typeof focusObj.padding !== "number") {
11773
+ errors.push({
11774
+ message: "Spec error: geo.focus.padding must be a number",
11775
+ path: "geo.focus.padding",
11776
+ code: "INVALID_TYPE",
11777
+ suggestion: "Provide padding as a number in map-local units, e.g. padding: 16"
11778
+ });
11779
+ }
11780
+ } else if (!("features" in focusObj) || focusObj.features == null) {
11762
11781
  errors.push({
11763
- message: 'Spec error: geo.focus object must have a "features" property',
11782
+ message: 'Spec error: geo.focus object must have a "features" or "points" property',
11764
11783
  path: "geo.focus.features",
11765
11784
  code: "MISSING_FIELD",
11766
- suggestion: 'Provide feature id(s) to focus on, e.g. geo: { focus: { features: "36", padding: 32 } }'
11785
+ suggestion: 'Provide feature id(s) to focus on, e.g. { features: "36", padding: 32 }, or { points: true } to fit the point cluster.'
11767
11786
  });
11768
11787
  } else {
11769
11788
  const feats = focusObj.features;
@@ -11797,10 +11816,10 @@ function validateMapSpec(spec, errors) {
11797
11816
  }
11798
11817
  } else {
11799
11818
  errors.push({
11800
- message: "Spec error: geo.focus must be a string, number, array of ids, or object with { features, padding? }",
11819
+ message: "Spec error: geo.focus must be a string, number, array of ids, or object with { features, padding? } or { points: true, padding? }",
11801
11820
  path: "geo.focus",
11802
11821
  code: "INVALID_TYPE",
11803
- suggestion: 'Use a feature id, array of ids, or { features: "36", padding: 16 }. Pass null to clear focus.'
11822
+ suggestion: 'Use a feature id, array of ids, { features: "36", padding: 16 }, or { points: true }. Pass null to clear focus.'
11804
11823
  });
11805
11824
  }
11806
11825
  }
@@ -17992,6 +18011,7 @@ function compileMap(spec, options) {
17992
18011
  fullArea,
17993
18012
  mapAreaHeight,
17994
18013
  legendReserveGap,
18014
+ legendPosition,
17995
18015
  showLegend,
17996
18016
  droppedFeatures
17997
18017
  });
@@ -18301,7 +18321,7 @@ function compileMap(spec, options) {
18301
18321
  role: "img",
18302
18322
  keyboardNavigable: featureMarks.length > 0 || pointMarks.length > 0
18303
18323
  };
18304
- const resolvedFocus = resolveFocus(mapSpec.geo.focus, featureMarks, compileWarnings);
18324
+ const resolvedFocus = resolveFocus(mapSpec.geo.focus, featureMarks, pointMarks, compileWarnings);
18305
18325
  const resolvedAnimation = resolveAnimation(mapSpec.animation);
18306
18326
  const mapAreaY = legendPosition === "top" && legendBlockHeight > 0 ? fullArea.y + legendBlockHeight + legendReserveGap : fullArea.y;
18307
18327
  const mapArea = {
@@ -18472,6 +18492,7 @@ function buildCategoricalMarks(opts) {
18472
18492
  fullArea,
18473
18493
  mapAreaHeight,
18474
18494
  legendReserveGap,
18495
+ legendPosition,
18475
18496
  showLegend,
18476
18497
  droppedFeatures
18477
18498
  } = opts;
@@ -18565,14 +18586,14 @@ function buildCategoricalMarks(opts) {
18565
18586
  shape: "square"
18566
18587
  }));
18567
18588
  const legendX = fullArea.x;
18568
- const legendY = fullArea.y + mapAreaHeight + legendReserveGap;
18589
+ const legendY = legendPosition === "bottom" ? fullArea.y + mapAreaHeight + legendReserveGap : fullArea.y + legendReserveGap;
18569
18590
  const legendWidth = fullArea.width;
18570
18591
  const swatchSize = 10;
18571
18592
  const swatchGap = 6;
18572
18593
  const entryGap = 16;
18573
18594
  legend = {
18574
18595
  type: "categorical",
18575
- position: "bottom",
18596
+ position: legendPosition,
18576
18597
  bounds: { x: legendX, y: legendY, width: legendWidth, height: swatchSize + 6 },
18577
18598
  labelStyle,
18578
18599
  entries,
@@ -18584,8 +18605,38 @@ function buildCategoricalMarks(opts) {
18584
18605
  }
18585
18606
  return { marks, legend };
18586
18607
  }
18587
- function resolveFocus(focus, features, warnings) {
18608
+ function resolveFocus(focus, features, pointMarks, warnings) {
18588
18609
  if (focus === null || focus === void 0) return null;
18610
+ if (typeof focus === "object" && !Array.isArray(focus) && "points" in focus) {
18611
+ const filter = focus.points;
18612
+ const matched = filter === true ? pointMarks : pointMarks.filter((p) => p.data[filter.field] === filter.value);
18613
+ if (matched.length === 0) {
18614
+ const detail = filter === true ? "the map has no points to fit" : `no points match { field: "${filter.field}", value: ${JSON.stringify(filter.value)} }`;
18615
+ warnings.push({
18616
+ code: "FOCUS_UNMATCHED",
18617
+ message: `geo.focus.points is set but ${detail}`,
18618
+ context: {}
18619
+ });
18620
+ return null;
18621
+ }
18622
+ const padding2 = focus.padding ?? 16;
18623
+ let minX2 = Infinity;
18624
+ let minY2 = Infinity;
18625
+ let maxX2 = -Infinity;
18626
+ let maxY2 = -Infinity;
18627
+ for (const p of matched) {
18628
+ minX2 = Math.min(minX2, p.cx - p.r);
18629
+ minY2 = Math.min(minY2, p.cy - p.r);
18630
+ maxX2 = Math.max(maxX2, p.cx + p.r);
18631
+ maxY2 = Math.max(maxY2, p.cy + p.r);
18632
+ }
18633
+ return {
18634
+ target: { x: minX2, y: minY2, width: maxX2 - minX2, height: maxY2 - minY2, padding: padding2 },
18635
+ // No feature ids: focus-dim is a feature concept, so a points focus
18636
+ // dims no features (there are no "other" features to mute).
18637
+ ids: []
18638
+ };
18639
+ }
18589
18640
  let ids;
18590
18641
  let padding = 16;
18591
18642
  if (typeof focus === "string" || typeof focus === "number") {