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

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
@@ -11806,21 +11806,25 @@ function validateMapSpec(spec, errors) {
11806
11806
  }
11807
11807
  const encoding = spec.encoding;
11808
11808
  const hasPoints = spec.points && typeof spec.points === "object" && !Array.isArray(spec.points);
11809
+ const specData = spec.data;
11810
+ const isBasemapOnly = hasPoints && (!specData || specData.length === 0);
11809
11811
  const keyCh = encoding.key;
11810
- if (!keyCh || typeof keyCh !== "object") {
11811
- errors.push({
11812
- message: 'Spec error: map encoding requires "key" channel',
11813
- path: "encoding.key",
11814
- code: "MISSING_FIELD",
11815
- suggestion: 'Add encoding.key, e.g. key: { field: "id", type: "nominal" }'
11816
- });
11817
- } else if (!keyCh.field || typeof keyCh.field !== "string") {
11818
- errors.push({
11819
- message: 'Spec error: encoding.key must have a "field" string',
11820
- path: "encoding.key.field",
11821
- code: "MISSING_FIELD",
11822
- suggestion: 'Add a field name, e.g. key: { field: "id" }'
11823
- });
11812
+ if (!isBasemapOnly) {
11813
+ if (!keyCh || typeof keyCh !== "object") {
11814
+ errors.push({
11815
+ message: 'Spec error: map encoding requires "key" channel',
11816
+ path: "encoding.key",
11817
+ code: "MISSING_FIELD",
11818
+ suggestion: 'Add encoding.key, e.g. key: { field: "id", type: "nominal" }'
11819
+ });
11820
+ } else if (!keyCh.field || typeof keyCh.field !== "string") {
11821
+ errors.push({
11822
+ message: 'Spec error: encoding.key must have a "field" string',
11823
+ path: "encoding.key.field",
11824
+ code: "MISSING_FIELD",
11825
+ suggestion: 'Add a field name, e.g. key: { field: "id" }'
11826
+ });
11827
+ }
11824
11828
  }
11825
11829
  const colorCh = encoding.color;
11826
11830
  if (!hasPoints) {
@@ -11875,6 +11879,17 @@ function validateMapSpec(spec, errors) {
11875
11879
  });
11876
11880
  }
11877
11881
  }
11882
+ for (const ch of ["size", "color", "key"]) {
11883
+ const enc = points[ch];
11884
+ if (enc && typeof enc === "object" && (!enc.field || typeof enc.field !== "string")) {
11885
+ errors.push({
11886
+ message: `Spec error: points.${ch} must have a "field" string when provided`,
11887
+ path: `points.${ch}.field`,
11888
+ code: "MISSING_FIELD",
11889
+ suggestion: `Add a field name, e.g. ${ch}: { field: "myField" }`
11890
+ });
11891
+ }
11892
+ }
11878
11893
  }
11879
11894
  }
11880
11895
  function validateBarListSpec(spec, errors) {
@@ -17882,18 +17897,22 @@ function compileMap(spec, options) {
17882
17897
  }
17883
17898
  }
17884
17899
  }
17885
- const featureObjs = geoFeatures.map((f) => ({
17886
- id: f.id,
17887
- properties: f.properties ?? {}
17888
- }));
17889
- const { joined, warnings: joinWarnings } = joinDataToFeatures(
17890
- featureObjs,
17891
- mapSpec.data,
17892
- mapSpec.encoding.key.field,
17893
- mapSpec.geo.idField
17894
- );
17895
- const filteredJoinWarnings = mapSpec.data.length === 0 && mapSpec.points ? joinWarnings.filter((w) => w.code !== "UNMATCHED_FEATURES") : joinWarnings;
17896
- compileWarnings.push(...filteredJoinWarnings);
17900
+ let joined = /* @__PURE__ */ new Map();
17901
+ if (mapSpec.encoding.key) {
17902
+ const featureObjs = geoFeatures.map((f) => ({
17903
+ id: f.id,
17904
+ properties: f.properties ?? {}
17905
+ }));
17906
+ const joinResult = joinDataToFeatures(
17907
+ featureObjs,
17908
+ mapSpec.data,
17909
+ mapSpec.encoding.key.field,
17910
+ mapSpec.geo.idField
17911
+ );
17912
+ joined = joinResult.joined;
17913
+ const filteredJoinWarnings = mapSpec.data.length === 0 && mapSpec.points ? joinResult.warnings.filter((w) => w.code !== "UNMATCHED_FEATURES") : joinResult.warnings;
17914
+ compileWarnings.push(...filteredJoinWarnings);
17915
+ }
17897
17916
  const droppedFeatures = [];
