@opendata-ai/openchart-engine 6.27.2 → 6.28.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
@@ -1,17 +1,17 @@
1
1
  // src/compile.ts
2
2
  import {
3
3
  AXIS_TITLE_TRAILING_PAD as AXIS_TITLE_TRAILING_PAD2,
4
- adaptTheme as adaptTheme4,
4
+ adaptTheme as adaptTheme5,
5
5
  BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX2,
6
6
  computeLabelBounds,
7
- estimateTextWidth as estimateTextWidth15,
7
+ estimateTextWidth as estimateTextWidth16,
8
8
  generateAltText,
9
9
  generateDataTable,
10
10
  getAxisTitleOffset as getAxisTitleOffset2,
11
11
  getBreakpoint,
12
12
  getHeightClass,
13
13
  getLayoutStrategy,
14
- resolveTheme as resolveTheme4,
14
+ resolveTheme as resolveTheme5,
15
15
  TICK_LABEL_OFFSET
16
16
  } from "@opendata-ai/openchart-core";
17
17
 
@@ -4312,8 +4312,9 @@ function computeBarMarks(spec, scales, _chartArea, _strategy) {
4312
4312
  const conditionalColor = encoding.color && isConditionalValueDef(encoding.color) ? encoding.color : void 0;
4313
4313
  const colorField = colorEnc?.field;
4314
4314
  const isSequentialColor = colorEnc?.type === "quantitative";
4315
+ let marks;
4315
4316
  if (!colorField || isSequentialColor) {
4316
- return computeSimpleBars(
4317
+ marks = computeSimpleBars(
4317
4318
  spec.data,
4318
4319
  xChannel.field,
4319
4320
  yChannel.field,
@@ -4325,13 +4326,40 @@ function computeBarMarks(spec, scales, _chartArea, _strategy) {
4325
4326
  isSequentialColor,
4326
4327
  conditionalColor
4327
4328
  );
4328
- }
4329
- const categoryGroups = groupByField(spec.data, yChannel.field);
4330
- const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
4331
- if (needsStacking) {
4332
- const stackDisabled = xChannel.stack === null || xChannel.stack === false;
4333
- if (stackDisabled) {
4334
- return computeGroupedBars(
4329
+ } else {
4330
+ const categoryGroups = groupByField(spec.data, yChannel.field);
4331
+ const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
4332
+ if (needsStacking) {
4333
+ const stackDisabled = xChannel.stack === null || xChannel.stack === false;
4334
+ if (stackDisabled) {
4335
+ marks = computeGroupedBars(
4336
+ spec.data,
4337
+ xChannel.field,
4338
+ yChannel.field,
4339
+ colorField,
4340
+ xScale,
4341
+ yScale,
4342
+ bandwidth,
4343
+ baseline,
4344
+ scales
4345
+ );
4346
+ } else {
4347
+ const stackMode = xChannel.stack === "normalize" ? "normalize" : xChannel.stack === "center" ? "center" : "zero";
4348
+ marks = computeStackedBars(
4349
+ spec.data,
4350
+ xChannel.field,
4351
+ yChannel.field,
4352
+ colorField,
4353
+ xScale,
4354
+ yScale,
4355
+ bandwidth,
4356
+ baseline,
4357
+ scales,
4358
+ stackMode
4359
+ );
4360
+ }
4361
+ } else {
4362
+ marks = computeColoredBars(
4335
4363
  spec.data,
4336
4364
  xChannel.field,
4337
4365
  yChannel.field,
@@ -4343,31 +4371,8 @@ function computeBarMarks(spec, scales, _chartArea, _strategy) {
4343
4371
  scales
4344
4372
  );
4345
4373
  }
4346
- const stackMode = xChannel.stack === "normalize" ? "normalize" : xChannel.stack === "center" ? "center" : "zero";
4347
- return computeStackedBars(
4348
- spec.data,
4349
- xChannel.field,
4350
- yChannel.field,
4351
- colorField,
4352
- xScale,
4353
- yScale,
4354
- bandwidth,
4355
- baseline,
4356
- scales,
4357
- stackMode
4358
- );
4359
4374
  }
4360
- return computeColoredBars(
4361
- spec.data,
4362
- xChannel.field,
4363
- yChannel.field,
4364
- colorField,
4365
- xScale,
4366
- yScale,
4367
- bandwidth,
4368
- baseline,
4369
- scales
4370
- );
4375
+ return applyMarkDefOverrides(marks, spec, bandwidth);
4371
4376
  }
4372
4377
  function computeStackedBars(data, valueField, categoryField, colorField, xScale, yScale, bandwidth, _baseline, scales, stackMode = "zero") {
4373
4378
  const marks = [];
@@ -4486,6 +4491,27 @@ function computeColoredBars(data, valueField, categoryField, colorField, xScale,
4486
4491
  }
4487
4492
  return marks;
4488
4493
  }
4494
+ function applyMarkDefOverrides(marks, spec, bandwidth) {
4495
+ const { markDef } = spec;
4496
+ const fixedSize = markDef.size;
4497
+ const crSpec = markDef.cornerRadius;
4498
+ if (fixedSize == null && crSpec == null) return marks;
4499
+ for (const mark of marks) {
4500
+ if (fixedSize != null && mark.stackGroup === void 0) {
4501
+ const barHeight = Math.min(fixedSize, bandwidth);
4502
+ const offset = (bandwidth - barHeight) / 2;
4503
+ mark.y = mark.y + offset;
4504
+ mark.height = barHeight;
4505
+ }
4506
+ const effectiveHeight = mark.height;
4507
+ if (crSpec === "pill") {
4508
+ mark.cornerRadius = effectiveHeight / 2;
4509
+ } else if (typeof crSpec === "number") {
4510
+ mark.cornerRadius = crSpec;
4511
+ }
4512
+ }
4513
+ return marks;
4514
+ }
4489
4515
  function computeSimpleBars(data, valueField, categoryField, xScale, yScale, bandwidth, baseline, scales, sequentialColor = false, conditionalColor) {
4490
4516
  const marks = [];
4491
4517
  for (const row of data) {
@@ -4572,7 +4598,7 @@ var LABEL_FONT_SIZE = 11;
4572
4598
  var LABEL_FONT_WEIGHT = 600;
4573
4599
  var LABEL_PADDING = 6;
4574
4600
  var MIN_WIDTH_FOR_INSIDE_LABEL = 40;
4575
- function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField) {
4601
+ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField, labelColor) {
4576
4602
  const targetMarks = filterByDensity(marks, density);
4577
4603
  const candidates = [];
4578
4604
  const fitsInSegment = [];
@@ -4623,11 +4649,11 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
4623
4649
  } else {
4624
4650
  if (isNegative) {
4625
4651
  anchorX = mark.x - LABEL_PADDING;
4626
- fill = getRepresentativeColor(mark.fill);
4652
+ fill = labelColor ?? getRepresentativeColor(mark.fill);
4627
4653
  textAnchor = "end";
4628
4654
  } else {
4629
4655
  anchorX = mark.x + mark.width + LABEL_PADDING;
4630
- fill = getRepresentativeColor(mark.fill);
4656
+ fill = labelColor ?? getRepresentativeColor(mark.fill);
4631
4657
  textAnchor = "start";
4632
4658
  }
4633
4659
  }
@@ -4700,7 +4726,8 @@ var barRenderer = (spec, scales, chartArea, strategy, _theme) => {
4700
4726
  spec.labels.density,
4701
4727
  spec.labels.format,
4702
4728
  spec.labels.prefix,
4703
- valueField
4729
+ valueField,
4730
+ spec.labels.color
4704
4731
  );
4705
4732
  for (let i = 0; i < marks.length && i < labels.length; i++) {
4706
4733
  marks[i].label = labels[i];
@@ -4729,13 +4756,14 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
4729
4756
  const conditionalColor = encoding.color && isConditionalValueDef(encoding.color) ? encoding.color : void 0;
4730
4757
  const colorField = colorEnc?.field;
4731
4758
  const isSequentialColor = colorEnc?.type === "quantitative";
4759
+ let marks;
4732
4760
  if (colorField && !isSequentialColor) {
4733
4761
  const categoryGroups = groupByField(spec.data, xChannel.field);
4734
4762
  const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
4735
4763
  if (needsStacking) {
4736
4764
  const stackDisabled = yChannel.stack === null || yChannel.stack === false;
4737
4765
  if (stackDisabled) {
4738
- return computeGroupedColumns(
4766
+ marks = computeGroupedColumns(
4739
4767
  spec.data,
4740
4768
  xChannel.field,
4741
4769
  yChannel.field,
@@ -4746,9 +4774,23 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
4746
4774
  baseline,
4747
4775
  scales
4748
4776
  );
4777
+ } else {
4778
+ const stackMode = yChannel.stack === "normalize" ? "normalize" : yChannel.stack === "center" ? "center" : "zero";
4779
+ marks = computeStackedColumns(
4780
+ spec.data,
4781
+ xChannel.field,
4782
+ yChannel.field,
4783
+ colorField,
4784
+ xScale,
4785
+ yScale,
4786
+ bandwidth,
4787
+ baseline,
4788
+ scales,
4789
+ stackMode
4790
+ );
4749
4791
  }
4750
- const stackMode = yChannel.stack === "normalize" ? "normalize" : yChannel.stack === "center" ? "center" : "zero";
4751
- return computeStackedColumns(
4792
+ } else {
4793
+ marks = computeColoredColumns(
4752
4794
  spec.data,
4753
4795
  xChannel.field,
4754
4796
  yChannel.field,
@@ -4757,34 +4799,24 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
4757
4799
  yScale,
4758
4800
  bandwidth,
4759
4801
  baseline,
4760
- scales,
4761
- stackMode
4802
+ scales
4762
4803
  );
4763
4804
  }
4764
- return computeColoredColumns(
4805
+ } else {
4806
+ marks = computeSimpleColumns(
4765
4807
  spec.data,
4766
4808
  xChannel.field,
4767
4809
  yChannel.field,
4768
- colorField,
4769
4810
  xScale,
4770
4811
  yScale,
4771
4812
  bandwidth,
4772
4813
  baseline,
4773
- scales
4814
+ scales,
4815
+ isSequentialColor,
4816
+ conditionalColor
4774
4817
  );
4775
4818
  }
4776
- return computeSimpleColumns(
4777
- spec.data,
4778
- xChannel.field,
4779
- yChannel.field,
4780
- xScale,
4781
- yScale,
4782
- bandwidth,
4783
- baseline,
4784
- scales,
4785
- isSequentialColor,
4786
- conditionalColor
4787
- );
4819
+ return applyMarkDefOverrides2(marks, spec, bandwidth);
4788
4820
  }
4789
4821
  function computeSimpleColumns(data, categoryField, valueField, xScale, yScale, bandwidth, baseline, scales, sequentialColor = false, conditionalColor) {
4790
4822
  const marks = [];
@@ -4950,6 +4982,27 @@ function computeStackedColumns(data, categoryField, valueField, colorField, xSca
4950
4982
  }
4951
4983
  return marks;
4952
4984
  }
4985
+ function applyMarkDefOverrides2(marks, spec, bandwidth) {
4986
+ const { markDef } = spec;
4987
+ const fixedSize = markDef.size;
4988
+ const crSpec = markDef.cornerRadius;
4989
+ if (fixedSize == null && crSpec == null) return marks;
4990
+ for (const mark of marks) {
4991
+ if (fixedSize != null && mark.stackGroup === void 0) {
4992
+ const barWidth = Math.min(fixedSize, bandwidth);
4993
+ const offset = (bandwidth - barWidth) / 2;
4994
+ mark.x = mark.x + offset;
4995
+ mark.width = barWidth;
4996
+ }
4997
+ const effectiveWidth = mark.width;
4998
+ if (crSpec === "pill") {
4999
+ mark.cornerRadius = effectiveWidth / 2;
5000
+ } else if (typeof crSpec === "number") {
5001
+ mark.cornerRadius = crSpec;
5002
+ }
5003
+ }
5004
+ return marks;
5005
+ }
4953
5006
 
4954
5007
  // src/charts/column/labels.ts
4955
5008
  import {
@@ -4961,7 +5014,7 @@ import {
4961
5014
  var LABEL_FONT_SIZE2 = 10;
4962
5015
  var LABEL_FONT_WEIGHT2 = 600;
4963
5016
  var LABEL_OFFSET_Y = 6;
4964
- function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField) {
5017
+ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField, labelColor) {
4965
5018
  const targetMarks = filterByDensity(marks, density);
4966
5019
  const formatter = buildD3Formatter2(labelFormat);
4967
5020
  const candidates = [];
@@ -5002,7 +5055,7 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, l
5002
5055
  fontFamily: "system-ui, -apple-system, sans-serif",
5003
5056
  fontSize: LABEL_FONT_SIZE2,
5004
5057
  fontWeight: LABEL_FONT_WEIGHT2,
5005
- fill: getRepresentativeColor2(mark.fill),
5058
+ fill: labelColor ?? getRepresentativeColor2(mark.fill),
5006
5059
  lineHeight: 1.2,
5007
5060
  textAnchor: "middle",
5008
5061
  dominantBaseline: isNegative ? "hanging" : "auto"
@@ -5032,7 +5085,8 @@ var columnRenderer = (spec, scales, chartArea, strategy, _theme) => {
5032
5085
  spec.labels.density,
5033
5086
  spec.labels.format,
5034
5087
  spec.labels.prefix,
5035
- valueField
5088
+ valueField,
5089
+ spec.labels.color
5036
5090
  );
5037
5091
  for (let i = 0; i < marks.length && i < labels.length; i++) {
5038
5092
  marks[i].label = labels[i];
@@ -5355,9 +5409,26 @@ function computeSingleArea(spec, scales, _chartArea) {
5355
5409
  const ariaLabel = seriesKey === "__default__" ? `Area with ${validPoints.length} data points` : `${seriesKey}: area with ${validPoints.length} data points`;
5356
5410
  const aria = { label: ariaLabel };
5357
5411
  const markFill = spec.markDef.fill;
5358
- const fillValue = markFill != null ? markFill : color2;
5359
- const defaultFillOpacity = y2Channel ? 0.25 : DEFAULT_FILL_OPACITY;
5360
- const fillOpacity = isGradientDef3(fillValue) ? 1 : spec.markDef.opacity ?? defaultFillOpacity;
5412
+ let fillValue;
5413
+ let fillOpacity;
5414
+ if (markFill != null) {
5415
+ fillValue = markFill;
5416
+ fillOpacity = isGradientDef3(markFill) ? 1 : spec.markDef.opacity ?? (y2Channel ? 0.25 : DEFAULT_FILL_OPACITY);
5417
+ } else {
5418
+ const colorStr = getRepresentativeColor4(color2);
5419
+ fillValue = {
5420
+ gradient: "linear",
5421
+ x1: 0,
5422
+ y1: 0,
5423
+ x2: 0,
5424
+ y2: 1,
5425
+ stops: [
5426
+ { offset: 0, color: colorStr, opacity: 0.12 },
5427
+ { offset: 1, color: colorStr, opacity: 0 }
5428
+ ]
5429
+ };
5430
+ fillOpacity = 1;
5431
+ }
5361
5432
  marks.push({
5362
5433
  type: "area",
5363
5434
  topPoints,
@@ -5367,7 +5438,7 @@ function computeSingleArea(spec, scales, _chartArea) {
5367
5438
  fill: fillValue,
5368
5439
  fillOpacity,
5369
5440
  stroke: getRepresentativeColor4(isGradientDef3(fillValue) ? color2 : fillValue),
5370
- strokeWidth: spec.display === "sparkline" ? 1.25 : 2,
5441
+ strokeWidth: spec.markDef.strokeWidth ?? (spec.display === "sparkline" ? 1.25 : 1.5),
5371
5442
  seriesKey: seriesKey === "__default__" ? void 0 : seriesKey,
5372
5443
  data: validPoints.map((p) => p.row),
5373
5444
  dataPoints: validPoints.map((p) => ({ x: p.x, y: p.yTop, datum: p.row })),
@@ -5483,7 +5554,7 @@ function computeAreaMarks(spec, scales, chartArea) {
5483
5554
 
5484
5555
  // src/charts/line/compute.ts
5485
5556
  import { getRepresentativeColor as getRepresentativeColor5 } from "@opendata-ai/openchart-core";
5486
- var DEFAULT_STROKE_WIDTH = 2.5;
5557
+ var DEFAULT_STROKE_WIDTH = 1.5;
5487
5558
  var SPARKLINE_STROKE_WIDTH = 1.25;
5488
5559
  var DEFAULT_POINT_RADIUS = 3;
5489
5560
  function computeLineMarks(spec, scales, _chartArea, _strategy) {
@@ -5551,7 +5622,7 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
5551
5622
  points: allPoints,
5552
5623
  path: combinedPath,
5553
5624
  stroke: strokeColor,
5554
- strokeWidth: styleOverride?.strokeWidth ?? (spec.display === "sparkline" ? SPARKLINE_STROKE_WIDTH : DEFAULT_STROKE_WIDTH),
5625
+ strokeWidth: styleOverride?.strokeWidth ?? spec.markDef.strokeWidth ?? (spec.display === "sparkline" ? SPARKLINE_STROKE_WIDTH : DEFAULT_STROKE_WIDTH),
5555
5626
  strokeDasharray,
5556
5627
  opacity: styleOverride?.opacity,
5557
5628
  seriesKey: seriesStyleKey,
@@ -5561,25 +5632,30 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
5561
5632
  };
5562
5633
  marks.push(lineMark);
5563
5634
  const markPoint = spec.markDef.point;
5564
- const showPoints = markPoint === true || markPoint === "transparent" || isSequentialColor;
5635
+ const showPoints = markPoint === true || markPoint === "transparent" || markPoint === "endpoints" || isSequentialColor;
5565
5636
  if (showPoints) {
5566
5637
  const isTransparent = markPoint === "transparent";
5638
+ const isEndpoints = markPoint === "endpoints";
5567
5639
  const seriesShowPoints = styleOverride?.showPoints !== false;
5640
+ const lastIdx = pointsWithData.length - 1;
5568
5641
  for (let i = 0; i < pointsWithData.length; i++) {
5569
5642
  const p = pointsWithData[i];
5570
- const visible = seriesShowPoints && !isTransparent;
5643
+ const isEndpoint = i === 0 || i === lastIdx;
5644
+ const visible = seriesShowPoints && !isTransparent && (!isEndpoints || isEndpoint);
5571
5645
  let pointColor = color2;
5572
5646
  if (isSequentialColor) {
5573
5647
  const val = Number(p.row[sequentialColorField]);
5574
5648
  pointColor = Number.isFinite(val) ? getSequentialColor(scales, val) : color2;
5575
5649
  }
5650
+ const hollow = isEndpoints && visible;
5651
+ const pointColorStr = getRepresentativeColor5(pointColor);
5576
5652
  const pointMark = {
5577
5653
  type: "point",
5578
5654
  cx: p.x,
5579
5655
  cy: p.y,
5580
5656
  r: visible ? DEFAULT_POINT_RADIUS : 0,
5581
- fill: pointColor,
5582
- stroke: visible ? "#ffffff" : "transparent",
5657
+ fill: hollow ? "transparent" : pointColorStr,
5658
+ stroke: hollow ? pointColorStr : visible ? "#ffffff" : "transparent",
5583
5659
  strokeWidth: visible ? 1.5 : 0,
5584
5660
  fillOpacity: isTransparent ? 0 : 1,
5585
5661
  data: p.row,
@@ -6309,262 +6385,90 @@ function registerBuiltinRenderers() {
6309
6385
  }
6310
6386
  registerBuiltinRenderers();
6311
6387
 
6312
- // src/charts/post-process.ts
6313
- function computeMarkObstacles(marks, scales) {
6314
- if (scales.y?.type === "band") {
6315
- return computeBandRowObstacles(marks, scales);
6388
+ // src/barlist/compile-barlist.ts
6389
+ import {
6390
+ adaptTheme,
6391
+ buildD3Formatter as buildD3Formatter4,
6392
+ computeChrome,
6393
+ estimateTextWidth as estimateTextWidth7,
6394
+ formatNumber as formatNumber2,
6395
+ resolveTheme
6396
+ } from "@opendata-ai/openchart-core";
6397
+
6398
+ // src/compiler/animation.ts
6399
+ var ENTER_DEFAULTS = {
6400
+ duration: 500,
6401
+ ease: "smooth",
6402
+ staggerDelay: 80,
6403
+ staggerOrder: "index",
6404
+ annotationDelay: 200
6405
+ };
6406
+ var MAX_TOTAL_STAGGER_MS = 2e3;
6407
+ function resolveAnimation(spec) {
6408
+ if (spec === void 0 || spec === false) return void 0;
6409
+ if (spec === true) {
6410
+ return {
6411
+ enabled: true,
6412
+ duration: ENTER_DEFAULTS.duration,
6413
+ ease: ENTER_DEFAULTS.ease,
6414
+ staggerDelay: ENTER_DEFAULTS.staggerDelay,
6415
+ staggerOrder: ENTER_DEFAULTS.staggerOrder,
6416
+ annotationDelay: ENTER_DEFAULTS.annotationDelay
6417
+ };
6316
6418
  }
6317
- const obstacles = [];
6318
- for (const mark of marks) {
6319
- if (mark.type === "rect") {
6320
- const rm = mark;
6321
- obstacles.push({ x: rm.x, y: rm.y, width: rm.width, height: rm.height });
6322
- } else if (mark.type === "point") {
6323
- const pm = mark;
6324
- obstacles.push({
6325
- x: pm.cx - pm.r,
6326
- y: pm.cy - pm.r,
6327
- width: pm.r * 2,
6328
- height: pm.r * 2
6329
- });
6330
- }
6419
+ const config = spec;
6420
+ if (config.enter === false || config.enter === void 0 && !hasAnyPhase(config)) {
6421
+ return void 0;
6331
6422
  }
6332
- return obstacles;
6423
+ const enterConfig = resolvePhaseConfig(config.enter);
6424
+ return {
6425
+ enabled: true,
6426
+ duration: enterConfig.duration,
6427
+ ease: enterConfig.ease,
6428
+ staggerDelay: enterConfig.staggerDelay,
6429
+ staggerOrder: enterConfig.staggerOrder,
6430
+ annotationDelay: config.annotationDelay ?? ENTER_DEFAULTS.annotationDelay
6431
+ };
6333
6432
  }
6334
- function computeBandRowObstacles(marks, scales) {
6335
- const rows = /* @__PURE__ */ new Map();
6336
- for (const mark of marks) {
6337
- let cy;
6338
- let left2;
6339
- let right2;
6340
- if (mark.type === "point") {
6341
- const pm = mark;
6342
- cy = pm.cy;
6343
- left2 = pm.cx - pm.r;
6344
- right2 = pm.cx + pm.r;
6345
- } else if (mark.type === "rect") {
6346
- const rm = mark;
6347
- cy = rm.y + rm.height / 2;
6348
- left2 = rm.x;
6349
- right2 = rm.x + rm.width;
6350
- } else {
6351
- continue;
6352
- }
6353
- const key = Math.round(cy);
6354
- const existing = rows.get(key);
6355
- if (existing) {
6356
- existing.minX = Math.min(existing.minX, left2);
6357
- existing.maxX = Math.max(existing.maxX, right2);
6358
- } else {
6359
- rows.set(key, { minX: left2, maxX: right2, bandY: cy });
6360
- }
6361
- }
6362
- const bandScale = scales.y.scale;
6363
- const bandwidth = bandScale.bandwidth?.() ?? 0;
6364
- if (bandwidth === 0) return [];
6365
- const obstacles = [];
6366
- for (const { minX, maxX, bandY } of rows.values()) {
6367
- obstacles.push({
6368
- x: minX,
6369
- y: bandY - bandwidth / 2,
6370
- width: maxX - minX,
6371
- height: bandwidth
6372
- });
6373
- }
6374
- return obstacles;
6433
+ function clampStaggerDelay(delay, elementCount) {
6434
+ if (elementCount <= 1) return 0;
6435
+ return Math.min(delay, MAX_TOTAL_STAGGER_MS / elementCount);
6375
6436
  }
6376
- function resolveRendererKey(markType, encoding, markDef) {
6377
- if (markType === "bar") {
6378
- const xType = encoding.x?.type;
6379
- const yType = encoding.y?.type;
6380
- const isVertical = (xType === "nominal" || xType === "ordinal" || xType === "temporal") && yType === "quantitative";
6381
- if (isVertical) {
6382
- return "bar:vertical";
6383
- }
6384
- } else if (markType === "arc") {
6385
- const innerRadius = markDef.innerRadius;
6386
- if (innerRadius && innerRadius > 0) {
6387
- return "arc:donut";
6388
- }
6437
+ function hasAnyPhase(config) {
6438
+ return config.enter !== void 0 || config.update !== void 0 || config.exit !== void 0;
6439
+ }
6440
+ function resolvePhaseConfig(phase) {
6441
+ if (phase === void 0 || phase === true) {
6442
+ return {
6443
+ duration: ENTER_DEFAULTS.duration,
6444
+ ease: ENTER_DEFAULTS.ease,
6445
+ staggerDelay: ENTER_DEFAULTS.staggerDelay,
6446
+ staggerOrder: ENTER_DEFAULTS.staggerOrder
6447
+ };
6389
6448
  }
6390
- return markType;
6449
+ const cfg = phase;
6450
+ const stagger = resolveStagger(cfg.stagger);
6451
+ return {
6452
+ duration: cfg.duration ?? ENTER_DEFAULTS.duration,
6453
+ ease: cfg.ease ?? ENTER_DEFAULTS.ease,
6454
+ staggerDelay: stagger.delay,
6455
+ staggerOrder: stagger.order
6456
+ };
6391
6457
  }
6392
- function getMarkPrimaryValue(mark) {
6393
- switch (mark.type) {
6394
- case "rect":
6395
- return mark.height;
6396
- // bar height is the primary value encoding
6397
- case "point":
6398
- return mark.cy;
6399
- // y position for scatter
6400
- case "arc":
6401
- return mark.endAngle - mark.startAngle;
6402
- // arc angle extent
6403
- case "line":
6404
- case "area":
6405
- return 0;
6406
- // series marks don't have individual values
6407
- default:
6408
- return 0;
6409
- }
6410
- }
6411
- function assignAnimationIndices(marks, animation) {
6412
- if (!animation?.enabled) return;
6413
- if (animation.staggerOrder === "value") {
6414
- const indexed = marks.map((m, i) => ({ mark: m, idx: i }));
6415
- indexed.sort((a, b) => {
6416
- const av = getMarkPrimaryValue(a.mark);
6417
- const bv = getMarkPrimaryValue(b.mark);
6418
- return av - bv;
6419
- });
6420
- for (let i = 0; i < indexed.length; i++) {
6421
- const m = indexed[i].mark;
6422
- if (m.type === "rect" && m.stackGroup) continue;
6423
- m.animationIndex = i;
6424
- }
6425
- }
6426
- const groupIndexMap = /* @__PURE__ */ new Map();
6427
- const groupStackPos = /* @__PURE__ */ new Map();
6428
- let nextGroupIndex = 0;
6429
- for (const mark of marks) {
6430
- if (mark.type === "rect" && mark.stackGroup) {
6431
- const rect = mark;
6432
- const group = rect.stackGroup;
6433
- if (!groupIndexMap.has(group)) {
6434
- groupIndexMap.set(group, nextGroupIndex++);
6435
- }
6436
- rect.animationIndex = groupIndexMap.get(group);
6437
- const pos = groupStackPos.get(group) ?? 0;
6438
- rect.stackPos = pos;
6439
- groupStackPos.set(group, pos + 1);
6440
- }
6458
+ function resolveStagger(stagger) {
6459
+ if (stagger === false) return { delay: 0, order: "index" };
6460
+ if (stagger === void 0 || stagger === true) {
6461
+ return { delay: ENTER_DEFAULTS.staggerDelay, order: ENTER_DEFAULTS.staggerOrder };
6441
6462
  }
6442
- }
6443
-
6444
- // src/compile/color-scale-range.ts
6445
- function applyColorScaleRange(scales, encoding, theme) {
6446
- if (!scales.color) return;
6447
- const hasExplicitRange = !!(encoding.color && "field" in encoding.color && encoding.color.scale?.range?.length);
6448
- if (hasExplicitRange) return;
6449
- if (scales.color.type === "sequential") {
6450
- const seqStops = Object.values(theme.colors.sequential)[0] ?? theme.colors.categorical;
6451
- scales.color.scale.range([
6452
- seqStops[0],
6453
- seqStops[seqStops.length - 1]
6454
- ]);
6455
- } else {
6456
- scales.color.scale.range(theme.colors.categorical);
6457
- }
6458
- }
6459
-
6460
- // src/compile/data-clip.ts
6461
- function filterClippedDomains(data, encoding) {
6462
- let result = data;
6463
- for (const channel of ["x", "y"]) {
6464
- const enc = encoding[channel];
6465
- if (!enc?.scale?.clip || !enc.scale.domain) continue;
6466
- const domain = enc.scale.domain;
6467
- const field = enc.field;
6468
- if (Array.isArray(domain) && domain.length === 2 && typeof domain[0] === "number") {
6469
- const [lo, hi] = domain;
6470
- result = result.filter((row) => {
6471
- const v = Number(row[field]);
6472
- return Number.isFinite(v) && v >= lo && v <= hi;
6473
- });
6474
- }
6475
- }
6476
- return result;
6477
- }
6478
-
6479
- // src/compile/watermark-obstacle.ts
6480
- import { BRAND_RESERVE_WIDTH } from "@opendata-ai/openchart-core";
6481
- var WATERMARK_HEIGHT = 30;
6482
- var X_AXIS_EXTENT_WITH_LABEL = 48;
6483
- var X_AXIS_EXTENT_TICKS_ONLY = 26;
6484
- function computeWatermarkObstacle(dims, watermark, axes, theme) {
6485
- if (!watermark) return null;
6486
- const chartArea = dims.chartArea;
6487
- const brandPadding = theme.spacing.padding;
6488
- const brandX = dims.total.width - brandPadding - BRAND_RESERVE_WIDTH;
6489
- const xAxisExtent = axes.x?.label ? X_AXIS_EXTENT_WITH_LABEL : axes.x ? X_AXIS_EXTENT_TICKS_ONLY : 0;
6490
- const firstBottomChrome = dims.chrome.source ?? dims.chrome.byline ?? dims.chrome.footer;
6491
- const brandY = firstBottomChrome ? chartArea.y + chartArea.height + xAxisExtent + firstBottomChrome.y : chartArea.y + chartArea.height + xAxisExtent + theme.spacing.chartToFooter;
6492
- return { x: brandX, y: brandY, width: BRAND_RESERVE_WIDTH, height: WATERMARK_HEIGHT };
6493
- }
6494
-
6495
- // src/compiler/animation.ts
6496
- var ENTER_DEFAULTS = {
6497
- duration: 500,
6498
- ease: "smooth",
6499
- staggerDelay: 80,
6500
- staggerOrder: "index",
6501
- annotationDelay: 200
6502
- };
6503
- var MAX_TOTAL_STAGGER_MS = 2e3;
6504
- function resolveAnimation(spec) {
6505
- if (spec === void 0 || spec === false) return void 0;
6506
- if (spec === true) {
6507
- return {
6508
- enabled: true,
6509
- duration: ENTER_DEFAULTS.duration,
6510
- ease: ENTER_DEFAULTS.ease,
6511
- staggerDelay: ENTER_DEFAULTS.staggerDelay,
6512
- staggerOrder: ENTER_DEFAULTS.staggerOrder,
6513
- annotationDelay: ENTER_DEFAULTS.annotationDelay
6514
- };
6515
- }
6516
- const config = spec;
6517
- if (config.enter === false || config.enter === void 0 && !hasAnyPhase(config)) {
6518
- return void 0;
6519
- }
6520
- const enterConfig = resolvePhaseConfig(config.enter);
6521
- return {
6522
- enabled: true,
6523
- duration: enterConfig.duration,
6524
- ease: enterConfig.ease,
6525
- staggerDelay: enterConfig.staggerDelay,
6526
- staggerOrder: enterConfig.staggerOrder,
6527
- annotationDelay: config.annotationDelay ?? ENTER_DEFAULTS.annotationDelay
6528
- };
6529
- }
6530
- function clampStaggerDelay(delay, elementCount) {
6531
- if (elementCount <= 1) return 0;
6532
- return Math.min(delay, MAX_TOTAL_STAGGER_MS / elementCount);
6533
- }
6534
- function hasAnyPhase(config) {
6535
- return config.enter !== void 0 || config.update !== void 0 || config.exit !== void 0;
6536
- }
6537
- function resolvePhaseConfig(phase) {
6538
- if (phase === void 0 || phase === true) {
6539
- return {
6540
- duration: ENTER_DEFAULTS.duration,
6541
- ease: ENTER_DEFAULTS.ease,
6542
- staggerDelay: ENTER_DEFAULTS.staggerDelay,
6543
- staggerOrder: ENTER_DEFAULTS.staggerOrder
6544
- };
6545
- }
6546
- const cfg = phase;
6547
- const stagger = resolveStagger(cfg.stagger);
6548
- return {
6549
- duration: cfg.duration ?? ENTER_DEFAULTS.duration,
6550
- ease: cfg.ease ?? ENTER_DEFAULTS.ease,
6551
- staggerDelay: stagger.delay,
6552
- staggerOrder: stagger.order
6553
- };
6554
- }
6555
- function resolveStagger(stagger) {
6556
- if (stagger === false) return { delay: 0, order: "index" };
6557
- if (stagger === void 0 || stagger === true) {
6558
- return { delay: ENTER_DEFAULTS.staggerDelay, order: ENTER_DEFAULTS.staggerOrder };
6559
- }
6560
- return {
6561
- delay: stagger.delay ?? ENTER_DEFAULTS.staggerDelay,
6562
- order: stagger.order ?? ENTER_DEFAULTS.staggerOrder
6563
- };
6463
+ return {
6464
+ delay: stagger.delay ?? ENTER_DEFAULTS.staggerDelay,
6465
+ order: stagger.order ?? ENTER_DEFAULTS.staggerOrder
6466
+ };
6564
6467
  }
6565
6468
 
6566
6469
  // src/compiler/normalize.ts
6567
6470
  import {
6471
+ isBarListSpec,
6568
6472
  isChartSpec,
6569
6473
  isGraphSpec,
6570
6474
  isLayerSpec,
@@ -6822,7 +6726,8 @@ function normalizeLabels(labels) {
6822
6726
  density: labels.density ?? "auto",
6823
6727
  format: labels.format ?? "",
6824
6728
  prefix: labels.prefix ?? "",
6825
- offsets: labels.offsets
6729
+ offsets: labels.offsets,
6730
+ color: labels.color
6826
6731
  };
6827
6732
  }
6828
6733
  function normalizeChartSpec(spec, warnings) {
@@ -6968,6 +6873,22 @@ function normalizeTileMapSpec(spec, warnings) {
6968
6873
  valueFormat: spec.valueFormat
6969
6874
  };
6970
6875
  }
6876
+ function normalizeBarListSpec(spec, _warnings) {
6877
+ return {
6878
+ type: "barlist",
6879
+ data: spec.data,
6880
+ encoding: spec.encoding,
6881
+ barHeight: spec.barHeight ?? 6,
6882
+ cornerRadius: spec.cornerRadius ?? "pill",
6883
+ maxItems: spec.maxItems ?? 20,
6884
+ chrome: normalizeChrome(spec.chrome),
6885
+ theme: spec.theme ?? {},
6886
+ darkMode: spec.darkMode ?? "off",
6887
+ watermark: spec.watermark ?? true,
6888
+ animation: spec.animation ?? true,
6889
+ valueFormat: spec.valueFormat
6890
+ };
6891
+ }
6971
6892
  function normalizeSpec(spec, warnings = []) {
6972
6893
  if (isLayerSpec(spec)) {
6973
6894
  const leaves = flattenLayers(spec);
@@ -6991,8 +6912,11 @@ function normalizeSpec(spec, warnings = []) {
6991
6912
  if (isTileMapSpec(spec)) {
6992
6913
  return normalizeTileMapSpec(spec, warnings);
6993
6914
  }
6915
+ if (isBarListSpec(spec)) {
6916
+ return normalizeBarListSpec(spec, warnings);
6917
+ }
6994
6918
  throw new Error(
6995
- `Unknown spec shape. Expected mark (chart), layer, type: 'table', type: 'graph', type: 'sankey', or type: 'tilemap'.`
6919
+ `Unknown spec shape. Expected mark (chart), layer, type: 'table', type: 'graph', type: 'sankey', type: 'tilemap', or type: 'barlist'.`
6996
6920
  );
6997
6921
  }
6998
6922
  function flattenLayers(spec, parentData, parentEncoding, parentTransforms, parentWatermark) {
@@ -7632,6 +7556,98 @@ function validateTileMapSpec(spec, errors) {
7632
7556
  });
7633
7557
  }
7634
7558
  }
7559
+ function validateBarListSpec(spec, errors) {
7560
+ if (!Array.isArray(spec.data)) {
7561
+ errors.push({
7562
+ message: 'Spec error: barlist spec requires a "data" array',
7563
+ path: "data",
7564
+ code: "INVALID_TYPE",
7565
+ suggestion: 'Provide data as an array of objects, e.g. data: [{ label: "Category A", value: 42 }]'
7566
+ });
7567
+ return;
7568
+ }
7569
+ if (spec.data.length === 0) {
7570
+ errors.push({
7571
+ message: 'Spec error: "data" array must be non-empty',
7572
+ path: "data",
7573
+ code: "EMPTY_DATA",
7574
+ suggestion: "Add at least one data row"
7575
+ });
7576
+ return;
7577
+ }
7578
+ const firstRow = spec.data[0];
7579
+ if (typeof firstRow !== "object" || firstRow === null || Array.isArray(firstRow)) {
7580
+ errors.push({
7581
+ message: 'Spec error: each item in "data" must be a plain object',
7582
+ path: "data[0]",
7583
+ code: "INVALID_TYPE",
7584
+ suggestion: 'Each data item should be an object, e.g. { label: "Category A", value: 42 }'
7585
+ });
7586
+ return;
7587
+ }
7588
+ if (!spec.encoding || typeof spec.encoding !== "object") {
7589
+ errors.push({
7590
+ message: 'Spec error: barlist spec requires an "encoding" object with label and value channels',
7591
+ path: "encoding",
7592
+ code: "MISSING_FIELD",
7593
+ suggestion: 'Add an encoding object, e.g. encoding: { label: { field: "name", type: "nominal" }, value: { field: "count", type: "quantitative" } }'
7594
+ });
7595
+ return;
7596
+ }
7597
+ const encoding = spec.encoding;
7598
+ const dataColumns = new Set(Object.keys(firstRow));
7599
+ const availableColumns = [...dataColumns].join(", ");
7600
+ for (const channel of ["label", "value"]) {
7601
+ const ch = encoding[channel];
7602
+ if (!ch || typeof ch !== "object") {
7603
+ errors.push({
7604
+ message: `Spec error: barlist encoding requires "${channel}" channel`,
7605
+ path: `encoding.${channel}`,
7606
+ code: "MISSING_FIELD",
7607
+ suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${[...dataColumns][0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
7608
+ });
7609
+ continue;
7610
+ }
7611
+ if (!ch.field || typeof ch.field !== "string") {
7612
+ errors.push({
7613
+ message: `Spec error: encoding.${channel} must have a "field" string`,
7614
+ path: `encoding.${channel}.field`,
7615
+ code: "MISSING_FIELD",
7616
+ suggestion: `Add a field name from your data columns: ${availableColumns}`
7617
+ });
7618
+ continue;
7619
+ }
7620
+ if (!dataColumns.has(ch.field)) {
7621
+ errors.push({
7622
+ message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist in data. Available columns: ${availableColumns}`,
7623
+ path: `encoding.${channel}.field`,
7624
+ code: "DATA_FIELD_MISSING",
7625
+ suggestion: `Use one of the available data columns: ${availableColumns}`
7626
+ });
7627
+ }
7628
+ }
7629
+ for (const channel of ["subtitle", "color", "tooltip"]) {
7630
+ const ch = encoding[channel];
7631
+ if (!ch) continue;
7632
+ const field = ch.field;
7633
+ if (field && typeof field === "string" && !dataColumns.has(field)) {
7634
+ errors.push({
7635
+ message: `Spec error: encoding.${channel}.field "${field}" does not exist in data. Available columns: ${availableColumns}`,
7636
+ path: `encoding.${channel}.field`,
7637
+ code: "DATA_FIELD_MISSING",
7638
+ suggestion: `Use one of the available data columns: ${availableColumns}`
7639
+ });
7640
+ }
7641
+ }
7642
+ if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
7643
+ errors.push({
7644
+ message: 'Spec error: darkMode must be "auto", "force", or "off"',
7645
+ path: "darkMode",
7646
+ code: "INVALID_VALUE",
7647
+ suggestion: 'Use one of: "auto" (system preference), "force" (always dark), or "off" (always light)'
7648
+ });
7649
+ }
7650
+ }
7635
7651
  function validateLayerSpec(spec, errors) {
7636
7652
  const layer = spec.layer;
7637
7653
  if (layer.length === 0) {
@@ -7704,105 +7720,550 @@ function validateLayerSpec(spec, errors) {
7704
7720
  }
7705
7721
  }
7706
7722
  }
7707
- }
7708
- function validateSpec(spec) {
7709
- const errors = [];
7710
- if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
7711
- return {
7712
- valid: false,
7713
- errors: [
7714
- {
7715
- message: "Spec error: spec must be a non-null object",
7716
- code: "INVALID_TYPE",
7717
- suggestion: 'Pass a spec object with at least a "mark" field for charts, e.g. { mark: "line", data: [...], encoding: {...} }'
7718
- }
7719
- ],
7720
- normalized: null
7721
- };
7723
+ }
7724
+ function validateSpec(spec) {
7725
+ const errors = [];
7726
+ if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
7727
+ return {
7728
+ valid: false,
7729
+ errors: [
7730
+ {
7731
+ message: "Spec error: spec must be a non-null object",
7732
+ code: "INVALID_TYPE",
7733
+ suggestion: 'Pass a spec object with at least a "mark" field for charts, e.g. { mark: "line", data: [...], encoding: {...} }'
7734
+ }
7735
+ ],
7736
+ normalized: null
7737
+ };
7738
+ }
7739
+ const obj = spec;
7740
+ const hasLayer = "layer" in obj && Array.isArray(obj.layer);
7741
+ const hasMark = "mark" in obj;
7742
+ const isTable = obj.type === "table";
7743
+ const isGraph = obj.type === "graph";
7744
+ const isSankey = obj.type === "sankey";
7745
+ const isTileMap = obj.type === "tilemap";
7746
+ const isBarList = obj.type === "barlist";
7747
+ const isLayer = hasLayer && !isTable && !isGraph && !isSankey && !isTileMap && !isBarList;
7748
+ const isChart = hasMark && !hasLayer && !isTable && !isGraph && !isSankey && !isTileMap && !isBarList;
7749
+ if (!isChart && !isTable && !isGraph && !isSankey && !isTileMap && !isBarList && !isLayer) {
7750
+ return {
7751
+ valid: false,
7752
+ errors: [
7753
+ {
7754
+ message: 'Spec error: spec must have a "mark" field for charts, a "layer" array for layered charts, or a "type" field for tables/graphs/sankey/tilemap/barlist',
7755
+ path: "mark",
7756
+ code: "MISSING_FIELD",
7757
+ suggestion: `Add a "mark" field for charts (e.g. mark: "bar"), a "layer" array for layered charts, or a "type" field (type: "table", type: "graph", type: "sankey", type: "tilemap", or type: "barlist"). Valid mark types: ${[...MARK_TYPES].join(", ")}`
7758
+ }
7759
+ ],
7760
+ normalized: null
7761
+ };
7762
+ }
7763
+ if (isLayer) {
7764
+ validateLayerSpec(obj, errors);
7765
+ }
7766
+ if (isChart) {
7767
+ const mark = obj.mark;
7768
+ let markValue;
7769
+ if (typeof mark === "string") {
7770
+ markValue = mark;
7771
+ } else if (mark && typeof mark === "object" && !Array.isArray(mark)) {
7772
+ markValue = mark.type;
7773
+ }
7774
+ if (!markValue || !MARK_TYPES.has(markValue)) {
7775
+ return {
7776
+ valid: false,
7777
+ errors: [
7778
+ {
7779
+ message: `Spec error: "${markValue ?? String(mark)}" is not a valid mark type. Valid mark types: ${[...MARK_TYPES].join(", ")}`,
7780
+ path: "mark",
7781
+ code: "INVALID_VALUE",
7782
+ suggestion: `Change mark to one of: ${[...MARK_TYPES].join(", ")}`
7783
+ }
7784
+ ],
7785
+ normalized: null
7786
+ };
7787
+ }
7788
+ validateChartSpec(obj, errors);
7789
+ } else if (isTable) {
7790
+ validateTableSpec(obj, errors);
7791
+ } else if (isGraph) {
7792
+ validateGraphSpec(obj, errors);
7793
+ } else if (isSankey) {
7794
+ validateSankeySpec(obj, errors);
7795
+ } else if (isTileMap) {
7796
+ validateTileMapSpec(obj, errors);
7797
+ } else if (isBarList) {
7798
+ validateBarListSpec(obj, errors);
7799
+ }
7800
+ if (errors.length > 0) {
7801
+ return { valid: false, errors, normalized: null };
7802
+ }
7803
+ return {
7804
+ valid: true,
7805
+ errors: [],
7806
+ normalized: spec
7807
+ };
7808
+ }
7809
+
7810
+ // src/compiler/index.ts
7811
+ function compile(spec) {
7812
+ const validation = validateSpec(spec);
7813
+ if (!validation.valid || !validation.normalized) {
7814
+ const errorMessages = validation.errors.map((e) => e.message).join("\n");
7815
+ throw new Error(`Invalid spec:
7816
+ ${errorMessages}`);
7817
+ }
7818
+ const warnings = [];
7819
+ const normalized = normalizeSpec(validation.normalized, warnings);
7820
+ return { spec: normalized, warnings };
7821
+ }
7822
+
7823
+ // src/barlist/compile-barlist.ts
7824
+ var DEFAULT_ROW_GAP = 8;
7825
+ var LABEL_BAR_GAP = 12;
7826
+ var BAR_VALUE_GAP = 12;
7827
+ var VALUE_WIDTH = 56;
7828
+ var LABEL_FONT_SIZE6 = 13;
7829
+ var LABEL_FONT_WEIGHT6 = 500;
7830
+ var SUBTITLE_FONT_SIZE = 12;
7831
+ var SUBTITLE_FONT_WEIGHT = 400;
7832
+ var VALUE_FONT_SIZE = 12;
7833
+ var VALUE_FONT_WEIGHT = 400;
7834
+ var BARLIST_COLORS = ["#06b6d4", "#34d399", "#fbbf24", "#f472b6", "#a78bfa"];
7835
+ function compileBarList(spec, options) {
7836
+ const { spec: normalized } = compile(spec);
7837
+ if (!("type" in normalized) || normalized.type !== "barlist") {
7838
+ throw new Error(
7839
+ "compileBarList received a non-barlist spec. Use compileChart, compileTable, compileGraph, compileSankey, or compileTileMap instead."
7840
+ );
7841
+ }
7842
+ const barlistSpec = normalized;
7843
+ const rawWatermark = spec.watermark;
7844
+ const watermark = rawWatermark !== void 0 ? barlistSpec.watermark : options.watermark ?? true;
7845
+ const mergedThemeConfig = options.theme ? { ...barlistSpec.theme, ...options.theme } : barlistSpec.theme;
7846
+ const lightTheme = resolveTheme(mergedThemeConfig);
7847
+ let theme = lightTheme;
7848
+ if (options.darkMode) {
7849
+ theme = adaptTheme(theme);
7850
+ }
7851
+ const chrome = computeChrome(
7852
+ {
7853
+ title: barlistSpec.chrome.title,
7854
+ subtitle: barlistSpec.chrome.subtitle,
7855
+ source: barlistSpec.chrome.source,
7856
+ byline: barlistSpec.chrome.byline,
7857
+ footer: barlistSpec.chrome.footer
7858
+ },
7859
+ theme,
7860
+ options.width,
7861
+ options.measureText,
7862
+ "full",
7863
+ void 0,
7864
+ watermark
7865
+ );
7866
+ const padding = theme.spacing.padding;
7867
+ const fullArea = {
7868
+ x: padding,
7869
+ y: padding + chrome.topHeight,
7870
+ width: options.width - padding * 2,
7871
+ height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
7872
+ };
7873
+ if (fullArea.width <= 0 || fullArea.height <= 0) {
7874
+ return emptyLayout(chrome, theme, options, watermark);
7875
+ }
7876
+ const labelField = barlistSpec.encoding.label.field;
7877
+ const valueField = barlistSpec.encoding.value.field;
7878
+ const subtitleField = barlistSpec.encoding.subtitle?.field;
7879
+ const colorField = barlistSpec.encoding.color?.field;
7880
+ const barHeight = barlistSpec.barHeight;
7881
+ const cornerRadius = barlistSpec.cornerRadius === "pill" ? barHeight / 2 : barlistSpec.cornerRadius;
7882
+ const rowContentHeight = Math.max(barHeight, LABEL_FONT_SIZE6 * 1.4);
7883
+ const rowHeight = rowContentHeight + DEFAULT_ROW_GAP;
7884
+ const maxFittingRows = Math.max(1, Math.floor(fullArea.height / rowHeight));
7885
+ const validRows = barlistSpec.data.filter((row) => {
7886
+ const val = row[valueField];
7887
+ return val !== null && val !== void 0 && !Number.isNaN(Number(val));
7888
+ }).sort((a, b) => Number(b[valueField]) - Number(a[valueField])).slice(0, Math.min(barlistSpec.maxItems, maxFittingRows));
7889
+ if (validRows.length === 0) {
7890
+ return emptyLayout(chrome, theme, options, watermark);
7891
+ }
7892
+ const maxValue = Math.max(...validRows.map((r) => Math.abs(Number(r[valueField]))));
7893
+ const colorMap = /* @__PURE__ */ new Map();
7894
+ let colorIndex = 0;
7895
+ const palette = BARLIST_COLORS;
7896
+ function getColor2(row, idx) {
7897
+ if (colorField) {
7898
+ const key = String(row[colorField] ?? "");
7899
+ if (!colorMap.has(key)) {
7900
+ colorMap.set(key, palette[colorIndex % palette.length]);
7901
+ colorIndex++;
7902
+ }
7903
+ return colorMap.get(key);
7904
+ }
7905
+ return palette[idx % palette.length];
7906
+ }
7907
+ const formatter = buildD3Formatter4(barlistSpec.valueFormat) ?? formatNumber2;
7908
+ const measureText = options.measureText ?? ((text, fontSize) => ({
7909
+ width: estimateTextWidth7(text, fontSize),
7910
+ height: fontSize
7911
+ }));
7912
+ const perRowLabelWidths = /* @__PURE__ */ new Map();
7913
+ let maxCombinedWidth = 0;
7914
+ for (let i = 0; i < validRows.length; i++) {
7915
+ const row = validRows[i];
7916
+ const label = String(row[labelField] ?? "");
7917
+ const labelW = measureText(label, LABEL_FONT_SIZE6, LABEL_FONT_WEIGHT6).width;
7918
+ perRowLabelWidths.set(i, labelW);
7919
+ let combined = labelW + 4;
7920
+ if (subtitleField && row[subtitleField] != null) {
7921
+ const subtitle = String(row[subtitleField]);
7922
+ combined = labelW + 6 + measureText(subtitle, SUBTITLE_FONT_SIZE, SUBTITLE_FONT_WEIGHT).width + 4;
7923
+ }
7924
+ maxCombinedWidth = Math.max(maxCombinedWidth, combined);
7925
+ }
7926
+ const isNarrow = fullArea.width < 400;
7927
+ const labelBarGap = isNarrow ? 8 : LABEL_BAR_GAP;
7928
+ const barValueGap = isNarrow ? 6 : BAR_VALUE_GAP;
7929
+ const valueWidth = isNarrow ? 44 : VALUE_WIDTH;
7930
+ const maxLabelPct = isNarrow ? 0.35 : 0.4;
7931
+ const labelWidth = Math.max(50, Math.min(maxCombinedWidth, fullArea.width * maxLabelPct));
7932
+ const barAreaWidth = fullArea.width - labelWidth - labelBarGap - barValueGap - valueWidth;
7933
+ const labelColor = theme.colors.text;
7934
+ const subtitleColor = options.darkMode ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.45)";
7935
+ const valueColor = options.darkMode ? "rgba(255,255,255,0.6)" : "rgba(0,0,0,0.55)";
7936
+ const rows = [];
7937
+ for (let i = 0; i < validRows.length; i++) {
7938
+ const row = validRows[i];
7939
+ const value2 = Number(row[valueField]);
7940
+ const labelText = String(row[labelField] ?? "");
7941
+ const formattedValue = formatter(value2);
7942
+ const barColor = getColor2(row, i);
7943
+ const pct = maxValue > 0 ? Math.abs(value2) / maxValue : 0;
7944
+ const rowY = fullArea.y + i * rowHeight;
7945
+ const centerY = rowY + rowContentHeight / 2;
7946
+ const labelX = fullArea.x;
7947
+ const labelStyle = {
7948
+ fontFamily: theme.fonts.family,
7949
+ fontSize: LABEL_FONT_SIZE6,
7950
+ fontWeight: LABEL_FONT_WEIGHT6,
7951
+ fill: labelColor,
7952
+ lineHeight: 1.4
7953
+ };
7954
+ let subtitle;
7955
+ if (subtitleField && row[subtitleField] != null) {
7956
+ const subtitleText = String(row[subtitleField]);
7957
+ const subtitleX = labelX + (perRowLabelWidths.get(i) ?? 0) + 6;
7958
+ subtitle = {
7959
+ text: subtitleText,
7960
+ x: subtitleX,
7961
+ y: centerY,
7962
+ style: {
7963
+ fontFamily: theme.fonts.family,
7964
+ fontSize: SUBTITLE_FONT_SIZE,
7965
+ fontWeight: SUBTITLE_FONT_WEIGHT,
7966
+ fill: subtitleColor,
7967
+ lineHeight: 1.4
7968
+ },
7969
+ visible: true
7970
+ };
7971
+ }
7972
+ const trackX = fullArea.x + labelWidth + labelBarGap;
7973
+ const trackY = centerY - barHeight / 2;
7974
+ const trackWidth = Math.max(barAreaWidth, 0);
7975
+ const barWidth = Math.max(pct * trackWidth, 0);
7976
+ const valueLabelX = trackX + trackWidth + barValueGap + valueWidth;
7977
+ const valueLabelStyle = {
7978
+ fontFamily: `${theme.fonts.family}, ui-monospace, monospace`,
7979
+ fontSize: VALUE_FONT_SIZE,
7980
+ fontWeight: VALUE_FONT_WEIGHT,
7981
+ fill: valueColor,
7982
+ lineHeight: 1.4
7983
+ };
7984
+ const rowMark = {
7985
+ type: "barlist-row",
7986
+ index: i,
7987
+ y: rowY,
7988
+ height: rowHeight,
7989
+ label: {
7990
+ text: labelText,
7991
+ x: labelX,
7992
+ y: centerY,
7993
+ style: labelStyle,
7994
+ visible: true
7995
+ },
7996
+ subtitle,
7997
+ track: {
7998
+ x: trackX,
7999
+ y: trackY,
8000
+ width: trackWidth,
8001
+ height: barHeight,
8002
+ cornerRadius
8003
+ },
8004
+ bar: {
8005
+ x: trackX,
8006
+ y: trackY,
8007
+ width: barWidth,
8008
+ height: barHeight,
8009
+ cornerRadius,
8010
+ fill: barColor
8011
+ },
8012
+ valueLabel: {
8013
+ text: formattedValue,
8014
+ x: valueLabelX,
8015
+ y: centerY,
8016
+ style: valueLabelStyle,
8017
+ visible: true
8018
+ },
8019
+ value: value2,
8020
+ formattedValue,
8021
+ aria: {
8022
+ role: "listitem",
8023
+ label: `${labelText}: ${formattedValue}`
8024
+ },
8025
+ animationIndex: i,
8026
+ data: row
8027
+ };
8028
+ rows.push(rowMark);
8029
+ }
8030
+ const tooltipDescriptors = /* @__PURE__ */ new Map();
8031
+ for (const row of rows) {
8032
+ const fields = [
8033
+ { label: barlistSpec.encoding.value.title ?? valueField, value: row.formattedValue }
8034
+ ];
8035
+ tooltipDescriptors.set(String(row.index), {
8036
+ title: row.label.text,
8037
+ fields
8038
+ });
8039
+ }
8040
+ const a11y = {
8041
+ altText: `Bar list showing ${rows.length} items ranked by ${valueField}`,
8042
+ dataTableFallback: rows.map((r) => [r.label.text, r.formattedValue]),
8043
+ role: "list",
8044
+ keyboardNavigable: rows.length > 0
8045
+ };
8046
+ const resolvedAnimation = resolveAnimation(barlistSpec.animation);
8047
+ return {
8048
+ area: fullArea,
8049
+ chrome,
8050
+ rows,
8051
+ tooltipDescriptors,
8052
+ a11y,
8053
+ theme,
8054
+ width: options.width,
8055
+ height: options.height,
8056
+ animation: resolvedAnimation,
8057
+ watermark,
8058
+ measureText
8059
+ };
8060
+ }
8061
+ function emptyLayout(chrome, theme, options, watermark) {
8062
+ return {
8063
+ area: { x: 0, y: 0, width: 0, height: 0 },
8064
+ chrome,
8065
+ rows: [],
8066
+ tooltipDescriptors: /* @__PURE__ */ new Map(),
8067
+ a11y: {
8068
+ altText: "Empty bar list",
8069
+ dataTableFallback: [],
8070
+ role: "list",
8071
+ keyboardNavigable: false
8072
+ },
8073
+ theme,
8074
+ width: options.width,
8075
+ height: options.height,
8076
+ watermark,
8077
+ animation: void 0,
8078
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth7(text, fontSize), height: fontSize }))
8079
+ };
8080
+ }
8081
+
8082
+ // src/charts/post-process.ts
8083
+ function computeMarkObstacles(marks, scales) {
8084
+ if (scales.y?.type === "band") {
8085
+ return computeBandRowObstacles(marks, scales);
8086
+ }
8087
+ const obstacles = [];
8088
+ for (const mark of marks) {
8089
+ if (mark.type === "rect") {
8090
+ const rm = mark;
8091
+ obstacles.push({ x: rm.x, y: rm.y, width: rm.width, height: rm.height });
8092
+ } else if (mark.type === "point") {
8093
+ const pm = mark;
8094
+ obstacles.push({
8095
+ x: pm.cx - pm.r,
8096
+ y: pm.cy - pm.r,
8097
+ width: pm.r * 2,
8098
+ height: pm.r * 2
8099
+ });
8100
+ }
8101
+ }
8102
+ return obstacles;
8103
+ }
8104
+ function computeBandRowObstacles(marks, scales) {
8105
+ const rows = /* @__PURE__ */ new Map();
8106
+ for (const mark of marks) {
8107
+ let cy;
8108
+ let left2;
8109
+ let right2;
8110
+ if (mark.type === "point") {
8111
+ const pm = mark;
8112
+ cy = pm.cy;
8113
+ left2 = pm.cx - pm.r;
8114
+ right2 = pm.cx + pm.r;
8115
+ } else if (mark.type === "rect") {
8116
+ const rm = mark;
8117
+ cy = rm.y + rm.height / 2;
8118
+ left2 = rm.x;
8119
+ right2 = rm.x + rm.width;
8120
+ } else {
8121
+ continue;
8122
+ }
8123
+ const key = Math.round(cy);
8124
+ const existing = rows.get(key);
8125
+ if (existing) {
8126
+ existing.minX = Math.min(existing.minX, left2);
8127
+ existing.maxX = Math.max(existing.maxX, right2);
8128
+ } else {
8129
+ rows.set(key, { minX: left2, maxX: right2, bandY: cy });
8130
+ }
8131
+ }
8132
+ const bandScale = scales.y.scale;
8133
+ const bandwidth = bandScale.bandwidth?.() ?? 0;
8134
+ if (bandwidth === 0) return [];
8135
+ const obstacles = [];
8136
+ for (const { minX, maxX, bandY } of rows.values()) {
8137
+ obstacles.push({
8138
+ x: minX,
8139
+ y: bandY - bandwidth / 2,
8140
+ width: maxX - minX,
8141
+ height: bandwidth
8142
+ });
7722
8143
  }
7723
- const obj = spec;
7724
- const hasLayer = "layer" in obj && Array.isArray(obj.layer);
7725
- const hasMark = "mark" in obj;
7726
- const isTable = obj.type === "table";
7727
- const isGraph = obj.type === "graph";
7728
- const isSankey = obj.type === "sankey";
7729
- const isTileMap = obj.type === "tilemap";
7730
- const isLayer = hasLayer && !isTable && !isGraph && !isSankey && !isTileMap;
7731
- const isChart = hasMark && !hasLayer && !isTable && !isGraph && !isSankey && !isTileMap;
7732
- if (!isChart && !isTable && !isGraph && !isSankey && !isTileMap && !isLayer) {
7733
- return {
7734
- valid: false,
7735
- errors: [
7736
- {
7737
- message: 'Spec error: spec must have a "mark" field for charts, a "layer" array for layered charts, or a "type" field for tables/graphs/sankey/tilemap',
7738
- path: "mark",
7739
- code: "MISSING_FIELD",
7740
- suggestion: `Add a "mark" field for charts (e.g. mark: "bar"), a "layer" array for layered charts, or a "type" field for tables/graphs/sankey/tilemap (type: "table", type: "graph", type: "sankey", or type: "tilemap"). Valid mark types: ${[...MARK_TYPES].join(", ")}`
7741
- }
7742
- ],
7743
- normalized: null
7744
- };
8144
+ return obstacles;
8145
+ }
8146
+ function resolveRendererKey(markType, encoding, markDef) {
8147
+ if (markType === "bar") {
8148
+ const xType = encoding.x?.type;
8149
+ const yType = encoding.y?.type;
8150
+ const isVertical = (xType === "nominal" || xType === "ordinal" || xType === "temporal") && yType === "quantitative";
8151
+ if (isVertical) {
8152
+ return "bar:vertical";
8153
+ }
8154
+ } else if (markType === "arc") {
8155
+ const innerRadius = markDef.innerRadius;
8156
+ if (innerRadius && innerRadius > 0) {
8157
+ return "arc:donut";
8158
+ }
7745
8159
  }
7746
- if (isLayer) {
7747
- validateLayerSpec(obj, errors);
8160
+ return markType;
8161
+ }
8162
+ function getMarkPrimaryValue(mark) {
8163
+ switch (mark.type) {
8164
+ case "rect":
8165
+ return mark.height;
8166
+ // bar height is the primary value encoding
8167
+ case "point":
8168
+ return mark.cy;
8169
+ // y position for scatter
8170
+ case "arc":
8171
+ return mark.endAngle - mark.startAngle;
8172
+ // arc angle extent
8173
+ case "line":
8174
+ case "area":
8175
+ return 0;
8176
+ // series marks don't have individual values
8177
+ default:
8178
+ return 0;
7748
8179
  }
7749
- if (isChart) {
7750
- const mark = obj.mark;
7751
- let markValue;
7752
- if (typeof mark === "string") {
7753
- markValue = mark;
7754
- } else if (mark && typeof mark === "object" && !Array.isArray(mark)) {
7755
- markValue = mark.type;
8180
+ }
8181
+ function assignAnimationIndices(marks, animation) {
8182
+ if (!animation?.enabled) return;
8183
+ if (animation.staggerOrder === "value") {
8184
+ const indexed = marks.map((m, i) => ({ mark: m, idx: i }));
8185
+ indexed.sort((a, b) => {
8186
+ const av = getMarkPrimaryValue(a.mark);
8187
+ const bv = getMarkPrimaryValue(b.mark);
8188
+ return av - bv;
8189
+ });
8190
+ for (let i = 0; i < indexed.length; i++) {
8191
+ const m = indexed[i].mark;
8192
+ if (m.type === "rect" && m.stackGroup) continue;
8193
+ m.animationIndex = i;
7756
8194
  }
7757
- if (!markValue || !MARK_TYPES.has(markValue)) {
7758
- return {
7759
- valid: false,
7760
- errors: [
7761
- {
7762
- message: `Spec error: "${markValue ?? String(mark)}" is not a valid mark type. Valid mark types: ${[...MARK_TYPES].join(", ")}`,
7763
- path: "mark",
7764
- code: "INVALID_VALUE",
7765
- suggestion: `Change mark to one of: ${[...MARK_TYPES].join(", ")}`
7766
- }
7767
- ],
7768
- normalized: null
7769
- };
8195
+ }
8196
+ const groupIndexMap = /* @__PURE__ */ new Map();
8197
+ const groupStackPos = /* @__PURE__ */ new Map();
8198
+ let nextGroupIndex = 0;
8199
+ for (const mark of marks) {
8200
+ if (mark.type === "rect" && mark.stackGroup) {
8201
+ const rect = mark;
8202
+ const group = rect.stackGroup;
8203
+ if (!groupIndexMap.has(group)) {
8204
+ groupIndexMap.set(group, nextGroupIndex++);
8205
+ }
8206
+ rect.animationIndex = groupIndexMap.get(group);
8207
+ const pos = groupStackPos.get(group) ?? 0;
8208
+ rect.stackPos = pos;
8209
+ groupStackPos.set(group, pos + 1);
7770
8210
  }
7771
- validateChartSpec(obj, errors);
7772
- } else if (isTable) {
7773
- validateTableSpec(obj, errors);
7774
- } else if (isGraph) {
7775
- validateGraphSpec(obj, errors);
7776
- } else if (isSankey) {
7777
- validateSankeySpec(obj, errors);
7778
- } else if (isTileMap) {
7779
- validateTileMapSpec(obj, errors);
7780
8211
  }
7781
- if (errors.length > 0) {
7782
- return { valid: false, errors, normalized: null };
8212
+ }
8213
+
8214
+ // src/compile/color-scale-range.ts
8215
+ function applyColorScaleRange(scales, encoding, theme) {
8216
+ if (!scales.color) return;
8217
+ const hasExplicitRange = !!(encoding.color && "field" in encoding.color && encoding.color.scale?.range?.length);
8218
+ if (hasExplicitRange) return;
8219
+ if (scales.color.type === "sequential") {
8220
+ const seqStops = Object.values(theme.colors.sequential)[0] ?? theme.colors.categorical;
8221
+ scales.color.scale.range([
8222
+ seqStops[0],
8223
+ seqStops[seqStops.length - 1]
8224
+ ]);
8225
+ } else {
8226
+ scales.color.scale.range(theme.colors.categorical);
7783
8227
  }
7784
- return {
7785
- valid: true,
7786
- errors: [],
7787
- normalized: spec
7788
- };
7789
8228
  }
7790
8229
 
7791
- // src/compiler/index.ts
7792
- function compile(spec) {
7793
- const validation = validateSpec(spec);
7794
- if (!validation.valid || !validation.normalized) {
7795
- const errorMessages = validation.errors.map((e) => e.message).join("\n");
7796
- throw new Error(`Invalid spec:
7797
- ${errorMessages}`);
8230
+ // src/compile/data-clip.ts
8231
+ function filterClippedDomains(data, encoding) {
8232
+ let result = data;
8233
+ for (const channel of ["x", "y"]) {
8234
+ const enc = encoding[channel];
8235
+ if (!enc?.scale?.clip || !enc.scale.domain) continue;
8236
+ const domain = enc.scale.domain;
8237
+ const field = enc.field;
8238
+ if (Array.isArray(domain) && domain.length === 2 && typeof domain[0] === "number") {
8239
+ const [lo, hi] = domain;
8240
+ result = result.filter((row) => {
8241
+ const v = Number(row[field]);
8242
+ return Number.isFinite(v) && v >= lo && v <= hi;
8243
+ });
8244
+ }
7798
8245
  }
7799
- const warnings = [];
7800
- const normalized = normalizeSpec(validation.normalized, warnings);
7801
- return { spec: normalized, warnings };
8246
+ return result;
8247
+ }
8248
+
8249
+ // src/compile/watermark-obstacle.ts
8250
+ import { BRAND_RESERVE_WIDTH } from "@opendata-ai/openchart-core";
8251
+ var WATERMARK_HEIGHT = 30;
8252
+ var X_AXIS_EXTENT_WITH_LABEL = 48;
8253
+ var X_AXIS_EXTENT_TICKS_ONLY = 26;
8254
+ function computeWatermarkObstacle(dims, watermark, axes, theme) {
8255
+ if (!watermark) return null;
8256
+ const chartArea = dims.chartArea;
8257
+ const brandPadding = theme.spacing.padding;
8258
+ const brandX = dims.total.width - brandPadding - BRAND_RESERVE_WIDTH;
8259
+ const xAxisExtent = axes.x?.label ? X_AXIS_EXTENT_WITH_LABEL : axes.x ? X_AXIS_EXTENT_TICKS_ONLY : 0;
8260
+ const firstBottomChrome = dims.chrome.source ?? dims.chrome.byline ?? dims.chrome.footer;
8261
+ const brandY = firstBottomChrome ? chartArea.y + chartArea.height + xAxisExtent + firstBottomChrome.y : chartArea.y + chartArea.height + xAxisExtent + theme.spacing.chartToFooter;
8262
+ return { x: brandX, y: brandY, width: BRAND_RESERVE_WIDTH, height: WATERMARK_HEIGHT };
7802
8263
  }
7803
8264
 
7804
8265
  // src/graphs/compile-graph.ts
7805
- import { adaptTheme, computeChrome, resolveTheme } from "@opendata-ai/openchart-core";
8266
+ import { adaptTheme as adaptTheme2, computeChrome as computeChrome2, resolveTheme as resolveTheme2 } from "@opendata-ai/openchart-core";
7806
8267
 
7807
8268
  // src/graphs/encoding.ts
7808
8269
  var DEFAULT_NODE_RADIUS = 5;
@@ -8106,9 +8567,9 @@ function compileGraph(spec, options) {
8106
8567
  const rawWatermark = spec.watermark;
8107
8568
  const watermark = rawWatermark !== void 0 ? graphSpec.watermark : options.watermark ?? true;
8108
8569
  const mergedThemeConfig = options.theme ? { ...graphSpec.theme, ...options.theme } : graphSpec.theme;
8109
- let theme = resolveTheme(mergedThemeConfig);
8570
+ let theme = resolveTheme2(mergedThemeConfig);
8110
8571
  if (options.darkMode) {
8111
- theme = adaptTheme(theme);
8572
+ theme = adaptTheme2(theme);
8112
8573
  }
8113
8574
  const compiledNodes = resolveNodeVisuals(
8114
8575
  graphSpec.nodes,
@@ -8162,7 +8623,7 @@ function compileGraph(spec, options) {
8162
8623
  linkStrength: graphSpec.layout.linkStrength,
8163
8624
  centerForce: graphSpec.layout.centerForce
8164
8625
  };
8165
- const chrome = computeChrome(
8626
+ const chrome = computeChrome2(
8166
8627
  {
8167
8628
  title: graphSpec.chrome.title,
8168
8629
  subtitle: graphSpec.chrome.subtitle,
@@ -8196,11 +8657,11 @@ function compileGraph(spec, options) {
8196
8657
  var DEFAULT_COLLISION_PADDING = 5;
8197
8658
 
8198
8659
  // src/layout/axes/thinning.ts
8199
- import { estimateTextWidth as estimateTextWidth7 } from "@opendata-ai/openchart-core";
8660
+ import { estimateTextWidth as estimateTextWidth8 } from "@opendata-ai/openchart-core";
8200
8661
  var MIN_TICK_GAP_FACTOR = 1;
8201
8662
  var MIN_TICK_COUNT = 2;
8202
8663
  function measureLabel(text, fontSize, fontWeight, measureText) {
8203
- return measureText ? measureText(text, fontSize, fontWeight).width : estimateTextWidth7(text, fontSize, fontWeight);
8664
+ return measureText ? measureText(text, fontSize, fontWeight).width : estimateTextWidth8(text, fontSize, fontWeight);
8204
8665
  }
8205
8666
  function ticksOverlap(ticks2, fontSize, fontWeight, measureText, orientation = "horizontal") {
8206
8667
  if (ticks2.length < 2) return false;
@@ -8242,11 +8703,11 @@ function thinTicksUntilFit(ticks2, fontSize, fontWeight, measureText, orientatio
8242
8703
  // src/layout/axes/ticks.ts
8243
8704
  import {
8244
8705
  abbreviateNumber as abbreviateNumber2,
8245
- buildD3Formatter as buildD3Formatter4,
8706
+ buildD3Formatter as buildD3Formatter5,
8246
8707
  buildTemporalFormatter,
8247
- estimateTextWidth as estimateTextWidth8,
8708
+ estimateTextWidth as estimateTextWidth9,
8248
8709
  formatDate,
8249
- formatNumber as formatNumber2
8710
+ formatNumber as formatNumber3
8250
8711
  } from "@opendata-ai/openchart-core";
8251
8712
  var Y_PX_PER_TICK = {
8252
8713
  full: 40,
@@ -8291,7 +8752,8 @@ var NUMERIC_SCALE_TYPES = /* @__PURE__ */ new Set([
8291
8752
  ]);
8292
8753
  var TEMPORAL_SCALE_TYPES = /* @__PURE__ */ new Set(["time", "utc"]);
8293
8754
  function formatTickLabel(value2, resolvedScale) {
8294
- const formatStr = resolvedScale.channel.axis?.format;
8755
+ const axisConfig = resolvedScale.channel.axis || void 0;
8756
+ const formatStr = axisConfig?.format;
8295
8757
  if (TEMPORAL_SCALE_TYPES.has(resolvedScale.type)) {
8296
8758
  const temporalFmt = buildTemporalFormatter(formatStr);
8297
8759
  if (temporalFmt) return temporalFmt(value2);
@@ -8301,11 +8763,11 @@ function formatTickLabel(value2, resolvedScale) {
8301
8763
  if (NUMERIC_SCALE_TYPES.has(resolvedScale.type)) {
8302
8764
  const num = value2;
8303
8765
  if (formatStr) {
8304
- const fmt = buildD3Formatter4(formatStr);
8766
+ const fmt = buildD3Formatter5(formatStr);
8305
8767
  if (fmt) return fmt(num);
8306
8768
  }
8307
8769
  if (Math.abs(num) >= 1e3) return abbreviateNumber2(num);
8308
- return formatNumber2(num);
8770
+ return formatNumber3(num);
8309
8771
  }
8310
8772
  return String(value2);
8311
8773
  }
@@ -8319,8 +8781,8 @@ function continuousTicks(resolvedScale, density, targetCount) {
8319
8781
  label: formatTickLabel(value2, resolvedScale)
8320
8782
  }));
8321
8783
  }
8322
- const explicitCount = resolvedScale.channel.axis?.tickCount;
8323
- const count = explicitCount ?? targetCount ?? TICK_COUNTS[density];
8784
+ const axCfg = resolvedScale.channel.axis || void 0;
8785
+ const count = axCfg?.tickCount ?? targetCount ?? TICK_COUNTS[density];
8324
8786
  return buildContinuousTicks(resolvedScale, count);
8325
8787
  }
8326
8788
  function buildContinuousTicks(resolvedScale, count) {
@@ -8356,12 +8818,13 @@ function scaleSupportsTickCount(resolvedScale) {
8356
8818
  function categoricalTicks(resolvedScale, density, orientation = "horizontal", bandwidth, labelAngle, fontSize, fontWeight, measureText, subtitleContext) {
8357
8819
  const scale = resolvedScale.scale;
8358
8820
  const domain = scale.domain();
8359
- const explicitTickCount = resolvedScale.channel.axis?.tickCount;
8821
+ const catAxisCfg = resolvedScale.channel.axis || void 0;
8822
+ const explicitTickCount = catAxisCfg?.tickCount;
8360
8823
  let selectedValues = domain;
8361
8824
  if (resolvedScale.type === "band" && orientation === "horizontal") {
8362
8825
  if (bandwidth !== void 0 && bandwidth > 0 && fontSize !== void 0) {
8363
8826
  const maxLabelWidth = domain.reduce((max4, v) => {
8364
- const w = measureText ? measureText(v, fontSize, fontWeight ?? 400).width : estimateTextWidth8(v, fontSize, fontWeight ?? 400);
8827
+ const w = measureText ? measureText(v, fontSize, fontWeight ?? 400).width : estimateTextWidth9(v, fontSize, fontWeight ?? 400);
8365
8828
  return Math.max(max4, w);
8366
8829
  }, 0);
8367
8830
  const angleRad = labelAngle !== void 0 ? Math.abs(labelAngle) * Math.PI / 180 : 0;
@@ -8523,7 +8986,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
8523
8986
  };
8524
8987
  const { fontSize } = tickLabelStyle;
8525
8988
  const { fontWeight } = tickLabelStyle;
8526
- if (scales.x && !dataContext?.skipX) {
8989
+ if (scales.x && !dataContext?.skipX && scales.x.channel.axis !== false) {
8527
8990
  const axisConfig = scales.x.channel.axis;
8528
8991
  const isContinuousX = scales.x.type !== "band" && scales.x.type !== "point" && scales.x.type !== "ordinal";
8529
8992
  const xTargetCount = isContinuousX ? targetTickCount(chartArea.width, xDensity, "x") : void 0;
@@ -8601,7 +9064,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
8601
9064
  labelFlush: axisConfig?.labelFlush
8602
9065
  };
8603
9066
  }
8604
- if (scales.y && !dataContext?.skipY) {
9067
+ if (scales.y && !dataContext?.skipY && scales.y.channel.axis !== false) {
8605
9068
  const axisConfig = scales.y.channel.axis;
8606
9069
  const isContinuousY = scales.y.type !== "band" && scales.y.type !== "point" && scales.y.type !== "ordinal";
8607
9070
  const yTargetCount = isContinuousY ? targetTickCount(chartArea.height, yDensity, "y") : void 0;
@@ -8677,8 +9140,8 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
8677
9140
  import {
8678
9141
  AXIS_TITLE_TRAILING_PAD,
8679
9142
  BREAKPOINT_COMPACT_MAX,
8680
- computeChrome as computeChrome2,
8681
- estimateTextWidth as estimateTextWidth10,
9143
+ computeChrome as computeChrome3,
9144
+ estimateTextWidth as estimateTextWidth11,
8682
9145
  getAxisTitleOffset,
8683
9146
  HPAD_COMPACT_FRACTION,
8684
9147
  HPAD_COMPACT_MIN,
@@ -8693,12 +9156,12 @@ import {
8693
9156
  } from "@opendata-ai/openchart-core";
8694
9157
 
8695
9158
  // src/legend/wrap.ts
8696
- import { COMPACT_WIDTH, estimateTextWidth as estimateTextWidth9 } from "@opendata-ai/openchart-core";
9159
+ import { COMPACT_WIDTH, estimateTextWidth as estimateTextWidth10 } from "@opendata-ai/openchart-core";
8697
9160
  var SWATCH_SIZE2 = 12;
8698
9161
  var SWATCH_GAP2 = 6;
8699
9162
  var ENTRY_GAP2 = 16;
8700
9163
  var ENTRY_GAP_COMPACT = 10;
8701
- var LEGEND_GAP = 4;
9164
+ var LEGEND_GAP = 8;
8702
9165
  function legendGap(width) {
8703
9166
  return width < COMPACT_WIDTH ? 0 : LEGEND_GAP;
8704
9167
  }
@@ -8712,7 +9175,7 @@ function measureLegendWrap(entries, maxWidth, labelStyle, maxRows, entryGap = EN
8712
9175
  let fittingCount = entries.length;
8713
9176
  let fittingCountLocked = false;
8714
9177
  for (let i = 0; i < entries.length; i++) {
8715
- const labelWidth = estimateTextWidth9(
9178
+ const labelWidth = estimateTextWidth10(
8716
9179
  entries[i].label,
8717
9180
  labelStyle.fontSize,
8718
9181
  labelStyle.fontWeight
@@ -8758,7 +9221,9 @@ function getMinChartDims(display) {
8758
9221
  }
8759
9222
  function getSparklinePad(spec) {
8760
9223
  const strokeWidth = spec.markDef.strokeWidth ?? 2;
8761
- return Math.max(strokeWidth / 2 + 1, 2);
9224
+ const hasPoints = !!spec.markDef.point;
9225
+ const pointRadius = hasPoints ? 3 : 0;
9226
+ return Math.max(strokeWidth / 2 + 1, pointRadius + 1, 2);
8762
9227
  }
8763
9228
  function computeDimensions(spec, options, legendLayout, theme, strategy, watermark = true) {
8764
9229
  const { width, height } = options;
@@ -8771,7 +9236,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8771
9236
  if (isSparkline && !userExplicit.chrome) {
8772
9237
  chromeMode = "hidden";
8773
9238
  }
8774
- const chrome = computeChrome2(
9239
+ const chrome = computeChrome3(
8775
9240
  chromeToInput(spec.chrome),
8776
9241
  theme,
8777
9242
  width,
@@ -8812,11 +9277,12 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8812
9277
  const total = { x: 0, y: 0, width, height };
8813
9278
  const isRadial = spec.markType === "arc";
8814
9279
  const encoding = spec.encoding;
8815
- const xAxis = encoding.x?.axis;
9280
+ const xAxisSuppressed = encoding.x?.axis === false;
9281
+ const xAxis = !xAxisSuppressed && encoding.x?.axis;
8816
9282
  const hasXAxisLabel = !!xAxis?.title;
8817
9283
  const xTickAngle = xAxis?.labelAngle;
8818
9284
  let xAxisHeight;
8819
- if (isRadial) {
9285
+ if (isRadial || xAxisSuppressed) {
8820
9286
  xAxisHeight = 0;
8821
9287
  } else if (xTickAngle && Math.abs(xTickAngle) > 10) {
8822
9288
  const angleRad = Math.abs(xTickAngle) * (Math.PI / 180);
@@ -8825,7 +9291,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8825
9291
  if (xField) {
8826
9292
  for (const row of spec.data) {
8827
9293
  const label = String(row[xField] ?? "");
8828
- const w = estimateTextWidth10(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
9294
+ const w = estimateTextWidth11(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8829
9295
  if (w > maxLabelWidth) maxLabelWidth = w;
8830
9296
  }
8831
9297
  }
@@ -8855,7 +9321,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8855
9321
  const label = String(row[colorField] ?? "");
8856
9322
  if (!seen.has(label)) {
8857
9323
  seen.add(label);
8858
- const w = estimateTextWidth10(label, 11, 600);
9324
+ const w = estimateTextWidth11(label, 11, 600);
8859
9325
  if (w > maxLabelWidth) maxLabelWidth = w;
8860
9326
  }
8861
9327
  }
@@ -8875,7 +9341,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8875
9341
  const maxXStr = String(maxX);
8876
9342
  for (const ann of spec.annotations) {
8877
9343
  if (ann.type === "text" && String(ann.x) === maxXStr) {
8878
- const textWidth = estimateTextWidth10(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
9344
+ const textWidth = estimateTextWidth11(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
8879
9345
  const dx = ann.offset?.dx ?? 0;
8880
9346
  const anchor = ann.anchor ?? "auto";
8881
9347
  const baseRightExtent = anchor === "left" ? textWidth : (
@@ -8893,19 +9359,20 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8893
9359
  }
8894
9360
  }
8895
9361
  }
8896
- if (encoding.y && !isRadial) {
9362
+ const yAxisSuppressed = encoding.y?.axis === false;
9363
+ if (encoding.y && !isRadial && !yAxisSuppressed) {
8897
9364
  if (spec.markType === "bar" || spec.markType === "circle" || spec.markType === "lollipop" || encoding.y.type === "nominal" || encoding.y.type === "ordinal") {
8898
9365
  const yField = encoding.y.field;
8899
9366
  const yLabelField = encoding.y.axis?.labelField;
8900
9367
  let maxLabelWidth = 0;
8901
9368
  for (const row of spec.data) {
8902
9369
  const label = String(row[yField] ?? "");
8903
- let w = estimateTextWidth10(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
9370
+ let w = estimateTextWidth11(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8904
9371
  if (yLabelField) {
8905
9372
  const subtitle = String(row[yLabelField] ?? "");
8906
9373
  if (subtitle) {
8907
9374
  const gap = theme.fonts.sizes.axisTick * 0.6;
8908
- const subtitleWidth = estimateTextWidth10(
9375
+ const subtitleWidth = estimateTextWidth11(
8909
9376
  subtitle,
8910
9377
  theme.fonts.sizes.axisTick,
8911
9378
  theme.fonts.weights.normal
@@ -8948,7 +9415,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8948
9415
  }
8949
9416
  const negPrefix = spec.data.some((r) => Number(r[yField]) < 0) ? "-" : "";
8950
9417
  const labelEst = negPrefix + sampleLabel;
8951
- const labelWidth = estimateTextWidth10(
9418
+ const labelWidth = estimateTextWidth11(
8952
9419
  labelEst,
8953
9420
  theme.fonts.sizes.axisTick,
8954
9421
  theme.fonts.weights.normal
@@ -8985,7 +9452,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8985
9452
  const minDims = getMinChartDims(spec.display);
8986
9453
  if ((chartArea.width < minDims.width || chartArea.height < minDims.height) && chromeMode !== "hidden") {
8987
9454
  const fallbackMode = chromeMode === "full" ? "compact" : "hidden";
8988
- const fallbackChrome = computeChrome2(
9455
+ const fallbackChrome = computeChrome3(
8989
9456
  chromeToInput(spec.chrome),
8990
9457
  theme,
8991
9458
  width,
@@ -9120,6 +9587,13 @@ function buildLinearScale(channel, data, rangeStart, rangeEnd) {
9120
9587
  const scale = linear2().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
9121
9588
  if (!channel.scale?.domain && channel.scale?.nice !== false) {
9122
9589
  scale.nice();
9590
+ if (channel.scale?.zero === false) {
9591
+ const [nicedMin, nicedMax] = scale.domain();
9592
+ if (nicedMin < domainMin || nicedMax > domainMax) {
9593
+ scale.domain([domainMin, domainMax]);
9594
+ scale.nice(20);
9595
+ }
9596
+ }
9123
9597
  }
9124
9598
  applyContinuousConfig(scale, channel);
9125
9599
  return { scale, type: "linear", channel };
@@ -9470,7 +9944,7 @@ function computeScales(spec, chartArea, data) {
9470
9944
  }
9471
9945
 
9472
9946
  // src/legend/compute.ts
9473
- import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as estimateTextWidth11 } from "@opendata-ai/openchart-core";
9947
+ import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as estimateTextWidth12 } from "@opendata-ai/openchart-core";
9474
9948
  var LEGEND_PADDING = 8;
9475
9949
  var LEGEND_RIGHT_WIDTH = 120;
9476
9950
  var RIGHT_LEGEND_MAX_HEIGHT_RATIO = 0.4;
@@ -9587,7 +10061,7 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
9587
10061
  }
9588
10062
  if (resolvedPosition === "right" || resolvedPosition === "bottom-right") {
9589
10063
  const maxLabelWidth = Math.max(
9590
- ...entries.map((e) => estimateTextWidth11(e.label, labelStyle.fontSize, labelStyle.fontWeight))
10064
+ ...entries.map((e) => estimateTextWidth12(e.label, labelStyle.fontSize, labelStyle.fontWeight))
9591
10065
  );
9592
10066
  const legendWidth = Math.min(
9593
10067
  LEGEND_RIGHT_WIDTH,
@@ -9647,7 +10121,7 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
9647
10121
  entries = truncateEntries(entries, fittingCount);
9648
10122
  }
9649
10123
  const totalWidth = entries.reduce((sum2, entry) => {
9650
- const labelWidth = estimateTextWidth11(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
10124
+ const labelWidth = estimateTextWidth12(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
9651
10125
  return sum2 + SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + effectiveEntryGap;
9652
10126
  }, 0);
9653
10127
  const { rowCount } = measureLegendWrap(
@@ -9679,12 +10153,12 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
9679
10153
 
9680
10154
  // src/sankey/compile-sankey.ts
9681
10155
  import {
9682
- adaptTheme as adaptTheme2,
9683
- buildD3Formatter as buildD3Formatter5,
9684
- computeChrome as computeChrome3,
9685
- estimateTextWidth as estimateTextWidth12,
9686
- formatNumber as formatNumber3,
9687
- resolveTheme as resolveTheme2
10156
+ adaptTheme as adaptTheme3,
10157
+ buildD3Formatter as buildD3Formatter6,
10158
+ computeChrome as computeChrome4,
10159
+ estimateTextWidth as estimateTextWidth13,
10160
+ formatNumber as formatNumber4,
10161
+ resolveTheme as resolveTheme3
9688
10162
  } from "@opendata-ai/openchart-core";
9689
10163
 
9690
10164
  // ../../node_modules/.bun/d3-array@2.12.1/node_modules/d3-array/src/max.js
@@ -10267,16 +10741,16 @@ function compileSankey(spec, options) {
10267
10741
  const rawWatermark = spec.watermark;
10268
10742
  const watermark = rawWatermark !== void 0 ? sankeySpec.watermark : options.watermark ?? true;
10269
10743
  const mergedThemeConfig = options.theme ? { ...sankeySpec.theme, ...options.theme } : sankeySpec.theme;
10270
- const lightTheme = resolveTheme2(mergedThemeConfig);
10744
+ const lightTheme = resolveTheme3(mergedThemeConfig);
10271
10745
  let theme = lightTheme;
10272
10746
  if (options.darkMode) {
10273
- theme = adaptTheme2(theme);
10747
+ theme = adaptTheme3(theme);
10274
10748
  theme = {
10275
10749
  ...theme,
10276
10750
  colors: { ...theme.colors, categorical: lightTheme.colors.categorical }
10277
10751
  };
10278
10752
  }
10279
- const chrome = computeChrome3(
10753
+ const chrome = computeChrome4(
10280
10754
  {
10281
10755
  title: sankeySpec.chrome.title,
10282
10756
  subtitle: sankeySpec.chrome.subtitle,
@@ -10299,7 +10773,7 @@ function compileSankey(spec, options) {
10299
10773
  height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
10300
10774
  };
10301
10775
  if (fullArea.width <= 0 || fullArea.height <= 0) {
10302
- return emptyLayout(fullArea, chrome, theme, options, watermark);
10776
+ return emptyLayout2(fullArea, chrome, theme, options, watermark);
10303
10777
  }
10304
10778
  const sourceField = sankeySpec.encoding.source.field;
10305
10779
  const targetField = sankeySpec.encoding.target.field;
@@ -10335,7 +10809,7 @@ function compileSankey(spec, options) {
10335
10809
  height: fullArea.height - legend.bounds.height - legendGap2
10336
10810
  };
10337
10811
  if (area.height <= 0) {
10338
- return emptyLayout(area, chrome, theme, options, watermark);
10812
+ return emptyLayout2(area, chrome, theme, options, watermark);
10339
10813
  }
10340
10814
  const labelFontSize = theme.fonts.sizes.small;
10341
10815
  const labelFontWeight = theme.fonts.weights.normal;
@@ -10363,7 +10837,7 @@ function compileSankey(spec, options) {
10363
10837
  if (labelsLeft) continue;
10364
10838
  const labelX = (node.x1 ?? nodeWidth) + LABEL_GAP;
10365
10839
  const labelText = node.label ?? node.id;
10366
- const labelWidth = estimateTextWidth12(labelText, labelFontSize, labelFontWeight);
10840
+ const labelWidth = estimateTextWidth13(labelText, labelFontSize, labelFontWeight);
10367
10841
  const overflow = labelX + labelWidth - rightEdge;
10368
10842
  if (overflow > maxOverflow) maxOverflow = overflow;
10369
10843
  }
@@ -10553,10 +11027,10 @@ function buildSankeyLegend(nodeColorMap, colorField, data, sourceField, targetFi
10553
11027
  }
10554
11028
  function formatFlowValue(value2, valueFormat) {
10555
11029
  if (valueFormat) {
10556
- const fmt = buildD3Formatter5(valueFormat);
11030
+ const fmt = buildD3Formatter6(valueFormat);
10557
11031
  if (fmt) return fmt(value2);
10558
11032
  }
10559
- return formatNumber3(value2);
11033
+ return formatNumber4(value2);
10560
11034
  }
10561
11035
  function buildTooltipDescriptors(nodes, links, valueFormat) {
10562
11036
  const descriptors = /* @__PURE__ */ new Map();
@@ -10587,7 +11061,7 @@ function buildTooltipDescriptors(nodes, links, valueFormat) {
10587
11061
  }
10588
11062
  return descriptors;
10589
11063
  }
10590
- function emptyLayout(area, chrome, theme, options, watermark) {
11064
+ function emptyLayout2(area, chrome, theme, options, watermark) {
10591
11065
  return {
10592
11066
  area,
10593
11067
  chrome,
@@ -10625,7 +11099,7 @@ function emptyLayout(area, chrome, theme, options, watermark) {
10625
11099
  }
10626
11100
 
10627
11101
  // src/tables/compile-table.ts
10628
- import { computeChrome as computeChrome4, estimateTextWidth as estimateTextWidth13 } from "@opendata-ai/openchart-core";
11102
+ import { computeChrome as computeChrome5, estimateTextWidth as estimateTextWidth14 } from "@opendata-ai/openchart-core";
10629
11103
 
10630
11104
  // src/tables/bar-column.ts
10631
11105
  var NEGATIVE_BAR_COLOR = "#c44e52";
@@ -10742,7 +11216,7 @@ function computeCategoryColors(data, column, theme, darkMode) {
10742
11216
  }
10743
11217
 
10744
11218
  // src/tables/format-cells.ts
10745
- import { buildD3Formatter as buildD3Formatter6, formatDate as formatDate2, formatNumber as formatNumber4 } from "@opendata-ai/openchart-core";
11219
+ import { buildD3Formatter as buildD3Formatter7, formatDate as formatDate2, formatNumber as formatNumber5 } from "@opendata-ai/openchart-core";
10746
11220
  function isNumericValue(value2) {
10747
11221
  if (typeof value2 === "number") return Number.isFinite(value2);
10748
11222
  return false;
@@ -10761,7 +11235,7 @@ function formatCell(value2, column) {
10761
11235
  };
10762
11236
  }
10763
11237
  if (column.format && isNumericValue(value2)) {
10764
- const formatter = buildD3Formatter6(column.format);
11238
+ const formatter = buildD3Formatter7(column.format);
10765
11239
  if (formatter) {
10766
11240
  return {
10767
11241
  value: value2,
@@ -10773,7 +11247,7 @@ function formatCell(value2, column) {
10773
11247
  if (isNumericValue(value2)) {
10774
11248
  return {
10775
11249
  value: value2,
10776
- formattedValue: formatNumber4(value2),
11250
+ formattedValue: formatNumber5(value2),
10777
11251
  style
10778
11252
  };
10779
11253
  }
@@ -10793,19 +11267,28 @@ function formatCell(value2, column) {
10793
11267
  function formatValueForSearch(value2, column) {
10794
11268
  if (value2 == null) return "";
10795
11269
  if (column.format && isNumericValue(value2)) {
10796
- const formatter = buildD3Formatter6(column.format);
11270
+ const formatter = buildD3Formatter7(column.format);
10797
11271
  if (formatter) {
10798
11272
  return formatter(value2);
10799
11273
  }
10800
11274
  }
10801
11275
  if (isNumericValue(value2)) {
10802
- return formatNumber4(value2);
11276
+ return formatNumber5(value2);
10803
11277
  }
10804
11278
  return String(value2);
10805
11279
  }
10806
11280
 
10807
11281
  // src/tables/heatmap.ts
10808
11282
  import { adaptColorForDarkMode as adaptColorForDarkMode2 } from "@opendata-ai/openchart-core";
11283
+ function relativeLuminance(hex2) {
11284
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex2);
11285
+ if (!m) return 0;
11286
+ const [r, g, b] = [m[1], m[2], m[3]].map((c) => {
11287
+ const v = Number.parseInt(c, 16) / 255;
11288
+ return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
11289
+ });
11290
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
11291
+ }
10809
11292
  function interpolatorFromStops(stops) {
10810
11293
  if (stops.length === 0) return () => "#ffffff";
10811
11294
  if (stops.length === 1) return () => stops[0];
@@ -10858,7 +11341,19 @@ function computeHeatmapColors(data, column, theme, darkMode) {
10858
11341
  if (darkMode) {
10859
11342
  const lightBg = "#ffffff";
10860
11343
  const darkBg = theme.colors.background;
11344
+ const originalStops = stops;
10861
11345
  stops = stops.map((c) => adaptColorForDarkMode2(c, lightBg, darkBg));
11346
+ if (originalStops.length >= 2) {
11347
+ const origDirection = Math.sign(
11348
+ relativeLuminance(originalStops[originalStops.length - 1]) - relativeLuminance(originalStops[0])
11349
+ );
11350
+ const adaptedDirection = Math.sign(
11351
+ relativeLuminance(stops[stops.length - 1]) - relativeLuminance(stops[0])
11352
+ );
11353
+ if (origDirection !== 0 && adaptedDirection !== 0 && origDirection !== adaptedDirection) {
11354
+ stops = stops.slice().reverse();
11355
+ }
11356
+ }
10862
11357
  }
10863
11358
  const interpolator = interpolatorFromStops(stops);
10864
11359
  const scale = sequential(interpolator).domain(domain).clamp(true);
@@ -11040,13 +11535,13 @@ function estimateColumnWidth(col, data, fontSize) {
11040
11535
  if (col.image) return (col.image.width ?? 24) + PADDING;
11041
11536
  if (col.flag) return 60;
11042
11537
  const label = col.label ?? col.key;
11043
- const headerWidth = estimateTextWidth13(label, fontSize, 600) + PADDING;
11538
+ const headerWidth = estimateTextWidth14(label, fontSize, 600) + PADDING;
11044
11539
  const sampleSize = Math.min(100, data.length);
11045
11540
  let maxDataWidth = 0;
11046
11541
  for (let i = 0; i < sampleSize; i++) {
11047
11542
  const val = data[i][col.key];
11048
11543
  const text = val == null ? "" : String(val);
11049
- const width = estimateTextWidth13(text, fontSize, 400) + PADDING;
11544
+ const width = estimateTextWidth14(text, fontSize, 400) + PADDING;
11050
11545
  if (width > maxDataWidth) maxDataWidth = width;
11051
11546
  }
11052
11547
  return Math.max(MIN_WIDTH, headerWidth, maxDataWidth);
@@ -11230,7 +11725,7 @@ function compileTableLayout(spec, options, theme) {
11230
11725
  return { id: rowId, cells, data: row };
11231
11726
  });
11232
11727
  const watermark = spec.watermark;
11233
- const chrome = computeChrome4(
11728
+ const chrome = computeChrome5(
11234
11729
  {
11235
11730
  title: spec.chrome.title,
11236
11731
  subtitle: spec.chrome.subtitle,
@@ -11272,16 +11767,16 @@ function compileTableLayout(spec, options, theme) {
11272
11767
 
11273
11768
  // src/tilemap/compile-tilemap.ts
11274
11769
  import {
11275
- adaptTheme as adaptTheme3,
11276
- buildD3Formatter as buildD3Formatter7,
11277
- computeChrome as computeChrome5,
11278
- estimateTextWidth as estimateTextWidth14,
11279
- formatNumber as formatNumber5,
11280
- resolveTheme as resolveTheme3,
11770
+ adaptTheme as adaptTheme4,
11771
+ buildD3Formatter as buildD3Formatter8,
11772
+ computeChrome as computeChrome6,
11773
+ estimateTextWidth as estimateTextWidth15,
11774
+ formatNumber as formatNumber6,
11775
+ resolveTheme as resolveTheme4,
11281
11776
  SEQUENTIAL_PALETTES
11282
11777
  } from "@opendata-ai/openchart-core";
11283
- var TILE_CORNER_RADIUS = 2;
11284
- var TILE_STROKE_WIDTH = 0;
11778
+ var TILE_CORNER_RADIUS = 6;
11779
+ var TILE_STROKE_WIDTH = 1;
11285
11780
  function compileTileMap(spec, options) {
11286
11781
  const { spec: normalized } = compile(spec);
11287
11782
  if (!("type" in normalized) || normalized.type !== "tilemap") {
@@ -11293,13 +11788,13 @@ function compileTileMap(spec, options) {
11293
11788
  const rawWatermark = spec.watermark;
11294
11789
  const watermark = rawWatermark !== void 0 ? tilemapSpec.watermark : options.watermark ?? true;
11295
11790
  const mergedThemeConfig = options.theme ? { ...tilemapSpec.theme, ...options.theme } : tilemapSpec.theme;
11296
- const lightTheme = resolveTheme3(mergedThemeConfig);
11791
+ const lightTheme = resolveTheme4(mergedThemeConfig);
11297
11792
  let theme = lightTheme;
11298
11793
  if (options.darkMode) {
11299
- theme = adaptTheme3(theme);
11794
+ theme = adaptTheme4(theme);
11300
11795
  }
11301
11796
  const isDarkMode = options.darkMode;
11302
- const chrome = computeChrome5(
11797
+ const chrome = computeChrome6(
11303
11798
  {
11304
11799
  title: tilemapSpec.chrome.title,
11305
11800
  subtitle: tilemapSpec.chrome.subtitle,
@@ -11322,7 +11817,7 @@ function compileTileMap(spec, options) {
11322
11817
  height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
11323
11818
  };
11324
11819
  if (fullArea.width <= 0 || fullArea.height <= 0) {
11325
- return emptyLayout2(chrome, theme, options, watermark);
11820
+ return emptyLayout3(chrome, theme, options, watermark);
11326
11821
  }
11327
11822
  const stateField = tilemapSpec.encoding.state.field;
11328
11823
  const valueField = tilemapSpec.encoding.value.field;
@@ -11341,26 +11836,26 @@ function compileTileMap(spec, options) {
11341
11836
  const min4 = values.length > 0 ? Math.min(...values) : 0;
11342
11837
  const max4 = values.length > 0 ? Math.max(...values) : 100;
11343
11838
  const paletteStops = [...SEQUENTIAL_PALETTES[tilemapSpec.palette] ?? SEQUENTIAL_PALETTES.blue];
11344
- if (isDarkMode) paletteStops.reverse();
11345
- const domain = paletteStops.map((_, i) => min4 + i / (paletteStops.length - 1) * (max4 - min4));
11346
- const colorScale = linear2().domain(domain).range(paletteStops).clamp(true);
11839
+ const baseColor = isDarkMode ? paletteStops[0] : paletteStops[paletteStops.length - 1];
11840
+ const opacityRange = isDarkMode ? [0.15, 1] : [0.2, 1];
11841
+ const opacityScale = linear2().domain([min4, max4]).range(opacityRange).clamp(true);
11347
11842
  const showLegend = tilemapSpec.legend?.show !== false;
11348
- const legendBarHeight = 12;
11349
- const legendLabelGap = 4;
11843
+ const legendBarHeight = 6;
11844
+ const legendLabelGap = 6;
11350
11845
  const legendTotalHeight = showLegend ? legendBarHeight + legendLabelGap + 14 : 0;
11351
11846
  const legendGap2 = showLegend ? 8 : 0;
11352
11847
  const tileAreaHeight = fullArea.height - legendTotalHeight - legendGap2;
11353
- const tilePositions = computeTilePositions(fullArea.width, tileAreaHeight, 4);
11848
+ const tilePositions = computeTilePositions(fullArea.width, tileAreaHeight, 5);
11354
11849
  const tileGridOffsetX = fullArea.x + (fullArea.width - tilePositions.gridWidth) / 2;
11355
11850
  const tileGridOffsetY = fullArea.y;
11356
11851
  const legendX = tileGridOffsetX;
11357
11852
  const legendY = tileGridOffsetY + tilePositions.gridHeight + legendGap2;
11358
11853
  const legendWidth = tilePositions.gridWidth;
11359
- const formatter = buildD3Formatter7(tilemapSpec.valueFormat) ?? formatNumber5;
11854
+ const formatter = buildD3Formatter8(tilemapSpec.valueFormat) ?? formatNumber6;
11360
11855
  const neutralFillLight = "#e0e0e0";
11361
- const neutralFillDark = "#2a2a3e";
11856
+ const neutralFillDark = "#1e2a30";
11362
11857
  const neutralStrokeLight = "#d0d0d0";
11363
- const neutralStrokeDark = "#3a3a50";
11858
+ const neutralStrokeDark = "rgba(255,255,255,0.08)";
11364
11859
  const neutralFill = isDarkMode ? neutralFillDark : neutralFillLight;
11365
11860
  const neutralStroke = isDarkMode ? neutralStrokeDark : neutralStrokeLight;
11366
11861
  const tiles = [];
@@ -11369,26 +11864,31 @@ function compileTileMap(spec, options) {
11369
11864
  if (!pos) continue;
11370
11865
  const hasData = stateValueMap.has(stateCode);
11371
11866
  const value2 = hasData ? stateValueMap.get(stateCode) : null;
11372
- const fill = hasData ? colorScale(value2) : neutralFill;
11867
+ const opacity = hasData ? opacityScale(value2) : 0;
11868
+ const fill = hasData ? baseColor : neutralFill;
11869
+ const stroke = hasData ? isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)" : neutralStroke;
11373
11870
  const formattedValue = hasData ? formatter(value2) : "\u2013";
11374
11871
  const labelStyle = {
11375
11872
  fontFamily: theme.fonts.family,
11376
- fontSize: tilePositions.tileSize > 24 ? 14 : 11,
11873
+ fontSize: tilePositions.tileSize > 24 ? 10 : 7,
11377
11874
  fontWeight: 700,
11378
11875
  fill: "#ffffff",
11379
11876
  lineHeight: 1.2
11380
11877
  };
11381
11878
  const valueLabelStyle = {
11382
11879
  fontFamily: theme.fonts.family,
11383
- fontSize: tilePositions.tileSize > 24 ? 12 : 10,
11384
- fontWeight: 400,
11385
- fill: "#ffffff",
11880
+ fontSize: tilePositions.tileSize > 24 ? 10 : 7,
11881
+ fontWeight: 300,
11882
+ fill: "rgba(255,255,255,0.6)",
11386
11883
  lineHeight: 1.2
11387
11884
  };
11388
- const valueLabel = tilePositions.tileSize < 24 ? { text: "", x: 0, y: 0, style: valueLabelStyle, visible: false } : {
11885
+ const tileCenterX = tileGridOffsetX + pos.x + tilePositions.tileSize / 2;
11886
+ const tileTopY = tileGridOffsetY + pos.y;
11887
+ const sz = tilePositions.tileSize;
11888
+ const valueLabel = sz < 24 ? { text: "", x: 0, y: 0, style: valueLabelStyle, visible: false } : {
11389
11889
  text: formattedValue,
11390
- x: tileGridOffsetX + pos.x + tilePositions.tileSize / 2,
11391
- y: tileGridOffsetY + pos.y + tilePositions.tileSize / 2 + 8,
11890
+ x: tileCenterX,
11891
+ y: tileTopY + sz * 0.78,
11392
11892
  style: valueLabelStyle,
11393
11893
  visible: true
11394
11894
  };
@@ -11396,10 +11896,11 @@ function compileTileMap(spec, options) {
11396
11896
  type: "tile",
11397
11897
  stateCode,
11398
11898
  x: tileGridOffsetX + pos.x,
11399
- y: tileGridOffsetY + pos.y,
11400
- size: tilePositions.tileSize,
11899
+ y: tileTopY,
11900
+ size: sz,
11401
11901
  fill,
11402
- stroke: neutralStroke,
11902
+ fillOpacity: hasData ? opacity : 1,
11903
+ stroke,
11403
11904
  strokeWidth: TILE_STROKE_WIDTH,
11404
11905
  cornerRadius: TILE_CORNER_RADIUS,
11405
11906
  value: value2 ?? null,
@@ -11407,8 +11908,8 @@ function compileTileMap(spec, options) {
11407
11908
  hasData,
11408
11909
  label: {
11409
11910
  text: stateCode,
11410
- x: tileGridOffsetX + pos.x + tilePositions.tileSize / 2,
11411
- y: tileGridOffsetY + pos.y + tilePositions.tileSize / 2 - 4,
11911
+ x: tileCenterX,
11912
+ y: tileTopY + sz * 0.28,
11412
11913
  style: labelStyle,
11413
11914
  visible: true
11414
11915
  },
@@ -11434,10 +11935,12 @@ function compileTileMap(spec, options) {
11434
11935
  }
11435
11936
  let gradientLegend = null;
11436
11937
  if (showLegend) {
11437
- const gradientColorStops = paletteStops.map((color2, i) => ({
11438
- offset: i / (paletteStops.length - 1),
11439
- color: color2
11440
- }));
11938
+ const numStops = 5;
11939
+ const gradientColorStops = Array.from({ length: numStops }, (_, i) => {
11940
+ const t = i / (numStops - 1);
11941
+ const o = opacityRange[0] + t * (opacityRange[1] - opacityRange[0]);
11942
+ return { offset: t, color: baseColor, opacity: o };
11943
+ });
11441
11944
  gradientLegend = {
11442
11945
  type: "gradient",
11443
11946
  position: "bottom",
@@ -11474,6 +11977,7 @@ function compileTileMap(spec, options) {
11474
11977
  keyboardNavigable: tiles.length > 0
11475
11978
  };
11476
11979
  const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
11980
+ const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
11477
11981
  return {
11478
11982
  area: fullArea,
11479
11983
  chrome,
@@ -11483,13 +11987,13 @@ function compileTileMap(spec, options) {
11483
11987
  a11y,
11484
11988
  theme,
11485
11989
  width: options.width,
11486
- height: options.height,
11990
+ height: contentHeight,
11487
11991
  animation: resolvedAnimation,
11488
11992
  watermark,
11489
- measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth14(text, fontSize), height: fontSize }))
11993
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth15(text, fontSize), height: fontSize }))
11490
11994
  };
11491
11995
  }
11492
- function emptyLayout2(chrome, theme, options, watermark) {
11996
+ function emptyLayout3(chrome, theme, options, watermark) {
11493
11997
  return {
11494
11998
  area: { x: 0, y: 0, width: 0, height: 0 },
11495
11999
  chrome,
@@ -11521,7 +12025,7 @@ function emptyLayout2(chrome, theme, options, watermark) {
11521
12025
  height: options.height,
11522
12026
  watermark,
11523
12027
  animation: void 0,
11524
- measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth14(text, fontSize), height: fontSize }))
12028
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth15(text, fontSize), height: fontSize }))
11525
12029
  };
11526
12030
  }
11527
12031
 
@@ -11529,7 +12033,7 @@ function emptyLayout2(chrome, theme, options, watermark) {
11529
12033
  import {
11530
12034
  buildTemporalFormatter as buildTemporalFormatter2,
11531
12035
  formatDate as formatDate3,
11532
- formatNumber as formatNumber6,
12036
+ formatNumber as formatNumber7,
11533
12037
  getRepresentativeColor as getRepresentativeColor10
11534
12038
  } from "@opendata-ai/openchart-core";
11535
12039
  function formatValue(value2, fieldType, format2) {
@@ -11544,18 +12048,20 @@ function formatValue(value2, fieldType, format2) {
11544
12048
  try {
11545
12049
  return format(format2)(value2);
11546
12050
  } catch {
11547
- return formatNumber6(value2);
12051
+ return formatNumber7(value2);
11548
12052
  }
11549
12053
  }
11550
- return formatNumber6(value2);
12054
+ return formatNumber7(value2);
11551
12055
  }
11552
12056
  return String(value2);
11553
12057
  }
11554
12058
  function resolveLabel(ch) {
11555
- return ch.title ?? ch.axis?.title ?? ch.field;
12059
+ const ax = ch.axis || void 0;
12060
+ return ch.title ?? ax?.title ?? ch.field;
11556
12061
  }
11557
12062
  function resolveFormat(ch) {
11558
- return ch.format ?? ch.axis?.format;
12063
+ const ax = ch.axis || void 0;
12064
+ return ch.format ?? ax?.format;
11559
12065
  }
11560
12066
  function buildExplicitTooltipFields(row, channels) {
11561
12067
  return channels.map((ch) => ({
@@ -12412,9 +12918,9 @@ function compileChart(spec, options) {
12412
12918
  chartSpec = { ...chartSpec, watermark: false };
12413
12919
  }
12414
12920
  const mergedThemeConfig = options.theme ? { ...chartSpec.theme, ...options.theme } : chartSpec.theme;
12415
- let theme = resolveTheme4(mergedThemeConfig);
12921
+ let theme = resolveTheme5(mergedThemeConfig);
12416
12922
  if (options.darkMode) {
12417
- theme = adaptTheme4(theme);
12923
+ theme = adaptTheme5(theme);
12418
12924
  }
12419
12925
  const preliminaryArea = {
12420
12926
  x: 0,
@@ -12600,7 +13106,7 @@ function estimateYAxisLabelWidth(data, encoding, baseFontSize) {
12600
13106
  let maxWidth = 0;
12601
13107
  for (const row of data) {
12602
13108
  const label = String(row[yField] ?? "");
12603
- const w = estimateTextWidth15(label, baseFontSize, 400);
13109
+ const w = estimateTextWidth16(label, baseFontSize, 400);
12604
13110
  if (w > maxWidth) maxWidth = w;
12605
13111
  }
12606
13112
  return maxWidth > 0 ? maxWidth + 10 : 40;
@@ -12629,7 +13135,7 @@ function estimateYAxisLabelWidth(data, encoding, baseFontSize) {
12629
13135
  }
12630
13136
  const hasNeg = data.some((r) => Number(r[yField]) < 0);
12631
13137
  const labelEst = (hasNeg ? "-" : "") + sampleLabel;
12632
- return estimateTextWidth15(labelEst, baseFontSize, 400) + 10;
13138
+ return estimateTextWidth16(labelEst, baseFontSize, 400) + 10;
12633
13139
  }
12634
13140
  function compileLayerIndependent(leaves, layerSpec, options) {
12635
13141
  if (leaves.length > 2) {
@@ -12646,10 +13152,11 @@ function compileLayerIndependent(leaves, layerSpec, options) {
12646
13152
  `Dual-axis charts require matching x-field types across layers. Layer 0 has '${xType0}', layer 1 has '${xType1}'.`
12647
13153
  );
12648
13154
  }
12649
- const theme = resolveTheme4(layerSpec.theme ?? leaf1.theme);
13155
+ const theme = resolveTheme5(layerSpec.theme ?? leaf1.theme);
12650
13156
  const axisFontSize = theme.fonts?.sizes?.axisTick ?? 11;
12651
13157
  const rightAxisWidth = estimateYAxisLabelWidth(leaf1.data, leaf1.encoding, axisFontSize);
12652
- const hasRightAxisTitle = !!leaf1.encoding?.y?.axis?.title;
13158
+ const yAxisConfig = leaf1.encoding?.y?.axis || void 0;
13159
+ const hasRightAxisTitle = !!yAxisConfig?.title;
12653
13160
  const tickExtent = TICK_LABEL_OFFSET + rightAxisWidth;
12654
13161
  const bodyFontSize = theme.fonts?.sizes?.body ?? 13;
12655
13162
  const axisTitleOffset = getAxisTitleOffset2(options.width);
@@ -12911,9 +13418,9 @@ function compileTable(spec, options) {
12911
13418
  }
12912
13419
  const tableSpec = normalized;
12913
13420
  const mergedThemeConfig = options.theme ? { ...tableSpec.theme, ...options.theme } : tableSpec.theme;
12914
- let theme = resolveTheme4(mergedThemeConfig);
13421
+ let theme = resolveTheme5(mergedThemeConfig);
12915
13422
  if (options.darkMode) {
12916
- theme = adaptTheme4(theme);
13423
+ theme = adaptTheme5(theme);
12917
13424
  }
12918
13425
  const rawWatermark = spec.watermark;
12919
13426
  const watermark = rawWatermark !== void 0 ? tableSpec.watermark : options.watermark ?? true;
@@ -12928,10 +13435,14 @@ function compileSankey2(spec, options) {
12928
13435
  function compileTileMap2(spec, options) {
12929
13436
  return compileTileMap(spec, options);
12930
13437
  }
13438
+ function compileBarList2(spec, options) {
13439
+ return compileBarList(spec, options);
13440
+ }
12931
13441
  export {
12932
13442
  clampStaggerDelay,
12933
13443
  clearRenderers,
12934
13444
  compile,
13445
+ compileBarList2 as compileBarList,
12935
13446
  compileChart,
12936
13447
  compileGraph2 as compileGraph,
12937
13448
  compileLayer,