@opendata-ai/openchart-engine 8.5.1 → 8.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.js +587 -56
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2618,16 +2618,16 @@ function computeStackedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2618
2618
|
}
|
|
2619
2619
|
let cumulativeValue = stackMode === "center" ? -categoryTotal / 2 : 0;
|
|
2620
2620
|
for (const row of rows) {
|
|
2621
|
-
const
|
|
2621
|
+
const groupKey4 = String(row[colorField] ?? "");
|
|
2622
2622
|
const rawValue = Number(row[valueField] ?? 0);
|
|
2623
2623
|
if (!Number.isFinite(rawValue) || rawValue <= 0) continue;
|
|
2624
2624
|
const value = stackMode === "normalize" && categoryTotal > 0 ? rawValue / categoryTotal : rawValue;
|
|
2625
|
-
const color = getColor(scales,
|
|
2625
|
+
const color = getColor(scales, groupKey4);
|
|
2626
2626
|
const xLeft = xScale(cumulativeValue);
|
|
2627
2627
|
const xRight = xScale(cumulativeValue + value);
|
|
2628
2628
|
const barWidth = Math.max(Math.abs(xRight - xLeft), MIN_BAR_WIDTH);
|
|
2629
2629
|
const aria = {
|
|
2630
|
-
label: `${category}, ${
|
|
2630
|
+
label: `${category}, ${groupKey4}: ${formatLabelValue(rawValue)}`
|
|
2631
2631
|
};
|
|
2632
2632
|
marks.push({
|
|
2633
2633
|
type: "rect",
|
|
@@ -2637,7 +2637,7 @@ function computeStackedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2637
2637
|
height: bandwidth,
|
|
2638
2638
|
fill: isGradientDef(color) ? orientGradientForHorizontalBar(color) : color,
|
|
2639
2639
|
cornerRadius: 0,
|
|
2640
|
-
seriesKey:
|
|
2640
|
+
seriesKey: groupKey4,
|
|
2641
2641
|
data: row,
|
|
2642
2642
|
aria,
|
|
2643
2643
|
orient: "horizontal",
|
|
@@ -2676,16 +2676,16 @@ function computeGroupedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2676
2676
|
const bandY = yScale(category);
|
|
2677
2677
|
if (bandY === void 0) continue;
|
|
2678
2678
|
for (const row of rows) {
|
|
2679
|
-
const
|
|
2679
|
+
const groupKey4 = String(row[colorField] ?? "");
|
|
2680
2680
|
const value = Number(row[valueField] ?? 0);
|
|
2681
2681
|
if (!Number.isFinite(value)) continue;
|
|
2682
|
-
const groupIndex = groupIndexMap.get(
|
|
2683
|
-
const color = getColor(scales,
|
|
2682
|
+
const groupIndex = groupIndexMap.get(groupKey4) ?? 0;
|
|
2683
|
+
const color = getColor(scales, groupKey4);
|
|
2684
2684
|
const xPos = value >= 0 ? baseline : xScale(value);
|
|
2685
2685
|
const barWidth = Math.max(Math.abs(xScale(value) - baseline), MIN_BAR_WIDTH);
|
|
2686
2686
|
const subY = bandY + groupOffset + groupIndex * (subBandHeight + gap);
|
|
2687
2687
|
const aria = {
|
|
2688
|
-
label: `${category}, ${
|
|
2688
|
+
label: `${category}, ${groupKey4}: ${formatLabelValue(value)}`
|
|
2689
2689
|
};
|
|
2690
2690
|
marks.push({
|
|
2691
2691
|
type: "rect",
|
|
@@ -2696,7 +2696,7 @@ function computeGroupedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2696
2696
|
fill: isGradientDef(color) ? orientGradientForHorizontalBar(color) : color,
|
|
2697
2697
|
cornerRadius: BAR_CORNER_RADIUS,
|
|
2698
2698
|
cornerRadiusSides: valueEndCorners("horizontal", value < 0),
|
|
2699
|
-
seriesKey:
|
|
2699
|
+
seriesKey: groupKey4,
|
|
2700
2700
|
data: row,
|
|
2701
2701
|
aria,
|
|
2702
2702
|
orient: "horizontal"
|
|
@@ -2713,12 +2713,12 @@ function computeColoredBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2713
2713
|
if (!Number.isFinite(value)) continue;
|
|
2714
2714
|
const bandY = yScale(category);
|
|
2715
2715
|
if (bandY === void 0) continue;
|
|
2716
|
-
const
|
|
2717
|
-
const color = getColor(scales,
|
|
2716
|
+
const groupKey4 = String(row[colorField] ?? "");
|
|
2717
|
+
const color = getColor(scales, groupKey4);
|
|
2718
2718
|
const xPos = value >= 0 ? baseline : xScale(value);
|
|
2719
2719
|
const barWidth = Math.max(Math.abs(xScale(value) - baseline), MIN_BAR_WIDTH);
|
|
2720
2720
|
const aria = {
|
|
2721
|
-
label: `${category}, ${
|
|
2721
|
+
label: `${category}, ${groupKey4}: ${formatLabelValue(value)}`
|
|
2722
2722
|
};
|
|
2723
2723
|
marks.push({
|
|
2724
2724
|
type: "rect",
|
|
@@ -2729,7 +2729,7 @@ function computeColoredBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2729
2729
|
fill: isGradientDef(color) ? orientGradientForHorizontalBar(color) : color,
|
|
2730
2730
|
cornerRadius: BAR_CORNER_RADIUS,
|
|
2731
2731
|
cornerRadiusSides: valueEndCorners("horizontal", value < 0),
|
|
2732
|
-
seriesKey:
|
|
2732
|
+
seriesKey: groupKey4,
|
|
2733
2733
|
data: row,
|
|
2734
2734
|
aria,
|
|
2735
2735
|
orient: "horizontal"
|
|
@@ -3248,6 +3248,140 @@ var beeswarmRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
|
3248
3248
|
return computeBeeswarmMarks(spec, scales, chartArea, strategy, theme);
|
|
3249
3249
|
};
|
|
3250
3250
|
|
|
3251
|
+
// src/charts/binned-bar/compute.ts
|
|
3252
|
+
import { formatNumber } from "@opendata-ai/openchart-core";
|
|
3253
|
+
var OVERLAP_FILL_OPACITY = 0.55;
|
|
3254
|
+
var BIN_GAP = 1;
|
|
3255
|
+
var MIN_BIN_WIDTH = 1;
|
|
3256
|
+
var MIN_BAR_HEIGHT = 1;
|
|
3257
|
+
function computeBinnedBarMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
3258
|
+
const encoding = spec.encoding;
|
|
3259
|
+
const xChannel = encoding.x;
|
|
3260
|
+
const x2Channel = encoding.x2;
|
|
3261
|
+
const yChannel = encoding.y;
|
|
3262
|
+
if (!xChannel || !x2Channel || !yChannel || !scales.x || !scales.y) return [];
|
|
3263
|
+
const xScale = scales.x.scale;
|
|
3264
|
+
const yScale = scales.y.scale;
|
|
3265
|
+
const yDomain = yScale.domain();
|
|
3266
|
+
const yIncludesZero = yDomain[0] <= 0 && yDomain[1] >= 0;
|
|
3267
|
+
const baseline = yIncludesZero ? yScale(0) : yScale(yDomain[0]);
|
|
3268
|
+
const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
|
|
3269
|
+
const colorField = colorEnc?.type === "quantitative" ? void 0 : colorEnc?.field;
|
|
3270
|
+
const groups = groupByField(spec.data, colorField);
|
|
3271
|
+
const stackEnabled = groups.size > 1 && (yChannel.stack === true || yChannel.stack === "zero" || yChannel.stack === "normalize" || yChannel.stack === "center");
|
|
3272
|
+
const marks = groups.size > 1 && stackEnabled ? stackedBins(groups, spec, scales, xScale, yScale, baseline, theme) : overlappingBins(groups, spec, scales, xScale, yScale, baseline, colorField);
|
|
3273
|
+
stampKeys(marks, xChannel.field, colorField);
|
|
3274
|
+
return marks;
|
|
3275
|
+
}
|
|
3276
|
+
function binGeometry(row, xField, x2Field, xScale) {
|
|
3277
|
+
const start = Number(row[xField]);
|
|
3278
|
+
const end = Number(row[x2Field]);
|
|
3279
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return void 0;
|
|
3280
|
+
const px0 = xScale(start);
|
|
3281
|
+
const px1 = xScale(end);
|
|
3282
|
+
const left = Math.min(px0, px1);
|
|
3283
|
+
const span = Math.abs(px1 - px0);
|
|
3284
|
+
return { x: left, width: Math.max(span - BIN_GAP, MIN_BIN_WIDTH) };
|
|
3285
|
+
}
|
|
3286
|
+
function ariaFor(row, xField, x2Field, yField, seriesKey) {
|
|
3287
|
+
const start = Number(row[xField]);
|
|
3288
|
+
const end = Number(row[x2Field]);
|
|
3289
|
+
const value = Number(row[yField]);
|
|
3290
|
+
const range = `${formatNumber(start)} to ${formatNumber(end)}`;
|
|
3291
|
+
const prefix = seriesKey ? `${seriesKey}, ` : "";
|
|
3292
|
+
return { label: `${prefix}${range}: ${formatNumber(value)}` };
|
|
3293
|
+
}
|
|
3294
|
+
function overlappingBins(groups, spec, scales, xScale, yScale, baseline, colorField) {
|
|
3295
|
+
const encoding = spec.encoding;
|
|
3296
|
+
const xField = encoding.x.field;
|
|
3297
|
+
const x2Field = encoding.x2.field;
|
|
3298
|
+
const yField = encoding.y.field;
|
|
3299
|
+
const multi = groups.size > 1;
|
|
3300
|
+
const fillOpacity = multi ? spec.markDef.fillOpacity ?? OVERLAP_FILL_OPACITY : spec.markDef.fillOpacity;
|
|
3301
|
+
const marks = [];
|
|
3302
|
+
for (const [key, rows] of groups) {
|
|
3303
|
+
const fill = getColor(scales, key);
|
|
3304
|
+
for (const row of rows) {
|
|
3305
|
+
const geom = binGeometry(row, xField, x2Field, xScale);
|
|
3306
|
+
if (!geom) continue;
|
|
3307
|
+
const value = Number(row[yField]);
|
|
3308
|
+
if (!Number.isFinite(value)) continue;
|
|
3309
|
+
const endY = yScale(value);
|
|
3310
|
+
const top = Math.min(baseline, endY);
|
|
3311
|
+
const height = Math.max(Math.abs(baseline - endY), MIN_BAR_HEIGHT);
|
|
3312
|
+
marks.push({
|
|
3313
|
+
type: "rect",
|
|
3314
|
+
x: geom.x,
|
|
3315
|
+
y: top,
|
|
3316
|
+
width: geom.width,
|
|
3317
|
+
height,
|
|
3318
|
+
fill,
|
|
3319
|
+
fillOpacity,
|
|
3320
|
+
// Histogram bars are contiguous; rounded corners read as gaps.
|
|
3321
|
+
cornerRadius: 0,
|
|
3322
|
+
orient: "vertical",
|
|
3323
|
+
...colorField ? { seriesKey: key } : {},
|
|
3324
|
+
data: row,
|
|
3325
|
+
aria: ariaFor(row, xField, x2Field, yField, colorField ? key : void 0)
|
|
3326
|
+
});
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
return marks;
|
|
3330
|
+
}
|
|
3331
|
+
function stackedBins(groups, spec, scales, xScale, yScale, baseline, theme) {
|
|
3332
|
+
const encoding = spec.encoding;
|
|
3333
|
+
const xField = encoding.x.field;
|
|
3334
|
+
const x2Field = encoding.x2.field;
|
|
3335
|
+
const yField = encoding.y.field;
|
|
3336
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
3337
|
+
const seam = stackSeamStroke(theme);
|
|
3338
|
+
const marks = [];
|
|
3339
|
+
for (const [key, rows] of groups) {
|
|
3340
|
+
const fill = getColor(scales, key);
|
|
3341
|
+
for (const row of rows) {
|
|
3342
|
+
const geom = binGeometry(row, xField, x2Field, xScale);
|
|
3343
|
+
if (!geom) continue;
|
|
3344
|
+
const value = Number(row[yField]);
|
|
3345
|
+
if (!Number.isFinite(value) || value <= 0) continue;
|
|
3346
|
+
const binKey = serializeKeyValue(row[xField]);
|
|
3347
|
+
const used = offsets.get(binKey) ?? 0;
|
|
3348
|
+
const segmentHeight = Math.max(baseline - yScale(value), MIN_BAR_HEIGHT);
|
|
3349
|
+
const top = baseline - used - segmentHeight;
|
|
3350
|
+
offsets.set(binKey, used + segmentHeight);
|
|
3351
|
+
marks.push({
|
|
3352
|
+
type: "rect",
|
|
3353
|
+
x: geom.x,
|
|
3354
|
+
y: top,
|
|
3355
|
+
width: geom.width,
|
|
3356
|
+
height: segmentHeight,
|
|
3357
|
+
fill,
|
|
3358
|
+
cornerRadius: 0,
|
|
3359
|
+
orient: "vertical",
|
|
3360
|
+
stackGroup: binKey,
|
|
3361
|
+
stroke: seam,
|
|
3362
|
+
strokeWidth: 1,
|
|
3363
|
+
seriesKey: key,
|
|
3364
|
+
data: row,
|
|
3365
|
+
aria: ariaFor(row, xField, x2Field, yField, key)
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
return marks;
|
|
3370
|
+
}
|
|
3371
|
+
function stampKeys(marks, xField, colorField) {
|
|
3372
|
+
const raw = marks.map((m) => {
|
|
3373
|
+
const series = colorField ? String(m.data[colorField] ?? "") : "";
|
|
3374
|
+
return `${series}|${serializeKeyValue(m.data[xField])}`;
|
|
3375
|
+
});
|
|
3376
|
+
const keys = dedupeKeys(raw);
|
|
3377
|
+
for (let i = 0; i < marks.length; i++) {
|
|
3378
|
+
marks[i].key = keys[i];
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
// src/charts/binned-bar/index.ts
|
|
3383
|
+
var binnedBarRenderer = (spec, scales, chartArea, strategy, theme) => computeBinnedBarMarks(spec, scales, chartArea, strategy, theme);
|
|
3384
|
+
|
|
3251
3385
|
// src/charts/calendar/compute.ts
|
|
3252
3386
|
import { defaultNumberFormatter as defaultNumberFormatter4, formatDate } from "@opendata-ai/openchart-core";
|
|
3253
3387
|
import { utcDay, utcMonday, utcSunday } from "d3-time";
|
|
@@ -3604,13 +3738,13 @@ function computeColoredColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3604
3738
|
if (!Number.isFinite(value)) continue;
|
|
3605
3739
|
const bandX = xScale(category);
|
|
3606
3740
|
if (bandX === void 0) continue;
|
|
3607
|
-
const
|
|
3608
|
-
const color = getColor(scales,
|
|
3741
|
+
const groupKey4 = String(row[colorField] ?? "");
|
|
3742
|
+
const color = getColor(scales, groupKey4);
|
|
3609
3743
|
const yPos = yScale(value);
|
|
3610
3744
|
const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
|
|
3611
3745
|
const y = value >= 0 ? yPos : baseline;
|
|
3612
3746
|
const aria = {
|
|
3613
|
-
label: `${category}, ${
|
|
3747
|
+
label: `${category}, ${groupKey4}: ${formatLabelValue(value)}`
|
|
3614
3748
|
};
|
|
3615
3749
|
marks.push({
|
|
3616
3750
|
type: "rect",
|
|
@@ -3621,7 +3755,7 @@ function computeColoredColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3621
3755
|
fill: color,
|
|
3622
3756
|
cornerRadius: BAR_CORNER_RADIUS,
|
|
3623
3757
|
cornerRadiusSides: valueEndCorners("vertical", value < 0),
|
|
3624
|
-
seriesKey:
|
|
3758
|
+
seriesKey: groupKey4,
|
|
3625
3759
|
data: row,
|
|
3626
3760
|
aria,
|
|
3627
3761
|
orient: "vertical"
|
|
@@ -3650,17 +3784,17 @@ function computeGroupedColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3650
3784
|
const bandX = xScale(category);
|
|
3651
3785
|
if (bandX === void 0) continue;
|
|
3652
3786
|
for (const row of rows) {
|
|
3653
|
-
const
|
|
3787
|
+
const groupKey4 = String(row[colorField] ?? "");
|
|
3654
3788
|
const value = Number(row[valueField] ?? 0);
|
|
3655
3789
|
if (!Number.isFinite(value)) continue;
|
|
3656
|
-
const groupIndex = groupIndexMap.get(
|
|
3657
|
-
const color = getColor(scales,
|
|
3790
|
+
const groupIndex = groupIndexMap.get(groupKey4) ?? 0;
|
|
3791
|
+
const color = getColor(scales, groupKey4);
|
|
3658
3792
|
const yPos = yScale(value);
|
|
3659
3793
|
const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
|
|
3660
3794
|
const y = value >= 0 ? yPos : baseline;
|
|
3661
3795
|
const subX = bandX + groupIndex * (subBandWidth + gap);
|
|
3662
3796
|
const aria = {
|
|
3663
|
-
label: `${category}, ${
|
|
3797
|
+
label: `${category}, ${groupKey4}: ${formatLabelValue(value)}`
|
|
3664
3798
|
};
|
|
3665
3799
|
marks.push({
|
|
3666
3800
|
type: "rect",
|
|
@@ -3671,7 +3805,7 @@ function computeGroupedColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3671
3805
|
fill: color,
|
|
3672
3806
|
cornerRadius: BAR_CORNER_RADIUS,
|
|
3673
3807
|
cornerRadiusSides: valueEndCorners("vertical", value < 0),
|
|
3674
|
-
seriesKey:
|
|
3808
|
+
seriesKey: groupKey4,
|
|
3675
3809
|
data: row,
|
|
3676
3810
|
aria,
|
|
3677
3811
|
orient: "vertical"
|
|
@@ -3693,16 +3827,16 @@ function computeStackedColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3693
3827
|
}
|
|
3694
3828
|
let cumulativeValue = stackMode === "center" ? -categoryTotal / 2 : 0;
|
|
3695
3829
|
for (const row of rows) {
|
|
3696
|
-
const
|
|
3830
|
+
const groupKey4 = String(row[colorField] ?? "");
|
|
3697
3831
|
const rawValue = Number(row[valueField] ?? 0);
|
|
3698
3832
|
if (!Number.isFinite(rawValue) || rawValue <= 0) continue;
|
|
3699
3833
|
const value = stackMode === "normalize" && categoryTotal > 0 ? rawValue / categoryTotal : rawValue;
|
|
3700
|
-
const color = getColor(scales,
|
|
3834
|
+
const color = getColor(scales, groupKey4);
|
|
3701
3835
|
const yTop = yScale(cumulativeValue + value);
|
|
3702
3836
|
const yBottom = yScale(cumulativeValue);
|
|
3703
3837
|
const columnHeight = Math.max(Math.abs(yBottom - yTop), MIN_COLUMN_HEIGHT);
|
|
3704
3838
|
const aria = {
|
|
3705
|
-
label: `${category}, ${
|
|
3839
|
+
label: `${category}, ${groupKey4}: ${formatLabelValue(rawValue)}`
|
|
3706
3840
|
};
|
|
3707
3841
|
marks.push({
|
|
3708
3842
|
type: "rect",
|
|
@@ -3712,7 +3846,7 @@ function computeStackedColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3712
3846
|
height: columnHeight,
|
|
3713
3847
|
fill: color,
|
|
3714
3848
|
cornerRadius: 0,
|
|
3715
|
-
seriesKey:
|
|
3849
|
+
seriesKey: groupKey4,
|
|
3716
3850
|
data: row,
|
|
3717
3851
|
aria,
|
|
3718
3852
|
orient: "vertical",
|
|
@@ -4515,7 +4649,10 @@ function computeSingleArea(spec, scales, _chartArea, theme) {
|
|
|
4515
4649
|
let fillOpacity;
|
|
4516
4650
|
if (markFill != null) {
|
|
4517
4651
|
fillValue = markFill;
|
|
4518
|
-
fillOpacity = isGradientDef3(markFill) ? 1 : spec.markDef.opacity ?? (y2Channel ? 0.25 : DEFAULT_FILL_OPACITY);
|
|
4652
|
+
fillOpacity = isGradientDef3(markFill) ? 1 : spec.markDef.fillOpacity ?? spec.markDef.opacity ?? (y2Channel ? 0.25 : DEFAULT_FILL_OPACITY);
|
|
4653
|
+
} else if (spec.markDef.fillOpacity != null) {
|
|
4654
|
+
fillValue = getRepresentativeColor6(color);
|
|
4655
|
+
fillOpacity = spec.markDef.fillOpacity;
|
|
4519
4656
|
} else {
|
|
4520
4657
|
const colorStr = getRepresentativeColor6(color);
|
|
4521
4658
|
fillValue = buildGradientFill(colorStr, defaultGradientStops);
|
|
@@ -4625,10 +4762,10 @@ function computeStackedArea(spec, scales, chartArea, theme) {
|
|
|
4625
4762
|
let fillOpacity;
|
|
4626
4763
|
if (markFill != null) {
|
|
4627
4764
|
fillValue = markFill;
|
|
4628
|
-
fillOpacity = isGradientDef3(markFill) ? 1 : spec.markDef.opacity ?? 0.7;
|
|
4765
|
+
fillOpacity = isGradientDef3(markFill) ? 1 : spec.markDef.fillOpacity ?? spec.markDef.opacity ?? 0.7;
|
|
4629
4766
|
} else {
|
|
4630
4767
|
fillValue = getRepresentativeColor6(color);
|
|
4631
|
-
fillOpacity = STACKED_FILL_OPACITY;
|
|
4768
|
+
fillOpacity = spec.markDef.fillOpacity ?? STACKED_FILL_OPACITY;
|
|
4632
4769
|
}
|
|
4633
4770
|
const rawPointKeys = validPoints.map((_p, idx) => serializeKeyValue(layer[idx]?.data.__x__));
|
|
4634
4771
|
marks.push({
|
|
@@ -5659,7 +5796,7 @@ var rangeRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
|
5659
5796
|
};
|
|
5660
5797
|
|
|
5661
5798
|
// src/charts/rect/index.ts
|
|
5662
|
-
import { formatNumber } from "@opendata-ai/openchart-core";
|
|
5799
|
+
import { formatNumber as formatNumber2 } from "@opendata-ai/openchart-core";
|
|
5663
5800
|
function bandwidthOf(scale) {
|
|
5664
5801
|
const band = scale?.scale;
|
|
5665
5802
|
return typeof band?.bandwidth === "function" ? band.bandwidth() : 0;
|
|
@@ -5690,7 +5827,7 @@ function computeRectMarks(spec, scales, _chartArea) {
|
|
|
5690
5827
|
fill = getColor(scales, "__default__");
|
|
5691
5828
|
}
|
|
5692
5829
|
const colorValue = colorEnc ? row[colorEnc.field] : void 0;
|
|
5693
|
-
const colorLabel = colorEnc && colorValue != null ? `, ${colorEnc.field}: ${typeof colorValue === "number" ?
|
|
5830
|
+
const colorLabel = colorEnc && colorValue != null ? `, ${colorEnc.field}: ${typeof colorValue === "number" ? formatNumber2(colorValue) : String(colorValue)}` : "";
|
|
5694
5831
|
const aria = {
|
|
5695
5832
|
label: `${row[xChannel.field]}, ${row[yChannel.field]}${colorLabel}`
|
|
5696
5833
|
};
|
|
@@ -6228,6 +6365,14 @@ var builtinRenderers = {
|
|
|
6228
6365
|
// horizontal bars
|
|
6229
6366
|
"bar:vertical": columnRenderer,
|
|
6230
6367
|
// vertical bars (old 'column')
|
|
6368
|
+
"bar:binned": binnedBarRenderer,
|
|
6369
|
+
// histogram: rects sized from a linear x scale via x2
|
|
6370
|
+
// Safety net. `compileChart` always expands sugar before dispatch, so these
|
|
6371
|
+
// keys should be unreachable — but an unregistered key yields zero marks
|
|
6372
|
+
// with no error, i.e. a silently blank chart, which is the exact failure
|
|
6373
|
+
// this whole mark family exists to remove.
|
|
6374
|
+
histogram: binnedBarRenderer,
|
|
6375
|
+
density: areaRenderer,
|
|
6231
6376
|
point: scatterRenderer,
|
|
6232
6377
|
// old 'scatter'
|
|
6233
6378
|
arc: pieRenderer,
|
|
@@ -6270,7 +6415,7 @@ import {
|
|
|
6270
6415
|
} from "@opendata-ai/openchart-core";
|
|
6271
6416
|
|
|
6272
6417
|
// src/compile/spec-sugar.ts
|
|
6273
|
-
import { resolveSchemeName } from "@opendata-ai/openchart-core";
|
|
6418
|
+
import { inferFieldType, resolveSchemeName } from "@opendata-ai/openchart-core";
|
|
6274
6419
|
|
|
6275
6420
|
// src/transforms/aggregate.ts
|
|
6276
6421
|
function computeAggregate(op, values) {
|
|
@@ -6382,9 +6527,11 @@ function uniqueFieldStrings(data, field) {
|
|
|
6382
6527
|
}
|
|
6383
6528
|
return out;
|
|
6384
6529
|
}
|
|
6385
|
-
function expandEncodingSugar(spec) {
|
|
6530
|
+
function expandEncodingSugar(spec, inheritedData) {
|
|
6386
6531
|
const encoding = spec.encoding;
|
|
6387
6532
|
if (!encoding) return spec;
|
|
6533
|
+
const markType = markTypeOf(spec);
|
|
6534
|
+
const specData = Array.isArray(spec.data) ? spec.data : inheritedData;
|
|
6388
6535
|
const generatedTransforms = [];
|
|
6389
6536
|
const updatedEncoding = { ...encoding };
|
|
6390
6537
|
let changed = false;
|
|
@@ -6394,14 +6541,26 @@ function expandEncodingSugar(spec) {
|
|
|
6394
6541
|
if (ch.bin != null && ch.bin !== false) {
|
|
6395
6542
|
const field = ch.field;
|
|
6396
6543
|
const outputField = `bin_${field}`;
|
|
6544
|
+
const resolvedType = ch.type ?? (specData && specData.length > 0 ? inferFieldType(specData, field) : void 0);
|
|
6545
|
+
const emitInterval = channel === "x" && resolvedType === "quantitative" && markType === "bar";
|
|
6546
|
+
const endField = `${outputField}_end`;
|
|
6397
6547
|
const binTransform = {
|
|
6398
6548
|
bin: ch.bin === true ? true : ch.bin,
|
|
6399
6549
|
field,
|
|
6400
|
-
as: outputField
|
|
6550
|
+
as: emitInterval ? [outputField, endField] : outputField
|
|
6401
6551
|
};
|
|
6402
6552
|
generatedTransforms.push(binTransform);
|
|
6403
6553
|
const { bin: _bin, ...rest } = ch;
|
|
6404
|
-
updatedEncoding[channel] = {
|
|
6554
|
+
updatedEncoding[channel] = {
|
|
6555
|
+
...rest,
|
|
6556
|
+
field: outputField,
|
|
6557
|
+
// The axis should read `amount`, not `bin_amount`. Scoped to the same
|
|
6558
|
+
// guard: retitling every binned channel would move existing baselines.
|
|
6559
|
+
...emitInterval && rest.title == null ? { title: field } : {}
|
|
6560
|
+
};
|
|
6561
|
+
if (emitInterval) {
|
|
6562
|
+
updatedEncoding.x2 = { field: endField, type: "quantitative" };
|
|
6563
|
+
}
|
|
6405
6564
|
changed = true;
|
|
6406
6565
|
}
|
|
6407
6566
|
const current = updatedEncoding[channel] ?? ch;
|
|
@@ -6622,7 +6781,9 @@ function expandChannelSugar(spec, warnings) {
|
|
|
6622
6781
|
}
|
|
6623
6782
|
var COUNT_GROUP_CHANNELS = [
|
|
6624
6783
|
"x",
|
|
6784
|
+
"x2",
|
|
6625
6785
|
"y",
|
|
6786
|
+
"y2",
|
|
6626
6787
|
"color",
|
|
6627
6788
|
"detail",
|
|
6628
6789
|
"strokeDash",
|
|
@@ -6743,6 +6904,123 @@ function resolveSortSugar(spec, inheritedData) {
|
|
|
6743
6904
|
}
|
|
6744
6905
|
return changed ? { ...spec, encoding: updated } : spec;
|
|
6745
6906
|
}
|
|
6907
|
+
var HISTOGRAM_DEFAULT_MAXBINS = 20;
|
|
6908
|
+
var PROPORTION_FIELD = "__proportion";
|
|
6909
|
+
var GROUP_TOTAL_FIELD = "__group_total";
|
|
6910
|
+
function expandHistogramMark(spec) {
|
|
6911
|
+
if (markTypeOf(spec) !== "histogram") return spec;
|
|
6912
|
+
const markDef = typeof spec.mark === "string" ? {} : { ...spec.mark };
|
|
6913
|
+
const { binCount, normalize, ...restMark } = markDef;
|
|
6914
|
+
const encoding = { ...spec.encoding ?? {} };
|
|
6915
|
+
const x = { ...encoding.x ?? {} };
|
|
6916
|
+
if (x.bin == null) {
|
|
6917
|
+
x.bin = { maxbins: typeof binCount === "number" ? binCount : HISTOGRAM_DEFAULT_MAXBINS };
|
|
6918
|
+
}
|
|
6919
|
+
x.type = x.type ?? "quantitative";
|
|
6920
|
+
encoding.x = x;
|
|
6921
|
+
if (encoding.y == null) {
|
|
6922
|
+
encoding.y = { aggregate: "count" };
|
|
6923
|
+
}
|
|
6924
|
+
const out = {
|
|
6925
|
+
...spec,
|
|
6926
|
+
mark: { ...restMark, type: "bar" },
|
|
6927
|
+
encoding
|
|
6928
|
+
};
|
|
6929
|
+
if (normalize === true) {
|
|
6930
|
+
out.__histogramNormalize = true;
|
|
6931
|
+
}
|
|
6932
|
+
return out;
|
|
6933
|
+
}
|
|
6934
|
+
var DENSITY_OVERLAP_FILL_OPACITY = 0.4;
|
|
6935
|
+
function distributionGroupFields(encoding) {
|
|
6936
|
+
const fields = [];
|
|
6937
|
+
for (const channel of ["color", "detail"]) {
|
|
6938
|
+
const ch = encoding[channel];
|
|
6939
|
+
if (typeof ch?.field === "string" && !fields.includes(ch.field)) fields.push(ch.field);
|
|
6940
|
+
}
|
|
6941
|
+
return fields;
|
|
6942
|
+
}
|
|
6943
|
+
var DENSITY_VALUE_FIELD = "value";
|
|
6944
|
+
var DENSITY_OUTPUT_FIELD = "density";
|
|
6945
|
+
function expandDensityMark(spec) {
|
|
6946
|
+
if (markTypeOf(spec) !== "density") return spec;
|
|
6947
|
+
const markDef = typeof spec.mark === "string" ? {} : { ...spec.mark };
|
|
6948
|
+
const { bandwidth, cumulative, steps, ...restMark } = markDef;
|
|
6949
|
+
const encoding = { ...spec.encoding ?? {} };
|
|
6950
|
+
const x = { ...encoding.x ?? {} };
|
|
6951
|
+
const field = typeof x.field === "string" ? x.field : void 0;
|
|
6952
|
+
if (field == null) return spec;
|
|
6953
|
+
const groupFields = distributionGroupFields(encoding);
|
|
6954
|
+
const densityTransform = {
|
|
6955
|
+
density: field,
|
|
6956
|
+
as: [DENSITY_VALUE_FIELD, DENSITY_OUTPUT_FIELD]
|
|
6957
|
+
};
|
|
6958
|
+
if (groupFields.length > 0) densityTransform.groupby = groupFields;
|
|
6959
|
+
if (typeof bandwidth === "number") densityTransform.bandwidth = bandwidth;
|
|
6960
|
+
if (cumulative === true) densityTransform.cumulative = true;
|
|
6961
|
+
if (typeof steps === "number") densityTransform.steps = steps;
|
|
6962
|
+
const transforms = (spec.transform ?? []).slice().concat(densityTransform);
|
|
6963
|
+
encoding.x = {
|
|
6964
|
+
...x,
|
|
6965
|
+
field: DENSITY_VALUE_FIELD,
|
|
6966
|
+
type: "quantitative",
|
|
6967
|
+
...x.title == null ? { title: field } : {}
|
|
6968
|
+
};
|
|
6969
|
+
const y = { ...encoding.y ?? {} };
|
|
6970
|
+
encoding.y = {
|
|
6971
|
+
title: cumulative === true ? "Cumulative share" : "Density",
|
|
6972
|
+
// A density's absolute height is not a quantity readers interpret, so the
|
|
6973
|
+
// axis is off unless the author asks for it.
|
|
6974
|
+
axis: false,
|
|
6975
|
+
...y,
|
|
6976
|
+
field: DENSITY_OUTPUT_FIELD,
|
|
6977
|
+
type: "quantitative",
|
|
6978
|
+
// Areas stack by default on a color field; overlapping translucent curves
|
|
6979
|
+
// are the whole point of a density comparison.
|
|
6980
|
+
stack: y.stack ?? null
|
|
6981
|
+
};
|
|
6982
|
+
const out = {
|
|
6983
|
+
...spec,
|
|
6984
|
+
mark: {
|
|
6985
|
+
interpolate: "linear",
|
|
6986
|
+
...groupFields.length > 0 ? { fillOpacity: DENSITY_OVERLAP_FILL_OPACITY } : {},
|
|
6987
|
+
...restMark,
|
|
6988
|
+
type: "area"
|
|
6989
|
+
},
|
|
6990
|
+
encoding,
|
|
6991
|
+
transform: transforms
|
|
6992
|
+
};
|
|
6993
|
+
if (out.crosshair === void 0) out.crosshair = false;
|
|
6994
|
+
if (out.endpointLabels === void 0) out.endpointLabels = false;
|
|
6995
|
+
return out;
|
|
6996
|
+
}
|
|
6997
|
+
function applyHistogramNormalize(spec) {
|
|
6998
|
+
if (spec.__histogramNormalize !== true) return spec;
|
|
6999
|
+
const { __histogramNormalize: _flag, ...rest } = spec;
|
|
7000
|
+
const encoding = { ...rest.encoding ?? {} };
|
|
7001
|
+
const y = { ...encoding.y ?? {} };
|
|
7002
|
+
const countField = typeof y.field === "string" ? y.field : "__count";
|
|
7003
|
+
const groupby = distributionGroupFields(encoding);
|
|
7004
|
+
const transforms = (rest.transform ?? []).slice();
|
|
7005
|
+
transforms.push({
|
|
7006
|
+
joinaggregate: [{ op: "sum", field: countField, as: GROUP_TOTAL_FIELD }],
|
|
7007
|
+
groupby
|
|
7008
|
+
});
|
|
7009
|
+
transforms.push({
|
|
7010
|
+
calculate: { op: "/", field: countField, field2: GROUP_TOTAL_FIELD },
|
|
7011
|
+
as: PROPORTION_FIELD
|
|
7012
|
+
});
|
|
7013
|
+
encoding.y = {
|
|
7014
|
+
...y,
|
|
7015
|
+
field: PROPORTION_FIELD,
|
|
7016
|
+
type: "quantitative",
|
|
7017
|
+
title: y.title === "Count" || y.title == null ? "Share" : y.title,
|
|
7018
|
+
// `axis: false` is an author turning the axis off; don't spread it into an
|
|
7019
|
+
// object and hand back an axis they asked not to have.
|
|
7020
|
+
axis: y.axis === false || y.axis === null ? y.axis : { format: "percent", ...y.axis ?? {} }
|
|
7021
|
+
};
|
|
7022
|
+
return { ...rest, encoding, transform: transforms };
|
|
7023
|
+
}
|
|
6746
7024
|
function expandChartSugar(spec, warnings, inheritedData) {
|
|
6747
7025
|
let out = unwrapDataValues(spec);
|
|
6748
7026
|
out = expandTopLevelTitle(out);
|
|
@@ -6752,9 +7030,12 @@ function expandChartSugar(spec, warnings, inheritedData) {
|
|
|
6752
7030
|
out = applyFixedSizeDefault(out);
|
|
6753
7031
|
out = expandAnnotationSugar(out, warnings);
|
|
6754
7032
|
out = expandChannelSugar(out, warnings);
|
|
6755
|
-
out =
|
|
7033
|
+
out = expandHistogramMark(out);
|
|
7034
|
+
out = expandDensityMark(out);
|
|
7035
|
+
out = expandEncodingSugar(out, inheritedData);
|
|
6756
7036
|
out = resolveSortSugar(out, inheritedData);
|
|
6757
7037
|
out = expandCountAggregate(out);
|
|
7038
|
+
out = applyHistogramNormalize(out);
|
|
6758
7039
|
return out;
|
|
6759
7040
|
}
|
|
6760
7041
|
function expandLayerSugar(spec, warnings, inheritedData, parentEncoding) {
|
|
@@ -7074,7 +7355,7 @@ function normalizeChrome(chrome) {
|
|
|
7074
7355
|
brand: normalizeChromeField(chrome.brand)
|
|
7075
7356
|
};
|
|
7076
7357
|
}
|
|
7077
|
-
function
|
|
7358
|
+
function inferFieldType2(data, field) {
|
|
7078
7359
|
const sampleSize = Math.min(50, data.length);
|
|
7079
7360
|
let numericCount = 0;
|
|
7080
7361
|
let dateCount = 0;
|
|
@@ -7116,13 +7397,13 @@ function inferEncodingTypes(encoding, data, warnings) {
|
|
|
7116
7397
|
if ("condition" in spec) continue;
|
|
7117
7398
|
if (!("field" in spec)) continue;
|
|
7118
7399
|
if (!spec.type) {
|
|
7119
|
-
const inferred =
|
|
7400
|
+
const inferred = inferFieldType2(data, spec.field);
|
|
7120
7401
|
result[channel] = { ...spec, type: inferred };
|
|
7121
7402
|
warnings.push(
|
|
7122
7403
|
`Inferred encoding.${channel}.type as "${inferred}" from data values for field "${spec.field}"`
|
|
7123
7404
|
);
|
|
7124
7405
|
} else {
|
|
7125
|
-
const actualType =
|
|
7406
|
+
const actualType = inferFieldType2(data, spec.field);
|
|
7126
7407
|
if (spec.type === "nominal" && actualType === "temporal") {
|
|
7127
7408
|
warnings.push(`Field "${spec.field}" looks temporal but was declared as nominal`);
|
|
7128
7409
|
}
|
|
@@ -7599,7 +7880,7 @@ function flattenLayers(spec, parentData, parentEncoding, parentTransforms, paren
|
|
|
7599
7880
|
|
|
7600
7881
|
// src/compiler/validate.ts
|
|
7601
7882
|
import {
|
|
7602
|
-
inferFieldType as
|
|
7883
|
+
inferFieldType as inferFieldType3,
|
|
7603
7884
|
MARK_ENCODING_RULES,
|
|
7604
7885
|
MARK_TYPES,
|
|
7605
7886
|
resolveSchemeName as resolveSchemeName2,
|
|
@@ -7718,8 +7999,8 @@ function validateRangeSpec(spec, errors) {
|
|
|
7718
7999
|
if (!x?.field || typeof x.field !== "string" || !y?.field || typeof y.field !== "string") {
|
|
7719
8000
|
return;
|
|
7720
8001
|
}
|
|
7721
|
-
const xType = x.type ??
|
|
7722
|
-
const yType = y.type ??
|
|
8002
|
+
const xType = x.type ?? inferFieldType3(data, x.field);
|
|
8003
|
+
const yType = y.type ?? inferFieldType3(data, y.field);
|
|
7723
8004
|
const xIsCategory = xType === "nominal" || xType === "ordinal";
|
|
7724
8005
|
const yIsCategory = yType === "nominal" || yType === "ordinal";
|
|
7725
8006
|
if (xIsCategory && yIsCategory) {
|
|
@@ -7856,7 +8137,7 @@ function validateCalendarSpec(spec, errors) {
|
|
|
7856
8137
|
}
|
|
7857
8138
|
const x = encoding.x;
|
|
7858
8139
|
if (!x?.field || typeof x.field !== "string") return;
|
|
7859
|
-
const xType = x.type ??
|
|
8140
|
+
const xType = x.type ?? inferFieldType3(data, x.field);
|
|
7860
8141
|
if (xType !== "temporal") {
|
|
7861
8142
|
if (!x.type) {
|
|
7862
8143
|
errors.push({
|
|
@@ -8065,12 +8346,16 @@ function validateChartSpec(spec, errors) {
|
|
|
8065
8346
|
if (typeof f === "string") transformFields.add(f);
|
|
8066
8347
|
}
|
|
8067
8348
|
}
|
|
8068
|
-
const opList = t.aggregate ?? t.window;
|
|
8349
|
+
const opList = t.aggregate ?? t.window ?? t.joinaggregate;
|
|
8069
8350
|
if (Array.isArray(opList)) {
|
|
8070
8351
|
for (const op of opList) {
|
|
8071
8352
|
if (op && typeof op.as === "string") transformFields.add(op.as);
|
|
8072
8353
|
}
|
|
8073
8354
|
}
|
|
8355
|
+
if (typeof t.density === "string" && t.as == null) {
|
|
8356
|
+
transformFields.add("value");
|
|
8357
|
+
transformFields.add("density");
|
|
8358
|
+
}
|
|
8074
8359
|
if (Array.isArray(t.groupby)) {
|
|
8075
8360
|
for (const g of t.groupby) {
|
|
8076
8361
|
if (typeof g === "string") transformFields.add(g);
|
|
@@ -8205,7 +8490,7 @@ function validateChartSpec(spec, errors) {
|
|
|
8205
8490
|
suggestion: "Provide a series name (string) or an array of series names."
|
|
8206
8491
|
});
|
|
8207
8492
|
} else {
|
|
8208
|
-
const colorType = colorCh.type ?? (colorCh.field && Array.isArray(spec.data) && spec.data.length > 0 ?
|
|
8493
|
+
const colorType = colorCh.type ?? (colorCh.field && Array.isArray(spec.data) && spec.data.length > 0 ? inferFieldType3(spec.data, colorCh.field) : void 0);
|
|
8209
8494
|
if (colorType === "quantitative") {
|
|
8210
8495
|
errors.push({
|
|
8211
8496
|
message: "Spec error: encoding.color.highlight is not supported on quantitative color channels",
|
|
@@ -8336,7 +8621,7 @@ function validateChartSpec(spec, errors) {
|
|
|
8336
8621
|
const data = Array.isArray(spec.data) ? spec.data : [];
|
|
8337
8622
|
const colorCh = encoding?.color;
|
|
8338
8623
|
if (colorCh && typeof colorCh === "object" && typeof colorCh.field === "string") {
|
|
8339
|
-
const colorType = colorCh.type ??
|
|
8624
|
+
const colorType = colorCh.type ?? inferFieldType3(data, colorCh.field);
|
|
8340
8625
|
if (colorType !== "quantitative") {
|
|
8341
8626
|
const distinct = new Set(data.map((row) => row[colorCh.field]));
|
|
8342
8627
|
if (distinct.size > 1) {
|
|
@@ -9756,8 +10041,14 @@ function computeBandRowObstacles(marks, scales) {
|
|
|
9756
10041
|
}
|
|
9757
10042
|
return obstacles;
|
|
9758
10043
|
}
|
|
10044
|
+
function isBinnedBarEncoding(markType, encoding) {
|
|
10045
|
+
return markType === "bar" && encoding.x?.type === "quantitative" && !!encoding.x2 && encoding.y?.type === "quantitative";
|
|
10046
|
+
}
|
|
9759
10047
|
function resolveRendererKey(markType, encoding, markDef) {
|
|
9760
10048
|
if (markType === "bar") {
|
|
10049
|
+
if (isBinnedBarEncoding(markType, encoding)) {
|
|
10050
|
+
return "bar:binned";
|
|
10051
|
+
}
|
|
9761
10052
|
const xType = encoding.x?.type;
|
|
9762
10053
|
const yType = encoding.y?.type;
|
|
9763
10054
|
const isVertical = (xType === "nominal" || xType === "ordinal" || xType === "temporal") && yType === "quantitative";
|
|
@@ -10750,10 +11041,25 @@ function computeScales(spec, chartArea, data) {
|
|
|
10750
11041
|
}
|
|
10751
11042
|
}
|
|
10752
11043
|
}
|
|
11044
|
+
if (spec.markType === "area" && encoding.x?.type === "quantitative" && encoding.x.scale?.zero === void 0) {
|
|
11045
|
+
if (!encoding.x.scale) {
|
|
11046
|
+
encoding.x.scale = { zero: false };
|
|
11047
|
+
} else {
|
|
11048
|
+
encoding.x.scale.zero = false;
|
|
11049
|
+
}
|
|
11050
|
+
}
|
|
11051
|
+
const binnedX = isBinnedBarEncoding(spec.markType, encoding) ? encoding.x : void 0;
|
|
11052
|
+
if (binnedX && binnedX.scale?.zero === void 0) {
|
|
11053
|
+
if (!binnedX.scale) {
|
|
11054
|
+
binnedX.scale = { zero: false };
|
|
11055
|
+
} else {
|
|
11056
|
+
binnedX.scale.zero = false;
|
|
11057
|
+
}
|
|
11058
|
+
}
|
|
10753
11059
|
if (encoding.x) {
|
|
10754
11060
|
let xData = data;
|
|
10755
11061
|
let xChannel = encoding.x;
|
|
10756
|
-
if (spec.markType === "range" && encoding.x2 && encoding.x.type === "quantitative") {
|
|
11062
|
+
if ((spec.markType === "range" || spec.markType === "bar") && encoding.x2 && encoding.x.type === "quantitative") {
|
|
10757
11063
|
const xField = encoding.x.field;
|
|
10758
11064
|
const x2Field = encoding.x2.field;
|
|
10759
11065
|
xData = [...data, ...data.map((row) => ({ [xField]: row[x2Field] }))];
|
|
@@ -10825,7 +11131,7 @@ function computeScales(spec, chartArea, data) {
|
|
|
10825
11131
|
if (encoding.y) {
|
|
10826
11132
|
let yData = data;
|
|
10827
11133
|
let yChannel = encoding.y;
|
|
10828
|
-
if (spec.markType === "range" && encoding.y2 && encoding.y.type === "quantitative") {
|
|
11134
|
+
if ((spec.markType === "range" || spec.markType === "bar") && encoding.y2 && encoding.y.type === "quantitative") {
|
|
10829
11135
|
const yField = encoding.y.field;
|
|
10830
11136
|
const y2Field = encoding.y2.field;
|
|
10831
11137
|
yData = [...data, ...data.map((row) => ({ [yField]: row[y2Field] }))];
|
|
@@ -11378,7 +11684,7 @@ import {
|
|
|
11378
11684
|
abbreviateNumber,
|
|
11379
11685
|
buildD3Formatter,
|
|
11380
11686
|
estimateTextWidth as estimateTextWidth13,
|
|
11381
|
-
formatNumber as
|
|
11687
|
+
formatNumber as formatNumber3
|
|
11382
11688
|
} from "@opendata-ai/openchart-core";
|
|
11383
11689
|
import { scaleLinear as scaleLinear3, scaleSqrt as scaleSqrt3 } from "d3-scale";
|
|
11384
11690
|
var LABEL_GAP = 6;
|
|
@@ -11399,7 +11705,7 @@ function formatValue(value, formatStr) {
|
|
|
11399
11705
|
if (fmt2) return fmt2(value);
|
|
11400
11706
|
}
|
|
11401
11707
|
if (Math.abs(value) >= 1e3) return abbreviateNumber(value);
|
|
11402
|
-
return
|
|
11708
|
+
return formatNumber3(value);
|
|
11403
11709
|
}
|
|
11404
11710
|
function niceFloor(value) {
|
|
11405
11711
|
if (!Number.isFinite(value) || value <= 0) return value;
|
|
@@ -12321,7 +12627,8 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
12321
12627
|
const yAxisSuppressed = encoding.y?.axis === false;
|
|
12322
12628
|
let leftGutter = hPad + axisMargin;
|
|
12323
12629
|
if (encoding.y && !isRadial && !yAxisSuppressed && !yIsInline) {
|
|
12324
|
-
|
|
12630
|
+
const isBinnedBar = isBinnedBarEncoding(renderSpec.markType, encoding);
|
|
12631
|
+
if (!isBinnedBar && (encoding.y.type === "nominal" || encoding.y.type === "ordinal" || renderSpec.markType === "bar" || renderSpec.markType === "circle" || renderSpec.markType === "lollipop")) {
|
|
12325
12632
|
const yField = encoding.y.field;
|
|
12326
12633
|
const yLabelField = yAxisCfg?.labelField;
|
|
12327
12634
|
let maxLabelWidth = 0;
|
|
@@ -13364,7 +13671,7 @@ import {
|
|
|
13364
13671
|
CATEGORICAL_PALETTE as CATEGORICAL_PALETTE2,
|
|
13365
13672
|
computeChrome as computeChrome3,
|
|
13366
13673
|
estimateTextWidth as estimateTextWidth18,
|
|
13367
|
-
formatNumber as
|
|
13674
|
+
formatNumber as formatNumber4,
|
|
13368
13675
|
getBreakpoint,
|
|
13369
13676
|
HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION2,
|
|
13370
13677
|
HPAD_COMPACT_MIN as HPAD_COMPACT_MIN2,
|
|
@@ -13816,7 +14123,7 @@ function compileGeoMap(spec, options) {
|
|
|
13816
14123
|
compileWarnings.push(...filteredJoinWarnings);
|
|
13817
14124
|
}
|
|
13818
14125
|
const droppedFeatures = [];
|
|
13819
|
-
const formatter = buildD3Formatter2(mapSpec.encoding?.color?.format ?? mapSpec.valueFormat) ??
|
|
14126
|
+
const formatter = buildD3Formatter2(mapSpec.encoding?.color?.format ?? mapSpec.valueFormat) ?? formatNumber4;
|
|
13820
14127
|
const neutralFill = isDarkMode ? theme.colors.neutral[800] : theme.colors.neutral[100];
|
|
13821
14128
|
const noDataPattern = {
|
|
13822
14129
|
type: "diagonal",
|
|
@@ -15727,7 +16034,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
15727
16034
|
const yAxisSuppressed = encoding.y?.axis === false;
|
|
15728
16035
|
const yIsInline = yIsInlinePre;
|
|
15729
16036
|
if (encoding.y && !isRadial && !yAxisSuppressed && !yIsInline) {
|
|
15730
|
-
|
|
16037
|
+
const isBinnedBar = isBinnedBarEncoding(spec.markType, encoding);
|
|
16038
|
+
if (!plan && !isBinnedBar && (spec.markType === "bar" || spec.markType === "circle" || spec.markType === "lollipop" || encoding.y.type === "nominal" || encoding.y.type === "ordinal")) {
|
|
15731
16039
|
const yField = encoding.y.field;
|
|
15732
16040
|
const yLabelField = encoding.y.axis?.labelField;
|
|
15733
16041
|
let maxLabelWidth = 0;
|
|
@@ -18593,6 +18901,60 @@ function computeWaffleTooltips(spec, marks) {
|
|
|
18593
18901
|
}
|
|
18594
18902
|
return descriptors;
|
|
18595
18903
|
}
|
|
18904
|
+
function isBinnedBarSpec(spec) {
|
|
18905
|
+
return isBinnedBarEncoding(spec.markType, spec.encoding);
|
|
18906
|
+
}
|
|
18907
|
+
function computeBinnedBarTooltips(spec, marks) {
|
|
18908
|
+
const encoding = spec.encoding;
|
|
18909
|
+
const descriptors = /* @__PURE__ */ new Map();
|
|
18910
|
+
const xCh = encoding.x;
|
|
18911
|
+
const x2Ch = encoding.x2;
|
|
18912
|
+
const yCh = encoding.y;
|
|
18913
|
+
if (!xCh || !x2Ch || !yCh) return descriptors;
|
|
18914
|
+
const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
|
|
18915
|
+
const allChannels = [
|
|
18916
|
+
xCh,
|
|
18917
|
+
yCh,
|
|
18918
|
+
colorEnc,
|
|
18919
|
+
...encoding.tooltip ? Array.isArray(encoding.tooltip) ? encoding.tooltip : [encoding.tooltip] : []
|
|
18920
|
+
].filter((ch) => !!ch && "field" in ch);
|
|
18921
|
+
const fmtCache = buildFormatterCache(spec.data, allChannels);
|
|
18922
|
+
const xFormatter = getFormatter(fmtCache, xCh);
|
|
18923
|
+
const stackTotals = computeRectStackTotals(marks, encoding, fmtCache);
|
|
18924
|
+
for (let i = 0; i < marks.length; i++) {
|
|
18925
|
+
const mark = marks[i];
|
|
18926
|
+
if (mark.type !== "rect") continue;
|
|
18927
|
+
const row = mark.data;
|
|
18928
|
+
const start = formatValue2(row[xCh.field], "quantitative", xFormatter);
|
|
18929
|
+
const end = formatValue2(row[x2Ch.field], "quantitative", xFormatter);
|
|
18930
|
+
const title = `${start} \u2013 ${end}`;
|
|
18931
|
+
if (encoding.tooltip) {
|
|
18932
|
+
const channels = Array.isArray(encoding.tooltip) ? encoding.tooltip : [encoding.tooltip];
|
|
18933
|
+
descriptors.set(`rect-${i}`, {
|
|
18934
|
+
title,
|
|
18935
|
+
fields: buildExplicitTooltipFields(row, channels, fmtCache)
|
|
18936
|
+
});
|
|
18937
|
+
continue;
|
|
18938
|
+
}
|
|
18939
|
+
const fields = [];
|
|
18940
|
+
if (colorEnc) {
|
|
18941
|
+
fields.push({
|
|
18942
|
+
label: resolveLabel(colorEnc),
|
|
18943
|
+
value: String(row[colorEnc.field] ?? ""),
|
|
18944
|
+
color: getRepresentativeColor14(mark.fill)
|
|
18945
|
+
});
|
|
18946
|
+
}
|
|
18947
|
+
fields.push({
|
|
18948
|
+
label: resolveLabel(yCh),
|
|
18949
|
+
value: formatValue2(row[yCh.field], yCh.type, getFormatter(fmtCache, yCh)),
|
|
18950
|
+
...colorEnc ? {} : { color: getRepresentativeColor14(mark.fill) }
|
|
18951
|
+
});
|
|
18952
|
+
const total = mark.stackGroup !== void 0 ? stackTotals?.get(mark.stackGroup) : void 0;
|
|
18953
|
+
if (total) fields.push(total);
|
|
18954
|
+
descriptors.set(`rect-${i}`, { title, fields });
|
|
18955
|
+
}
|
|
18956
|
+
return descriptors;
|
|
18957
|
+
}
|
|
18596
18958
|
function computeParliamentTooltips(spec, marks) {
|
|
18597
18959
|
const encoding = spec.encoding;
|
|
18598
18960
|
const descriptors = /* @__PURE__ */ new Map();
|
|
@@ -18657,6 +19019,9 @@ function computeTooltipDescriptors(spec, marks) {
|
|
|
18657
19019
|
if (spec.markType === "parliament") {
|
|
18658
19020
|
return computeParliamentTooltips(spec, marks);
|
|
18659
19021
|
}
|
|
19022
|
+
if (isBinnedBarSpec(spec)) {
|
|
19023
|
+
return computeBinnedBarTooltips(spec, marks);
|
|
19024
|
+
}
|
|
18660
19025
|
const encoding = spec.encoding;
|
|
18661
19026
|
const descriptors = /* @__PURE__ */ new Map();
|
|
18662
19027
|
const allChannels = [
|
|
@@ -18788,6 +19153,135 @@ function runCalculate(data, transform) {
|
|
|
18788
19153
|
}));
|
|
18789
19154
|
}
|
|
18790
19155
|
|
|
19156
|
+
// src/transforms/density.ts
|
|
19157
|
+
import { deviation, quantile } from "d3-array";
|
|
19158
|
+
var DEFAULT_STEPS = 200;
|
|
19159
|
+
var MAX_STEPS = 1e4;
|
|
19160
|
+
var EXTENT_PADDING_BANDWIDTHS = 3;
|
|
19161
|
+
var DEGENERATE_BANDWIDTH = 1;
|
|
19162
|
+
var SQRT_2PI = Math.sqrt(2 * Math.PI);
|
|
19163
|
+
function silvermanBandwidth(values) {
|
|
19164
|
+
const n = values.length;
|
|
19165
|
+
if (n < 2) return DEGENERATE_BANDWIDTH;
|
|
19166
|
+
const sd = deviation(values) ?? 0;
|
|
19167
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
19168
|
+
const q1 = quantile(sorted, 0.25) ?? 0;
|
|
19169
|
+
const q3 = quantile(sorted, 0.75) ?? 0;
|
|
19170
|
+
const iqrScale = (q3 - q1) / 1.34;
|
|
19171
|
+
const spread = iqrScale > 0 ? Math.min(sd, iqrScale) : sd;
|
|
19172
|
+
if (!(spread > 0)) return DEGENERATE_BANDWIDTH;
|
|
19173
|
+
return 1.06 * spread * n ** (-1 / 5);
|
|
19174
|
+
}
|
|
19175
|
+
function resolveSteps(requested) {
|
|
19176
|
+
if (requested == null || !Number.isFinite(requested)) return DEFAULT_STEPS;
|
|
19177
|
+
return Math.min(MAX_STEPS, Math.max(2, Math.round(requested)));
|
|
19178
|
+
}
|
|
19179
|
+
function isUsableExtent(extent3) {
|
|
19180
|
+
const [lo, hi] = extent3;
|
|
19181
|
+
return Number.isFinite(lo) && Number.isFinite(hi) && hi > lo;
|
|
19182
|
+
}
|
|
19183
|
+
function gaussian(u) {
|
|
19184
|
+
return Math.exp(-0.5 * u * u) / SQRT_2PI;
|
|
19185
|
+
}
|
|
19186
|
+
function groupRows(data, groupby) {
|
|
19187
|
+
if (groupby.length === 0) return /* @__PURE__ */ new Map([["", data]]);
|
|
19188
|
+
const groups = /* @__PURE__ */ new Map();
|
|
19189
|
+
for (const row of data) {
|
|
19190
|
+
const key = groupby.map((f) => String(row[f] ?? "")).join("\0");
|
|
19191
|
+
const bucket = groups.get(key);
|
|
19192
|
+
if (bucket) bucket.push(row);
|
|
19193
|
+
else groups.set(key, [row]);
|
|
19194
|
+
}
|
|
19195
|
+
return groups;
|
|
19196
|
+
}
|
|
19197
|
+
function runDensity(data, transform) {
|
|
19198
|
+
const field = transform.density;
|
|
19199
|
+
const groupby = transform.groupby ?? [];
|
|
19200
|
+
const steps = resolveSteps(transform.steps);
|
|
19201
|
+
const [valueAs, densityAs] = transform.as ?? ["value", "density"];
|
|
19202
|
+
const cumulative = transform.cumulative === true;
|
|
19203
|
+
const counts = transform.counts === true;
|
|
19204
|
+
const groups = groupRows(data, groupby);
|
|
19205
|
+
const prepared = [];
|
|
19206
|
+
let widestBandwidth = 0;
|
|
19207
|
+
for (const rows of groups.values()) {
|
|
19208
|
+
const samples = [];
|
|
19209
|
+
for (const r of rows) {
|
|
19210
|
+
const raw = r[field];
|
|
19211
|
+
if (raw == null || raw === "") continue;
|
|
19212
|
+
const v = Number(raw);
|
|
19213
|
+
if (Number.isFinite(v)) samples.push(v);
|
|
19214
|
+
}
|
|
19215
|
+
if (samples.length === 0) continue;
|
|
19216
|
+
const bandwidth = transform.bandwidth && transform.bandwidth > 0 ? transform.bandwidth : silvermanBandwidth(samples);
|
|
19217
|
+
widestBandwidth = Math.max(widestBandwidth, bandwidth);
|
|
19218
|
+
prepared.push({ rows, samples, bandwidth });
|
|
19219
|
+
}
|
|
19220
|
+
if (prepared.length === 0) return [];
|
|
19221
|
+
let lo;
|
|
19222
|
+
let hi;
|
|
19223
|
+
if (transform.extent && isUsableExtent(transform.extent)) {
|
|
19224
|
+
[lo, hi] = transform.extent;
|
|
19225
|
+
} else {
|
|
19226
|
+
let dataMin = Number.POSITIVE_INFINITY;
|
|
19227
|
+
let dataMax = Number.NEGATIVE_INFINITY;
|
|
19228
|
+
for (const p of prepared) {
|
|
19229
|
+
for (const v of p.samples) {
|
|
19230
|
+
if (v < dataMin) dataMin = v;
|
|
19231
|
+
if (v > dataMax) dataMax = v;
|
|
19232
|
+
}
|
|
19233
|
+
}
|
|
19234
|
+
const pad = widestBandwidth * EXTENT_PADDING_BANDWIDTHS;
|
|
19235
|
+
lo = dataMin - pad;
|
|
19236
|
+
hi = dataMax + pad;
|
|
19237
|
+
}
|
|
19238
|
+
if (!(hi > lo)) {
|
|
19239
|
+
lo -= DEGENERATE_BANDWIDTH;
|
|
19240
|
+
hi += DEGENERATE_BANDWIDTH;
|
|
19241
|
+
}
|
|
19242
|
+
const stride = (hi - lo) / (steps - 1);
|
|
19243
|
+
const out = [];
|
|
19244
|
+
for (const { rows, samples, bandwidth } of prepared) {
|
|
19245
|
+
const groupValues = {};
|
|
19246
|
+
for (const f of groupby) groupValues[f] = rows[0][f];
|
|
19247
|
+
const n = samples.length;
|
|
19248
|
+
const scale = counts ? n : 1;
|
|
19249
|
+
const densities = new Array(steps);
|
|
19250
|
+
for (let i = 0; i < steps; i++) {
|
|
19251
|
+
const x = lo + i * stride;
|
|
19252
|
+
let sum = 0;
|
|
19253
|
+
for (const v of samples) sum += gaussian((x - v) / bandwidth);
|
|
19254
|
+
densities[i] = sum / (n * bandwidth);
|
|
19255
|
+
}
|
|
19256
|
+
if (cumulative) {
|
|
19257
|
+
const running = new Array(steps);
|
|
19258
|
+
let acc = 0;
|
|
19259
|
+
running[0] = 0;
|
|
19260
|
+
for (let i = 1; i < steps; i++) {
|
|
19261
|
+
acc += (densities[i] + densities[i - 1]) / 2 * stride;
|
|
19262
|
+
running[i] = acc;
|
|
19263
|
+
}
|
|
19264
|
+
const total = running[steps - 1] || 1;
|
|
19265
|
+
for (let i = 0; i < steps; i++) {
|
|
19266
|
+
out.push({
|
|
19267
|
+
...groupValues,
|
|
19268
|
+
[valueAs]: lo + i * stride,
|
|
19269
|
+
[densityAs]: running[i] / total * scale
|
|
19270
|
+
});
|
|
19271
|
+
}
|
|
19272
|
+
continue;
|
|
19273
|
+
}
|
|
19274
|
+
for (let i = 0; i < steps; i++) {
|
|
19275
|
+
out.push({
|
|
19276
|
+
...groupValues,
|
|
19277
|
+
[valueAs]: lo + i * stride,
|
|
19278
|
+
[densityAs]: densities[i] * scale
|
|
19279
|
+
});
|
|
19280
|
+
}
|
|
19281
|
+
}
|
|
19282
|
+
return out;
|
|
19283
|
+
}
|
|
19284
|
+
|
|
18791
19285
|
// src/transforms/filter.ts
|
|
18792
19286
|
function applyOffset2(anchor, offset, unit) {
|
|
18793
19287
|
const d = new Date(anchor.getTime());
|
|
@@ -18887,6 +19381,31 @@ function runFold(data, transform) {
|
|
|
18887
19381
|
return result;
|
|
18888
19382
|
}
|
|
18889
19383
|
|
|
19384
|
+
// src/transforms/joinaggregate.ts
|
|
19385
|
+
function groupKey2(row, groupby) {
|
|
19386
|
+
return groupby.map((f) => String(row[f] ?? "")).join("\0");
|
|
19387
|
+
}
|
|
19388
|
+
function runJoinAggregate(data, transform) {
|
|
19389
|
+
const groupby = transform.groupby ?? [];
|
|
19390
|
+
const groups = /* @__PURE__ */ new Map();
|
|
19391
|
+
for (const row of data) {
|
|
19392
|
+
const key = groupKey2(row, groupby);
|
|
19393
|
+
const bucket = groups.get(key);
|
|
19394
|
+
if (bucket) bucket.push(row);
|
|
19395
|
+
else groups.set(key, [row]);
|
|
19396
|
+
}
|
|
19397
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
19398
|
+
for (const [key, rows] of groups) {
|
|
19399
|
+
const summary = {};
|
|
19400
|
+
for (const { op, field, as } of transform.joinaggregate) {
|
|
19401
|
+
const values = op === "count" ? rows.map(() => 1) : rows.map((r) => Number(r[field])).filter((v) => Number.isFinite(v));
|
|
19402
|
+
summary[as] = computeAggregate(op, values);
|
|
19403
|
+
}
|
|
19404
|
+
summaries.set(key, summary);
|
|
19405
|
+
}
|
|
19406
|
+
return data.map((row) => ({ ...row, ...summaries.get(groupKey2(row, groupby)) }));
|
|
19407
|
+
}
|
|
19408
|
+
|
|
18890
19409
|
// src/transforms/timeunit.ts
|
|
18891
19410
|
function extractTimeUnit(date, unit) {
|
|
18892
19411
|
switch (unit) {
|
|
@@ -18953,7 +19472,7 @@ function runTimeUnit(data, transform) {
|
|
|
18953
19472
|
}
|
|
18954
19473
|
|
|
18955
19474
|
// src/transforms/window.ts
|
|
18956
|
-
function
|
|
19475
|
+
function groupKey3(row, groupby) {
|
|
18957
19476
|
return groupby.map((f) => String(row[f] ?? "")).join("\0");
|
|
18958
19477
|
}
|
|
18959
19478
|
function tryParseDate(val) {
|
|
@@ -18985,7 +19504,7 @@ function runWindow(data, transform) {
|
|
|
18985
19504
|
const indexed = data.map((row, i) => ({ row, originalIndex: i }));
|
|
18986
19505
|
const groups = /* @__PURE__ */ new Map();
|
|
18987
19506
|
for (const entry of indexed) {
|
|
18988
|
-
const key = groupby.length > 0 ?
|
|
19507
|
+
const key = groupby.length > 0 ? groupKey3(entry.row, groupby) : "";
|
|
18989
19508
|
const existing = groups.get(key);
|
|
18990
19509
|
if (existing) {
|
|
18991
19510
|
existing.push(entry);
|
|
@@ -19091,6 +19610,10 @@ function runTransforms(data, transforms) {
|
|
|
19091
19610
|
result = runTimeUnit(result, transform);
|
|
19092
19611
|
} else if ("aggregate" in transform) {
|
|
19093
19612
|
result = runAggregate(result, transform);
|
|
19613
|
+
} else if ("joinaggregate" in transform) {
|
|
19614
|
+
result = runJoinAggregate(result, transform);
|
|
19615
|
+
} else if ("density" in transform) {
|
|
19616
|
+
result = runDensity(result, transform);
|
|
19094
19617
|
} else if ("fold" in transform) {
|
|
19095
19618
|
result = runFold(result, transform);
|
|
19096
19619
|
} else if ("window" in transform) {
|
|
@@ -19455,6 +19978,14 @@ function compileChart(spec, optionsInput) {
|
|
|
19455
19978
|
height: chartArea.height
|
|
19456
19979
|
};
|
|
19457
19980
|
}
|
|
19981
|
+
if (chartSpec.userExplicit.labels && chartSpec.labels?.density !== "none" && isBinnedBarEncoding(chartSpec.markType, renderSpec.encoding)) {
|
|
19982
|
+
emitSpecWarnings(
|
|
19983
|
+
[
|
|
19984
|
+
"[openchart] `labels` has no effect on a histogram (binned bar): a value over every bin obscures the distribution. Remove it, or use mark: 'bar' over pre-aggregated categories if you need per-bar values."
|
|
19985
|
+
],
|
|
19986
|
+
options.onWarn
|
|
19987
|
+
);
|
|
19988
|
+
}
|
|
19458
19989
|
const scales = computeScales(renderSpec, scaleArea, renderSpec.data);
|
|
19459
19990
|
emitSpecWarnings(
|
|
19460
19991
|
applyColorScaleRange(
|