@opendata-ai/openchart-engine 6.24.0 → 6.24.1

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
@@ -467,7 +467,9 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark) {
467
467
  return null;
468
468
  }
469
469
  let strokeDasharray;
470
- if (annotation.style === "dashed" || annotation.style === void 0) {
470
+ if (annotation.strokeDash && annotation.strokeDash.length > 0) {
471
+ strokeDasharray = annotation.strokeDash.join(" ");
472
+ } else if (annotation.style === "dashed" || annotation.style === void 0) {
471
473
  strokeDasharray = DEFAULT_REFLINE_DASH;
472
474
  } else if (annotation.style === "dotted") {
473
475
  strokeDasharray = "2 2";
@@ -549,11 +551,12 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark) {
549
551
 
550
552
  // src/annotations/compute.ts
551
553
  function computeAnnotations(spec, scales, chartArea, strategy, isDark = false, obstacles = [], svgDimensions) {
552
- if (strategy.annotationPosition === "tooltip-only") {
553
- return [];
554
- }
554
+ const isCompact = strategy.annotationPosition === "tooltip-only";
555
555
  const annotations = [];
556
556
  for (const annotation of spec.annotations) {
557
+ if (isCompact && annotation.responsive !== false) {
558
+ continue;
559
+ }
557
560
  let resolved = null;
558
561
  switch (annotation.type) {
559
562
  case "text":
@@ -979,6 +982,7 @@ function computeSimpleBars(data, valueField, categoryField, xScale, yScale, band
979
982
  import {
980
983
  buildD3Formatter,
981
984
  estimateTextWidth as estimateTextWidth2,
985
+ findAccessibleColor,
982
986
  getRepresentativeColor,
983
987
  resolveCollisions
984
988
  } from "@opendata-ai/openchart-core";
@@ -1047,21 +1051,35 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
1047
1051
  const textHeight = LABEL_FONT_SIZE * 1.2;
1048
1052
  const isStacked = mark.cornerRadius === 0;
1049
1053
  const isInside = mark.width >= MIN_WIDTH_FOR_INSIDE_LABEL;
1054
+ const isNegative = Number.isFinite(rawNum) ? rawNum < 0 : false;
1055
+ const bgColor = getRepresentativeColor(mark.fill);
1050
1056
  let anchorX;
1051
1057
  let fill;
1052
1058
  let textAnchor;
1053
1059
  if (isStacked && isInside) {
1054
1060
  anchorX = mark.x + mark.width / 2;
1055
- fill = "#ffffff";
1061
+ fill = findAccessibleColor("#ffffff", bgColor, 4.5);
1056
1062
  textAnchor = "middle";
1057
1063
  } else if (isInside) {
1058
- anchorX = mark.x + mark.width - LABEL_PADDING;
1059
- fill = "#ffffff";
1060
- textAnchor = "end";
1064
+ if (isNegative) {
1065
+ anchorX = mark.x + LABEL_PADDING;
1066
+ fill = findAccessibleColor("#ffffff", bgColor, 4.5);
1067
+ textAnchor = "start";
1068
+ } else {
1069
+ anchorX = mark.x + mark.width - LABEL_PADDING;
1070
+ fill = findAccessibleColor("#ffffff", bgColor, 4.5);
1071
+ textAnchor = "end";
1072
+ }
1061
1073
  } else {
1062
- anchorX = mark.x + mark.width + LABEL_PADDING;
1063
- fill = getRepresentativeColor(mark.fill);
1064
- textAnchor = "start";
1074
+ if (isNegative) {
1075
+ anchorX = mark.x - LABEL_PADDING;
1076
+ fill = getRepresentativeColor(mark.fill);
1077
+ textAnchor = "end";
1078
+ } else {
1079
+ anchorX = mark.x + mark.width + LABEL_PADDING;
1080
+ fill = getRepresentativeColor(mark.fill);
1081
+ textAnchor = "start";
1082
+ }
1065
1083
  }
1066
1084
  const anchorY = mark.y + mark.height / 2;
1067
1085
  const fits = !(isStacked && textWidth > mark.width - 2 * LABEL_PADDING);
@@ -2662,14 +2680,16 @@ function computeSingleArea(spec, scales, _chartArea) {
2662
2680
  const color2 = getColor(scales, seriesKey);
2663
2681
  const sortedRows = xChannel.type === "nominal" || xChannel.type === "ordinal" ? rows : sortByField(rows, xChannel.field);
2664
2682
  const validPoints = [];
2683
+ const y2Channel = encoding.y2;
2665
2684
  for (const row of sortedRows) {
2666
2685
  const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
2667
2686
  const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
2668
2687
  if (xVal === null || yVal === null) continue;
2688
+ const yBottomVal = y2Channel && row[y2Channel.field] != null ? scaleValue(scales.y.scale, scales.y.type, row[y2Channel.field]) : null;
2669
2689
  validPoints.push({
2670
2690
  x: xVal,
2671
2691
  yTop: yVal,
2672
- yBottom: baselineY,
2692
+ yBottom: yBottomVal ?? baselineY,
2673
2693
  row
2674
2694
  });
2675
2695
  }
@@ -2683,6 +2703,7 @@ function computeSingleArea(spec, scales, _chartArea) {
2683
2703
  const bottomPoints = validPoints.map((p) => ({ x: p.x, y: p.yBottom }));
2684
2704
  const ariaLabel = seriesKey === "__default__" ? `Area with ${validPoints.length} data points` : `${seriesKey}: area with ${validPoints.length} data points`;
2685
2705
  const aria = { label: ariaLabel };
2706
+ const fillOpacity = y2Channel ? 0.25 : DEFAULT_FILL_OPACITY;
2686
2707
  marks.push({
2687
2708
  type: "area",
2688
2709
  topPoints,
@@ -2690,7 +2711,7 @@ function computeSingleArea(spec, scales, _chartArea) {
2690
2711
  path: pathStr,
2691
2712
  topPath: topPathStr,
2692
2713
  fill: color2,
2693
- fillOpacity: DEFAULT_FILL_OPACITY,
2714
+ fillOpacity,
2694
2715
  stroke: getRepresentativeColor4(color2),
2695
2716
  strokeWidth: 2,
2696
2717
  seriesKey: seriesKey === "__default__" ? void 0 : seriesKey,
@@ -6607,8 +6628,10 @@ function normalizeAnnotations(annotations) {
6607
6628
  fill: ann.fill ?? "#000000"
6608
6629
  };
6609
6630
  case "refline":
6631
+ case "rule":
6610
6632
  return {
6611
6633
  ...ann,
6634
+ type: "refline",
6612
6635
  style: ann.style ?? "dashed",
6613
6636
  strokeWidth: ann.strokeWidth ?? 1,
6614
6637
  stroke: ann.stroke ?? "#666666",
@@ -8342,7 +8365,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8342
8365
  left: padding + (isRadial ? padding : axisMargin)
8343
8366
  };
8344
8367
  const labelDensity = spec.labels.density;
8345
- if ((spec.markType === "line" || spec.markType === "area") && labelDensity !== "none") {
8368
+ const labelsHiddenByStrategy = strategy?.labelMode === "none";
8369
+ if ((spec.markType === "line" || spec.markType === "area") && labelDensity !== "none" && !labelsHiddenByStrategy) {
8346
8370
  const colorEnc = encoding.color;
8347
8371
  const colorField = colorEnc && "field" in colorEnc ? colorEnc.field : void 0;
8348
8372
  if (colorField) {
@@ -8979,6 +9003,7 @@ function extractColorEntries(spec, theme) {
8979
9003
  ...explicitDomain.filter((v) => dataValues.includes(v)),
8980
9004
  ...dataValues.filter((v) => !explicitDomain.includes(v))
8981
9005
  ] : dataValues;
9006
+ const excludeSet = new Set(spec.legend?.exclude ?? []);
8982
9007
  return uniqueValues.map((value2, i) => {
8983
9008
  let colorIndex = i;
8984
9009
  if (explicitDomain && explicitRange) {
@@ -8991,7 +9016,7 @@ function extractColorEntries(spec, theme) {
8991
9016
  shape,
8992
9017
  active: true
8993
9018
  };
8994
- });
9019
+ }).filter((entry) => !excludeSet.has(entry.label));
8995
9020
  }
8996
9021
  function truncateEntries(entries, maxCount) {
8997
9022
  if (maxCount >= entries.length || maxCount <= 0) return entries;
@@ -9029,7 +9054,10 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
9029
9054
  const hasLabels = spec.labels.density !== "none";
9030
9055
  const labelsWillRender = strategy.labelMode !== "none";
9031
9056
  const hasColorEncoding = spec.encoding.color != null;
9032
- const legendNotForced = spec.legend?.show !== true;
9057
+ const userConfiguredLegend = spec.legend != null && Object.keys(spec.legend).some(
9058
+ (k) => k !== "show" || spec.legend[k] !== false
9059
+ );
9060
+ const legendNotForced = !userConfiguredLegend;
9033
9061
  if (isLineOrArea && hasLabels && labelsWillRender && hasColorEncoding && legendNotForced) {
9034
9062
  const isArea = spec.markType === "area";
9035
9063
  const quantChannel = spec.encoding.y?.type === "quantitative" ? spec.encoding.y : spec.encoding.x;