@opendata-ai/openchart-engine 2.3.5 → 2.5.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
@@ -1299,27 +1299,35 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
1299
1299
  label: ariaLabel
1300
1300
  };
1301
1301
  const combinedPath = pathParts.join(" ");
1302
+ const seriesStyleKey = seriesKey === "__default__" ? void 0 : seriesKey;
1303
+ const styleOverride = seriesStyleKey ? spec.seriesStyles?.[seriesStyleKey] : void 0;
1304
+ let strokeDasharray;
1305
+ if (styleOverride?.lineStyle === "dashed") strokeDasharray = "6 4";
1306
+ else if (styleOverride?.lineStyle === "dotted") strokeDasharray = "2 3";
1302
1307
  const lineMark = {
1303
1308
  type: "line",
1304
1309
  points: allPoints,
1305
1310
  path: combinedPath,
1306
1311
  stroke: color,
1307
- strokeWidth: DEFAULT_STROKE_WIDTH,
1308
- seriesKey: seriesKey === "__default__" ? void 0 : seriesKey,
1312
+ strokeWidth: styleOverride?.strokeWidth ?? DEFAULT_STROKE_WIDTH,
1313
+ strokeDasharray,
1314
+ opacity: styleOverride?.opacity,
1315
+ seriesKey: seriesStyleKey,
1309
1316
  data: pointsWithData.map((p) => p.row),
1310
1317
  aria
1311
1318
  };
1312
1319
  marks.push(lineMark);