17898
17917
  const formatter = buildD3Formatter2(mapSpec.valueFormat) ?? formatNumber3;
17899
17918
  const neutralFill = isDarkMode ? "#2a2a2a" : "#e8e8e8";
@@ -17965,6 +17984,7 @@ function compileMap(spec, options) {
17965
17984
  pathGen,
17966
17985
  joined,
17967
17986
  colorField: colorEncoding.field,
17987
+ colorScale: colorEncoding.scale,
17968
17988
  isDarkMode: !!isDarkMode,
17969
17989
  neutralFill,
17970
17990
  neutralStroke,
@@ -18032,7 +18052,14 @@ function compileMap(spec, options) {
18032
18052
  };
18033
18053
  }
18034
18054
  } else {
18035
- const seen = /* @__PURE__ */ new Set();
18055
+ const explicitDomain = pts.color.scale?.domain;
18056
+ const explicitRange = pts.color.scale?.range;
18057
+ if (explicitDomain && Array.isArray(explicitDomain)) {
18058
+ for (const cat of explicitDomain) {
18059
+ pointCategories.push(String(cat));
18060
+ }
18061
+ }
18062
+ const seen = new Set(pointCategories);
18036
18063
  for (const row of pts.data) {
18037
18064
  const raw = row[pts.color.field];
18038
18065
  if (raw != null) {
@@ -18040,21 +18067,23 @@ function compileMap(spec, options) {
18040
18067
  if (!seen.has(cat)) {
18041
18068
  seen.add(cat);
18042
18069
  pointCategories.push(cat);
18043
- pointCategoryColors.set(
18044
- cat,
18045
- CATEGORICAL_PALETTE[(pointCategories.length - 1) % CATEGORICAL_PALETTE.length]
18046
- );
18047
18070
  }
18048
18071
  }
18049
18072
  }
18073
+ const colorSource = explicitRange && Array.isArray(explicitRange) ? explicitRange : CATEGORICAL_PALETTE;
18074
+ for (let i = 0; i < pointCategories.length; i++) {
18075
+ pointCategoryColors.set(pointCategories[i], colorSource[i % colorSource.length]);
18076
+ }
18050
18077
  pointColorScale = (val) => {
18051
- if (val == null) return CATEGORICAL_PALETTE[0];
18052
- return pointCategoryColors.get(String(val)) ?? CATEGORICAL_PALETTE[0];
18078
+ if (val == null) return colorSource[0];
18079
+ return pointCategoryColors.get(String(val)) ?? colorSource[0];
18053
18080
  };
18054
18081
  }
18055
18082
  }
18056
18083
  const defaultFill = CATEGORICAL_PALETTE[0];
18084
+ const pointOpacity = pts.opacity ?? 0.65;
18057
18085
  let animIndex = 0;
18086
+ const droppedPoints = [];
18058
18087
  for (let i = 0; i < pts.data.length; i++) {
18059
18088
  const row = pts.data[i];
18060
18089
  const lon = Number(row[lonField]);
@@ -18062,11 +18091,7 @@ function compileMap(spec, options) {
18062
18091
  if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;
18063
18092
  const projected = projection2([lon, lat]);
18064
18093
  if (!projected) {
18065
- compileWarnings.push({
18066
- code: "POINT_NULL_PROJECTION",
18067
- message: `Point at [${lon}, ${lat}] projects to null and was dropped.`,
18068
- context: { lon, lat }
18069
- });
18094
+ droppedPoints.push([lon, lat]);
18070
18095
  continue;
18071
18096
  }
18072
18097
  const r = sizeScale ? sizeScale.scale(Number(row[sizeScale.field])) : 5;
@@ -18081,12 +18106,21 @@ function compileMap(spec, options) {
18081
18106
  fill,
18082
18107
  stroke,
18083
18108
  strokeWidth: 1,
18109
+ fillOpacity: pointOpacity,
18084
18110
  key,
18085
18111
  data: row,
18086
18112
  aria: { role: "img", label: key },
18087
18113
  animationIndex: animIndex++
18088
18114
  });
18089
18115
  }
