@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 +2 -0
- package/dist/index.js +71 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__test-fixtures__/specs.ts +3 -0
- package/src/__tests__/axes.test.ts +185 -1
- package/src/__tests__/compile-chart.test.ts +52 -0
- package/src/__tests__/dimensions.test.ts +68 -0
- package/src/charts/line/__tests__/compute.test.ts +109 -0
- package/src/charts/line/compute.ts +18 -6
- package/src/compiler/normalize.ts +1 -0
- package/src/compiler/types.ts +2 -0
- package/src/layout/axes.ts +76 -5
- package/src/layout/dimensions.ts +28 -2
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
2922
|
-
const
|
|
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),
|