@ggterm/core 0.2.9 → 0.2.11
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/cli-plot.js +534 -2
- package/dist/cli.js +515 -1
- package/dist/geoms/beeswarm.d.ts +60 -0
- package/dist/geoms/beeswarm.d.ts.map +1 -0
- package/dist/geoms/index.d.ts +2 -0
- package/dist/geoms/index.d.ts.map +1 -1
- package/dist/geoms/ridgeline.d.ts +52 -0
- package/dist/geoms/ridgeline.d.ts.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +527 -1
- package/dist/pipeline/pipeline.d.ts.map +1 -1
- package/dist/pipeline/render-geoms.d.ts +11 -0
- package/dist/pipeline/render-geoms.d.ts.map +1 -1
- package/dist/stats/beeswarm.d.ts +38 -0
- package/dist/stats/beeswarm.d.ts.map +1 -0
- package/dist/stats/density.d.ts +5 -0
- package/dist/stats/density.d.ts.map +1 -1
- package/dist/stats/index.d.ts +3 -1
- package/dist/stats/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1806,6 +1806,120 @@ function renderGeomViolin(data, geom, _aes, scales, canvas) {
|
|
|
1806
1806
|
}
|
|
1807
1807
|
}
|
|
1808
1808
|
}
|
|
1809
|
+
function renderGeomRidgeline(data, geom, aes, scales, canvas) {
|
|
1810
|
+
const scaleFactor = geom.params.scale ?? 0.9;
|
|
1811
|
+
const alpha = geom.params.alpha ?? 0.8;
|
|
1812
|
+
const showOutline = geom.params.outline ?? true;
|
|
1813
|
+
const fixedFill = geom.params.fill;
|
|
1814
|
+
const fixedColor = geom.params.color;
|
|
1815
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
1816
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
1817
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
1818
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
1819
|
+
const groups = new Map;
|
|
1820
|
+
const groupKeys = [];
|
|
1821
|
+
for (const row of data) {
|
|
1822
|
+
const key = String(row.y ?? "default");
|
|
1823
|
+
if (!groups.has(key)) {
|
|
1824
|
+
groups.set(key, { data: [], index: Number(row.yIndex) ?? groupKeys.length });
|
|
1825
|
+
groupKeys.push(key);
|
|
1826
|
+
}
|
|
1827
|
+
groups.get(key).data.push(row);
|
|
1828
|
+
}
|
|
1829
|
+
const numGroups = groupKeys.length;
|
|
1830
|
+
if (numGroups === 0)
|
|
1831
|
+
return;
|
|
1832
|
+
const plotHeight = Math.abs(plotBottom - plotTop);
|
|
1833
|
+
const ridgeBaseHeight = plotHeight / numGroups;
|
|
1834
|
+
const defaultColors = [
|
|
1835
|
+
{ r: 79, g: 169, b: 238, a: 1 },
|
|
1836
|
+
{ r: 238, g: 136, b: 102, a: 1 },
|
|
1837
|
+
{ r: 102, g: 204, b: 153, a: 1 },
|
|
1838
|
+
{ r: 204, g: 102, b: 204, a: 1 },
|
|
1839
|
+
{ r: 255, g: 200, b: 87, a: 1 },
|
|
1840
|
+
{ r: 138, g: 201, b: 222, a: 1 },
|
|
1841
|
+
{ r: 255, g: 153, b: 153, a: 1 },
|
|
1842
|
+
{ r: 170, g: 170, b: 170, a: 1 }
|
|
1843
|
+
];
|
|
1844
|
+
let parsedFillColor = null;
|
|
1845
|
+
if (fixedFill) {
|
|
1846
|
+
parsedFillColor = parseColorToRgba(fixedFill);
|
|
1847
|
+
}
|
|
1848
|
+
const sortedKeys = [...groupKeys].sort((a, b) => {
|
|
1849
|
+
const idxA = groups.get(a)?.index ?? 0;
|
|
1850
|
+
const idxB = groups.get(b)?.index ?? 0;
|
|
1851
|
+
return idxB - idxA;
|
|
1852
|
+
});
|
|
1853
|
+
for (const groupKey of sortedKeys) {
|
|
1854
|
+
const group = groups.get(groupKey);
|
|
1855
|
+
if (!group)
|
|
1856
|
+
continue;
|
|
1857
|
+
const groupIndex = group.index;
|
|
1858
|
+
const groupData = group.data;
|
|
1859
|
+
const sorted = [...groupData].sort((a, b) => {
|
|
1860
|
+
const ax = Number(a.x) || 0;
|
|
1861
|
+
const bx = Number(b.x) || 0;
|
|
1862
|
+
return ax - bx;
|
|
1863
|
+
});
|
|
1864
|
+
if (sorted.length < 2)
|
|
1865
|
+
continue;
|
|
1866
|
+
const baseline = plotTop + (groupIndex + 0.5) * ridgeBaseHeight;
|
|
1867
|
+
let fillColor;
|
|
1868
|
+
if (parsedFillColor) {
|
|
1869
|
+
fillColor = parsedFillColor;
|
|
1870
|
+
} else if ((aes.fill || aes.color) && scales.color) {
|
|
1871
|
+
const mappedColor = scales.color.map(groupKey);
|
|
1872
|
+
if (typeof mappedColor === "object" && "r" in mappedColor) {
|
|
1873
|
+
fillColor = mappedColor;
|
|
1874
|
+
} else {
|
|
1875
|
+
fillColor = defaultColors[groupIndex % defaultColors.length];
|
|
1876
|
+
}
|
|
1877
|
+
} else {
|
|
1878
|
+
fillColor = defaultColors[groupIndex % defaultColors.length];
|
|
1879
|
+
}
|
|
1880
|
+
const alphaFillColor = {
|
|
1881
|
+
r: Math.round(fillColor.r * alpha + 255 * (1 - alpha) * 0.1),
|
|
1882
|
+
g: Math.round(fillColor.g * alpha + 255 * (1 - alpha) * 0.1),
|
|
1883
|
+
b: Math.round(fillColor.b * alpha + 255 * (1 - alpha) * 0.1),
|
|
1884
|
+
a: 1
|
|
1885
|
+
};
|
|
1886
|
+
const maxRidgeHeight = ridgeBaseHeight * scaleFactor * 1.5;
|
|
1887
|
+
const outlinePoints = [];
|
|
1888
|
+
for (const row of sorted) {
|
|
1889
|
+
const xVal = Number(row.x);
|
|
1890
|
+
const scaled = Number(row.scaled) || 0;
|
|
1891
|
+
const px = Math.round(scales.x.map(xVal));
|
|
1892
|
+
const ridgeHeight = scaled * maxRidgeHeight;
|
|
1893
|
+
const py = Math.round(baseline - ridgeHeight);
|
|
1894
|
+
if (px < plotLeft || px > plotRight)
|
|
1895
|
+
continue;
|
|
1896
|
+
const fillTop = Math.max(plotTop, py);
|
|
1897
|
+
const fillBottom = Math.min(plotBottom, Math.round(baseline));
|
|
1898
|
+
for (let y = fillTop;y <= fillBottom; y++) {
|
|
1899
|
+
canvas.drawChar(px, y, "█", alphaFillColor);
|
|
1900
|
+
}
|
|
1901
|
+
if (showOutline) {
|
|
1902
|
+
outlinePoints.push({ x: px, y: Math.max(plotTop, py) });
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
if (showOutline && outlinePoints.length > 1) {
|
|
1906
|
+
const outlineColor = fixedColor ? parseColorToRgba(fixedColor) : { r: Math.min(255, fillColor.r + 40), g: Math.min(255, fillColor.g + 40), b: Math.min(255, fillColor.b + 40), a: 1 };
|
|
1907
|
+
for (let i = 0;i < outlinePoints.length - 1; i++) {
|
|
1908
|
+
const p1 = outlinePoints[i];
|
|
1909
|
+
const p2 = outlinePoints[i + 1];
|
|
1910
|
+
if (Math.abs(p2.x - p1.x) <= 1) {
|
|
1911
|
+
if (p1.y >= plotTop && p1.y <= plotBottom) {
|
|
1912
|
+
canvas.drawChar(p1.x, p1.y, "▄", outlineColor);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
const last = outlinePoints[outlinePoints.length - 1];
|
|
1917
|
+
if (last.y >= plotTop && last.y <= plotBottom) {
|
|
1918
|
+
canvas.drawChar(last.x, last.y, "▄", outlineColor);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1809
1923
|
function renderGeomTile(data, geom, aes, scales, canvas) {
|
|
1810
1924
|
const alpha = geom.params.alpha ?? 1;
|
|
1811
1925
|
const plotLeft = Math.round(scales.x.range[0]);
|
|
@@ -2195,6 +2309,58 @@ function parseColor(color) {
|
|
|
2195
2309
|
}
|
|
2196
2310
|
return { r: 128, g: 128, b: 128, a: 1 };
|
|
2197
2311
|
}
|
|
2312
|
+
function renderGeomBeeswarm(data, geom, aes, scales, canvas) {
|
|
2313
|
+
const alpha = geom.params.alpha ?? 1;
|
|
2314
|
+
const fixedColor = geom.params.color;
|
|
2315
|
+
const shape = getPointShape(geom.params.shape);
|
|
2316
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
2317
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
2318
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
2319
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
2320
|
+
const defaultColors = [
|
|
2321
|
+
{ r: 79, g: 169, b: 238, a: 1 },
|
|
2322
|
+
{ r: 238, g: 136, b: 102, a: 1 },
|
|
2323
|
+
{ r: 102, g: 204, b: 153, a: 1 },
|
|
2324
|
+
{ r: 204, g: 102, b: 204, a: 1 },
|
|
2325
|
+
{ r: 255, g: 200, b: 87, a: 1 },
|
|
2326
|
+
{ r: 138, g: 201, b: 222, a: 1 },
|
|
2327
|
+
{ r: 255, g: 153, b: 153, a: 1 },
|
|
2328
|
+
{ r: 170, g: 170, b: 170, a: 1 }
|
|
2329
|
+
];
|
|
2330
|
+
const categories = new Set;
|
|
2331
|
+
for (const row of data) {
|
|
2332
|
+
categories.add(String(row.xOriginal ?? row[aes.x] ?? "default"));
|
|
2333
|
+
}
|
|
2334
|
+
const categoryList = [...categories];
|
|
2335
|
+
for (const row of data) {
|
|
2336
|
+
const xVal = row.x;
|
|
2337
|
+
const yVal = row.y;
|
|
2338
|
+
if (xVal === null || xVal === undefined || yVal === null || yVal === undefined) {
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2341
|
+
const numGroups = categoryList.length;
|
|
2342
|
+
const xRange = plotRight - plotLeft;
|
|
2343
|
+
const xNormalized = (Number(xVal) + 0.5) / numGroups;
|
|
2344
|
+
const cx = Math.round(plotLeft + xNormalized * xRange);
|
|
2345
|
+
const cy = Math.round(scales.y.map(yVal));
|
|
2346
|
+
let color;
|
|
2347
|
+
if (fixedColor) {
|
|
2348
|
+
color = parseColorToRgba(fixedColor);
|
|
2349
|
+
} else if (scales.color && aes.color) {
|
|
2350
|
+
color = getPointColor(row, aes, scales.color);
|
|
2351
|
+
} else {
|
|
2352
|
+
const category = String(row.xOriginal ?? row[aes.x] ?? "default");
|
|
2353
|
+
const categoryIdx = categoryList.indexOf(category);
|
|
2354
|
+
color = defaultColors[categoryIdx % defaultColors.length];
|
|
2355
|
+
}
|
|
2356
|
+
if (alpha < 1) {
|
|
2357
|
+
color = { ...color, a: alpha };
|
|
2358
|
+
}
|
|
2359
|
+
if (cx >= plotLeft && cx <= plotRight && cy >= plotTop && cy <= plotBottom) {
|
|
2360
|
+
canvas.drawChar(cx, cy, shape, color);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2198
2364
|
function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
2199
2365
|
switch (geom.type) {
|
|
2200
2366
|
case "point":
|
|
@@ -2244,6 +2410,10 @@ function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
|
2244
2410
|
case "violin":
|
|
2245
2411
|
renderGeomViolin(data, geom, aes, scales, canvas);
|
|
2246
2412
|
break;
|
|
2413
|
+
case "ridgeline":
|
|
2414
|
+
case "joy":
|
|
2415
|
+
renderGeomRidgeline(data, geom, aes, scales, canvas);
|
|
2416
|
+
break;
|
|
2247
2417
|
case "tile":
|
|
2248
2418
|
case "raster":
|
|
2249
2419
|
case "bin2d":
|
|
@@ -2272,6 +2442,10 @@ function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
|
2272
2442
|
case "smooth":
|
|
2273
2443
|
renderGeomSmooth(data, geom, aes, scales, canvas);
|
|
2274
2444
|
break;
|
|
2445
|
+
case "beeswarm":
|
|
2446
|
+
case "quasirandom":
|
|
2447
|
+
renderGeomBeeswarm(data, geom, aes, scales, canvas);
|
|
2448
|
+
break;
|
|
2275
2449
|
default:
|
|
2276
2450
|
break;
|
|
2277
2451
|
}
|
|
@@ -3352,6 +3526,270 @@ function stat_ydensity(params = {}) {
|
|
|
3352
3526
|
}
|
|
3353
3527
|
};
|
|
3354
3528
|
}
|
|
3529
|
+
function stat_xdensity(params = {}) {
|
|
3530
|
+
return {
|
|
3531
|
+
type: "xdensity",
|
|
3532
|
+
compute(data, aes) {
|
|
3533
|
+
const groups = new Map;
|
|
3534
|
+
const groupOrder = [];
|
|
3535
|
+
for (const row of data) {
|
|
3536
|
+
const groupKey = String(row[aes.y] ?? "default");
|
|
3537
|
+
const xVal = row[aes.x];
|
|
3538
|
+
if (xVal === null || xVal === undefined)
|
|
3539
|
+
continue;
|
|
3540
|
+
const numX = Number(xVal);
|
|
3541
|
+
if (isNaN(numX))
|
|
3542
|
+
continue;
|
|
3543
|
+
if (!groups.has(groupKey)) {
|
|
3544
|
+
groups.set(groupKey, []);
|
|
3545
|
+
groupOrder.push(groupKey);
|
|
3546
|
+
}
|
|
3547
|
+
groups.get(groupKey).push(numX);
|
|
3548
|
+
}
|
|
3549
|
+
const result = [];
|
|
3550
|
+
let groupIndex = 0;
|
|
3551
|
+
for (const groupKey of groupOrder) {
|
|
3552
|
+
const xValues = groups.get(groupKey);
|
|
3553
|
+
if (xValues.length < 2) {
|
|
3554
|
+
groupIndex++;
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3557
|
+
const tempData = xValues.map((v) => ({ x: v }));
|
|
3558
|
+
const densityResult = computeDensity(tempData, "x", params);
|
|
3559
|
+
for (const d of densityResult) {
|
|
3560
|
+
result.push({
|
|
3561
|
+
x: d.x,
|
|
3562
|
+
y: groupKey,
|
|
3563
|
+
yIndex: groupIndex,
|
|
3564
|
+
density: d.density,
|
|
3565
|
+
scaled: d.scaled,
|
|
3566
|
+
height: d.scaled
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
groupIndex++;
|
|
3570
|
+
}
|
|
3571
|
+
return result;
|
|
3572
|
+
}
|
|
3573
|
+
};
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
// src/stats/beeswarm.ts
|
|
3577
|
+
function swarmArrange(yValues, params) {
|
|
3578
|
+
const n = yValues.length;
|
|
3579
|
+
if (n === 0)
|
|
3580
|
+
return { offsets: [], indices: [] };
|
|
3581
|
+
const cex = params.cex ?? 1;
|
|
3582
|
+
const spacing = params.spacing ?? 1;
|
|
3583
|
+
const side = params.side ?? 0;
|
|
3584
|
+
const yRange = Math.max(...yValues) - Math.min(...yValues);
|
|
3585
|
+
const pointSize = yRange / Math.max(n, 10) * cex * spacing;
|
|
3586
|
+
let indices = yValues.map((_, i) => i);
|
|
3587
|
+
const priority = params.priority ?? "ascending";
|
|
3588
|
+
switch (priority) {
|
|
3589
|
+
case "ascending":
|
|
3590
|
+
indices.sort((a, b) => yValues[a] - yValues[b]);
|
|
3591
|
+
break;
|
|
3592
|
+
case "descending":
|
|
3593
|
+
indices.sort((a, b) => yValues[b] - yValues[a]);
|
|
3594
|
+
break;
|
|
3595
|
+
case "random":
|
|
3596
|
+
for (let i = indices.length - 1;i > 0; i--) {
|
|
3597
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
3598
|
+
[indices[i], indices[j]] = [indices[j], indices[i]];
|
|
3599
|
+
}
|
|
3600
|
+
break;
|
|
3601
|
+
case "density":
|
|
3602
|
+
const median = yValues.slice().sort((a, b) => a - b)[Math.floor(n / 2)];
|
|
3603
|
+
indices.sort((a, b) => Math.abs(yValues[a] - median) - Math.abs(yValues[b] - median));
|
|
3604
|
+
break;
|
|
3605
|
+
}
|
|
3606
|
+
const placed = [];
|
|
3607
|
+
const offsets = new Array(n).fill(0);
|
|
3608
|
+
for (const idx of indices) {
|
|
3609
|
+
const y = yValues[idx];
|
|
3610
|
+
let bestOffset = 0;
|
|
3611
|
+
if (placed.length > 0) {
|
|
3612
|
+
const nearby = placed.filter((p) => Math.abs(p.y - y) < pointSize * 2);
|
|
3613
|
+
if (nearby.length > 0) {
|
|
3614
|
+
const maxOffset = nearby.length * pointSize;
|
|
3615
|
+
let foundSpot = false;
|
|
3616
|
+
for (let tryOffset = 0;tryOffset <= maxOffset && !foundSpot; tryOffset += pointSize * 0.5) {
|
|
3617
|
+
if (side >= 0) {
|
|
3618
|
+
let collision = false;
|
|
3619
|
+
for (const p of nearby) {
|
|
3620
|
+
const dx = tryOffset - p.x;
|
|
3621
|
+
const dy = y - p.y;
|
|
3622
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
3623
|
+
if (dist < pointSize) {
|
|
3624
|
+
collision = true;
|
|
3625
|
+
break;
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
if (!collision) {
|
|
3629
|
+
bestOffset = tryOffset;
|
|
3630
|
+
foundSpot = true;
|
|
3631
|
+
break;
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
if (side <= 0 && tryOffset > 0 && !foundSpot) {
|
|
3635
|
+
let collision = false;
|
|
3636
|
+
for (const p of nearby) {
|
|
3637
|
+
const dx = -tryOffset - p.x;
|
|
3638
|
+
const dy = y - p.y;
|
|
3639
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
3640
|
+
if (dist < pointSize) {
|
|
3641
|
+
collision = true;
|
|
3642
|
+
break;
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
if (!collision) {
|
|
3646
|
+
bestOffset = -tryOffset;
|
|
3647
|
+
foundSpot = true;
|
|
3648
|
+
break;
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
offsets[idx] = bestOffset;
|
|
3655
|
+
placed.push({ y, x: bestOffset });
|
|
3656
|
+
}
|
|
3657
|
+
const maxAbsOffset = Math.max(...offsets.map(Math.abs), 0.001);
|
|
3658
|
+
const dodge = params.dodge ?? 0.8;
|
|
3659
|
+
const scale = dodge * 0.5 / maxAbsOffset;
|
|
3660
|
+
for (let i = 0;i < offsets.length; i++) {
|
|
3661
|
+
offsets[i] *= scale;
|
|
3662
|
+
}
|
|
3663
|
+
return { offsets, indices };
|
|
3664
|
+
}
|
|
3665
|
+
function centerArrange(yValues, params) {
|
|
3666
|
+
const n = yValues.length;
|
|
3667
|
+
if (n === 0)
|
|
3668
|
+
return { offsets: [], indices: [] };
|
|
3669
|
+
const dodge = params.dodge ?? 0.8;
|
|
3670
|
+
const side = params.side ?? 0;
|
|
3671
|
+
const indices = yValues.map((_, i) => i);
|
|
3672
|
+
indices.sort((a, b) => yValues[a] - yValues[b]);
|
|
3673
|
+
const offsets = new Array(n).fill(0);
|
|
3674
|
+
const maxOffset = dodge * 0.4;
|
|
3675
|
+
for (let i = 0;i < indices.length; i++) {
|
|
3676
|
+
const idx = indices[i];
|
|
3677
|
+
const layer = Math.floor(i / 2) + 1;
|
|
3678
|
+
const offset = layer / Math.ceil(n / 2) * maxOffset;
|
|
3679
|
+
if (side === 0) {
|
|
3680
|
+
offsets[idx] = i % 2 === 0 ? offset : -offset;
|
|
3681
|
+
} else {
|
|
3682
|
+
offsets[idx] = side * offset;
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
return { offsets, indices };
|
|
3686
|
+
}
|
|
3687
|
+
function squareArrange(yValues, params) {
|
|
3688
|
+
const n = yValues.length;
|
|
3689
|
+
if (n === 0)
|
|
3690
|
+
return { offsets: [], indices: [] };
|
|
3691
|
+
const dodge = params.dodge ?? 0.8;
|
|
3692
|
+
const side = params.side ?? 0;
|
|
3693
|
+
const indices = yValues.map((_, i) => i);
|
|
3694
|
+
indices.sort((a, b) => yValues[a] - yValues[b]);
|
|
3695
|
+
const offsets = new Array(n).fill(0);
|
|
3696
|
+
const yRange = Math.max(...yValues) - Math.min(...yValues);
|
|
3697
|
+
const binSize = yRange / Math.max(Math.sqrt(n), 3);
|
|
3698
|
+
const bins = new Map;
|
|
3699
|
+
for (let i = 0;i < indices.length; i++) {
|
|
3700
|
+
const idx = indices[i];
|
|
3701
|
+
const y = yValues[idx];
|
|
3702
|
+
const binKey = Math.floor(y / binSize);
|
|
3703
|
+
if (!bins.has(binKey)) {
|
|
3704
|
+
bins.set(binKey, []);
|
|
3705
|
+
}
|
|
3706
|
+
bins.get(binKey).push(idx);
|
|
3707
|
+
}
|
|
3708
|
+
for (const binIndices of bins.values()) {
|
|
3709
|
+
const binN = binIndices.length;
|
|
3710
|
+
const maxOffset = dodge * 0.4;
|
|
3711
|
+
for (let i = 0;i < binN; i++) {
|
|
3712
|
+
const idx = binIndices[i];
|
|
3713
|
+
const offset = (i - (binN - 1) / 2) * (maxOffset * 2 / Math.max(binN - 1, 1));
|
|
3714
|
+
if (side === 0) {
|
|
3715
|
+
offsets[idx] = offset;
|
|
3716
|
+
} else if (side > 0) {
|
|
3717
|
+
offsets[idx] = Math.abs(offset);
|
|
3718
|
+
} else {
|
|
3719
|
+
offsets[idx] = -Math.abs(offset);
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
return { offsets, indices };
|
|
3724
|
+
}
|
|
3725
|
+
function computeBeeswarm(data, _xField, yField, groupKey, groupIndex, params = {}) {
|
|
3726
|
+
const yValues = [];
|
|
3727
|
+
const originalRows = [];
|
|
3728
|
+
for (const row of data) {
|
|
3729
|
+
const yVal = row[yField];
|
|
3730
|
+
if (yVal === null || yVal === undefined)
|
|
3731
|
+
continue;
|
|
3732
|
+
const numY = Number(yVal);
|
|
3733
|
+
if (isNaN(numY))
|
|
3734
|
+
continue;
|
|
3735
|
+
yValues.push(numY);
|
|
3736
|
+
originalRows.push(row);
|
|
3737
|
+
}
|
|
3738
|
+
if (yValues.length === 0)
|
|
3739
|
+
return [];
|
|
3740
|
+
const method = params.method ?? "swarm";
|
|
3741
|
+
let result;
|
|
3742
|
+
switch (method) {
|
|
3743
|
+
case "center":
|
|
3744
|
+
result = centerArrange(yValues, params);
|
|
3745
|
+
break;
|
|
3746
|
+
case "square":
|
|
3747
|
+
result = squareArrange(yValues, params);
|
|
3748
|
+
break;
|
|
3749
|
+
case "swarm":
|
|
3750
|
+
default:
|
|
3751
|
+
result = swarmArrange(yValues, params);
|
|
3752
|
+
}
|
|
3753
|
+
const output = [];
|
|
3754
|
+
for (let i = 0;i < yValues.length; i++) {
|
|
3755
|
+
const originalRow = originalRows[i];
|
|
3756
|
+
output.push({
|
|
3757
|
+
x: groupIndex + result.offsets[i],
|
|
3758
|
+
y: yValues[i],
|
|
3759
|
+
xOriginal: groupKey,
|
|
3760
|
+
yOriginal: yValues[i],
|
|
3761
|
+
xOffset: result.offsets[i],
|
|
3762
|
+
...originalRow
|
|
3763
|
+
});
|
|
3764
|
+
}
|
|
3765
|
+
return output;
|
|
3766
|
+
}
|
|
3767
|
+
function stat_beeswarm(params = {}) {
|
|
3768
|
+
return {
|
|
3769
|
+
type: "beeswarm",
|
|
3770
|
+
compute(data, aes) {
|
|
3771
|
+
const groups = new Map;
|
|
3772
|
+
const groupOrder = [];
|
|
3773
|
+
for (const row of data) {
|
|
3774
|
+
const groupKey = String(row[aes.x] ?? "default");
|
|
3775
|
+
if (!groups.has(groupKey)) {
|
|
3776
|
+
groups.set(groupKey, []);
|
|
3777
|
+
groupOrder.push(groupKey);
|
|
3778
|
+
}
|
|
3779
|
+
groups.get(groupKey).push(row);
|
|
3780
|
+
}
|
|
3781
|
+
const result = [];
|
|
3782
|
+
let groupIndex = 0;
|
|
3783
|
+
for (const groupKey of groupOrder) {
|
|
3784
|
+
const groupData = groups.get(groupKey);
|
|
3785
|
+
const swarmResult = computeBeeswarm(groupData, aes.x, aes.y, groupKey, groupIndex, params);
|
|
3786
|
+
result.push(...swarmResult);
|
|
3787
|
+
groupIndex++;
|
|
3788
|
+
}
|
|
3789
|
+
return result;
|
|
3790
|
+
}
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3355
3793
|
|
|
3356
3794
|
// src/stats/smooth.ts
|
|
3357
3795
|
function linearRegression(xs, ys) {
|
|
@@ -4224,6 +4662,23 @@ function applyStatTransform(data, geom, aes) {
|
|
|
4224
4662
|
adjust: geom.params.adjust
|
|
4225
4663
|
});
|
|
4226
4664
|
return ydensityStat.compute(data, aes);
|
|
4665
|
+
} else if (geom.stat === "xdensity") {
|
|
4666
|
+
const xdensityStat = stat_xdensity({
|
|
4667
|
+
bw: geom.params.bw,
|
|
4668
|
+
kernel: geom.params.kernel,
|
|
4669
|
+
n: geom.params.n,
|
|
4670
|
+
adjust: geom.params.adjust
|
|
4671
|
+
});
|
|
4672
|
+
return xdensityStat.compute(data, aes);
|
|
4673
|
+
} else if (geom.stat === "beeswarm") {
|
|
4674
|
+
const beeswarmStat = stat_beeswarm({
|
|
4675
|
+
method: geom.params.method,
|
|
4676
|
+
cex: geom.params.cex,
|
|
4677
|
+
side: geom.params.side,
|
|
4678
|
+
priority: geom.params.priority,
|
|
4679
|
+
dodge: geom.params.dodge
|
|
4680
|
+
});
|
|
4681
|
+
return beeswarmStat.compute(data, aes);
|
|
4227
4682
|
} else if (geom.stat === "smooth") {
|
|
4228
4683
|
const smoothStat = stat_smooth({
|
|
4229
4684
|
method: geom.params.method,
|
|
@@ -4346,6 +4801,14 @@ function renderToCanvas(spec, options) {
|
|
|
4346
4801
|
scaleData = applyStatTransform(spec.data, geom, spec.aes);
|
|
4347
4802
|
scaleAes = { ...spec.aes, x: "x", y: "y" };
|
|
4348
4803
|
break;
|
|
4804
|
+
} else if (geom.stat === "xdensity") {
|
|
4805
|
+
scaleData = applyStatTransform(spec.data, geom, spec.aes);
|
|
4806
|
+
scaleAes = { ...spec.aes, x: "x", y: "y" };
|
|
4807
|
+
break;
|
|
4808
|
+
} else if (geom.stat === "beeswarm") {
|
|
4809
|
+
scaleData = spec.data;
|
|
4810
|
+
scaleAes = spec.aes;
|
|
4811
|
+
break;
|
|
4349
4812
|
}
|
|
4350
4813
|
}
|
|
4351
4814
|
scaleData = applyCoordTransform(scaleData, scaleAes, spec.coord);
|
|
@@ -4384,6 +4847,10 @@ function renderToCanvas(spec, options) {
|
|
|
4384
4847
|
geomAes = { ...spec.aes, x: "x", y: "y", fill: "fill" };
|
|
4385
4848
|
} else if (geom.stat === "density_2d") {
|
|
4386
4849
|
geomAes = { ...spec.aes, x: "x", y: "y" };
|
|
4850
|
+
} else if (geom.stat === "xdensity") {
|
|
4851
|
+
geomAes = { ...spec.aes, x: "x", y: "y" };
|
|
4852
|
+
} else if (geom.stat === "beeswarm") {
|
|
4853
|
+
geomAes = { ...spec.aes, x: "x", y: "y" };
|
|
4387
4854
|
}
|
|
4388
4855
|
geomData = applyCoordTransform(geomData, geomAes, spec.coord);
|
|
4389
4856
|
}
|
|
@@ -5301,8 +5768,55 @@ function geom_qq_line(options = {}) {
|
|
|
5301
5768
|
};
|
|
5302
5769
|
}
|
|
5303
5770
|
|
|
5771
|
+
// src/geoms/ridgeline.ts
|
|
5772
|
+
function geom_ridgeline(options = {}) {
|
|
5773
|
+
return {
|
|
5774
|
+
type: "ridgeline",
|
|
5775
|
+
stat: "xdensity",
|
|
5776
|
+
position: "identity",
|
|
5777
|
+
params: {
|
|
5778
|
+
scale: options.scale ?? 0.9,
|
|
5779
|
+
alpha: options.alpha ?? 0.8,
|
|
5780
|
+
fill: options.fill,
|
|
5781
|
+
color: options.color,
|
|
5782
|
+
adjust: options.adjust ?? 1,
|
|
5783
|
+
n: options.n ?? 128,
|
|
5784
|
+
outline: options.outline ?? true
|
|
5785
|
+
}
|
|
5786
|
+
};
|
|
5787
|
+
}
|
|
5788
|
+
var geom_joy;
|
|
5789
|
+
var init_ridgeline = __esm(() => {
|
|
5790
|
+
geom_joy = geom_ridgeline;
|
|
5791
|
+
});
|
|
5792
|
+
|
|
5793
|
+
// src/geoms/beeswarm.ts
|
|
5794
|
+
function geom_beeswarm(options = {}) {
|
|
5795
|
+
return {
|
|
5796
|
+
type: "beeswarm",
|
|
5797
|
+
stat: "beeswarm",
|
|
5798
|
+
position: "identity",
|
|
5799
|
+
params: {
|
|
5800
|
+
method: options.method ?? "swarm",
|
|
5801
|
+
size: options.size ?? 1,
|
|
5802
|
+
cex: options.cex ?? 1,
|
|
5803
|
+
alpha: options.alpha ?? 1,
|
|
5804
|
+
color: options.color,
|
|
5805
|
+
shape: options.shape ?? "circle",
|
|
5806
|
+
side: options.side ?? 0,
|
|
5807
|
+
priority: options.priority ?? "ascending",
|
|
5808
|
+
dodge: options.dodge ?? 0.8
|
|
5809
|
+
}
|
|
5810
|
+
};
|
|
5811
|
+
}
|
|
5812
|
+
function geom_quasirandom(options = {}) {
|
|
5813
|
+
return geom_beeswarm({ ...options, method: "center" });
|
|
5814
|
+
}
|
|
5815
|
+
|
|
5304
5816
|
// src/geoms/index.ts
|
|
5305
|
-
var init_geoms = () => {
|
|
5817
|
+
var init_geoms = __esm(() => {
|
|
5818
|
+
init_ridgeline();
|
|
5819
|
+
});
|
|
5306
5820
|
|
|
5307
5821
|
// src/scales/continuous.ts
|
|
5308
5822
|
function createContinuousScale(aesthetic, options = {}) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* geom_beeswarm - Beeswarm plot geometry
|
|
3
|
+
*
|
|
4
|
+
* Creates jittered point plots where points are arranged to avoid overlap,
|
|
5
|
+
* showing both individual data points and distribution shape.
|
|
6
|
+
*/
|
|
7
|
+
import type { Geom } from '../types';
|
|
8
|
+
export interface BeeswarmOptions {
|
|
9
|
+
/** Method for arranging points: 'swarm' | 'center' | 'square' (default: 'swarm') */
|
|
10
|
+
method?: 'swarm' | 'center' | 'square';
|
|
11
|
+
/** Point size multiplier (default: 1) */
|
|
12
|
+
size?: number;
|
|
13
|
+
/** Point size for collision detection (default: 1) */
|
|
14
|
+
cex?: number;
|
|
15
|
+
/** Alpha transparency (default: 1) */
|
|
16
|
+
alpha?: number;
|
|
17
|
+
/** Point color */
|
|
18
|
+
color?: string;
|
|
19
|
+
/** Point shape: 'circle' | 'square' | 'triangle' | 'diamond' | 'cross' */
|
|
20
|
+
shape?: string;
|
|
21
|
+
/** Side to place points: 0 (both), -1 (left), 1 (right) (default: 0) */
|
|
22
|
+
side?: -1 | 0 | 1;
|
|
23
|
+
/** Priority for placing points: 'ascending' | 'descending' | 'density' | 'random' */
|
|
24
|
+
priority?: 'ascending' | 'descending' | 'density' | 'random';
|
|
25
|
+
/** Dodge width for categorical spacing (default: 0.8) */
|
|
26
|
+
dodge?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create beeswarm point geometry
|
|
30
|
+
*
|
|
31
|
+
* Beeswarm plots arrange points to avoid overlap while showing their
|
|
32
|
+
* exact values, combining the benefits of jitter and violin plots.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { gg, geom_beeswarm } from '@ggterm/core'
|
|
37
|
+
*
|
|
38
|
+
* // Basic beeswarm plot
|
|
39
|
+
* gg(data)
|
|
40
|
+
* .aes({ x: 'group', y: 'value' })
|
|
41
|
+
* .geom(geom_beeswarm())
|
|
42
|
+
*
|
|
43
|
+
* // Customized with color and method
|
|
44
|
+
* gg(data)
|
|
45
|
+
* .aes({ x: 'treatment', y: 'response', color: 'treatment' })
|
|
46
|
+
* .geom(geom_beeswarm({ method: 'swarm', size: 2, alpha: 0.8 }))
|
|
47
|
+
*
|
|
48
|
+
* // One-sided swarm
|
|
49
|
+
* gg(data)
|
|
50
|
+
* .aes({ x: 'category', y: 'measurement' })
|
|
51
|
+
* .geom(geom_beeswarm({ side: 1 })) // Points only to the right
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function geom_beeswarm(options?: BeeswarmOptions): Geom;
|
|
55
|
+
/**
|
|
56
|
+
* Alias for beeswarm with quasirandom-like arrangement
|
|
57
|
+
* Uses 'center' method for a more uniform look
|
|
58
|
+
*/
|
|
59
|
+
export declare function geom_quasirandom(options?: Omit<BeeswarmOptions, 'method'>): Geom;
|
|
60
|
+
//# sourceMappingURL=beeswarm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"beeswarm.d.ts","sourceRoot":"","sources":["../../src/geoms/beeswarm.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAEpC,MAAM,WAAW,eAAe;IAC9B,oFAAoF;IACpF,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAA;IACtC,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,IAAI,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACjB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,WAAW,GAAG,YAAY,GAAG,SAAS,GAAG,QAAQ,CAAA;IAC5D,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,eAAoB,GAAG,IAAI,CAiBjE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAM,GAAG,IAAI,CAEpF"}
|
package/dist/geoms/index.d.ts
CHANGED
|
@@ -20,4 +20,6 @@ export { geom_contour, geom_contour_filled, geom_density_2d, type ContourOptions
|
|
|
20
20
|
export { geom_errorbar, geom_errorbarh, geom_crossbar, geom_linerange, geom_pointrange, type ErrorbarOptions, } from './errorbar';
|
|
21
21
|
export { geom_rect, geom_abline, type RectOptions, type AblineOptions } from './rect';
|
|
22
22
|
export { geom_qq, geom_qq_line, type QQOptions, type QQLineOptions } from './qq';
|
|
23
|
+
export { geom_ridgeline, geom_joy, type RidgelineOptions } from './ridgeline';
|
|
24
|
+
export { geom_beeswarm, geom_quasirandom, type BeeswarmOptions } from './beeswarm';
|
|
23
25
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/geoms/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AAC5E,OAAO,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AACpD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,OAAO,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AAChE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AACjE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA;AACxG,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AAC7D,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AACzE,OAAO,EAAE,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAA;AAC1D,OAAO,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AACpD,OAAO,EAAE,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,OAAO,CAAA;AAGjD,OAAO,EAAE,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAA;AAC1D,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AACjE,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AACnG,OAAO,EACL,aAAa,EACb,cAAc,EACd,aAAa,EACb,cAAc,EACd,eAAe,EACf,KAAK,eAAe,GACrB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAA;AACrF,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,MAAM,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/geoms/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AAC5E,OAAO,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AACpD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,OAAO,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AAChE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AACjE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA;AACxG,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AAC7D,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AACzE,OAAO,EAAE,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAA;AAC1D,OAAO,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AACpD,OAAO,EAAE,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,OAAO,CAAA;AAGjD,OAAO,EAAE,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAA;AAC1D,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AACjE,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AACnG,OAAO,EACL,aAAa,EACb,cAAc,EACd,aAAa,EACb,cAAc,EACd,eAAe,EACf,KAAK,eAAe,GACrB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAA;AACrF,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,MAAM,CAAA;AAChF,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAA"}
|