18116
+ if (droppedPoints.length > 0) {
18117
+ const sample = droppedPoints.slice(0, 3).map(([lo, la]) => `[${lo}, ${la}]`).join(", ");
18118
+ compileWarnings.push({
18119
+ code: "POINT_NULL_PROJECTION",
18120
+ message: `${droppedPoints.length} point(s) project to null and were dropped (e.g. ${sample}). This is common with albersUsa which excludes territories. Consider using projection: "mercator" or "equalEarth".`,
18121
+ context: { count: droppedPoints.length, samples: droppedPoints.slice(0, 5) }
18122
+ });
18123
+ }
18090
18124
  if (pts.color && pts.color.type !== "quantitative" && pointCategories.length > 0 && showLegend) {
18091
18125
  const labelStyle = {
18092
18126
  fontFamily: theme.fonts.family,
@@ -18100,10 +18134,10 @@ function compileMap(spec, options) {
18100
18134
  color: pointCategoryColors.get(cat),
18101
18135
  shape: "circle"
18102
18136
  }));
18103
- const choroplethLegendHeight = continuousLegend ? continuousLegend.bounds.height : categoricalLegend ? categoricalLegend.bounds.height : 0;
18104
- const choroplethLegendGap = choroplethLegendHeight > 0 ? 8 : 0;
18137
+ const bottomChoroplethHeight = legendPosition === "bottom" ? continuousLegend?.bounds.height ?? categoricalLegend?.bounds.height ?? 0 : 0;
18138
+ const choroplethLegendGap = bottomChoroplethHeight > 0 ? 8 : 0;
18105
18139
  const legendX = fullArea.x;
18106
- const legendY = fullArea.y + mapAreaHeight + legendReserveGap + choroplethLegendHeight + choroplethLegendGap;
18140
+ const legendY = fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethHeight + choroplethLegendGap;
18107
18141
  const legendWidth = fullArea.width;
18108
18142
  const swatchSize = 10;
18109
18143
  const swatchGap = 6;
@@ -18148,10 +18182,10 @@ function compileMap(spec, options) {
18148
18182
  lineHeight: 1.3,
18149
18183
  fontVariant: "tabular-nums"
18150
18184
  };
18151
- const choroplethLegendHeight = continuousLegend ? continuousLegend.bounds.height : categoricalLegend ? categoricalLegend.bounds.height : 0;
18152
- const choroplethLegendGap = choroplethLegendHeight > 0 ? 8 : 0;
18185
+ const bottomChoroplethH = legendPosition === "bottom" ? continuousLegend?.bounds.height ?? categoricalLegend?.bounds.height ?? 0 : 0;
18186
+ const choroplethLegendGap = bottomChoroplethH > 0 ? 8 : 0;
18153
18187
  const legendX = fullArea.x;
18154
- const legendY = fullArea.y + mapAreaHeight + legendReserveGap + choroplethLegendHeight + choroplethLegendGap;
18188
+ const legendY = fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethH + choroplethLegendGap;
18155
18189
  const ptLabelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
18156
18190
  const ptLegendHeight = ptContinuousContent.barHeight + CONTINUOUS_LABEL_GAP + ptLabelRowHeight;
18157
18191
  const bounds = {
@@ -18430,6 +18464,7 @@ function buildCategoricalMarks(opts) {
18430
18464
  pathGen,
18431
18465
  joined,
18432
18466
  colorField,
18467
+ colorScale: scaleConfig,
18433
18468
  isDarkMode,
18434
18469
  neutralFill,
18435
18470
  neutralStroke,
@@ -18441,7 +18476,13 @@ function buildCategoricalMarks(opts) {
18441
18476
  droppedFeatures
18442
18477
  } = opts;
18443
18478
  const categories = [];
18444
- const seen = /* @__PURE__ */ new Set();
18479
+ const explicitDomain = scaleConfig?.domain;
18480
+ if (explicitDomain && Array.isArray(explicitDomain)) {
18481
+ for (const cat of explicitDomain) {
18482
+ categories.push(String(cat));
18483
+ }
18484
+ }
18485
+ const seen = new Set(categories);
18445
18486
  for (const row of joined.values()) {
18446
18487
  const raw = row[colorField];
18447
18488
  if (raw != null) {
@@ -18452,9 +18493,11 @@ function buildCategoricalMarks(opts) {
18452
18493
  }
18453
18494
  }
18454
18495
  }
18496
+ const explicitRange = scaleConfig?.range;
18497
+ const colorSource = explicitRange && Array.isArray(explicitRange) ? explicitRange : CATEGORICAL_PALETTE;
18455
18498
  const categoryColors = /* @__PURE__ */ new Map();
18456
18499
  for (let i = 0; i < categories.length; i++) {
18457
- categoryColors.set(categories[i], CATEGORICAL_PALETTE[i % CATEGORICAL_PALETTE.length]);
18500
+ categoryColors.set(categories[i], colorSource[i % colorSource.length]);
18458
18501
  }
18459
18502
  const marks = [];
18460
18503
  let animIndex = 0;