@opendata-ai/openchart-engine 2.4.0 → 2.6.0

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.d.ts CHANGED
@@ -180,6 +180,8 @@ interface NormalizedChartSpec {
180
180
  darkMode: DarkMode;
181
181
  /** Series names to hide from rendering. */
182
182
  hiddenSeries: string[];
183
+ /** Per-series visual style overrides. */
184
+ seriesStyles: Record<string, _opendata_ai_openchart_core.SeriesStyle>;
183
185
  }
184
186
  /** A TableSpec with all optional fields filled with sensible defaults. */
185
187
  interface NormalizedTableSpec {
package/dist/index.js CHANGED
@@ -202,14 +202,23 @@ function resolveRangeAnnotation(annotation, scales, chartArea, isDark) {
202
202
  const rect = { x, y, width, height };
203
203
  let label;
204
204
  if (annotation.label) {
205
- const baseDx = 4;
205
+ const anchor = annotation.labelAnchor ?? "left";
206
+ const centered = anchor === "top" || anchor === "bottom" || anchor === "auto";
207
+ const baseDx = centered ? 0 : anchor === "right" ? -4 : 4;
206
208
  const baseDy = 14;
207
209
  const labelDelta = applyOffset({ dx: baseDx, dy: baseDy }, annotation.labelOffset);
210
+ const style = makeAnnotationLabelStyle(11, 500, void 0, isDark);
211
+ if (centered) {
212
+ style.textAnchor = "middle";
213
+ } else if (anchor === "right") {
214
+ style.textAnchor = "end";
215
+ }
216
+ const baseX = centered ? x + width / 2 : anchor === "right" ? x + width : x;
208
217
  label = {
209
218
  text: annotation.label,
210
- x: x + labelDelta.dx,
219
+ x: baseX + labelDelta.dx,
211
220
  y: y + labelDelta.dy,
212
- style: makeAnnotationLabelStyle(11, 500, void 0, isDark),
221
+ style,
213
222
  visible: true
214
223
  };
215
224
  }
@@ -249,8 +258,9 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark) {
249
258
  let label;
250
259
  if (annotation.label) {
251
260
  const isHorizontal = annotation.y !== void 0;
261
+ const anchor = annotation.labelAnchor ?? "top";
252
262
  const baseDx = isHorizontal ? -4 : 4;
253
- const baseDy = -4;
263
+ const baseDy = anchor === "bottom" ? 14 : -4;
254
264
  const labelDelta = applyOffset({ dx: baseDx, dy: baseDy }, annotation.labelOffset);
255
265
  const defaultStroke2 = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
256
266
  const style = makeAnnotationLabelStyle(11, 400, annotation.stroke ?? defaultStroke2, isDark);
@@ -580,19 +590,41 @@ function computeSimpleBars(data, valueField, categoryField, xScale, yScale, band
580
590
 
581
591
  // src/charts/bar/labels.ts
582
592
  import { estimateTextWidth as estimateTextWidth2, resolveCollisions } from "@opendata-ai/openchart-core";
593
+ import { format as d3Format } from "d3-format";
583
594
  var LABEL_FONT_SIZE = 11;
584
595
  var LABEL_FONT_WEIGHT = 600;
585
596
  var LABEL_PADDING = 6;
586
597
  var MIN_WIDTH_FOR_INSIDE_LABEL = 40;
587
- function computeBarLabels(marks, _chartArea, density = "auto") {
598
+ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat) {
588
599
  if (density === "none") return [];
589
600
  const targetMarks = density === "endpoints" && marks.length > 1 ? [marks[0], marks[marks.length - 1]] : marks;
590
601
  const candidates = [];
602
+ let formatter = null;
603
+ if (labelFormat) {
604
+ try {
605
+ formatter = d3Format(labelFormat);
606
+ } catch {
607
+ const suffixMatch = labelFormat.match(/^(.+[a-z])([^a-z]+)$/i);
608
+ if (suffixMatch) {
609
+ try {
610
+ const d3Fmt = d3Format(suffixMatch[1]);
611
+ const suffix = suffixMatch[2];
612
+ formatter = (v) => d3Fmt(v) + suffix;
613
+ } catch {
614
+ }
615
+ }
616
+ }
617
+ }
591
618
  for (const mark of targetMarks) {
592
619
  const ariaLabel = mark.aria.label;
593
620
  const lastColon = ariaLabel.lastIndexOf(":");
594
- const valuePart = lastColon >= 0 ? ariaLabel.slice(lastColon + 1).trim() : "";
595
- if (!valuePart) continue;
621
+ const rawValue = lastColon >= 0 ? ariaLabel.slice(lastColon + 1).trim() : "";
622
+ if (!rawValue) continue;
623
+ let valuePart = rawValue;
624
+ if (formatter) {
625
+ const num = Number(rawValue.replace(/[^0-9.-]/g, ""));
626
+ if (!Number.isNaN(num)) valuePart = formatter(num);
627
+ }
596
628
  const textWidth = estimateTextWidth2(valuePart, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
597
629
  const textHeight = LABEL_FONT_SIZE * 1.2;
598
630
  const isStacked = mark.cornerRadius === 0;
@@ -648,7 +680,7 @@ function computeBarLabels(marks, _chartArea, density = "auto") {
648
680
  // src/charts/bar/index.ts
649
681
  var barRenderer = (spec, scales, chartArea, strategy, _theme) => {
650
682
  const marks = computeBarMarks(spec, scales, chartArea, strategy);
651
- const labels = computeBarLabels(marks, chartArea, spec.labels.density);
683
+ const labels = computeBarLabels(marks, chartArea, spec.labels.density, spec.labels.format);
652
684
  for (let i = 0; i < marks.length && i < labels.length; i++) {
653
685
  marks[i].label = labels[i];
654
686
  }
@@ -1299,27 +1331,35 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
1299
1331
  label: ariaLabel
1300
1332
  };
1301
1333
  const combinedPath = pathParts.join(" ");
1334
+ const seriesStyleKey = seriesKey === "__default__" ? void 0 : seriesKey;
1335
+ const styleOverride = seriesStyleKey ? spec.seriesStyles?.[seriesStyleKey] : void 0;
1336
+ let strokeDasharray;
1337
+ if (styleOverride?.lineStyle === "dashed") strokeDasharray = "6 4";
1338
+ else if (styleOverride?.lineStyle === "dotted") strokeDasharray = "2 3";
1302
1339
  const lineMark = {
1303
1340
  type: "line",
1304
1341
  points: allPoints,
1305
1342
  path: combinedPath,
1306
1343
  stroke: color,
1307
- strokeWidth: DEFAULT_STROKE_WIDTH,
1308
- seriesKey: seriesKey === "__default__" ? void 0 : seriesKey,
1344
+ strokeWidth: styleOverride?.strokeWidth ?? DEFAULT_STROKE_WIDTH,
1345
+ strokeDasharray,
1346
+ opacity: styleOverride?.opacity,
1347
+ seriesKey: seriesStyleKey,
1309
1348
  data: pointsWithData.map((p) => p.row),
1310
1349
  aria
1311
1350
  };
1312
1351
  marks.push(lineMark);
1352
+ const showPoints = styleOverride?.showPoints !== false;
1313
1353
  for (let i = 0; i < pointsWithData.length; i++) {
1314
1354
  const p = pointsWithData[i];
1315
1355
  const pointMark = {
1316
1356
  type: "point",
1317
1357
  cx: p.x,
1318
1358
  cy: p.y,
1319
- r: DEFAULT_POINT_RADIUS,
1359
+ r: showPoints ? DEFAULT_POINT_RADIUS : 0,
1320
1360
  fill: color,
1321
- stroke: "#ffffff",
1322
- strokeWidth: 1.5,
1361
+ stroke: showPoints ? "#ffffff" : "transparent",
1362
+ strokeWidth: showPoints ? 1.5 : 0,
1323
1363
  fillOpacity: 0,
1324
1364
  data: p.row,
1325
1365
  aria: {
@@ -1899,7 +1939,8 @@ function normalizeChartSpec(spec, warnings) {
1899
1939
  responsive: spec.responsive ?? true,
1900
1940
  theme: spec.theme ?? {},
1901
1941
  darkMode: spec.darkMode ?? "off",
1902
- hiddenSeries: spec.hiddenSeries ?? []
1942
+ hiddenSeries: spec.hiddenSeries ?? [],
1943
+ seriesStyles: spec.seriesStyles ?? {}
1903
1944
  };
1904
1945
  }
1905
1946
  function normalizeTableSpec(spec, _warnings) {
@@ -2796,6 +2837,7 @@ var DEFAULT_COLLISION_PADDING = 5;
2796
2837
 
2797
2838
  // src/layout/axes.ts
2798
2839
  import { abbreviateNumber as abbreviateNumber3, formatDate, formatNumber as formatNumber3 } from "@opendata-ai/openchart-core";
2840
+ import { format as d3Format2 } from "d3-format";
2799
2841
  var TICK_COUNTS = {
2800
2842
  full: 8,
2801
2843
  reduced: 5,
@@ -2855,7 +2897,19 @@ function formatTickLabel(value, resolvedScale) {
2855
2897
  }
2856
2898
  if (resolvedScale.type === "linear" || resolvedScale.type === "log") {
2857
2899
  const num = value;
2858
- if (formatStr) return formatNumber3(num);
2900
+ if (formatStr) {
2901
+ try {
2902
+ return d3Format2(formatStr)(num);
2903
+ } catch {
2904
+ const suffixMatch = formatStr.match(/^(.+[a-z])([^a-z]+)$/i);
2905
+ if (suffixMatch) {
2906
+ try {
2907
+ return d3Format2(suffixMatch[1])(num) + suffixMatch[2];
2908
+ } catch {
2909
+ }
2910
+ }
2911
+ }
2912
+ }
2859
2913
  if (Math.abs(num) >= 1e3) return abbreviateNumber3(num);
2860
2914
  return formatNumber3(num);
2861
2915
  }
@@ -2977,7 +3031,8 @@ function computeDimensions(spec, options, legendLayout, theme) {
2977
3031
  bottom: padding + chrome.bottomHeight + xAxisHeight,
2978
3032
  left: padding + (isRadial ? padding : axisMargin)
2979
3033
  };
2980
- if (spec.type === "line" || spec.type === "area") {
3034
+ const labelDensity = spec.labels.density;
3035
+ if ((spec.type === "line" || spec.type === "area") && labelDensity !== "none") {
2981
3036
  const colorField = encoding.color?.field;
2982
3037
  if (colorField) {
2983
3038
  let maxLabelWidth = 0;
@@ -3508,7 +3563,7 @@ function computeCategoryColors(data, column, theme, darkMode) {
3508
3563
 
3509
3564
  // src/tables/format-cells.ts
3510
3565
  import { formatDate as formatDate2, formatNumber as formatNumber4 } from "@opendata-ai/openchart-core";
3511
- import { format as d3Format } from "d3-format";
3566
+ import { format as d3Format3 } from "d3-format";
3512
3567
  function isNumericValue(value) {
3513
3568
  if (typeof value === "number") return Number.isFinite(value);
3514
3569
  return false;
@@ -3528,7 +3583,7 @@ function formatCell(value, column) {
3528
3583
  }
3529
3584
  if (column.format && isNumericValue(value)) {
3530
3585
  try {
3531
- const formatter = d3Format(column.format);
3586
+ const formatter = d3Format3(column.format);
3532
3587
  return {
3533
3588
  value,
3534
3589
  formattedValue: formatter(value),
@@ -3561,7 +3616,7 @@ function formatValueForSearch(value, column) {
3561
3616
  if (value == null) return "";
3562
3617
  if (column.format && isNumericValue(value)) {
3563
3618
  try {
3564
- return d3Format(column.format)(value);
3619
+ return d3Format3(column.format)(value);
3565
3620
  } catch {
3566
3621
  }
3567
3622
  }
@@ -4035,7 +4090,7 @@ function compileTableLayout(spec, options, theme) {
4035
4090
 
4036
4091
  // src/tooltips/compute.ts
4037
4092
  import { formatDate as formatDate3, formatNumber as formatNumber5 } from "@opendata-ai/openchart-core";
4038
- import { format as d3Format2 } from "d3-format";
4093
+ import { format as d3Format4 } from "d3-format";
4039
4094
  function formatValue(value, fieldType, format) {
4040
4095
  if (value == null) return "";
4041
4096
  if (fieldType === "temporal" || value instanceof Date) {
@@ -4044,7 +4099,7 @@ function formatValue(value, fieldType, format) {
4044
4099
  if (typeof value === "number") {
4045
4100
  if (format) {
4046
4101
  try {
4047
- return d3Format2(format)(value);
4102
+ return d3Format4(format)(value);
4048
4103
  } catch {
4049
4104
  return formatNumber5(value);
4050
4105
  }