@opendata-ai/openchart-engine 8.0.0-rc.5 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-engine",
3
- "version": "8.0.0-rc.5",
3
+ "version": "8.0.0-rc.6",
4
4
  "description": "Headless compiler for openchart: spec validation, data compilation, scales, and layout",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Riley Hilliard",
@@ -48,7 +48,7 @@
48
48
  "typecheck": "tsc --noEmit"
49
49
  },
50
50
  "dependencies": {
51
- "@opendata-ai/openchart-core": "8.0.0-rc.5",
51
+ "@opendata-ai/openchart-core": "8.0.0-rc.6",
52
52
  "d3-array": "^3.2.0",
53
53
  "d3-format": "^3.1.2",
54
54
  "d3-interpolate": "^3.0.0",
@@ -296,6 +296,55 @@ describe('compileMap', () => {
296
296
  expect(layout.continuousLegend).toBeNull();
297
297
  });
298
298
 
299
+ it('categorical legend defaults to top and sits above the map area', () => {
300
+ const layout = compileMap(
301
+ {
302
+ type: 'map',
303
+ geo: { features: MINI_TOPO, projection: 'mercator' },
304
+ data: [
305
+ { fips: '06', category: 'west' },
306
+ { fips: '48', category: 'south' },
307
+ ],
308
+ encoding: {
309
+ key: { field: 'fips', type: 'nominal' },
310
+ color: { field: 'category', type: 'nominal' },
311
+ },
312
+ },
313
+ DEFAULT_OPTIONS,
314
+ );
315
+
316
+ const legend = layout.categoricalLegend!;
317
+ expect(legend.position).toBe('top');
318
+ // The map area shifts down to make room for a top legend; the legend row
319
+ // must land above it, not inside it (the old bottom-anchored legend
320
+ // overlapped the map because the two disagreed about the layout).
321
+ expect(legend.bounds.y + legend.bounds.height).toBeLessThanOrEqual(layout.area.y);
322
+ });
323
+
324
+ it('honors position: bottom for categorical legend', () => {
325
+ const layout = compileMap(
326
+ {
327
+ type: 'map',
328
+ geo: { features: MINI_TOPO, projection: 'mercator' },
329
+ data: [
330
+ { fips: '06', category: 'west' },
331
+ { fips: '48', category: 'south' },
332
+ ],
333
+ encoding: {
334
+ key: { field: 'fips', type: 'nominal' },
335
+ color: { field: 'category', type: 'nominal' },
336
+ },
337
+ legend: { position: 'bottom' },
338
+ },
339
+ DEFAULT_OPTIONS,
340
+ );
341
+
342
+ const legend = layout.categoricalLegend!;
343
+ expect(legend.position).toBe('bottom');
344
+ // Bottom legend starts at or below the map area's bottom edge.
345
+ expect(legend.bounds.y).toBeGreaterThanOrEqual(layout.area.y + layout.area.height);
346
+ });
347
+
299
348
  it('assigns neutral fill to features without data', () => {
300
349
  const layout = compileMap(
301
350
  {
@@ -360,6 +360,7 @@ export function compileMap(spec: unknown, options: CompileOptions): MapLayout {
360
360
  fullArea,
361
361
  mapAreaHeight,
362
362
  legendReserveGap,
363
+ legendPosition,
363
364
  showLegend,
364
365
  droppedFeatures,
365
366
  });
@@ -1010,6 +1011,7 @@ interface CategoricalOptions {
1010
1011
  fullArea: { x: number; y: number; width: number; height: number };
1011
1012
  mapAreaHeight: number;
1012
1013
  legendReserveGap: number;
1014
+ legendPosition: 'top' | 'bottom';
1013
1015
  showLegend: boolean;
1014
1016
  droppedFeatures: string[];
1015
1017
  }
@@ -1031,6 +1033,7 @@ function buildCategoricalMarks(opts: CategoricalOptions): {
1031
1033
  fullArea,
1032
1034
  mapAreaHeight,
1033
1035
  legendReserveGap,
1036
+ legendPosition,
1034
1037
  showLegend,
1035
1038
  droppedFeatures,
1036
1039
  } = opts;
@@ -1142,8 +1145,15 @@ function buildCategoricalMarks(opts: CategoricalOptions): {
1142
1145
  shape: 'square' as const,
1143
1146
  }));
1144
1147
 
1148
+ // Honor legend.position like the continuous path does. The map drawing
1149
+ // area (mapAreaY in compileMap) already shifts down for top legends, so
1150
+ // the legend row here must agree with it — a bottom-anchored legend with
1151
+ // a top-shifted map is how legends used to land on top of Alaska.
1145
1152
  const legendX = fullArea.x;
1146
- const legendY = fullArea.y + mapAreaHeight + legendReserveGap;
1153
+ const legendY =
1154
+ legendPosition === 'bottom'
1155
+ ? fullArea.y + mapAreaHeight + legendReserveGap
1156
+ : fullArea.y + legendReserveGap;
1147
1157
  const legendWidth = fullArea.width;
1148
1158
  const swatchSize = 10;
1149
1159
  const swatchGap = 6;
@@ -1151,7 +1161,7 @@ function buildCategoricalMarks(opts: CategoricalOptions): {
1151
1161
 
1152
1162
  legend = {
1153
1163
  type: 'categorical',
1154
- position: 'bottom',
1164
+ position: legendPosition,
1155
1165
  bounds: { x: legendX, y: legendY, width: legendWidth, height: swatchSize + 6 },
1156
1166
  labelStyle,
1157
1167
  entries,