@opendata-ai/openchart-engine 6.8.0 → 6.10.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 +1 -0
- package/dist/index.js +141 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/charts/bar/__tests__/compute.test.ts +92 -0
- package/src/charts/bar/compute.ts +83 -0
- package/src/charts/column/__tests__/compute.test.ts +66 -0
- package/src/charts/column/compute.ts +89 -1
- package/src/compiler/normalize.ts +1 -0
- package/src/layout/scales.ts +10 -2
- package/src/sankey/compile-sankey.ts +22 -12
- package/src/sankey/types.ts +1 -0
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -774,6 +774,20 @@ function computeBarMarks(spec, scales, _chartArea, _strategy) {
|
|
|
774
774
|
const categoryGroups = groupByField(spec.data, yChannel.field);
|
|
775
775
|
const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
|
|
776
776
|
if (needsStacking) {
|
|
777
|
+
const stackDisabled = xChannel.stack === null || xChannel.stack === false;
|
|
778
|
+
if (stackDisabled) {
|
|
779
|
+
return computeGroupedBars(
|
|
780
|
+
spec.data,
|
|
781
|
+
xChannel.field,
|
|
782
|
+
yChannel.field,
|
|
783
|
+
colorField,
|
|
784
|
+
xScale,
|
|
785
|
+
yScale,
|
|
786
|
+
bandwidth,
|
|
787
|
+
baseline,
|
|
788
|
+
scales
|
|
789
|
+
);
|
|
790
|
+
}
|
|
777
791
|
return computeStackedBars(
|
|
778
792
|
spec.data,
|
|
779
793
|
xChannel.field,
|
|
@@ -834,6 +848,51 @@ function computeStackedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
834
848
|
}
|
|
835
849
|
return marks;
|
|
836
850
|
}
|
|
851
|
+
function computeGroupedBars(data, valueField, categoryField, colorField, xScale, yScale, bandwidth, baseline, scales) {
|
|
852
|
+
const marks = [];
|
|
853
|
+
const categoryGroups = groupByField(data, categoryField);
|
|
854
|
+
const groupIndexMap = /* @__PURE__ */ new Map();
|
|
855
|
+
for (const row of data) {
|
|
856
|
+
const key = String(row[colorField] ?? "");
|
|
857
|
+
if (!groupIndexMap.has(key)) {
|
|
858
|
+
groupIndexMap.set(key, groupIndexMap.size);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const groupCount = groupIndexMap.size;
|
|
862
|
+
if (groupCount === 0) return marks;
|
|
863
|
+
const gap = Math.min(1, bandwidth * 0.05);
|
|
864
|
+
const subBandHeight = Math.max((bandwidth - gap * (groupCount - 1)) / groupCount, MIN_BAR_WIDTH);
|
|
865
|
+
for (const [category, rows] of categoryGroups) {
|
|
866
|
+
const bandY = yScale(category);
|
|
867
|
+
if (bandY === void 0) continue;
|
|
868
|
+
for (const row of rows) {
|
|
869
|
+
const groupKey = String(row[colorField] ?? "");
|
|
870
|
+
const value2 = Number(row[valueField] ?? 0);
|
|
871
|
+
if (!Number.isFinite(value2)) continue;
|
|
872
|
+
const groupIndex = groupIndexMap.get(groupKey) ?? 0;
|
|
873
|
+
const color2 = getColor(scales, groupKey);
|
|
874
|
+
const xPos = value2 >= 0 ? baseline : xScale(value2);
|
|
875
|
+
const barWidth = Math.max(Math.abs(xScale(value2) - baseline), MIN_BAR_WIDTH);
|
|
876
|
+
const subY = bandY + groupIndex * (subBandHeight + gap);
|
|
877
|
+
const aria = {
|
|
878
|
+
label: `${category}, ${groupKey}: ${formatBarValue(value2)}`
|
|
879
|
+
};
|
|
880
|
+
marks.push({
|
|
881
|
+
type: "rect",
|
|
882
|
+
x: xPos,
|
|
883
|
+
y: subY,
|
|
884
|
+
width: barWidth,
|
|
885
|
+
height: subBandHeight,
|
|
886
|
+
fill: color2,
|
|
887
|
+
cornerRadius: 2,
|
|
888
|
+
data: row,
|
|
889
|
+
aria,
|
|
890
|
+
orient: "horizontal"
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return marks;
|
|
895
|
+
}
|
|
837
896
|
function computeColoredBars(data, valueField, categoryField, colorField, xScale, yScale, bandwidth, baseline, scales) {
|
|
838
897
|
const marks = [];
|
|
839
898
|
for (const row of data) {
|
|
@@ -1072,6 +1131,20 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
|
|
|
1072
1131
|
const categoryGroups = groupByField(spec.data, xChannel.field);
|
|
1073
1132
|
const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
|
|
1074
1133
|
if (needsStacking) {
|
|
1134
|
+
const stackDisabled = yChannel.stack === null || yChannel.stack === false;
|
|
1135
|
+
if (stackDisabled) {
|
|
1136
|
+
return computeGroupedColumns(
|
|
1137
|
+
spec.data,
|
|
1138
|
+
xChannel.field,
|
|
1139
|
+
yChannel.field,
|
|
1140
|
+
colorField,
|
|
1141
|
+
xScale,
|
|
1142
|
+
yScale,
|
|
1143
|
+
bandwidth,
|
|
1144
|
+
baseline,
|
|
1145
|
+
scales
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1075
1148
|
return computeStackedColumns(
|
|
1076
1149
|
spec.data,
|
|
1077
1150
|
xChannel.field,
|
|
@@ -1179,6 +1252,55 @@ function computeColoredColumns(data, categoryField, valueField, colorField, xSca
|
|
|
1179
1252
|
}
|
|
1180
1253
|
return marks;
|
|
1181
1254
|
}
|
|
1255
|
+
function computeGroupedColumns(data, categoryField, valueField, colorField, xScale, yScale, bandwidth, baseline, scales) {
|
|
1256
|
+
const marks = [];
|
|
1257
|
+
const categoryGroups = groupByField(data, categoryField);
|
|
1258
|
+
const groupIndexMap = /* @__PURE__ */ new Map();
|
|
1259
|
+
for (const row of data) {
|
|
1260
|
+
const key = String(row[colorField] ?? "");
|
|
1261
|
+
if (!groupIndexMap.has(key)) {
|
|
1262
|
+
groupIndexMap.set(key, groupIndexMap.size);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
const groupCount = groupIndexMap.size;
|
|
1266
|
+
if (groupCount === 0) return marks;
|
|
1267
|
+
const gap = Math.min(1, bandwidth * 0.05);
|
|
1268
|
+
const subBandWidth = Math.max(
|
|
1269
|
+
(bandwidth - gap * (groupCount - 1)) / groupCount,
|
|
1270
|
+
MIN_COLUMN_HEIGHT
|
|
1271
|
+
);
|
|
1272
|
+
for (const [category, rows] of categoryGroups) {
|
|
1273
|
+
const bandX = xScale(category);
|
|
1274
|
+
if (bandX === void 0) continue;
|
|
1275
|
+
for (const row of rows) {
|
|
1276
|
+
const groupKey = String(row[colorField] ?? "");
|
|
1277
|
+
const value2 = Number(row[valueField] ?? 0);
|
|
1278
|
+
if (!Number.isFinite(value2)) continue;
|
|
1279
|
+
const groupIndex = groupIndexMap.get(groupKey) ?? 0;
|
|
1280
|
+
const color2 = getColor(scales, groupKey);
|
|
1281
|
+
const yPos = yScale(value2);
|
|
1282
|
+
const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
|
|
1283
|
+
const y2 = value2 >= 0 ? yPos : baseline;
|
|
1284
|
+
const subX = bandX + groupIndex * (subBandWidth + gap);
|
|
1285
|
+
const aria = {
|
|
1286
|
+
label: `${category}, ${groupKey}: ${formatColumnValue(value2)}`
|
|
1287
|
+
};
|
|
1288
|
+
marks.push({
|
|
1289
|
+
type: "rect",
|
|
1290
|
+
x: subX,
|
|
1291
|
+
y: y2,
|
|
1292
|
+
width: subBandWidth,
|
|
1293
|
+
height: columnHeight,
|
|
1294
|
+
fill: color2,
|
|
1295
|
+
cornerRadius: 2,
|
|
1296
|
+
data: row,
|
|
1297
|
+
aria,
|
|
1298
|
+
orient: "vertical"
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return marks;
|
|
1303
|
+
}
|
|
1182
1304
|
function computeStackedColumns(data, categoryField, valueField, colorField, xScale, yScale, bandwidth, _baseline, scales) {
|
|
1183
1305
|
const marks = [];
|
|
1184
1306
|
const categoryGroups = groupByField(data, categoryField);
|
|
@@ -6111,6 +6233,7 @@ function normalizeSankeySpec(spec, _warnings) {
|
|
|
6111
6233
|
nodeAlign: spec.nodeAlign ?? "justify",
|
|
6112
6234
|
iterations: spec.iterations ?? 6,
|
|
6113
6235
|
linkStyle: spec.linkStyle ?? "gradient",
|
|
6236
|
+
nodeLabelAlign: spec.nodeLabelAlign ?? "auto",
|
|
6114
6237
|
chrome: normalizeChrome(spec.chrome),
|
|
6115
6238
|
legend: spec.legend,
|
|
6116
6239
|
theme: spec.theme ?? {},
|
|
@@ -8129,7 +8252,8 @@ function computeScales(spec, chartArea, data) {
|
|
|
8129
8252
|
}
|
|
8130
8253
|
if (encoding.x) {
|
|
8131
8254
|
let xData = data;
|
|
8132
|
-
|
|
8255
|
+
const xStackDisabled = encoding.x.stack === null || encoding.x.stack === false;
|
|
8256
|
+
if (spec.markType === "bar" && encoding.color && encoding.x.type === "quantitative" && !xStackDisabled) {
|
|
8133
8257
|
const yField = encoding.y?.field;
|
|
8134
8258
|
const xField = encoding.x.field;
|
|
8135
8259
|
if (yField) {
|
|
@@ -8157,7 +8281,8 @@ function computeScales(spec, chartArea, data) {
|
|
|
8157
8281
|
if (encoding.y) {
|
|
8158
8282
|
let yData = data;
|
|
8159
8283
|
const isVerticalBar = spec.markType === "bar" && (encoding.x?.type === "nominal" || encoding.x?.type === "ordinal") && encoding.y.type === "quantitative";
|
|
8160
|
-
|
|
8284
|
+
const yStackDisabled = encoding.y.stack === null || encoding.y.stack === false;
|
|
8285
|
+
if ((isVerticalBar || spec.markType === "area") && encoding.color && encoding.y.type === "quantitative" && !yStackDisabled) {
|
|
8161
8286
|
const xField = encoding.x?.field;
|
|
8162
8287
|
const yField = encoding.y.field;
|
|
8163
8288
|
if (xField) {
|
|
@@ -8924,9 +9049,16 @@ function getLinkColors(linkStyle, sourceColor, targetColor, neutralColor) {
|
|
|
8924
9049
|
return { sourceColor, targetColor };
|
|
8925
9050
|
}
|
|
8926
9051
|
}
|
|
8927
|
-
function computeNodeLabel(node, maxDepth, theme, nodeWidth) {
|
|
9052
|
+
function computeNodeLabel(node, maxDepth, theme, nodeWidth, nodeLabelAlign = "auto") {
|
|
8928
9053
|
const depth = node.depth ?? 0;
|
|
8929
|
-
|
|
9054
|
+
let placeLeft;
|
|
9055
|
+
if (nodeLabelAlign === "left") {
|
|
9056
|
+
placeLeft = true;
|
|
9057
|
+
} else if (nodeLabelAlign === "right") {
|
|
9058
|
+
placeLeft = false;
|
|
9059
|
+
} else {
|
|
9060
|
+
placeLeft = depth === maxDepth;
|
|
9061
|
+
}
|
|
8930
9062
|
const style = {
|
|
8931
9063
|
fontFamily: theme.fonts.family,
|
|
8932
9064
|
fontSize: theme.fonts.sizes.small,
|
|
@@ -8939,7 +9071,7 @@ function computeNodeLabel(node, maxDepth, theme, nodeWidth) {
|
|
|
8939
9071
|
const y0 = node.y0 ?? 0;
|
|
8940
9072
|
const y1 = node.y1 ?? 0;
|
|
8941
9073
|
const midY = (y0 + y1) / 2;
|
|
8942
|
-
if (
|
|
9074
|
+
if (placeLeft) {
|
|
8943
9075
|
return {
|
|
8944
9076
|
text: node.label ?? node.id,
|
|
8945
9077
|
x: x0 - LABEL_GAP,
|
|
@@ -9047,12 +9179,14 @@ function compileSankey(spec, options) {
|
|
|
9047
9179
|
sankeySpec.nodeAlign,
|
|
9048
9180
|
sankeySpec.iterations
|
|
9049
9181
|
);
|
|
9182
|
+
const nodeLabelAlign = sankeySpec.nodeLabelAlign ?? "auto";
|
|
9050
9183
|
const maxDepthFirst = nodes.reduce((max4, n) => Math.max(max4, n.depth ?? 0), 0);
|
|
9051
9184
|
const rightEdge = area.x + area.width;
|
|
9052
9185
|
let maxOverflow = 0;
|
|
9053
9186
|
for (const node of nodes) {
|
|
9054
9187
|
const depth = node.depth ?? 0;
|
|
9055
|
-
|
|
9188
|
+
const labelsLeft = nodeLabelAlign === "left" || nodeLabelAlign === "auto" && depth === maxDepthFirst;
|
|
9189
|
+
if (labelsLeft) continue;
|
|
9056
9190
|
const labelX = (node.x1 ?? nodeWidth) + LABEL_GAP;
|
|
9057
9191
|
const labelText = node.label ?? node.id;
|
|
9058
9192
|
const labelWidth = estimateTextWidth10(labelText, labelFontSize, labelFontWeight);
|
|
@@ -9099,7 +9233,7 @@ function compileSankey(spec, options) {
|
|
|
9099
9233
|
height: (node.y1 ?? 0) - (node.y0 ?? 0),
|
|
9100
9234
|
fill,
|
|
9101
9235
|
cornerRadius: NODE_CORNER_RADIUS,
|
|
9102
|
-
label: computeNodeLabel(node, maxDepth, theme, sankeySpec.nodeWidth),
|
|
9236
|
+
label: computeNodeLabel(node, maxDepth, theme, sankeySpec.nodeWidth, nodeLabelAlign),
|
|
9103
9237
|
nodeId: node.id,
|
|
9104
9238
|
value: node.value ?? 0,
|
|
9105
9239
|
depth,
|