@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 CHANGED
@@ -298,6 +298,7 @@ interface NormalizedSankeySpec {
298
298
  nodeAlign: SankeyNodeAlign;
299
299
  iterations: number;
300
300
  linkStyle: SankeyLinkColor;
301
+ nodeLabelAlign: 'auto' | 'left' | 'right';
301
302
  chrome: NormalizedChrome;
302
303
  legend?: LegendConfig;
303
304
  theme: ThemeConfig;
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
- if (spec.markType === "bar" && encoding.color && encoding.x.type === "quantitative") {
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
- if ((isVerticalBar || spec.markType === "area") && encoding.color && encoding.y.type === "quantitative") {
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
- const isRightmost = depth === maxDepth;
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 (isRightmost) {
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
- if (depth === maxDepthFirst) continue;
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,