@ggterm/core 0.2.10 → 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 CHANGED
@@ -2309,6 +2309,58 @@ function parseColor(color) {
2309
2309
  }
2310
2310
  return { r: 128, g: 128, b: 128, a: 1 };
2311
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
+ }
2312
2364
  function renderGeom(data, geom, aes, scales, canvas, coordType) {
2313
2365
  switch (geom.type) {
2314
2366
  case "point":
@@ -2390,6 +2442,10 @@ function renderGeom(data, geom, aes, scales, canvas, coordType) {
2390
2442
  case "smooth":
2391
2443
  renderGeomSmooth(data, geom, aes, scales, canvas);
2392
2444
  break;
2445
+ case "beeswarm":
2446
+ case "quasirandom":
2447
+ renderGeomBeeswarm(data, geom, aes, scales, canvas);
2448
+ break;
2393
2449
  default:
2394
2450
  break;
2395
2451
  }
@@ -3517,6 +3573,224 @@ function stat_xdensity(params = {}) {
3517
3573
  };
3518
3574
  }
3519
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
+ }
3793
+
3520
3794
  // src/stats/smooth.ts
3521
3795
  function linearRegression(xs, ys) {
3522
3796
  const n = xs.length;
@@ -4396,6 +4670,15 @@ function applyStatTransform(data, geom, aes) {
4396
4670
  adjust: geom.params.adjust
4397
4671
  });
4398
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);
4399
4682
  } else if (geom.stat === "smooth") {
4400
4683
  const smoothStat = stat_smooth({
4401
4684
  method: geom.params.method,
@@ -4522,6 +4805,10 @@ function renderToCanvas(spec, options) {
4522
4805
  scaleData = applyStatTransform(spec.data, geom, spec.aes);
4523
4806
  scaleAes = { ...spec.aes, x: "x", y: "y" };
4524
4807
  break;
4808
+ } else if (geom.stat === "beeswarm") {
4809
+ scaleData = spec.data;
4810
+ scaleAes = spec.aes;
4811
+ break;
4525
4812
  }
4526
4813
  }
4527
4814
  scaleData = applyCoordTransform(scaleData, scaleAes, spec.coord);
@@ -4562,6 +4849,8 @@ function renderToCanvas(spec, options) {
4562
4849
  geomAes = { ...spec.aes, x: "x", y: "y" };
4563
4850
  } else if (geom.stat === "xdensity") {
4564
4851
  geomAes = { ...spec.aes, x: "x", y: "y" };
4852
+ } else if (geom.stat === "beeswarm") {
4853
+ geomAes = { ...spec.aes, x: "x", y: "y" };
4565
4854
  }
4566
4855
  geomData = applyCoordTransform(geomData, geomAes, spec.coord);
4567
4856
  }
@@ -5506,6 +5795,29 @@ var init_ridgeline = __esm(() => {
5506
5795
  geom_joy = geom_ridgeline;
5507
5796
  });
5508
5797
 
5798
+ // src/geoms/beeswarm.ts
5799
+ function geom_beeswarm(options = {}) {
5800
+ return {
5801
+ type: "beeswarm",
5802
+ stat: "beeswarm",
5803
+ position: "identity",
5804
+ params: {
5805
+ method: options.method ?? "swarm",
5806
+ size: options.size ?? 1,
5807
+ cex: options.cex ?? 1,
5808
+ alpha: options.alpha ?? 1,
5809
+ color: options.color,
5810
+ shape: options.shape ?? "circle",
5811
+ side: options.side ?? 0,
5812
+ priority: options.priority ?? "ascending",
5813
+ dodge: options.dodge ?? 0.8
5814
+ }
5815
+ };
5816
+ }
5817
+ function geom_quasirandom(options = {}) {
5818
+ return geom_beeswarm({ ...options, method: "center" });
5819
+ }
5820
+
5509
5821
  // src/geoms/index.ts
5510
5822
  var init_geoms = __esm(() => {
5511
5823
  init_ridgeline();
@@ -9914,6 +10226,7 @@ __export(exports_src, {
9914
10226
  stat_boxplot: () => stat_boxplot,
9915
10227
  stat_bin2d: () => stat_bin2d,
9916
10228
  stat_bin: () => stat_bin,
10229
+ stat_beeswarm: () => stat_beeswarm,
9917
10230
  startREPL: () => startREPL,
9918
10231
  selectRenderer: () => selectRenderer,
9919
10232
  selectColorMode: () => selectColorMode,
@@ -10022,6 +10335,7 @@ __export(exports_src, {
10022
10335
  geom_ribbon: () => geom_ribbon,
10023
10336
  geom_rect: () => geom_rect,
10024
10337
  geom_raster: () => geom_raster,
10338
+ geom_quasirandom: () => geom_quasirandom,
10025
10339
  geom_qq_line: () => geom_qq_line,
10026
10340
  geom_qq: () => geom_qq,
10027
10341
  geom_pointrange: () => geom_pointrange,
@@ -10044,6 +10358,7 @@ __export(exports_src, {
10044
10358
  geom_col: () => geom_col,
10045
10359
  geom_boxplot: () => geom_boxplot,
10046
10360
  geom_bin2d: () => geom_bin2d,
10361
+ geom_beeswarm: () => geom_beeswarm,
10047
10362
  geom_bar: () => geom_bar,
10048
10363
  geom_area: () => geom_area,
10049
10364
  geom_abline: () => geom_abline,
@@ -10090,6 +10405,7 @@ __export(exports_src, {
10090
10405
  computeBoxplotStats: () => computeBoxplotStats,
10091
10406
  computeBins2d: () => computeBins2d,
10092
10407
  computeBins: () => computeBins,
10408
+ computeBeeswarm: () => computeBeeswarm,
10093
10409
  colorDistance: () => colorDistance,
10094
10410
  clearCapabilityCache: () => clearCapabilityCache,
10095
10411
  calculatePanelLayouts: () => calculatePanelLayouts,
@@ -10941,6 +11257,8 @@ var GEOM_TYPES = [
10941
11257
  "violin",
10942
11258
  "ridgeline",
10943
11259
  "joy",
11260
+ "beeswarm",
11261
+ "quasirandom",
10944
11262
  "area",
10945
11263
  "ribbon",
10946
11264
  "rug",
@@ -11335,7 +11653,7 @@ Error: Unknown geometry type "${geomType}"`);
11335
11653
  Available geom types:`);
11336
11654
  console.error(` Points/Lines: point, line, path, step, smooth, segment`);
11337
11655
  console.error(` Bars/Areas: bar, col, histogram, freqpoly, area, ribbon`);
11338
- console.error(` Distributions: boxplot, violin, ridgeline, joy, qq, density_2d`);
11656
+ console.error(` Distributions: boxplot, violin, ridgeline, joy, beeswarm, quasirandom, qq, density_2d`);
11339
11657
  console.error(` Uncertainty: errorbar, errorbarh, crossbar, linerange, pointrange`);
11340
11658
  console.error(` 2D: tile, rect, raster, bin2d, contour, contour_filled`);
11341
11659
  console.error(` Text: text, label`);
@@ -11484,6 +11802,10 @@ If you want a univariate plot, try: histogram, bar, qq, or freqpoly`);
11484
11802
  case "joy":
11485
11803
  plot = plot.geom(geom_ridgeline());
11486
11804
  break;
11805
+ case "beeswarm":
11806
+ case "quasirandom":
11807
+ plot = plot.geom(geom_beeswarm());
11808
+ break;
11487
11809
  case "bar":
11488
11810
  plot = plot.geom(geom_bar());
11489
11811
  break;