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

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
  }
@@ -18301,7 +18320,7 @@ function compileMap(spec, options) {
18301
18320
  role: "img",
18302
18321
  keyboardNavigable: featureMarks.length > 0 || pointMarks.length > 0
18303
18322
  };
18304
- const resolvedFocus = resolveFocus(mapSpec.geo.focus, featureMarks, compileWarnings);
18323
+ const resolvedFocus = resolveFocus(mapSpec.geo.focus, featureMarks, pointMarks, compileWarnings);
18305
18324
  const resolvedAnimation = resolveAnimation(mapSpec.animation);
18306
18325
  const mapAreaY = legendPosition === "top" && legendBlockHeight > 0 ? fullArea.y + legendBlockHeight + legendReserveGap : fullArea.y;
18307
18326
  const mapArea = {
@@ -18584,8 +18603,38 @@ function buildCategoricalMarks(opts) {
18584
18603
  }
18585
18604
  return { marks, legend };
18586
18605
  }
18587
- function resolveFocus(focus, features, warnings) {
18606
+ function resolveFocus(focus, features, pointMarks, warnings) {
18588
18607
  if (focus === null || focus === void 0) return null;
18608
+ if (typeof focus === "object" && !Array.isArray(focus) && "points" in focus) {
18609
+ const filter = focus.points;
18610
+ const matched = filter === true ? pointMarks : pointMarks.filter((p) => p.data[filter.field] === filter.value);
18611
+ if (matched.length === 0) {
18612
+ const detail = filter === true ? "the map has no points to fit" : `no points match { field: "${filter.field}", value: ${JSON.stringify(filter.value)} }`;
18613
+ warnings.push({
18614
+ code: "FOCUS_UNMATCHED",
18615
+ message: `geo.focus.points is set but ${detail}`,
18616
+ context: {}
18617
+ });
18618
+ return null;
18619
+ }
18620
+ const padding2 = focus.padding ?? 16;
18621
+ let minX2 = Infinity;
18622
+ let minY2 = Infinity;
18623
+ let maxX2 = -Infinity;
18624
+ let maxY2 = -Infinity;
18625
+ for (const p of matched) {
18626
+ minX2 = Math.min(minX2, p.cx - p.r);
18627
+ minY2 = Math.min(minY2, p.cy - p.r);
18628
+ maxX2 = Math.max(maxX2, p.cx + p.r);
18629
+ maxY2 = Math.max(maxY2, p.cy + p.r);
18630
+ }
18631
+ return {
18632
+ target: { x: minX2, y: minY2, width: maxX2 - minX2, height: maxY2 - minY2, padding: padding2 },
18633
+ // No feature ids: focus-dim is a feature concept, so a points focus
18634
+ // dims no features (there are no "other" features to mute).
18635
+ ids: []
18636
+ };
18637
+ }
18589
18638
  let ids;
18590
18639
  let padding = 16;
18591
18640
  if (typeof focus === "string" || typeof focus === "number") {