1320
+ const showPoints = styleOverride?.showPoints !== false;
1313
1321
  for (let i = 0; i < pointsWithData.length; i++) {
1314
1322
  const p = pointsWithData[i];
1315
1323
  const pointMark = {
1316
1324
  type: "point",
1317
1325
  cx: p.x,
1318
1326
  cy: p.y,
1319
- r: DEFAULT_POINT_RADIUS,
1327
+ r: showPoints ? DEFAULT_POINT_RADIUS : 0,
1320
1328
  fill: color,
1321
- stroke: "#ffffff",
1322
- strokeWidth: 1.5,
1329
+ stroke: showPoints ? "#ffffff" : "transparent",
1330
+ strokeWidth: showPoints ? 1.5 : 0,
1323
1331
  fillOpacity: 0,
1324
1332
  data: p.row,
1325
1333
  aria: {
@@ -1899,7 +1907,8 @@ function normalizeChartSpec(spec, warnings) {
1899
1907
  responsive: spec.responsive ?? true,
1900
1908
  theme: spec.theme ?? {},
1901
1909
  darkMode: spec.darkMode ?? "off",
1902
- hiddenSeries: spec.hiddenSeries ?? []
1910
+ hiddenSeries: spec.hiddenSeries ?? [],
1911
+ seriesStyles: spec.seriesStyles ?? {}
1903
1912
  };
1904
1913
  }
1905
1914
  function normalizeTableSpec(spec, _warnings) {
@@ -2801,6 +2810,22 @@ var TICK_COUNTS = {
2801
2810
  reduced: 5,
2802
2811
  minimal: 3
2803
2812
  };
2813
+ var HEIGHT_MINIMAL_THRESHOLD = 120;
2814
+ var HEIGHT_REDUCED_THRESHOLD = 200;
2815
+ var WIDTH_MINIMAL_THRESHOLD = 150;
2816
+ var WIDTH_REDUCED_THRESHOLD = 300;
2817
+ var DENSITY_ORDER = ["full", "reduced", "minimal"];
2818
+ function effectiveDensity(baseDensity, axisLength, minimalThreshold, reducedThreshold) {
2819
+ let density = baseDensity;
2820
+ if (axisLength < minimalThreshold) {
2821
+ density = "minimal";
2822
+ } else if (axisLength < reducedThreshold) {
2823
+ const baseIdx = DENSITY_ORDER.indexOf(baseDensity);
2824
+ const reducedIdx = DENSITY_ORDER.indexOf("reduced");
2825
+ density = DENSITY_ORDER[Math.max(baseIdx, reducedIdx)];
2826
+ }
2827
+ return density;
2828
+ }
2804
2829
  function continuousTicks(resolvedScale, density) {
2805
2830
  const scale = resolvedScale.scale;
2806
2831
  const count = resolvedScale.channel.axis?.tickCount ?? TICK_COUNTS[density];
@@ -2847,7 +2872,19 @@ function formatTickLabel(value, resolvedScale) {
2847
2872
  }
2848
2873
  function computeAxes(scales, chartArea, strategy, theme) {
2849
2874
  const result = {};
2850
- const density = strategy.axisLabelDensity;
2875
+ const baseDensity = strategy.axisLabelDensity;
2876
+ const yDensity = effectiveDensity(
2877
+ baseDensity,
2878
+ chartArea.height,
2879
+ HEIGHT_MINIMAL_THRESHOLD,
2880
+ HEIGHT_REDUCED_THRESHOLD
2881
+ );
2882
+ const xDensity = effectiveDensity(
2883
+ baseDensity,
2884
+ chartArea.width,
2885
+ WIDTH_MINIMAL_THRESHOLD,
2886
+ WIDTH_REDUCED_THRESHOLD
2887
+ );
2851
2888
  const tickLabelStyle = {
2852
2889
  fontFamily: theme.fonts.family,
2853
2890
  fontSize: theme.fonts.sizes.axisTick,
@@ -2864,7 +2901,7 @@ function computeAxes(scales, chartArea, strategy, theme) {
2864
2901
  lineHeight: 1.3
2865
2902
  };
2866
2903
  if (scales.x) {
2867
- const ticks = scales.x.type === "band" || scales.x.type === "point" || scales.x.type === "ordinal" ? categoricalTicks(scales.x, density) : continuousTicks(scales.x, density);
2904
+ const ticks = scales.x.type === "band" || scales.x.type === "point" || scales.x.type === "ordinal" ? categoricalTicks(scales.x, xDensity) : continuousTicks(scales.x, xDensity);
2868
2905
  const gridlines = ticks.map((t) => ({
2869
2906
  position: t.position,
2870
2907
  major: true
@@ -2875,12 +2912,13 @@ function computeAxes(scales, chartArea, strategy, theme) {
2875
2912
  label: scales.x.channel.axis?.label,
2876
2913
  labelStyle: axisLabelStyle,
2877
2914
  tickLabelStyle,
2915
+ tickAngle: scales.x.channel.axis?.tickAngle,
2878
2916
  start: { x: chartArea.x, y: chartArea.y + chartArea.height },
2879
2917
  end: { x: chartArea.x + chartArea.width, y: chartArea.y + chartArea.height }
2880
2918
  };
2881
2919
  }
2882
2920
  if (scales.y) {
2883
- const ticks = scales.y.type === "band" || scales.y.type === "point" || scales.y.type === "ordinal" ? categoricalTicks(scales.y, density) : continuousTicks(scales.y, density);
2921
+ const ticks = scales.y.type === "band" || scales.y.type === "point" || scales.y.type === "ordinal" ? categoricalTicks(scales.y, yDensity) : continuousTicks(scales.y, yDensity);
2884
2922
  const gridlines = ticks.map((t) => ({
2885
2923
  position: t.position,
2886
2924
  major: true
@@ -2892,6 +2930,7 @@ function computeAxes(scales, chartArea, strategy, theme) {
2892
2930
  label: scales.y.channel.axis?.label,
2893
2931
  labelStyle: axisLabelStyle,
2894
2932
  tickLabelStyle,
2933
+ tickAngle: scales.y.channel.axis?.tickAngle,
2895
2934
  start: { x: chartArea.x, y: chartArea.y },
2896
2935
  end: { x: chartArea.x, y: chartArea.y + chartArea.height }
2897
2936
  };
@@ -2918,8 +2957,29 @@ function computeDimensions(spec, options, legendLayout, theme) {
2918
2957
  const total = { x: 0, y: 0, width, height };
2919
2958
  const isRadial = spec.type === "pie" || spec.type === "donut";
2920
2959
  const encoding = spec.encoding;
2921
- const hasXAxisLabel = !!encoding.x?.axis?.label;
2922
- const xAxisHeight = isRadial ? 0 : hasXAxisLabel ? 48 : 26;
2960
+ const xAxis = encoding.x?.axis;
2961
+ const hasXAxisLabel = !!xAxis?.label;
2962
+ const xTickAngle = xAxis?.tickAngle;
2963
+ let xAxisHeight;
2964
+ if (isRadial) {
2965
+ xAxisHeight = 0;
2966
+ } else if (xTickAngle && Math.abs(xTickAngle) > 10) {
2967
+ const angleRad = Math.abs(xTickAngle) * (Math.PI / 180);
2968
+ const xField = encoding.x?.field;
2969
+ let maxLabelWidth = 40;
2970
+ if (xField) {
2971
+ for (const row of spec.data) {
2972
+ const label = String(row[xField] ?? "");
2973
+ const w = estimateTextWidth7(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
2974
+ if (w > maxLabelWidth) maxLabelWidth = w;
2975
+ }
2976
+ }
2977
+ const rotatedHeight = maxLabelWidth * Math.sin(angleRad) + 6;
2978
+ const labelHeight = Math.min(rotatedHeight, 120);
2979
+ xAxisHeight = hasXAxisLabel ? labelHeight + 20 : labelHeight;
2980
+ } else {
2981
+ xAxisHeight = hasXAxisLabel ? 48 : 26;
2982
+ }
2923
2983
  const margins = {
2924
2984
  top: padding + chrome.topHeight + axisMargin,
2925
2985
  right: padding + (isRadial ? padding : axisMargin),