@ggterm/core 0.2.16 → 0.2.20
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 +2526 -26
- package/dist/cli.js +2378 -25
- package/dist/geoms/biplot.d.ts +35 -0
- package/dist/geoms/biplot.d.ts.map +1 -0
- package/dist/geoms/bland-altman.d.ts +50 -0
- package/dist/geoms/bland-altman.d.ts.map +1 -0
- package/dist/geoms/control.d.ts +118 -0
- package/dist/geoms/control.d.ts.map +1 -0
- package/dist/geoms/dendrogram.d.ts +74 -0
- package/dist/geoms/dendrogram.d.ts.map +1 -0
- package/dist/geoms/ecdf.d.ts +66 -0
- package/dist/geoms/ecdf.d.ts.map +1 -0
- package/dist/geoms/forest.d.ts +45 -0
- package/dist/geoms/forest.d.ts.map +1 -0
- package/dist/geoms/funnel.d.ts +78 -0
- package/dist/geoms/funnel.d.ts.map +1 -0
- package/dist/geoms/heatmap.d.ts +34 -0
- package/dist/geoms/heatmap.d.ts.map +1 -0
- package/dist/geoms/index.d.ts +16 -1
- package/dist/geoms/index.d.ts.map +1 -1
- package/dist/geoms/kaplan-meier.d.ts +39 -0
- package/dist/geoms/kaplan-meier.d.ts.map +1 -0
- package/dist/geoms/ma.d.ts +77 -0
- package/dist/geoms/ma.d.ts.map +1 -0
- package/dist/geoms/manhattan.d.ts +29 -0
- package/dist/geoms/manhattan.d.ts.map +1 -0
- package/dist/geoms/qq.d.ts +51 -59
- package/dist/geoms/qq.d.ts.map +1 -1
- package/dist/geoms/roc.d.ts +44 -0
- package/dist/geoms/roc.d.ts.map +1 -0
- package/dist/geoms/scree.d.ts +97 -0
- package/dist/geoms/scree.d.ts.map +1 -0
- package/dist/geoms/upset.d.ts +63 -0
- package/dist/geoms/upset.d.ts.map +1 -0
- package/dist/geoms/volcano.d.ts +71 -0
- package/dist/geoms/volcano.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2408 -27
- package/dist/pipeline/render-geoms.d.ts +8 -0
- package/dist/pipeline/render-geoms.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3636,6 +3636,1550 @@ function renderGeomTreemap(data, geom, aes, scales, canvas) {
|
|
|
3636
3636
|
renderNode(validRoots[i], i, 0);
|
|
3637
3637
|
}
|
|
3638
3638
|
}
|
|
3639
|
+
function renderGeomVolcano(data, geom, aes, scales, canvas) {
|
|
3640
|
+
const fcThreshold = geom.params.fc_threshold ?? 1;
|
|
3641
|
+
const pThreshold = geom.params.p_threshold ?? 0.05;
|
|
3642
|
+
const yIsNegLog10 = geom.params.y_is_neglog10 ?? false;
|
|
3643
|
+
const upColor = parseColorToRgba(geom.params.up_color ?? "#e41a1c");
|
|
3644
|
+
const downColor = parseColorToRgba(geom.params.down_color ?? "#377eb8");
|
|
3645
|
+
const nsColor = parseColorToRgba(geom.params.ns_color ?? "#999999");
|
|
3646
|
+
const showThresholds = geom.params.show_thresholds ?? true;
|
|
3647
|
+
const nLabels = geom.params.n_labels ?? 0;
|
|
3648
|
+
const pointChar = geom.params.point_char ?? "●";
|
|
3649
|
+
const negLog10PThreshold = -Math.log10(pThreshold);
|
|
3650
|
+
const points = [];
|
|
3651
|
+
for (const row of data) {
|
|
3652
|
+
const xVal = Number(row[aes.x]);
|
|
3653
|
+
let yVal = Number(row[aes.y]);
|
|
3654
|
+
if (isNaN(xVal) || isNaN(yVal) || yVal <= 0)
|
|
3655
|
+
continue;
|
|
3656
|
+
if (!yIsNegLog10) {
|
|
3657
|
+
yVal = -Math.log10(yVal);
|
|
3658
|
+
}
|
|
3659
|
+
let status = "ns";
|
|
3660
|
+
if (yVal >= negLog10PThreshold) {
|
|
3661
|
+
if (xVal >= fcThreshold) {
|
|
3662
|
+
status = "up";
|
|
3663
|
+
} else if (xVal <= -fcThreshold) {
|
|
3664
|
+
status = "down";
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
const label = aes.label ? String(row[aes.label] ?? "") : undefined;
|
|
3668
|
+
points.push({
|
|
3669
|
+
row,
|
|
3670
|
+
x: xVal,
|
|
3671
|
+
y: yVal,
|
|
3672
|
+
significance: yVal,
|
|
3673
|
+
status,
|
|
3674
|
+
label
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3677
|
+
if (showThresholds) {
|
|
3678
|
+
const thresholdColor = { r: 150, g: 150, b: 150, a: 0.7 };
|
|
3679
|
+
const cy = Math.round(scales.y.map(negLog10PThreshold));
|
|
3680
|
+
const startX = Math.round(scales.x.range[0]);
|
|
3681
|
+
const endX = Math.round(scales.x.range[1]);
|
|
3682
|
+
for (let x = startX;x <= endX; x += 2) {
|
|
3683
|
+
canvas.drawChar(x, cy, "─", thresholdColor);
|
|
3684
|
+
}
|
|
3685
|
+
const cxPos = Math.round(scales.x.map(fcThreshold));
|
|
3686
|
+
const cxNeg = Math.round(scales.x.map(-fcThreshold));
|
|
3687
|
+
const startY = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
3688
|
+
const endY = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
3689
|
+
for (let y = startY;y <= endY; y += 2) {
|
|
3690
|
+
canvas.drawChar(cxPos, y, "│", thresholdColor);
|
|
3691
|
+
canvas.drawChar(cxNeg, y, "│", thresholdColor);
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
for (const point of points) {
|
|
3695
|
+
if (point.status === "ns") {
|
|
3696
|
+
const cx = Math.round(scales.x.map(point.x));
|
|
3697
|
+
const cy = Math.round(scales.y.map(point.y));
|
|
3698
|
+
canvas.drawPoint(cx, cy, nsColor, pointChar);
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
for (const point of points) {
|
|
3702
|
+
if (point.status !== "ns") {
|
|
3703
|
+
const cx = Math.round(scales.x.map(point.x));
|
|
3704
|
+
const cy = Math.round(scales.y.map(point.y));
|
|
3705
|
+
const color = point.status === "up" ? upColor : downColor;
|
|
3706
|
+
canvas.drawPoint(cx, cy, color, pointChar);
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
if (nLabels > 0 && aes.label) {
|
|
3710
|
+
const significantPoints = points.filter((p) => p.status !== "ns" && p.label).sort((a, b) => b.significance - a.significance).slice(0, nLabels);
|
|
3711
|
+
const labelColor = { r: 50, g: 50, b: 50, a: 1 };
|
|
3712
|
+
for (const point of significantPoints) {
|
|
3713
|
+
const cx = Math.round(scales.x.map(point.x));
|
|
3714
|
+
const cy = Math.round(scales.y.map(point.y));
|
|
3715
|
+
const label = point.label;
|
|
3716
|
+
const labelX = cx + 1;
|
|
3717
|
+
const labelY = cy;
|
|
3718
|
+
for (let i = 0;i < label.length; i++) {
|
|
3719
|
+
canvas.drawChar(labelX + i, labelY, label[i], labelColor);
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
function renderGeomMA(data, geom, aes, scales, canvas) {
|
|
3725
|
+
const fcThreshold = geom.params.fc_threshold ?? 1;
|
|
3726
|
+
const pThreshold = geom.params.p_threshold ?? 0.05;
|
|
3727
|
+
const pCol = geom.params.p_col;
|
|
3728
|
+
const xIsLog2 = geom.params.x_is_log2 ?? false;
|
|
3729
|
+
const upColor = parseColorToRgba(geom.params.up_color ?? "#e41a1c");
|
|
3730
|
+
const downColor = parseColorToRgba(geom.params.down_color ?? "#377eb8");
|
|
3731
|
+
const nsColor = parseColorToRgba(geom.params.ns_color ?? "#999999");
|
|
3732
|
+
const showBaseline = geom.params.show_baseline ?? true;
|
|
3733
|
+
const showThresholds = geom.params.show_thresholds ?? true;
|
|
3734
|
+
const nLabels = geom.params.n_labels ?? 0;
|
|
3735
|
+
const pointChar = geom.params.point_char ?? "●";
|
|
3736
|
+
const points = [];
|
|
3737
|
+
for (const row of data) {
|
|
3738
|
+
let xVal = Number(row[aes.x]);
|
|
3739
|
+
const yVal = Number(row[aes.y]);
|
|
3740
|
+
if (isNaN(xVal) || isNaN(yVal))
|
|
3741
|
+
continue;
|
|
3742
|
+
if (xVal <= 0)
|
|
3743
|
+
continue;
|
|
3744
|
+
if (!xIsLog2) {
|
|
3745
|
+
xVal = Math.log2(xVal);
|
|
3746
|
+
}
|
|
3747
|
+
let status = "ns";
|
|
3748
|
+
const passesFcThreshold = Math.abs(yVal) >= fcThreshold;
|
|
3749
|
+
let passesPThreshold = true;
|
|
3750
|
+
if (pCol && row[pCol] !== undefined) {
|
|
3751
|
+
const pVal = Number(row[pCol]);
|
|
3752
|
+
passesPThreshold = !isNaN(pVal) && pVal < pThreshold;
|
|
3753
|
+
}
|
|
3754
|
+
if (passesFcThreshold && passesPThreshold) {
|
|
3755
|
+
status = yVal > 0 ? "up" : "down";
|
|
3756
|
+
}
|
|
3757
|
+
const label = aes.label ? String(row[aes.label] ?? "") : undefined;
|
|
3758
|
+
points.push({
|
|
3759
|
+
row,
|
|
3760
|
+
x: xVal,
|
|
3761
|
+
y: yVal,
|
|
3762
|
+
absM: Math.abs(yVal),
|
|
3763
|
+
status,
|
|
3764
|
+
label
|
|
3765
|
+
});
|
|
3766
|
+
}
|
|
3767
|
+
const lineColor = { r: 150, g: 150, b: 150, a: 0.7 };
|
|
3768
|
+
const startX = Math.round(scales.x.range[0]);
|
|
3769
|
+
const endX = Math.round(scales.x.range[1]);
|
|
3770
|
+
if (showBaseline) {
|
|
3771
|
+
const cy = Math.round(scales.y.map(0));
|
|
3772
|
+
for (let x = startX;x <= endX; x++) {
|
|
3773
|
+
canvas.drawChar(x, cy, "─", lineColor);
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
if (showThresholds) {
|
|
3777
|
+
const cyUp = Math.round(scales.y.map(fcThreshold));
|
|
3778
|
+
const cyDown = Math.round(scales.y.map(-fcThreshold));
|
|
3779
|
+
for (let x = startX;x <= endX; x += 2) {
|
|
3780
|
+
canvas.drawChar(x, cyUp, "─", lineColor);
|
|
3781
|
+
canvas.drawChar(x, cyDown, "─", lineColor);
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
for (const point of points) {
|
|
3785
|
+
if (point.status === "ns") {
|
|
3786
|
+
const cx = Math.round(scales.x.map(point.x));
|
|
3787
|
+
const cy = Math.round(scales.y.map(point.y));
|
|
3788
|
+
canvas.drawPoint(cx, cy, nsColor, pointChar);
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
for (const point of points) {
|
|
3792
|
+
if (point.status !== "ns") {
|
|
3793
|
+
const cx = Math.round(scales.x.map(point.x));
|
|
3794
|
+
const cy = Math.round(scales.y.map(point.y));
|
|
3795
|
+
const color = point.status === "up" ? upColor : downColor;
|
|
3796
|
+
canvas.drawPoint(cx, cy, color, pointChar);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
if (nLabels > 0 && aes.label) {
|
|
3800
|
+
const significantPoints = points.filter((p) => p.status !== "ns" && p.label).sort((a, b) => b.absM - a.absM).slice(0, nLabels);
|
|
3801
|
+
const labelColor = { r: 50, g: 50, b: 50, a: 1 };
|
|
3802
|
+
for (const point of significantPoints) {
|
|
3803
|
+
const cx = Math.round(scales.x.map(point.x));
|
|
3804
|
+
const cy = Math.round(scales.y.map(point.y));
|
|
3805
|
+
const label = point.label;
|
|
3806
|
+
const labelX = cx + 1;
|
|
3807
|
+
const labelY = cy;
|
|
3808
|
+
for (let i = 0;i < label.length; i++) {
|
|
3809
|
+
canvas.drawChar(labelX + i, labelY, label[i], labelColor);
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
function renderGeomManhattan(data, geom, aes, scales, canvas) {
|
|
3815
|
+
const params = geom.params || {};
|
|
3816
|
+
const suggestiveThreshold = Number(params.suggestive_threshold ?? 0.00001);
|
|
3817
|
+
const genomeWideThreshold = Number(params.genome_wide_threshold ?? 0.00000005);
|
|
3818
|
+
const yIsNegLog10 = Boolean(params.y_is_neglog10 ?? false);
|
|
3819
|
+
const chrColors = params.chr_colors ?? ["#1f78b4", "#a6cee3"];
|
|
3820
|
+
const highlightColor = String(params.highlight_color ?? "#e41a1c");
|
|
3821
|
+
const suggestiveColor = String(params.suggestive_color ?? "#ff7f00");
|
|
3822
|
+
const showThresholds = Boolean(params.show_thresholds ?? true);
|
|
3823
|
+
const nLabels = Number(params.n_labels ?? 0);
|
|
3824
|
+
const pointChar = String(params.point_char ?? "●");
|
|
3825
|
+
const chrGap = Number(params.chr_gap ?? 0.02);
|
|
3826
|
+
if (!Array.isArray(data) || data.length === 0)
|
|
3827
|
+
return;
|
|
3828
|
+
const parseHex = (hex) => {
|
|
3829
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
3830
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
3831
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
3832
|
+
return { r, g, b, a: 1 };
|
|
3833
|
+
};
|
|
3834
|
+
const chrColorsParsed = chrColors.map((c) => parseHex(c));
|
|
3835
|
+
const highlightColorParsed = parseHex(highlightColor);
|
|
3836
|
+
const suggestiveColorParsed = parseHex(suggestiveColor);
|
|
3837
|
+
const suggestiveLine = -Math.log10(suggestiveThreshold);
|
|
3838
|
+
const genomeWideLine = -Math.log10(genomeWideThreshold);
|
|
3839
|
+
const xField = aes.x;
|
|
3840
|
+
const yField = aes.y;
|
|
3841
|
+
const labelField = aes.label;
|
|
3842
|
+
const points = [];
|
|
3843
|
+
const chrMap = new Map;
|
|
3844
|
+
for (const row of data) {
|
|
3845
|
+
let chr;
|
|
3846
|
+
let pos;
|
|
3847
|
+
const rawX = row[xField];
|
|
3848
|
+
const rawY = row[yField];
|
|
3849
|
+
if (typeof rawX === "string" && rawX.includes(":")) {
|
|
3850
|
+
const parts = rawX.split(":");
|
|
3851
|
+
chr = parts[0];
|
|
3852
|
+
pos = parseFloat(parts[1]);
|
|
3853
|
+
} else {
|
|
3854
|
+
chr = aes.color ? String(row[aes.color] ?? "1") : "1";
|
|
3855
|
+
pos = typeof rawX === "number" ? rawX : parseFloat(String(rawX));
|
|
3856
|
+
}
|
|
3857
|
+
const pval = typeof rawY === "number" ? rawY : parseFloat(String(rawY));
|
|
3858
|
+
if (isNaN(pos) || isNaN(pval) || pval <= 0)
|
|
3859
|
+
continue;
|
|
3860
|
+
const negLogP = yIsNegLog10 ? pval : -Math.log10(pval);
|
|
3861
|
+
const label = labelField ? String(row[labelField] ?? "") : undefined;
|
|
3862
|
+
const point = {
|
|
3863
|
+
chr,
|
|
3864
|
+
pos,
|
|
3865
|
+
pval: yIsNegLog10 ? Math.pow(10, -pval) : pval,
|
|
3866
|
+
negLogP,
|
|
3867
|
+
cumPos: 0,
|
|
3868
|
+
label,
|
|
3869
|
+
chrIndex: 0
|
|
3870
|
+
};
|
|
3871
|
+
if (!chrMap.has(chr)) {
|
|
3872
|
+
chrMap.set(chr, []);
|
|
3873
|
+
}
|
|
3874
|
+
chrMap.get(chr).push(point);
|
|
3875
|
+
points.push(point);
|
|
3876
|
+
}
|
|
3877
|
+
if (points.length === 0)
|
|
3878
|
+
return;
|
|
3879
|
+
const chrOrder = Array.from(chrMap.keys()).sort((a, b) => {
|
|
3880
|
+
const aNum = parseInt(String(a).replace(/^chr/i, ""));
|
|
3881
|
+
const bNum = parseInt(String(b).replace(/^chr/i, ""));
|
|
3882
|
+
if (!isNaN(aNum) && !isNaN(bNum))
|
|
3883
|
+
return aNum - bNum;
|
|
3884
|
+
if (!isNaN(aNum))
|
|
3885
|
+
return -1;
|
|
3886
|
+
if (!isNaN(bNum))
|
|
3887
|
+
return 1;
|
|
3888
|
+
return String(a).localeCompare(String(b));
|
|
3889
|
+
});
|
|
3890
|
+
let cumOffset = 0;
|
|
3891
|
+
const chrOffsets = new Map;
|
|
3892
|
+
for (let i = 0;i < chrOrder.length; i++) {
|
|
3893
|
+
const chr = chrOrder[i];
|
|
3894
|
+
chrOffsets.set(chr, cumOffset);
|
|
3895
|
+
const chrPoints = chrMap.get(chr);
|
|
3896
|
+
const maxPos = Math.max(...chrPoints.map((p) => p.pos));
|
|
3897
|
+
for (const point of chrPoints) {
|
|
3898
|
+
point.cumPos = cumOffset + point.pos;
|
|
3899
|
+
point.chrIndex = i;
|
|
3900
|
+
}
|
|
3901
|
+
cumOffset += maxPos * (1 + chrGap);
|
|
3902
|
+
}
|
|
3903
|
+
const minX = Math.min(...points.map((p) => p.cumPos));
|
|
3904
|
+
const maxX = Math.max(...points.map((p) => p.cumPos));
|
|
3905
|
+
const minY = 0;
|
|
3906
|
+
const maxY = Math.max(...points.map((p) => p.negLogP)) * 1.1;
|
|
3907
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
3908
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
3909
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
3910
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
3911
|
+
const mapX = (v) => plotLeft + (v - minX) / (maxX - minX) * (plotRight - plotLeft);
|
|
3912
|
+
const mapY = (v) => plotBottom - (v - minY) / (maxY - minY) * (plotBottom - plotTop);
|
|
3913
|
+
if (showThresholds) {
|
|
3914
|
+
const lineColor = { r: 150, g: 150, b: 150, a: 1 };
|
|
3915
|
+
if (suggestiveLine <= maxY) {
|
|
3916
|
+
const sy = Math.round(mapY(suggestiveLine));
|
|
3917
|
+
for (let x = plotLeft;x <= plotRight; x += 2) {
|
|
3918
|
+
canvas.drawChar(x, sy, "─", lineColor);
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
if (genomeWideLine <= maxY) {
|
|
3922
|
+
const gy = Math.round(mapY(genomeWideLine));
|
|
3923
|
+
for (let x = plotLeft;x <= plotRight; x += 2) {
|
|
3924
|
+
canvas.drawChar(x, gy, "─", highlightColorParsed);
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
for (const point of points) {
|
|
3929
|
+
if (point.pval >= suggestiveThreshold) {
|
|
3930
|
+
const cx = Math.round(mapX(point.cumPos));
|
|
3931
|
+
const cy = Math.round(mapY(point.negLogP));
|
|
3932
|
+
const color = chrColorsParsed[point.chrIndex % chrColorsParsed.length];
|
|
3933
|
+
canvas.drawPoint(cx, cy, color, pointChar);
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
for (const point of points) {
|
|
3937
|
+
if (point.pval < suggestiveThreshold && point.pval >= genomeWideThreshold) {
|
|
3938
|
+
const cx = Math.round(mapX(point.cumPos));
|
|
3939
|
+
const cy = Math.round(mapY(point.negLogP));
|
|
3940
|
+
canvas.drawPoint(cx, cy, suggestiveColorParsed, pointChar);
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
for (const point of points) {
|
|
3944
|
+
if (point.pval < genomeWideThreshold) {
|
|
3945
|
+
const cx = Math.round(mapX(point.cumPos));
|
|
3946
|
+
const cy = Math.round(mapY(point.negLogP));
|
|
3947
|
+
canvas.drawPoint(cx, cy, highlightColorParsed, pointChar);
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
if (nLabels > 0) {
|
|
3951
|
+
const labelColor = { r: 50, g: 50, b: 50, a: 1 };
|
|
3952
|
+
const topPoints = points.filter((p) => p.label && p.pval < suggestiveThreshold).sort((a, b) => a.pval - b.pval).slice(0, nLabels);
|
|
3953
|
+
for (const point of topPoints) {
|
|
3954
|
+
const cx = Math.round(mapX(point.cumPos));
|
|
3955
|
+
const cy = Math.round(mapY(point.negLogP));
|
|
3956
|
+
const label = point.label;
|
|
3957
|
+
for (let i = 0;i < label.length; i++) {
|
|
3958
|
+
canvas.drawChar(cx + 1 + i, cy, label[i], labelColor);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
function renderGeomHeatmap(data, geom, aes, scales, canvas) {
|
|
3964
|
+
const params = geom.params || {};
|
|
3965
|
+
const valueCol = String(params.value_col ?? "value");
|
|
3966
|
+
const lowColor = String(params.low_color ?? "#313695");
|
|
3967
|
+
const midColor = String(params.mid_color ?? "#ffffbf");
|
|
3968
|
+
const highColor = String(params.high_color ?? "#a50026");
|
|
3969
|
+
const naColor = String(params.na_color ?? "#808080");
|
|
3970
|
+
const clusterRows = Boolean(params.cluster_rows ?? false);
|
|
3971
|
+
const clusterCols = Boolean(params.cluster_cols ?? false);
|
|
3972
|
+
const showRowLabels = Boolean(params.show_row_labels ?? true);
|
|
3973
|
+
const showColLabels = Boolean(params.show_col_labels ?? true);
|
|
3974
|
+
const cellChar = String(params.cell_char ?? "█");
|
|
3975
|
+
const scaleMethod = String(params.scale ?? "none");
|
|
3976
|
+
if (!Array.isArray(data) || data.length === 0)
|
|
3977
|
+
return;
|
|
3978
|
+
const xField = typeof aes.x === "string" ? aes.x : "x";
|
|
3979
|
+
const yField = typeof aes.y === "string" ? aes.y : "y";
|
|
3980
|
+
const fillField = typeof aes.fill === "string" ? aes.fill : valueCol;
|
|
3981
|
+
const parseHex = (hex) => {
|
|
3982
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
3983
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
3984
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
3985
|
+
return { r, g, b, a: 1 };
|
|
3986
|
+
};
|
|
3987
|
+
const lowRgb = parseHex(lowColor);
|
|
3988
|
+
const midRgb = parseHex(midColor);
|
|
3989
|
+
const highRgb = parseHex(highColor);
|
|
3990
|
+
const naRgb = parseHex(naColor);
|
|
3991
|
+
const rowSet = new Set;
|
|
3992
|
+
const colSet = new Set;
|
|
3993
|
+
const valueMap = new Map;
|
|
3994
|
+
for (const row of data) {
|
|
3995
|
+
const rowKey = String(row[yField] ?? "");
|
|
3996
|
+
const colKey = String(row[xField] ?? "");
|
|
3997
|
+
const val = row[fillField];
|
|
3998
|
+
if (rowKey && colKey) {
|
|
3999
|
+
rowSet.add(rowKey);
|
|
4000
|
+
colSet.add(colKey);
|
|
4001
|
+
if (typeof val === "number" && !isNaN(val)) {
|
|
4002
|
+
valueMap.set(`${rowKey}|${colKey}`, val);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
let rowKeys = Array.from(rowSet);
|
|
4007
|
+
let colKeys = Array.from(colSet);
|
|
4008
|
+
if (rowKeys.length === 0 || colKeys.length === 0)
|
|
4009
|
+
return;
|
|
4010
|
+
const clusterOrder = (keys, getDistance) => {
|
|
4011
|
+
if (keys.length <= 2)
|
|
4012
|
+
return keys;
|
|
4013
|
+
const remaining = [...keys];
|
|
4014
|
+
const result = [];
|
|
4015
|
+
let minAvg = Infinity;
|
|
4016
|
+
let startIdx = 0;
|
|
4017
|
+
for (let i = 0;i < remaining.length; i++) {
|
|
4018
|
+
let sum = 0;
|
|
4019
|
+
for (let j = 0;j < remaining.length; j++) {
|
|
4020
|
+
if (i !== j)
|
|
4021
|
+
sum += getDistance(remaining[i], remaining[j]);
|
|
4022
|
+
}
|
|
4023
|
+
const avg = sum / (remaining.length - 1);
|
|
4024
|
+
if (avg < minAvg) {
|
|
4025
|
+
minAvg = avg;
|
|
4026
|
+
startIdx = i;
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
result.push(remaining.splice(startIdx, 1)[0]);
|
|
4030
|
+
while (remaining.length > 0) {
|
|
4031
|
+
let minDist = Infinity;
|
|
4032
|
+
let minIdx = 0;
|
|
4033
|
+
for (let i = 0;i < remaining.length; i++) {
|
|
4034
|
+
const dist = getDistance(result[result.length - 1], remaining[i]);
|
|
4035
|
+
if (dist < minDist) {
|
|
4036
|
+
minDist = dist;
|
|
4037
|
+
minIdx = i;
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
result.push(remaining.splice(minIdx, 1)[0]);
|
|
4041
|
+
}
|
|
4042
|
+
return result;
|
|
4043
|
+
};
|
|
4044
|
+
if (clusterRows) {
|
|
4045
|
+
const rowDistance = (a, b) => {
|
|
4046
|
+
let sum = 0;
|
|
4047
|
+
let count = 0;
|
|
4048
|
+
for (const col of colKeys) {
|
|
4049
|
+
const va = valueMap.get(`${a}|${col}`);
|
|
4050
|
+
const vb = valueMap.get(`${b}|${col}`);
|
|
4051
|
+
if (va !== undefined && vb !== undefined) {
|
|
4052
|
+
sum += (va - vb) ** 2;
|
|
4053
|
+
count++;
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
return count > 0 ? Math.sqrt(sum / count) : Infinity;
|
|
4057
|
+
};
|
|
4058
|
+
rowKeys = clusterOrder(rowKeys, rowDistance);
|
|
4059
|
+
}
|
|
4060
|
+
if (clusterCols) {
|
|
4061
|
+
const colDistance = (a, b) => {
|
|
4062
|
+
let sum = 0;
|
|
4063
|
+
let count = 0;
|
|
4064
|
+
for (const row of rowKeys) {
|
|
4065
|
+
const va = valueMap.get(`${row}|${a}`);
|
|
4066
|
+
const vb = valueMap.get(`${row}|${b}`);
|
|
4067
|
+
if (va !== undefined && vb !== undefined) {
|
|
4068
|
+
sum += (va - vb) ** 2;
|
|
4069
|
+
count++;
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
return count > 0 ? Math.sqrt(sum / count) : Infinity;
|
|
4073
|
+
};
|
|
4074
|
+
colKeys = clusterOrder(colKeys, colDistance);
|
|
4075
|
+
}
|
|
4076
|
+
const values = Array.from(valueMap.values());
|
|
4077
|
+
let minVal = Math.min(...values);
|
|
4078
|
+
let maxVal = Math.max(...values);
|
|
4079
|
+
const scaledValues = new Map;
|
|
4080
|
+
if (scaleMethod === "row") {
|
|
4081
|
+
for (const rowKey of rowKeys) {
|
|
4082
|
+
const rowVals = colKeys.map((c) => valueMap.get(`${rowKey}|${c}`)).filter((v) => v !== undefined);
|
|
4083
|
+
if (rowVals.length > 0) {
|
|
4084
|
+
const mean = rowVals.reduce((a, b) => a + b, 0) / rowVals.length;
|
|
4085
|
+
const std = Math.sqrt(rowVals.reduce((a, b) => a + (b - mean) ** 2, 0) / rowVals.length) || 1;
|
|
4086
|
+
for (const colKey of colKeys) {
|
|
4087
|
+
const v = valueMap.get(`${rowKey}|${colKey}`);
|
|
4088
|
+
if (v !== undefined) {
|
|
4089
|
+
scaledValues.set(`${rowKey}|${colKey}`, (v - mean) / std);
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
} else if (scaleMethod === "column") {
|
|
4095
|
+
for (const colKey of colKeys) {
|
|
4096
|
+
const colVals = rowKeys.map((r) => valueMap.get(`${r}|${colKey}`)).filter((v) => v !== undefined);
|
|
4097
|
+
if (colVals.length > 0) {
|
|
4098
|
+
const mean = colVals.reduce((a, b) => a + b, 0) / colVals.length;
|
|
4099
|
+
const std = Math.sqrt(colVals.reduce((a, b) => a + (b - mean) ** 2, 0) / colVals.length) || 1;
|
|
4100
|
+
for (const rowKey of rowKeys) {
|
|
4101
|
+
const v = valueMap.get(`${rowKey}|${colKey}`);
|
|
4102
|
+
if (v !== undefined) {
|
|
4103
|
+
scaledValues.set(`${rowKey}|${colKey}`, (v - mean) / std);
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
const finalValues = scaleMethod === "none" ? valueMap : scaledValues;
|
|
4110
|
+
if (scaleMethod !== "none") {
|
|
4111
|
+
const scaled = Array.from(finalValues.values());
|
|
4112
|
+
minVal = Math.min(...scaled);
|
|
4113
|
+
maxVal = Math.max(...scaled);
|
|
4114
|
+
}
|
|
4115
|
+
const interpolateColor2 = (val) => {
|
|
4116
|
+
if (isNaN(val))
|
|
4117
|
+
return naRgb;
|
|
4118
|
+
const t = (val - minVal) / (maxVal - minVal || 1);
|
|
4119
|
+
if (t <= 0.5) {
|
|
4120
|
+
const t2 = t * 2;
|
|
4121
|
+
return {
|
|
4122
|
+
r: Math.round(lowRgb.r + (midRgb.r - lowRgb.r) * t2),
|
|
4123
|
+
g: Math.round(lowRgb.g + (midRgb.g - lowRgb.g) * t2),
|
|
4124
|
+
b: Math.round(lowRgb.b + (midRgb.b - lowRgb.b) * t2),
|
|
4125
|
+
a: 1
|
|
4126
|
+
};
|
|
4127
|
+
} else {
|
|
4128
|
+
const t2 = (t - 0.5) * 2;
|
|
4129
|
+
return {
|
|
4130
|
+
r: Math.round(midRgb.r + (highRgb.r - midRgb.r) * t2),
|
|
4131
|
+
g: Math.round(midRgb.g + (highRgb.g - midRgb.g) * t2),
|
|
4132
|
+
b: Math.round(midRgb.b + (highRgb.b - midRgb.b) * t2),
|
|
4133
|
+
a: 1
|
|
4134
|
+
};
|
|
4135
|
+
}
|
|
4136
|
+
};
|
|
4137
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4138
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4139
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4140
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4141
|
+
const labelWidth = showRowLabels ? Math.min(10, Math.max(...rowKeys.map((k) => k.length))) + 1 : 0;
|
|
4142
|
+
const labelHeight = showColLabels ? 1 : 0;
|
|
4143
|
+
const availWidth = plotRight - plotLeft - labelWidth;
|
|
4144
|
+
const availHeight = plotBottom - plotTop - labelHeight;
|
|
4145
|
+
const cellWidth = Math.max(1, Math.floor(availWidth / colKeys.length));
|
|
4146
|
+
const cellHeight = Math.max(1, Math.floor(availHeight / rowKeys.length));
|
|
4147
|
+
for (let ri = 0;ri < rowKeys.length; ri++) {
|
|
4148
|
+
const rowKey = rowKeys[ri];
|
|
4149
|
+
const baseY = plotTop + labelHeight + ri * cellHeight;
|
|
4150
|
+
for (let ci = 0;ci < colKeys.length; ci++) {
|
|
4151
|
+
const colKey = colKeys[ci];
|
|
4152
|
+
const baseX = plotLeft + labelWidth + ci * cellWidth;
|
|
4153
|
+
const val = finalValues.get(`${rowKey}|${colKey}`);
|
|
4154
|
+
const color = val !== undefined ? interpolateColor2(val) : naRgb;
|
|
4155
|
+
for (let dy = 0;dy < cellHeight; dy++) {
|
|
4156
|
+
for (let dx = 0;dx < cellWidth; dx++) {
|
|
4157
|
+
canvas.drawChar(baseX + dx, baseY + dy, cellChar, color);
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
if (showRowLabels) {
|
|
4163
|
+
const labelColor = { r: 180, g: 180, b: 180, a: 1 };
|
|
4164
|
+
for (let ri = 0;ri < rowKeys.length; ri++) {
|
|
4165
|
+
const label = rowKeys[ri].slice(0, labelWidth - 1);
|
|
4166
|
+
const y = plotTop + labelHeight + ri * cellHeight + Math.floor(cellHeight / 2);
|
|
4167
|
+
for (let i = 0;i < label.length; i++) {
|
|
4168
|
+
canvas.drawChar(plotLeft + i, y, label[i], labelColor);
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
if (showColLabels) {
|
|
4173
|
+
const labelColor = { r: 180, g: 180, b: 180, a: 1 };
|
|
4174
|
+
for (let ci = 0;ci < colKeys.length; ci++) {
|
|
4175
|
+
const label = colKeys[ci].slice(0, cellWidth);
|
|
4176
|
+
const x = plotLeft + labelWidth + ci * cellWidth + Math.floor(cellWidth / 2);
|
|
4177
|
+
for (let i = 0;i < Math.min(label.length, 1); i++) {
|
|
4178
|
+
canvas.drawChar(x, plotTop + i, label[i], labelColor);
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
function renderGeomBiplot(data, geom, aes, scales, canvas) {
|
|
4184
|
+
const params = geom.params || {};
|
|
4185
|
+
const pc1Col = params.pc1_col ?? "PC1";
|
|
4186
|
+
const pc2Col = params.pc2_col ?? "PC2";
|
|
4187
|
+
const loadings = params.loadings;
|
|
4188
|
+
const showScores = params.show_scores ?? true;
|
|
4189
|
+
const scoreChar = String(params.score_char ?? "●");
|
|
4190
|
+
const showScoreLabels = params.show_score_labels ?? false;
|
|
4191
|
+
const showLoadings = params.show_loadings ?? true;
|
|
4192
|
+
const loadingColor = String(params.loading_color ?? "#e41a1c");
|
|
4193
|
+
const loadingScale = params.loading_scale;
|
|
4194
|
+
const showLoadingLabels = params.show_loading_labels ?? true;
|
|
4195
|
+
const showOrigin = params.show_origin ?? true;
|
|
4196
|
+
const originColor = String(params.origin_color ?? "#999999");
|
|
4197
|
+
if (!Array.isArray(data) || data.length === 0)
|
|
4198
|
+
return;
|
|
4199
|
+
const parseHex = (hex) => {
|
|
4200
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
4201
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
4202
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
4203
|
+
return { r, g, b, a: 1 };
|
|
4204
|
+
};
|
|
4205
|
+
const loadingColorParsed = parseHex(loadingColor);
|
|
4206
|
+
const originColorParsed = parseHex(originColor);
|
|
4207
|
+
const scores = [];
|
|
4208
|
+
const xField = typeof aes.x === "string" ? aes.x : pc1Col;
|
|
4209
|
+
const yField = typeof aes.y === "string" ? aes.y : pc2Col;
|
|
4210
|
+
const labelField = typeof aes.label === "string" ? aes.label : undefined;
|
|
4211
|
+
const colorField = typeof aes.color === "string" ? aes.color : undefined;
|
|
4212
|
+
for (const row of data) {
|
|
4213
|
+
const rawPc1 = row[xField];
|
|
4214
|
+
const rawPc2 = row[yField];
|
|
4215
|
+
const pc1 = typeof rawPc1 === "number" ? rawPc1 : parseFloat(String(rawPc1));
|
|
4216
|
+
const pc2 = typeof rawPc2 === "number" ? rawPc2 : parseFloat(String(rawPc2));
|
|
4217
|
+
if (!isNaN(pc1) && !isNaN(pc2)) {
|
|
4218
|
+
const point = { pc1, pc2 };
|
|
4219
|
+
if (labelField)
|
|
4220
|
+
point.label = String(row[labelField] ?? "");
|
|
4221
|
+
if (colorField && scales.color) {
|
|
4222
|
+
point.color = scales.color.map(row[colorField]);
|
|
4223
|
+
}
|
|
4224
|
+
scores.push(point);
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
if (scores.length === 0)
|
|
4228
|
+
return;
|
|
4229
|
+
let minX = Math.min(...scores.map((s) => s.pc1));
|
|
4230
|
+
let maxX = Math.max(...scores.map((s) => s.pc1));
|
|
4231
|
+
let minY = Math.min(...scores.map((s) => s.pc2));
|
|
4232
|
+
let maxY = Math.max(...scores.map((s) => s.pc2));
|
|
4233
|
+
let actualLoadingScale = loadingScale;
|
|
4234
|
+
if (loadings && loadings.length > 0 && !actualLoadingScale) {
|
|
4235
|
+
const maxLoading = Math.max(...loadings.map((l) => Math.sqrt(l.pc1 ** 2 + l.pc2 ** 2)));
|
|
4236
|
+
const maxScore = Math.max(Math.abs(minX), Math.abs(maxX), Math.abs(minY), Math.abs(maxY));
|
|
4237
|
+
actualLoadingScale = maxScore * 0.8 / (maxLoading || 1);
|
|
4238
|
+
}
|
|
4239
|
+
if (loadings && actualLoadingScale) {
|
|
4240
|
+
for (const l of loadings) {
|
|
4241
|
+
const lx = l.pc1 * actualLoadingScale;
|
|
4242
|
+
const ly = l.pc2 * actualLoadingScale;
|
|
4243
|
+
minX = Math.min(minX, lx);
|
|
4244
|
+
maxX = Math.max(maxX, lx);
|
|
4245
|
+
minY = Math.min(minY, ly);
|
|
4246
|
+
maxY = Math.max(maxY, ly);
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
const rangeX = maxX - minX || 1;
|
|
4250
|
+
const rangeY = maxY - minY || 1;
|
|
4251
|
+
minX -= rangeX * 0.1;
|
|
4252
|
+
maxX += rangeX * 0.1;
|
|
4253
|
+
minY -= rangeY * 0.1;
|
|
4254
|
+
maxY += rangeY * 0.1;
|
|
4255
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4256
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4257
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4258
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4259
|
+
const mapX = (v) => plotLeft + (v - minX) / (maxX - minX) * (plotRight - plotLeft);
|
|
4260
|
+
const mapY = (v) => plotBottom - (v - minY) / (maxY - minY) * (plotBottom - plotTop);
|
|
4261
|
+
if (showOrigin && minX <= 0 && maxX >= 0 && minY <= 0 && maxY >= 0) {
|
|
4262
|
+
const originX = Math.round(mapX(0));
|
|
4263
|
+
const originY = Math.round(mapY(0));
|
|
4264
|
+
for (let x = plotLeft;x <= plotRight; x++) {
|
|
4265
|
+
canvas.drawChar(x, originY, "─", originColorParsed);
|
|
4266
|
+
}
|
|
4267
|
+
for (let y = plotTop;y <= plotBottom; y++) {
|
|
4268
|
+
canvas.drawChar(originX, y, "│", originColorParsed);
|
|
4269
|
+
}
|
|
4270
|
+
canvas.drawChar(originX, originY, "┼", originColorParsed);
|
|
4271
|
+
}
|
|
4272
|
+
if (showLoadings && loadings && actualLoadingScale) {
|
|
4273
|
+
for (const loading of loadings) {
|
|
4274
|
+
const endX = loading.pc1 * actualLoadingScale;
|
|
4275
|
+
const endY = loading.pc2 * actualLoadingScale;
|
|
4276
|
+
const sx = Math.round(mapX(0));
|
|
4277
|
+
const sy = Math.round(mapY(0));
|
|
4278
|
+
const ex = Math.round(mapX(endX));
|
|
4279
|
+
const ey = Math.round(mapY(endY));
|
|
4280
|
+
const steps = Math.max(Math.abs(ex - sx), Math.abs(ey - sy));
|
|
4281
|
+
for (let i = 0;i <= steps; i++) {
|
|
4282
|
+
const t = steps > 0 ? i / steps : 0;
|
|
4283
|
+
const px = Math.round(sx + (ex - sx) * t);
|
|
4284
|
+
const py = Math.round(sy + (ey - sy) * t);
|
|
4285
|
+
const dx = ex - sx;
|
|
4286
|
+
const dy = ey - sy;
|
|
4287
|
+
let char = "·";
|
|
4288
|
+
if (Math.abs(dx) > Math.abs(dy) * 2) {
|
|
4289
|
+
char = dx > 0 ? "─" : "─";
|
|
4290
|
+
} else if (Math.abs(dy) > Math.abs(dx) * 2) {
|
|
4291
|
+
char = "│";
|
|
4292
|
+
} else if (dx > 0 && dy < 0 || dx < 0 && dy > 0) {
|
|
4293
|
+
char = "/";
|
|
4294
|
+
} else {
|
|
4295
|
+
char = "\\";
|
|
4296
|
+
}
|
|
4297
|
+
canvas.drawChar(px, py, char, loadingColorParsed);
|
|
4298
|
+
}
|
|
4299
|
+
const angle = Math.atan2(ey - sy, ex - sx);
|
|
4300
|
+
let arrowChar = "→";
|
|
4301
|
+
if (angle > Math.PI * 3 / 4 || angle < -Math.PI * 3 / 4)
|
|
4302
|
+
arrowChar = "←";
|
|
4303
|
+
else if (angle > Math.PI / 4)
|
|
4304
|
+
arrowChar = "↓";
|
|
4305
|
+
else if (angle < -Math.PI / 4)
|
|
4306
|
+
arrowChar = "↑";
|
|
4307
|
+
canvas.drawChar(ex, ey, arrowChar, loadingColorParsed);
|
|
4308
|
+
if (showLoadingLabels) {
|
|
4309
|
+
const labelX = ex + (ex >= sx ? 1 : -loading.variable.length);
|
|
4310
|
+
for (let i = 0;i < loading.variable.length; i++) {
|
|
4311
|
+
canvas.drawChar(labelX + i, ey, loading.variable[i], loadingColorParsed);
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
if (showScores) {
|
|
4317
|
+
const defaultColor = { r: 31, g: 120, b: 180, a: 1 };
|
|
4318
|
+
for (const score of scores) {
|
|
4319
|
+
const cx = Math.round(mapX(score.pc1));
|
|
4320
|
+
const cy = Math.round(mapY(score.pc2));
|
|
4321
|
+
const color = score.color ?? defaultColor;
|
|
4322
|
+
canvas.drawPoint(cx, cy, color, scoreChar);
|
|
4323
|
+
if (showScoreLabels && score.label) {
|
|
4324
|
+
const labelColor = { r: 50, g: 50, b: 50, a: 1 };
|
|
4325
|
+
for (let i = 0;i < score.label.length; i++) {
|
|
4326
|
+
canvas.drawChar(cx + 1 + i, cy, score.label[i], labelColor);
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
function renderGeomKaplanMeier(data, geom, aes, scales, canvas) {
|
|
4333
|
+
const params = geom.params || {};
|
|
4334
|
+
const showCensored = Boolean(params.show_censored ?? true);
|
|
4335
|
+
const censorChar = String(params.censor_char ?? "+");
|
|
4336
|
+
const showMedian = Boolean(params.show_median ?? false);
|
|
4337
|
+
if (!Array.isArray(data) || data.length === 0)
|
|
4338
|
+
return;
|
|
4339
|
+
const xField = typeof aes.x === "string" ? aes.x : "time";
|
|
4340
|
+
const yField = typeof aes.y === "string" ? aes.y : "status";
|
|
4341
|
+
const colorField = typeof aes.color === "string" ? aes.color : undefined;
|
|
4342
|
+
const groups = new Map;
|
|
4343
|
+
for (const row of data) {
|
|
4344
|
+
const time = Number(row[xField] ?? 0);
|
|
4345
|
+
const status = Number(row[yField] ?? 0);
|
|
4346
|
+
const group = colorField ? String(row[colorField] ?? "default") : "default";
|
|
4347
|
+
if (!groups.has(group))
|
|
4348
|
+
groups.set(group, []);
|
|
4349
|
+
groups.get(group).push({ time, status });
|
|
4350
|
+
}
|
|
4351
|
+
const colors = [
|
|
4352
|
+
{ r: 31, g: 119, b: 180, a: 1 },
|
|
4353
|
+
{ r: 255, g: 127, b: 14, a: 1 },
|
|
4354
|
+
{ r: 44, g: 160, b: 44, a: 1 },
|
|
4355
|
+
{ r: 214, g: 39, b: 40, a: 1 },
|
|
4356
|
+
{ r: 148, g: 103, b: 189, a: 1 }
|
|
4357
|
+
];
|
|
4358
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4359
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4360
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4361
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4362
|
+
let maxTime = 0;
|
|
4363
|
+
for (const [, events] of groups) {
|
|
4364
|
+
for (const e of events) {
|
|
4365
|
+
if (e.time > maxTime)
|
|
4366
|
+
maxTime = e.time;
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
const mapX = (t) => plotLeft + t / maxTime * (plotRight - plotLeft);
|
|
4370
|
+
const mapY = (s) => plotBottom - s * (plotBottom - plotTop);
|
|
4371
|
+
let colorIndex = 0;
|
|
4372
|
+
for (const [, events] of groups) {
|
|
4373
|
+
const color = colors[colorIndex % colors.length];
|
|
4374
|
+
colorIndex++;
|
|
4375
|
+
events.sort((a, b) => a.time - b.time);
|
|
4376
|
+
const n = events.length;
|
|
4377
|
+
let survival = 1;
|
|
4378
|
+
let atRisk = n;
|
|
4379
|
+
const survivalCurve = [];
|
|
4380
|
+
survivalCurve.push({ time: 0, survival: 1, censored: false });
|
|
4381
|
+
for (const event of events) {
|
|
4382
|
+
if (event.status === 1) {
|
|
4383
|
+
survival *= (atRisk - 1) / atRisk;
|
|
4384
|
+
survivalCurve.push({ time: event.time, survival, censored: false });
|
|
4385
|
+
} else {
|
|
4386
|
+
survivalCurve.push({ time: event.time, survival, censored: true });
|
|
4387
|
+
}
|
|
4388
|
+
atRisk--;
|
|
4389
|
+
}
|
|
4390
|
+
for (let i = 0;i < survivalCurve.length; i++) {
|
|
4391
|
+
const point = survivalCurve[i];
|
|
4392
|
+
const x = Math.round(mapX(point.time));
|
|
4393
|
+
const y = Math.round(mapY(point.survival));
|
|
4394
|
+
if (i > 0) {
|
|
4395
|
+
const prevPoint = survivalCurve[i - 1];
|
|
4396
|
+
const px = Math.round(mapX(prevPoint.time));
|
|
4397
|
+
const py = Math.round(mapY(prevPoint.survival));
|
|
4398
|
+
for (let hx = px;hx <= x; hx++) {
|
|
4399
|
+
canvas.drawChar(hx, py, "─", color);
|
|
4400
|
+
}
|
|
4401
|
+
if (py !== y) {
|
|
4402
|
+
for (let vy = Math.min(py, y);vy <= Math.max(py, y); vy++) {
|
|
4403
|
+
canvas.drawChar(x, vy, "│", color);
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
if (point.censored && showCensored) {
|
|
4408
|
+
canvas.drawChar(x, y, censorChar, color);
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
if (showMedian) {
|
|
4412
|
+
const medianY = mapY(0.5);
|
|
4413
|
+
for (let mx = plotLeft;mx <= plotRight; mx += 2) {
|
|
4414
|
+
canvas.drawChar(mx, Math.round(medianY), "·", { r: 150, g: 150, b: 150, a: 1 });
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
function renderGeomForest(data, geom, aes, scales, canvas) {
|
|
4420
|
+
const params = geom.params || {};
|
|
4421
|
+
const nullLine = Number(params.null_line ?? 1);
|
|
4422
|
+
const logScale = Boolean(params.log_scale ?? false);
|
|
4423
|
+
const nullLineColor = String(params.null_line_color ?? "#888888");
|
|
4424
|
+
const pointChar = String(params.point_char ?? "■");
|
|
4425
|
+
if (!Array.isArray(data) || data.length === 0)
|
|
4426
|
+
return;
|
|
4427
|
+
const parseHex = (hex) => {
|
|
4428
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
4429
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
4430
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
4431
|
+
return { r, g, b, a: 1 };
|
|
4432
|
+
};
|
|
4433
|
+
const nullColor = parseHex(nullLineColor);
|
|
4434
|
+
const xField = typeof aes.x === "string" ? aes.x : "estimate";
|
|
4435
|
+
const yField = typeof aes.y === "string" ? aes.y : "study";
|
|
4436
|
+
const xminField = typeof aes.xmin === "string" ? aes.xmin : "ci_lower";
|
|
4437
|
+
const xmaxField = typeof aes.xmax === "string" ? aes.xmax : "ci_upper";
|
|
4438
|
+
const sizeField = typeof aes.size === "string" ? aes.size : undefined;
|
|
4439
|
+
const rows = [];
|
|
4440
|
+
let minWeight = Infinity;
|
|
4441
|
+
let maxWeight = -Infinity;
|
|
4442
|
+
for (const row of data) {
|
|
4443
|
+
const estimate = Number(row[xField] ?? 0);
|
|
4444
|
+
const ci_lower = Number(row[xminField] ?? estimate);
|
|
4445
|
+
const ci_upper = Number(row[xmaxField] ?? estimate);
|
|
4446
|
+
const study = String(row[yField] ?? "");
|
|
4447
|
+
const weight = sizeField ? Number(row[sizeField] ?? 1) : 1;
|
|
4448
|
+
if (weight < minWeight)
|
|
4449
|
+
minWeight = weight;
|
|
4450
|
+
if (weight > maxWeight)
|
|
4451
|
+
maxWeight = weight;
|
|
4452
|
+
rows.push({ study, estimate, ci_lower, ci_upper, weight });
|
|
4453
|
+
}
|
|
4454
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4455
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4456
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4457
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4458
|
+
let xMin = Math.min(...rows.map((r) => r.ci_lower), nullLine);
|
|
4459
|
+
let xMax = Math.max(...rows.map((r) => r.ci_upper), nullLine);
|
|
4460
|
+
if (logScale) {
|
|
4461
|
+
xMin = Math.log10(Math.max(xMin, 0.001));
|
|
4462
|
+
xMax = Math.log10(Math.max(xMax, 0.001));
|
|
4463
|
+
}
|
|
4464
|
+
const mapX = (v) => {
|
|
4465
|
+
const val = logScale ? Math.log10(Math.max(v, 0.001)) : v;
|
|
4466
|
+
return plotLeft + (val - xMin) / (xMax - xMin) * (plotRight - plotLeft);
|
|
4467
|
+
};
|
|
4468
|
+
const nullX = Math.round(mapX(nullLine));
|
|
4469
|
+
for (let y = plotTop;y <= plotBottom; y++) {
|
|
4470
|
+
if ((y - plotTop) % 2 === 0) {
|
|
4471
|
+
canvas.drawChar(nullX, y, "│", nullColor);
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
const rowHeight = (plotBottom - plotTop) / rows.length;
|
|
4475
|
+
const pointColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
4476
|
+
const ciColor = { r: 80, g: 80, b: 80, a: 1 };
|
|
4477
|
+
for (let i = 0;i < rows.length; i++) {
|
|
4478
|
+
const row = rows[i];
|
|
4479
|
+
const y = Math.round(plotTop + (i + 0.5) * rowHeight);
|
|
4480
|
+
const x1 = Math.round(mapX(row.ci_lower));
|
|
4481
|
+
const x2 = Math.round(mapX(row.ci_upper));
|
|
4482
|
+
for (let x = x1;x <= x2; x++) {
|
|
4483
|
+
canvas.drawChar(x, y, "─", ciColor);
|
|
4484
|
+
}
|
|
4485
|
+
canvas.drawChar(x1, y, "├", ciColor);
|
|
4486
|
+
canvas.drawChar(x2, y, "┤", ciColor);
|
|
4487
|
+
const px = Math.round(mapX(row.estimate));
|
|
4488
|
+
canvas.drawChar(px, y, pointChar, pointColor);
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
function renderGeomRoc(data, geom, aes, scales, canvas) {
|
|
4492
|
+
const params = geom.params || {};
|
|
4493
|
+
const showDiagonal = Boolean(params.show_diagonal ?? true);
|
|
4494
|
+
const diagonalColor = String(params.diagonal_color ?? "#888888");
|
|
4495
|
+
const showAuc = Boolean(params.show_auc ?? true);
|
|
4496
|
+
const showOptimal = Boolean(params.show_optimal ?? false);
|
|
4497
|
+
const optimalChar = String(params.optimal_char ?? "●");
|
|
4498
|
+
if (!Array.isArray(data) || data.length === 0)
|
|
4499
|
+
return;
|
|
4500
|
+
const parseHex = (hex) => {
|
|
4501
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
4502
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
4503
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
4504
|
+
return { r, g, b, a: 1 };
|
|
4505
|
+
};
|
|
4506
|
+
const diagColor = parseHex(diagonalColor);
|
|
4507
|
+
const xField = typeof aes.x === "string" ? aes.x : "fpr";
|
|
4508
|
+
const yField = typeof aes.y === "string" ? aes.y : "tpr";
|
|
4509
|
+
const colorField = typeof aes.color === "string" ? aes.color : undefined;
|
|
4510
|
+
const groups = new Map;
|
|
4511
|
+
for (const row of data) {
|
|
4512
|
+
const fpr = Number(row[xField] ?? 0);
|
|
4513
|
+
const tpr = Number(row[yField] ?? 0);
|
|
4514
|
+
const group = colorField ? String(row[colorField] ?? "default") : "default";
|
|
4515
|
+
if (!groups.has(group))
|
|
4516
|
+
groups.set(group, []);
|
|
4517
|
+
groups.get(group).push({ fpr, tpr });
|
|
4518
|
+
}
|
|
4519
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4520
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4521
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4522
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4523
|
+
const mapX = (v) => plotLeft + v * (plotRight - plotLeft);
|
|
4524
|
+
const mapY = (v) => plotBottom - v * (plotBottom - plotTop);
|
|
4525
|
+
if (showDiagonal) {
|
|
4526
|
+
const steps = plotRight - plotLeft;
|
|
4527
|
+
for (let i = 0;i <= steps; i += 2) {
|
|
4528
|
+
const t = i / steps;
|
|
4529
|
+
const x = Math.round(mapX(t));
|
|
4530
|
+
const y = Math.round(mapY(t));
|
|
4531
|
+
canvas.drawChar(x, y, "·", diagColor);
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
const colors = [
|
|
4535
|
+
{ r: 31, g: 119, b: 180, a: 1 },
|
|
4536
|
+
{ r: 255, g: 127, b: 14, a: 1 },
|
|
4537
|
+
{ r: 44, g: 160, b: 44, a: 1 },
|
|
4538
|
+
{ r: 214, g: 39, b: 40, a: 1 }
|
|
4539
|
+
];
|
|
4540
|
+
let colorIndex = 0;
|
|
4541
|
+
for (const [, points] of groups) {
|
|
4542
|
+
const color = colors[colorIndex % colors.length];
|
|
4543
|
+
colorIndex++;
|
|
4544
|
+
points.sort((a, b) => a.fpr - b.fpr);
|
|
4545
|
+
let auc = 0;
|
|
4546
|
+
for (let i = 1;i < points.length; i++) {
|
|
4547
|
+
const dx = points[i].fpr - points[i - 1].fpr;
|
|
4548
|
+
const avgY = (points[i].tpr + points[i - 1].tpr) / 2;
|
|
4549
|
+
auc += dx * avgY;
|
|
4550
|
+
}
|
|
4551
|
+
let optimalPoint = points[0];
|
|
4552
|
+
let maxJ = -Infinity;
|
|
4553
|
+
for (const p of points) {
|
|
4554
|
+
const j = p.tpr - p.fpr;
|
|
4555
|
+
if (j > maxJ) {
|
|
4556
|
+
maxJ = j;
|
|
4557
|
+
optimalPoint = p;
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
for (let i = 0;i < points.length; i++) {
|
|
4561
|
+
const p = points[i];
|
|
4562
|
+
const x = Math.round(mapX(p.fpr));
|
|
4563
|
+
const y = Math.round(mapY(p.tpr));
|
|
4564
|
+
if (i > 0) {
|
|
4565
|
+
const prev = points[i - 1];
|
|
4566
|
+
const px = Math.round(mapX(prev.fpr));
|
|
4567
|
+
const py = Math.round(mapY(prev.tpr));
|
|
4568
|
+
const steps = Math.max(Math.abs(x - px), Math.abs(y - py));
|
|
4569
|
+
for (let s = 0;s <= steps; s++) {
|
|
4570
|
+
const t = steps > 0 ? s / steps : 0;
|
|
4571
|
+
const lx = Math.round(px + (x - px) * t);
|
|
4572
|
+
const ly = Math.round(py + (y - py) * t);
|
|
4573
|
+
canvas.drawChar(lx, ly, "─", color);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
canvas.drawChar(x, y, "●", color);
|
|
4577
|
+
}
|
|
4578
|
+
if (showOptimal) {
|
|
4579
|
+
const ox = Math.round(mapX(optimalPoint.fpr));
|
|
4580
|
+
const oy = Math.round(mapY(optimalPoint.tpr));
|
|
4581
|
+
canvas.drawChar(ox, oy, optimalChar, { r: 255, g: 0, b: 0, a: 1 });
|
|
4582
|
+
}
|
|
4583
|
+
if (showAuc && colorIndex === 1) {
|
|
4584
|
+
const aucText = `AUC=${auc.toFixed(3)}`;
|
|
4585
|
+
const labelColor = { r: 100, g: 100, b: 100, a: 1 };
|
|
4586
|
+
for (let i = 0;i < aucText.length; i++) {
|
|
4587
|
+
canvas.drawChar(plotRight - aucText.length + i, plotTop + 1, aucText[i], labelColor);
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
function renderGeomBlandAltman(data, geom, aes, scales, canvas) {
|
|
4593
|
+
const params = geom.params || {};
|
|
4594
|
+
const showLimits = Boolean(params.show_limits ?? true);
|
|
4595
|
+
const showBias = Boolean(params.show_bias ?? true);
|
|
4596
|
+
const limitMultiplier = Number(params.limit_multiplier ?? 1.96);
|
|
4597
|
+
const biasColor = String(params.bias_color ?? "#0000ff");
|
|
4598
|
+
const limitColor = String(params.limit_color ?? "#ff0000");
|
|
4599
|
+
const pointChar = String(params.point_char ?? "●");
|
|
4600
|
+
const precomputed = Boolean(params.precomputed ?? false);
|
|
4601
|
+
if (!Array.isArray(data) || data.length === 0)
|
|
4602
|
+
return;
|
|
4603
|
+
const parseHex = (hex) => {
|
|
4604
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
4605
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
4606
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
4607
|
+
return { r, g, b, a: 1 };
|
|
4608
|
+
};
|
|
4609
|
+
const biasColorParsed = parseHex(biasColor);
|
|
4610
|
+
const limitColorParsed = parseHex(limitColor);
|
|
4611
|
+
const xField = typeof aes.x === "string" ? aes.x : "method1";
|
|
4612
|
+
const yField = typeof aes.y === "string" ? aes.y : "method2";
|
|
4613
|
+
const points = [];
|
|
4614
|
+
if (precomputed) {
|
|
4615
|
+
for (const row of data) {
|
|
4616
|
+
const mean = Number(row[xField] ?? 0);
|
|
4617
|
+
const diff = Number(row[yField] ?? 0);
|
|
4618
|
+
points.push({ mean, diff });
|
|
4619
|
+
}
|
|
4620
|
+
} else {
|
|
4621
|
+
for (const row of data) {
|
|
4622
|
+
const m1 = Number(row[xField] ?? 0);
|
|
4623
|
+
const m2 = Number(row[yField] ?? 0);
|
|
4624
|
+
const mean = (m1 + m2) / 2;
|
|
4625
|
+
const diff = m1 - m2;
|
|
4626
|
+
points.push({ mean, diff });
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
if (points.length === 0)
|
|
4630
|
+
return;
|
|
4631
|
+
const diffs = points.map((p) => p.diff);
|
|
4632
|
+
const bias = diffs.reduce((a, b) => a + b, 0) / diffs.length;
|
|
4633
|
+
const variance = diffs.reduce((a, b) => a + Math.pow(b - bias, 2), 0) / (diffs.length - 1);
|
|
4634
|
+
const sd = Math.sqrt(variance);
|
|
4635
|
+
const upperLimit = bias + limitMultiplier * sd;
|
|
4636
|
+
const lowerLimit = bias - limitMultiplier * sd;
|
|
4637
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4638
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4639
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4640
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4641
|
+
const minMean = Math.min(...points.map((p) => p.mean));
|
|
4642
|
+
const maxMean = Math.max(...points.map((p) => p.mean));
|
|
4643
|
+
const minDiff = Math.min(...points.map((p) => p.diff), lowerLimit);
|
|
4644
|
+
const maxDiff = Math.max(...points.map((p) => p.diff), upperLimit);
|
|
4645
|
+
const mapX = (v) => plotLeft + (v - minMean) / (maxMean - minMean) * (plotRight - plotLeft);
|
|
4646
|
+
const mapY = (v) => plotBottom - (v - minDiff) / (maxDiff - minDiff) * (plotBottom - plotTop);
|
|
4647
|
+
if (showBias) {
|
|
4648
|
+
const biasY = Math.round(mapY(bias));
|
|
4649
|
+
for (let x = plotLeft;x <= plotRight; x++) {
|
|
4650
|
+
canvas.drawChar(x, biasY, "─", biasColorParsed);
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
if (showLimits) {
|
|
4654
|
+
const upperY = Math.round(mapY(upperLimit));
|
|
4655
|
+
const lowerY = Math.round(mapY(lowerLimit));
|
|
4656
|
+
for (let x = plotLeft;x <= plotRight; x += 2) {
|
|
4657
|
+
canvas.drawChar(x, upperY, "─", limitColorParsed);
|
|
4658
|
+
canvas.drawChar(x, lowerY, "─", limitColorParsed);
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
const pointColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
4662
|
+
for (const p of points) {
|
|
4663
|
+
const x = Math.round(mapX(p.mean));
|
|
4664
|
+
const y = Math.round(mapY(p.diff));
|
|
4665
|
+
canvas.drawChar(x, y, pointChar, pointColor);
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
function renderGeomQQ(data, geom, aes, scales, canvas) {
|
|
4669
|
+
const params = geom.params || {};
|
|
4670
|
+
const showLine = params.show_line ?? true;
|
|
4671
|
+
const lineColor = params.line_color ?? "#ff0000";
|
|
4672
|
+
const pointChar = params.point_char ?? "●";
|
|
4673
|
+
const standardize = params.standardize ?? true;
|
|
4674
|
+
const parseHex = (hex) => {
|
|
4675
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
4676
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
4677
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
4678
|
+
return { r, g, b, a: 1 };
|
|
4679
|
+
};
|
|
4680
|
+
const lineColorParsed = parseHex(lineColor);
|
|
4681
|
+
const sampleField = typeof aes.x === "string" ? aes.x : "x";
|
|
4682
|
+
const values = [];
|
|
4683
|
+
for (const row of data) {
|
|
4684
|
+
const v = Number(row[sampleField]);
|
|
4685
|
+
if (!isNaN(v))
|
|
4686
|
+
values.push(v);
|
|
4687
|
+
}
|
|
4688
|
+
if (values.length === 0)
|
|
4689
|
+
return;
|
|
4690
|
+
values.sort((a, b) => a - b);
|
|
4691
|
+
const n = values.length;
|
|
4692
|
+
const mean = values.reduce((a, b) => a + b, 0) / n;
|
|
4693
|
+
const variance = values.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / (n - 1);
|
|
4694
|
+
const sd = Math.sqrt(variance);
|
|
4695
|
+
const qnorm = (p) => {
|
|
4696
|
+
if (p <= 0)
|
|
4697
|
+
return -Infinity;
|
|
4698
|
+
if (p >= 1)
|
|
4699
|
+
return Infinity;
|
|
4700
|
+
if (p === 0.5)
|
|
4701
|
+
return 0;
|
|
4702
|
+
const a = [
|
|
4703
|
+
-39.69683028665376,
|
|
4704
|
+
220.9460984245205,
|
|
4705
|
+
-275.9285104469687,
|
|
4706
|
+
138.357751867269,
|
|
4707
|
+
-30.66479806614716,
|
|
4708
|
+
2.506628277459239
|
|
4709
|
+
];
|
|
4710
|
+
const b = [
|
|
4711
|
+
-54.47609879822406,
|
|
4712
|
+
161.5858368580409,
|
|
4713
|
+
-155.6989798598866,
|
|
4714
|
+
66.80131188771972,
|
|
4715
|
+
-13.28068155288572
|
|
4716
|
+
];
|
|
4717
|
+
const c = [
|
|
4718
|
+
-0.007784894002430293,
|
|
4719
|
+
-0.3223964580411365,
|
|
4720
|
+
-2.400758277161838,
|
|
4721
|
+
-2.549732539343734,
|
|
4722
|
+
4.374664141464968,
|
|
4723
|
+
2.938163982698783
|
|
4724
|
+
];
|
|
4725
|
+
const d = [
|
|
4726
|
+
0.007784695709041462,
|
|
4727
|
+
0.3224671290700398,
|
|
4728
|
+
2.445134137142996,
|
|
4729
|
+
3.754408661907416
|
|
4730
|
+
];
|
|
4731
|
+
const pLow = 0.02425;
|
|
4732
|
+
const pHigh = 1 - pLow;
|
|
4733
|
+
let q;
|
|
4734
|
+
if (p < pLow) {
|
|
4735
|
+
q = Math.sqrt(-2 * Math.log(p));
|
|
4736
|
+
return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);
|
|
4737
|
+
} else if (p <= pHigh) {
|
|
4738
|
+
q = p - 0.5;
|
|
4739
|
+
const r = q * q;
|
|
4740
|
+
return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1);
|
|
4741
|
+
} else {
|
|
4742
|
+
q = Math.sqrt(-2 * Math.log(1 - p));
|
|
4743
|
+
return -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);
|
|
4744
|
+
}
|
|
4745
|
+
};
|
|
4746
|
+
const points = [];
|
|
4747
|
+
for (let i = 0;i < n; i++) {
|
|
4748
|
+
const p = (i + 0.5) / n;
|
|
4749
|
+
const theoretical = qnorm(p);
|
|
4750
|
+
const sample = standardize ? (values[i] - mean) / sd : values[i];
|
|
4751
|
+
points.push({ theoretical, sample });
|
|
4752
|
+
}
|
|
4753
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4754
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4755
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4756
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4757
|
+
const minT = Math.min(...points.map((p) => p.theoretical));
|
|
4758
|
+
const maxT = Math.max(...points.map((p) => p.theoretical));
|
|
4759
|
+
const minS = Math.min(...points.map((p) => p.sample));
|
|
4760
|
+
const maxS = Math.max(...points.map((p) => p.sample));
|
|
4761
|
+
const minVal = Math.min(minT, minS);
|
|
4762
|
+
const maxVal = Math.max(maxT, maxS);
|
|
4763
|
+
const mapX = (v) => plotLeft + (v - minVal) / (maxVal - minVal) * (plotRight - plotLeft);
|
|
4764
|
+
const mapY = (v) => plotBottom - (v - minVal) / (maxVal - minVal) * (plotBottom - plotTop);
|
|
4765
|
+
if (showLine) {
|
|
4766
|
+
const steps = plotRight - plotLeft;
|
|
4767
|
+
for (let i = 0;i <= steps; i++) {
|
|
4768
|
+
const v = minVal + i / steps * (maxVal - minVal);
|
|
4769
|
+
const x = Math.round(mapX(v));
|
|
4770
|
+
const y = Math.round(mapY(v));
|
|
4771
|
+
if (y >= plotTop && y <= plotBottom) {
|
|
4772
|
+
canvas.drawChar(x, y, "─", lineColorParsed);
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
const pointColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
4777
|
+
for (const p of points) {
|
|
4778
|
+
const x = Math.round(mapX(p.theoretical));
|
|
4779
|
+
const y = Math.round(mapY(p.sample));
|
|
4780
|
+
canvas.drawChar(x, y, pointChar, pointColor);
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
function renderGeomECDF(data, geom, aes, scales, canvas) {
|
|
4784
|
+
const params = geom.params || {};
|
|
4785
|
+
const complement = params.complement ?? false;
|
|
4786
|
+
const showPoints = params.show_points ?? false;
|
|
4787
|
+
const xField = typeof aes.x === "string" ? aes.x : "x";
|
|
4788
|
+
const colorField = typeof aes.color === "string" ? aes.color : null;
|
|
4789
|
+
const groups = new Map;
|
|
4790
|
+
for (const row of data) {
|
|
4791
|
+
const v = Number(row[xField]);
|
|
4792
|
+
if (isNaN(v))
|
|
4793
|
+
continue;
|
|
4794
|
+
const groupKey = colorField ? String(row[colorField] ?? "default") : "default";
|
|
4795
|
+
if (!groups.has(groupKey))
|
|
4796
|
+
groups.set(groupKey, []);
|
|
4797
|
+
groups.get(groupKey).push(v);
|
|
4798
|
+
}
|
|
4799
|
+
if (groups.size === 0)
|
|
4800
|
+
return;
|
|
4801
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4802
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4803
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4804
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4805
|
+
let globalMin = Infinity;
|
|
4806
|
+
let globalMax = -Infinity;
|
|
4807
|
+
for (const values of groups.values()) {
|
|
4808
|
+
globalMin = Math.min(globalMin, ...values);
|
|
4809
|
+
globalMax = Math.max(globalMax, ...values);
|
|
4810
|
+
}
|
|
4811
|
+
const mapX = (v) => plotLeft + (v - globalMin) / (globalMax - globalMin) * (plotRight - plotLeft);
|
|
4812
|
+
const mapY = (v) => {
|
|
4813
|
+
const ecdf = complement ? 1 - v : v;
|
|
4814
|
+
return plotBottom - ecdf * (plotBottom - plotTop);
|
|
4815
|
+
};
|
|
4816
|
+
const colors = [
|
|
4817
|
+
{ r: 31, g: 119, b: 180, a: 1 },
|
|
4818
|
+
{ r: 255, g: 127, b: 14, a: 1 },
|
|
4819
|
+
{ r: 44, g: 160, b: 44, a: 1 },
|
|
4820
|
+
{ r: 214, g: 39, b: 40, a: 1 },
|
|
4821
|
+
{ r: 148, g: 103, b: 189, a: 1 }
|
|
4822
|
+
];
|
|
4823
|
+
let colorIdx = 0;
|
|
4824
|
+
for (const [, values] of groups) {
|
|
4825
|
+
const color = colors[colorIdx % colors.length];
|
|
4826
|
+
colorIdx++;
|
|
4827
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4828
|
+
const n = sorted.length;
|
|
4829
|
+
let prevX = plotLeft;
|
|
4830
|
+
let prevY = Math.round(mapY(0));
|
|
4831
|
+
for (let i = 0;i < n; i++) {
|
|
4832
|
+
const ecdfVal = (i + 1) / n;
|
|
4833
|
+
const x = Math.round(mapX(sorted[i]));
|
|
4834
|
+
const y = Math.round(mapY(ecdfVal));
|
|
4835
|
+
for (let px = prevX;px <= x; px++) {
|
|
4836
|
+
canvas.drawChar(px, prevY, "─", color);
|
|
4837
|
+
}
|
|
4838
|
+
const stepDir = y < prevY ? -1 : 1;
|
|
4839
|
+
for (let py = prevY;stepDir > 0 ? py <= y : py >= y; py += stepDir) {
|
|
4840
|
+
canvas.drawChar(x, py, "│", color);
|
|
4841
|
+
}
|
|
4842
|
+
if (showPoints) {
|
|
4843
|
+
canvas.drawChar(x, y, "●", color);
|
|
4844
|
+
}
|
|
4845
|
+
prevX = x;
|
|
4846
|
+
prevY = y;
|
|
4847
|
+
}
|
|
4848
|
+
for (let px = prevX;px <= plotRight; px++) {
|
|
4849
|
+
canvas.drawChar(px, prevY, "─", color);
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
function renderGeomFunnel(data, geom, aes, scales, canvas) {
|
|
4854
|
+
const params = geom.params || {};
|
|
4855
|
+
const showContours = params.show_contours ?? true;
|
|
4856
|
+
const showSummaryLine = params.show_summary_line ?? true;
|
|
4857
|
+
const summaryEffect = params.summary_effect;
|
|
4858
|
+
const pointChar = params.point_char ?? "●";
|
|
4859
|
+
const contourColor = params.contour_color ?? "#888888";
|
|
4860
|
+
const invertY = params.invert_y ?? true;
|
|
4861
|
+
const parseHex = (hex) => {
|
|
4862
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
4863
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
4864
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
4865
|
+
return { r, g, b, a: 1 };
|
|
4866
|
+
};
|
|
4867
|
+
const contourColorParsed = parseHex(contourColor);
|
|
4868
|
+
const xField = typeof aes.x === "string" ? aes.x : "effect";
|
|
4869
|
+
const yField = typeof aes.y === "string" ? aes.y : "se";
|
|
4870
|
+
const points = [];
|
|
4871
|
+
for (const row of data) {
|
|
4872
|
+
const effect = Number(row[xField]);
|
|
4873
|
+
const se = Number(row[yField]);
|
|
4874
|
+
if (!isNaN(effect) && !isNaN(se)) {
|
|
4875
|
+
points.push({ effect, se });
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
if (points.length === 0)
|
|
4879
|
+
return;
|
|
4880
|
+
const summary = summaryEffect ?? points.reduce((a, b) => a + b.effect, 0) / points.length;
|
|
4881
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4882
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4883
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4884
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4885
|
+
const minEffect = Math.min(...points.map((p) => p.effect));
|
|
4886
|
+
const maxEffect = Math.max(...points.map((p) => p.effect));
|
|
4887
|
+
const maxSE = Math.max(...points.map((p) => p.se));
|
|
4888
|
+
const effectPad = (maxEffect - minEffect) * 0.2;
|
|
4889
|
+
const effectMin = minEffect - effectPad;
|
|
4890
|
+
const effectMax = maxEffect + effectPad;
|
|
4891
|
+
const mapX = (v) => plotLeft + (v - effectMin) / (effectMax - effectMin) * (plotRight - plotLeft);
|
|
4892
|
+
const mapY = (v) => {
|
|
4893
|
+
if (invertY) {
|
|
4894
|
+
return plotTop + v / maxSE * (plotBottom - plotTop);
|
|
4895
|
+
}
|
|
4896
|
+
return plotBottom - v / maxSE * (plotBottom - plotTop);
|
|
4897
|
+
};
|
|
4898
|
+
if (showContours) {
|
|
4899
|
+
const z = 1.96;
|
|
4900
|
+
for (let se = 0;se <= maxSE; se += maxSE / 40) {
|
|
4901
|
+
const leftBound = summary - z * se;
|
|
4902
|
+
const rightBound = summary + z * se;
|
|
4903
|
+
const y = Math.round(mapY(se));
|
|
4904
|
+
const leftX = Math.round(mapX(leftBound));
|
|
4905
|
+
const rightX = Math.round(mapX(rightBound));
|
|
4906
|
+
if (leftX >= plotLeft && leftX <= plotRight) {
|
|
4907
|
+
canvas.drawChar(leftX, y, "·", contourColorParsed);
|
|
4908
|
+
}
|
|
4909
|
+
if (rightX >= plotLeft && rightX <= plotRight) {
|
|
4910
|
+
canvas.drawChar(rightX, y, "·", contourColorParsed);
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
if (showSummaryLine) {
|
|
4915
|
+
const summaryX = Math.round(mapX(summary));
|
|
4916
|
+
for (let y = plotTop;y <= plotBottom; y += 2) {
|
|
4917
|
+
canvas.drawChar(summaryX, y, "│", contourColorParsed);
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
const pointColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
4921
|
+
for (const p of points) {
|
|
4922
|
+
const x = Math.round(mapX(p.effect));
|
|
4923
|
+
const y = Math.round(mapY(p.se));
|
|
4924
|
+
canvas.drawChar(x, y, pointChar, pointColor);
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
function renderGeomControl(data, geom, aes, scales, canvas) {
|
|
4928
|
+
const params = geom.params || {};
|
|
4929
|
+
const sigma = params.sigma ?? 3;
|
|
4930
|
+
const showCenter = params.show_center ?? true;
|
|
4931
|
+
const showUCL = params.show_ucl ?? true;
|
|
4932
|
+
const showLCL = params.show_lcl ?? true;
|
|
4933
|
+
const showWarning = params.show_warning ?? false;
|
|
4934
|
+
const customCenter = params.center;
|
|
4935
|
+
const customUCL = params.ucl;
|
|
4936
|
+
const customLCL = params.lcl;
|
|
4937
|
+
const centerColor = params.center_color ?? "#0000ff";
|
|
4938
|
+
const limitColor = params.limit_color ?? "#ff0000";
|
|
4939
|
+
const warningColor = params.warning_color ?? "#ffa500";
|
|
4940
|
+
const connectPoints = params.connect_points ?? true;
|
|
4941
|
+
const highlightOOC = params.highlight_ooc ?? true;
|
|
4942
|
+
const oocChar = params.ooc_char ?? "◆";
|
|
4943
|
+
const pointChar = params.point_char ?? "●";
|
|
4944
|
+
const parseHex = (hex) => {
|
|
4945
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
4946
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
4947
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
4948
|
+
return { r, g, b, a: 1 };
|
|
4949
|
+
};
|
|
4950
|
+
const centerColorParsed = parseHex(centerColor);
|
|
4951
|
+
const limitColorParsed = parseHex(limitColor);
|
|
4952
|
+
const warningColorParsed = parseHex(warningColor);
|
|
4953
|
+
const xField = typeof aes.x === "string" ? aes.x : "x";
|
|
4954
|
+
const yField = typeof aes.y === "string" ? aes.y : "y";
|
|
4955
|
+
const points = [];
|
|
4956
|
+
for (const row of data) {
|
|
4957
|
+
const x = Number(row[xField]);
|
|
4958
|
+
const y = Number(row[yField]);
|
|
4959
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
4960
|
+
points.push({ x, y });
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
if (points.length === 0)
|
|
4964
|
+
return;
|
|
4965
|
+
points.sort((a, b) => a.x - b.x);
|
|
4966
|
+
const yValues = points.map((p) => p.y);
|
|
4967
|
+
const mean = customCenter ?? yValues.reduce((a, b) => a + b, 0) / yValues.length;
|
|
4968
|
+
let sigmaEst;
|
|
4969
|
+
if (points.length > 1) {
|
|
4970
|
+
const movingRanges = [];
|
|
4971
|
+
for (let i = 1;i < points.length; i++) {
|
|
4972
|
+
movingRanges.push(Math.abs(points[i].y - points[i - 1].y));
|
|
4973
|
+
}
|
|
4974
|
+
const avgMR = movingRanges.reduce((a, b) => a + b, 0) / movingRanges.length;
|
|
4975
|
+
sigmaEst = avgMR / 1.128;
|
|
4976
|
+
} else {
|
|
4977
|
+
const variance = yValues.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / (yValues.length - 1);
|
|
4978
|
+
sigmaEst = Math.sqrt(variance);
|
|
4979
|
+
}
|
|
4980
|
+
const ucl = customUCL ?? mean + sigma * sigmaEst;
|
|
4981
|
+
const lcl = customLCL ?? mean - sigma * sigmaEst;
|
|
4982
|
+
const uwl = mean + 2 * sigmaEst;
|
|
4983
|
+
const lwl = mean - 2 * sigmaEst;
|
|
4984
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4985
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4986
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4987
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4988
|
+
const minX = Math.min(...points.map((p) => p.x));
|
|
4989
|
+
const maxX = Math.max(...points.map((p) => p.x));
|
|
4990
|
+
const minY = Math.min(...points.map((p) => p.y), lcl);
|
|
4991
|
+
const maxY = Math.max(...points.map((p) => p.y), ucl);
|
|
4992
|
+
const mapX = (v) => plotLeft + (v - minX) / (maxX - minX) * (plotRight - plotLeft);
|
|
4993
|
+
const mapY = (v) => plotBottom - (v - minY) / (maxY - minY) * (plotBottom - plotTop);
|
|
4994
|
+
if (showCenter) {
|
|
4995
|
+
const centerY = Math.round(mapY(mean));
|
|
4996
|
+
for (let x = plotLeft;x <= plotRight; x++) {
|
|
4997
|
+
canvas.drawChar(x, centerY, "─", centerColorParsed);
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
if (showUCL) {
|
|
5001
|
+
const uclY = Math.round(mapY(ucl));
|
|
5002
|
+
for (let x = plotLeft;x <= plotRight; x += 2) {
|
|
5003
|
+
canvas.drawChar(x, uclY, "─", limitColorParsed);
|
|
5004
|
+
}
|
|
5005
|
+
}
|
|
5006
|
+
if (showLCL) {
|
|
5007
|
+
const lclY = Math.round(mapY(lcl));
|
|
5008
|
+
for (let x = plotLeft;x <= plotRight; x += 2) {
|
|
5009
|
+
canvas.drawChar(x, lclY, "─", limitColorParsed);
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
if (showWarning) {
|
|
5013
|
+
const uwlY = Math.round(mapY(uwl));
|
|
5014
|
+
const lwlY = Math.round(mapY(lwl));
|
|
5015
|
+
for (let x = plotLeft;x <= plotRight; x += 3) {
|
|
5016
|
+
canvas.drawChar(x, uwlY, "·", warningColorParsed);
|
|
5017
|
+
canvas.drawChar(x, lwlY, "·", warningColorParsed);
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
if (connectPoints && points.length > 1) {
|
|
5021
|
+
const lineColor = { r: 100, g: 100, b: 100, a: 1 };
|
|
5022
|
+
for (let i = 1;i < points.length; i++) {
|
|
5023
|
+
const x1 = Math.round(mapX(points[i - 1].x));
|
|
5024
|
+
const y1 = Math.round(mapY(points[i - 1].y));
|
|
5025
|
+
const x2 = Math.round(mapX(points[i].x));
|
|
5026
|
+
const y2 = Math.round(mapY(points[i].y));
|
|
5027
|
+
const dx = Math.abs(x2 - x1);
|
|
5028
|
+
const dy = Math.abs(y2 - y1);
|
|
5029
|
+
const sx = x1 < x2 ? 1 : -1;
|
|
5030
|
+
const sy = y1 < y2 ? 1 : -1;
|
|
5031
|
+
let err = dx - dy;
|
|
5032
|
+
let x = x1;
|
|
5033
|
+
let y = y1;
|
|
5034
|
+
while (true) {
|
|
5035
|
+
canvas.drawChar(x, y, "·", lineColor);
|
|
5036
|
+
if (x === x2 && y === y2)
|
|
5037
|
+
break;
|
|
5038
|
+
const e2 = 2 * err;
|
|
5039
|
+
if (e2 > -dy) {
|
|
5040
|
+
err -= dy;
|
|
5041
|
+
x += sx;
|
|
5042
|
+
}
|
|
5043
|
+
if (e2 < dx) {
|
|
5044
|
+
err += dx;
|
|
5045
|
+
y += sy;
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
const inControlColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
5051
|
+
const oocColor = { r: 214, g: 39, b: 40, a: 1 };
|
|
5052
|
+
for (const p of points) {
|
|
5053
|
+
const x = Math.round(mapX(p.x));
|
|
5054
|
+
const y = Math.round(mapY(p.y));
|
|
5055
|
+
const isOOC = p.y > ucl || p.y < lcl;
|
|
5056
|
+
if (highlightOOC && isOOC) {
|
|
5057
|
+
canvas.drawChar(x, y, oocChar, oocColor);
|
|
5058
|
+
} else {
|
|
5059
|
+
canvas.drawChar(x, y, pointChar, inControlColor);
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
}
|
|
5063
|
+
function renderGeomScree(data, geom, aes, scales, canvas) {
|
|
5064
|
+
const params = geom.params || {};
|
|
5065
|
+
const showCumulative = params.show_cumulative ?? false;
|
|
5066
|
+
const showKaiser = params.show_kaiser ?? false;
|
|
5067
|
+
const connectPoints = params.connect_points ?? true;
|
|
5068
|
+
const showBars = params.show_bars ?? false;
|
|
5069
|
+
const pointChar = params.point_char ?? "●";
|
|
5070
|
+
const cumulativeColor = params.cumulative_color ?? "#ff0000";
|
|
5071
|
+
const kaiserColor = params.kaiser_color ?? "#888888";
|
|
5072
|
+
const threshold = params.threshold;
|
|
5073
|
+
const thresholdColor = params.threshold_color ?? "#00aa00";
|
|
5074
|
+
const parseHex = (hex) => {
|
|
5075
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
5076
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
5077
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
5078
|
+
return { r, g, b, a: 1 };
|
|
5079
|
+
};
|
|
5080
|
+
const cumulativeColorParsed = parseHex(cumulativeColor);
|
|
5081
|
+
const kaiserColorParsed = parseHex(kaiserColor);
|
|
5082
|
+
const thresholdColorParsed = parseHex(thresholdColor);
|
|
5083
|
+
const xField = typeof aes.x === "string" ? aes.x : "component";
|
|
5084
|
+
const yField = typeof aes.y === "string" ? aes.y : "variance";
|
|
5085
|
+
const points = [];
|
|
5086
|
+
for (const row of data) {
|
|
5087
|
+
const component = Number(row[xField]);
|
|
5088
|
+
const variance = Number(row[yField]);
|
|
5089
|
+
if (!isNaN(component) && !isNaN(variance)) {
|
|
5090
|
+
points.push({ component, variance });
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
if (points.length === 0)
|
|
5094
|
+
return;
|
|
5095
|
+
points.sort((a, b) => a.component - b.component);
|
|
5096
|
+
const total = points.reduce((a, b) => a + b.variance, 0);
|
|
5097
|
+
let cumSum = 0;
|
|
5098
|
+
const cumulativePoints = points.map((p) => {
|
|
5099
|
+
cumSum += p.variance;
|
|
5100
|
+
return { component: p.component, cumulative: cumSum / total };
|
|
5101
|
+
});
|
|
5102
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
5103
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
5104
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
5105
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
5106
|
+
const minX = Math.min(...points.map((p) => p.component));
|
|
5107
|
+
const maxX = Math.max(...points.map((p) => p.component));
|
|
5108
|
+
const maxY = Math.max(...points.map((p) => p.variance));
|
|
5109
|
+
const yMax = showCumulative ? Math.max(maxY, total) : maxY;
|
|
5110
|
+
const mapX = (v) => plotLeft + (v - minX) / (maxX - minX) * (plotRight - plotLeft);
|
|
5111
|
+
const mapY = (v) => plotBottom - v / yMax * (plotBottom - plotTop);
|
|
5112
|
+
const mapYCumulative = (v) => plotBottom - v * (plotBottom - plotTop);
|
|
5113
|
+
if (showKaiser) {
|
|
5114
|
+
const kaiserY = Math.round(mapY(1));
|
|
5115
|
+
if (kaiserY >= plotTop && kaiserY <= plotBottom) {
|
|
5116
|
+
for (let x = plotLeft;x <= plotRight; x += 2) {
|
|
5117
|
+
canvas.drawChar(x, kaiserY, "─", kaiserColorParsed);
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
if (threshold !== undefined) {
|
|
5122
|
+
const thresholdY = Math.round(mapYCumulative(threshold));
|
|
5123
|
+
for (let x = plotLeft;x <= plotRight; x += 2) {
|
|
5124
|
+
canvas.drawChar(x, thresholdY, "─", thresholdColorParsed);
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
if (showBars) {
|
|
5128
|
+
const barColor = { r: 180, g: 180, b: 180, a: 1 };
|
|
5129
|
+
const barWidth = Math.max(1, Math.floor((plotRight - plotLeft) / points.length / 2));
|
|
5130
|
+
for (const p of points) {
|
|
5131
|
+
const x = Math.round(mapX(p.component));
|
|
5132
|
+
const y = Math.round(mapY(p.variance));
|
|
5133
|
+
for (let bx = x - barWidth;bx <= x + barWidth; bx++) {
|
|
5134
|
+
for (let by = y;by <= plotBottom; by++) {
|
|
5135
|
+
canvas.drawChar(bx, by, "░", barColor);
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
if (connectPoints && points.length > 1) {
|
|
5141
|
+
const lineColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
5142
|
+
for (let i = 1;i < points.length; i++) {
|
|
5143
|
+
const x1 = Math.round(mapX(points[i - 1].component));
|
|
5144
|
+
const y1 = Math.round(mapY(points[i - 1].variance));
|
|
5145
|
+
const x2 = Math.round(mapX(points[i].component));
|
|
5146
|
+
const y2 = Math.round(mapY(points[i].variance));
|
|
5147
|
+
const steps = Math.max(Math.abs(x2 - x1), 1);
|
|
5148
|
+
for (let s = 0;s <= steps; s++) {
|
|
5149
|
+
const t = s / steps;
|
|
5150
|
+
const x = Math.round(x1 + t * (x2 - x1));
|
|
5151
|
+
const y = Math.round(y1 + t * (y2 - y1));
|
|
5152
|
+
canvas.drawChar(x, y, "─", lineColor);
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
if (showCumulative && cumulativePoints.length > 1) {
|
|
5157
|
+
for (let i = 1;i < cumulativePoints.length; i++) {
|
|
5158
|
+
const x1 = Math.round(mapX(cumulativePoints[i - 1].component));
|
|
5159
|
+
const y1 = Math.round(mapYCumulative(cumulativePoints[i - 1].cumulative));
|
|
5160
|
+
const x2 = Math.round(mapX(cumulativePoints[i].component));
|
|
5161
|
+
const y2 = Math.round(mapYCumulative(cumulativePoints[i].cumulative));
|
|
5162
|
+
const steps = Math.max(Math.abs(x2 - x1), 1);
|
|
5163
|
+
for (let s = 0;s <= steps; s++) {
|
|
5164
|
+
const t = s / steps;
|
|
5165
|
+
const x = Math.round(x1 + t * (x2 - x1));
|
|
5166
|
+
const y = Math.round(y1 + t * (y2 - y1));
|
|
5167
|
+
canvas.drawChar(x, y, "─", cumulativeColorParsed);
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
for (const p of cumulativePoints) {
|
|
5171
|
+
const x = Math.round(mapX(p.component));
|
|
5172
|
+
const y = Math.round(mapYCumulative(p.cumulative));
|
|
5173
|
+
canvas.drawChar(x, y, "○", cumulativeColorParsed);
|
|
5174
|
+
}
|
|
5175
|
+
}
|
|
5176
|
+
const pointColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
5177
|
+
for (const p of points) {
|
|
5178
|
+
const x = Math.round(mapX(p.component));
|
|
5179
|
+
const y = Math.round(mapY(p.variance));
|
|
5180
|
+
canvas.drawChar(x, y, pointChar, pointColor);
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
3639
5183
|
function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
3640
5184
|
switch (geom.type) {
|
|
3641
5185
|
case "point":
|
|
@@ -3755,16 +5299,464 @@ function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
|
3755
5299
|
case "corrmat":
|
|
3756
5300
|
renderGeomCorrmat(data, geom, aes, scales, canvas);
|
|
3757
5301
|
break;
|
|
3758
|
-
case "sankey":
|
|
3759
|
-
renderGeomSankey(data, geom, aes, scales, canvas);
|
|
5302
|
+
case "sankey":
|
|
5303
|
+
renderGeomSankey(data, geom, aes, scales, canvas);
|
|
5304
|
+
break;
|
|
5305
|
+
case "treemap":
|
|
5306
|
+
renderGeomTreemap(data, geom, aes, scales, canvas);
|
|
5307
|
+
break;
|
|
5308
|
+
case "volcano":
|
|
5309
|
+
renderGeomVolcano(data, geom, aes, scales, canvas);
|
|
5310
|
+
break;
|
|
5311
|
+
case "ma":
|
|
5312
|
+
renderGeomMA(data, geom, aes, scales, canvas);
|
|
5313
|
+
break;
|
|
5314
|
+
case "manhattan":
|
|
5315
|
+
renderGeomManhattan(data, geom, aes, scales, canvas);
|
|
5316
|
+
break;
|
|
5317
|
+
case "heatmap":
|
|
5318
|
+
renderGeomHeatmap(data, geom, aes, scales, canvas);
|
|
5319
|
+
break;
|
|
5320
|
+
case "biplot":
|
|
5321
|
+
renderGeomBiplot(data, geom, aes, scales, canvas);
|
|
5322
|
+
break;
|
|
5323
|
+
case "kaplan_meier":
|
|
5324
|
+
renderGeomKaplanMeier(data, geom, aes, scales, canvas);
|
|
5325
|
+
break;
|
|
5326
|
+
case "forest":
|
|
5327
|
+
renderGeomForest(data, geom, aes, scales, canvas);
|
|
5328
|
+
break;
|
|
5329
|
+
case "roc":
|
|
5330
|
+
renderGeomRoc(data, geom, aes, scales, canvas);
|
|
5331
|
+
break;
|
|
5332
|
+
case "bland_altman":
|
|
5333
|
+
renderGeomBlandAltman(data, geom, aes, scales, canvas);
|
|
5334
|
+
break;
|
|
5335
|
+
case "qq":
|
|
5336
|
+
renderGeomQQ(data, geom, aes, scales, canvas);
|
|
5337
|
+
break;
|
|
5338
|
+
case "ecdf":
|
|
5339
|
+
renderGeomECDF(data, geom, aes, scales, canvas);
|
|
5340
|
+
break;
|
|
5341
|
+
case "funnel":
|
|
5342
|
+
renderGeomFunnel(data, geom, aes, scales, canvas);
|
|
5343
|
+
break;
|
|
5344
|
+
case "control":
|
|
5345
|
+
renderGeomControl(data, geom, aes, scales, canvas);
|
|
5346
|
+
break;
|
|
5347
|
+
case "scree":
|
|
5348
|
+
renderGeomScree(data, geom, aes, scales, canvas);
|
|
5349
|
+
break;
|
|
5350
|
+
case "upset":
|
|
5351
|
+
renderGeomUpset(data, geom, aes, scales, canvas);
|
|
3760
5352
|
break;
|
|
3761
|
-
case "
|
|
3762
|
-
|
|
5353
|
+
case "dendrogram":
|
|
5354
|
+
renderGeomDendrogram(data, geom, aes, scales, canvas);
|
|
3763
5355
|
break;
|
|
3764
5356
|
default:
|
|
3765
5357
|
break;
|
|
3766
5358
|
}
|
|
3767
5359
|
}
|
|
5360
|
+
function renderGeomUpset(data, geom, aes, scales, canvas) {
|
|
5361
|
+
const params = geom.params || {};
|
|
5362
|
+
const sets = params.sets;
|
|
5363
|
+
const minSize = params.min_size ?? 1;
|
|
5364
|
+
const maxIntersections = params.max_intersections ?? 20;
|
|
5365
|
+
const sortBy = params.sort_by ?? "size";
|
|
5366
|
+
const sortOrder = params.sort_order ?? "desc";
|
|
5367
|
+
const showSetSizes = params.show_set_sizes ?? true;
|
|
5368
|
+
const dotChar = params.dot_char ?? "●";
|
|
5369
|
+
const emptyChar = params.empty_char ?? "○";
|
|
5370
|
+
const lineChar = params.line_char ?? "│";
|
|
5371
|
+
const barChar = params.bar_char ?? "█";
|
|
5372
|
+
let setNames = [];
|
|
5373
|
+
if (sets && sets.length > 0) {
|
|
5374
|
+
setNames = sets;
|
|
5375
|
+
} else if (data.length > 0) {
|
|
5376
|
+
const firstRow = data[0];
|
|
5377
|
+
for (const key of Object.keys(firstRow)) {
|
|
5378
|
+
const values = data.map((row) => row[key]);
|
|
5379
|
+
const isBinary = values.every((v) => v === 0 || v === 1 || v === "0" || v === "1");
|
|
5380
|
+
if (isBinary && key !== "id" && key !== "name" && key !== "element") {
|
|
5381
|
+
setNames.push(key);
|
|
5382
|
+
}
|
|
5383
|
+
}
|
|
5384
|
+
if (setNames.length === 0) {
|
|
5385
|
+
const setsField2 = typeof aes.x === "string" ? aes.x : "sets";
|
|
5386
|
+
const allSets = new Set;
|
|
5387
|
+
for (const row of data) {
|
|
5388
|
+
const val = row[setsField2];
|
|
5389
|
+
if (typeof val === "string") {
|
|
5390
|
+
val.split(",").forEach((s) => allSets.add(s.trim()));
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
5393
|
+
setNames = Array.from(allSets).sort();
|
|
5394
|
+
}
|
|
5395
|
+
}
|
|
5396
|
+
if (setNames.length === 0)
|
|
5397
|
+
return;
|
|
5398
|
+
const intersectionMap = new Map;
|
|
5399
|
+
const setsField = typeof aes.x === "string" ? aes.x : "sets";
|
|
5400
|
+
const hasListFormat = data.length > 0 && typeof data[0][setsField] === "string";
|
|
5401
|
+
for (const row of data) {
|
|
5402
|
+
let memberSets;
|
|
5403
|
+
if (hasListFormat) {
|
|
5404
|
+
const val = row[setsField];
|
|
5405
|
+
memberSets = typeof val === "string" ? val.split(",").map((s) => s.trim()).filter((s) => setNames.includes(s)) : [];
|
|
5406
|
+
} else {
|
|
5407
|
+
memberSets = setNames.filter((s) => {
|
|
5408
|
+
const v = row[s];
|
|
5409
|
+
return v === 1 || v === "1";
|
|
5410
|
+
});
|
|
5411
|
+
}
|
|
5412
|
+
if (memberSets.length > 0) {
|
|
5413
|
+
const key = memberSets.sort().join("|");
|
|
5414
|
+
intersectionMap.set(key, (intersectionMap.get(key) || 0) + 1);
|
|
5415
|
+
}
|
|
5416
|
+
}
|
|
5417
|
+
let intersections = Array.from(intersectionMap.entries()).map(([key, count]) => ({
|
|
5418
|
+
sets: new Set(key.split("|")),
|
|
5419
|
+
count,
|
|
5420
|
+
key
|
|
5421
|
+
})).filter((i) => i.count >= minSize);
|
|
5422
|
+
if (sortBy === "size") {
|
|
5423
|
+
intersections.sort((a, b) => sortOrder === "desc" ? b.count - a.count : a.count - b.count);
|
|
5424
|
+
} else if (sortBy === "degree") {
|
|
5425
|
+
intersections.sort((a, b) => sortOrder === "desc" ? b.sets.size - a.sets.size : a.sets.size - b.sets.size);
|
|
5426
|
+
}
|
|
5427
|
+
intersections = intersections.slice(0, maxIntersections);
|
|
5428
|
+
if (intersections.length === 0)
|
|
5429
|
+
return;
|
|
5430
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
5431
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
5432
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
5433
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
5434
|
+
const plotWidth = plotRight - plotLeft;
|
|
5435
|
+
const plotHeight = plotBottom - plotTop;
|
|
5436
|
+
const matrixHeight = Math.min(setNames.length * 2 + 2, Math.floor(plotHeight * 0.4));
|
|
5437
|
+
const barHeight = plotHeight - matrixHeight - 2;
|
|
5438
|
+
const barTop = plotTop;
|
|
5439
|
+
const barBottom = plotTop + barHeight;
|
|
5440
|
+
const matrixTop = barBottom + 2;
|
|
5441
|
+
const setLabelWidth = showSetSizes ? Math.max(...setNames.map((s) => s.length)) + 8 : 0;
|
|
5442
|
+
const colWidth = Math.max(2, Math.floor((plotWidth - setLabelWidth) / intersections.length));
|
|
5443
|
+
const maxCount = Math.max(...intersections.map((i) => i.count));
|
|
5444
|
+
const barColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
5445
|
+
const dotColor = { r: 50, g: 50, b: 50, a: 1 };
|
|
5446
|
+
const lineColor = { r: 100, g: 100, b: 100, a: 1 };
|
|
5447
|
+
const labelColor = { r: 150, g: 150, b: 150, a: 1 };
|
|
5448
|
+
for (let i = 0;i < intersections.length; i++) {
|
|
5449
|
+
const inter = intersections[i];
|
|
5450
|
+
const x = plotLeft + setLabelWidth + i * colWidth + Math.floor(colWidth / 2);
|
|
5451
|
+
const barHeightPx = Math.round(inter.count / maxCount * barHeight);
|
|
5452
|
+
for (let y = barBottom - barHeightPx;y <= barBottom; y++) {
|
|
5453
|
+
canvas.drawChar(x, y, barChar, barColor);
|
|
5454
|
+
}
|
|
5455
|
+
const countStr = inter.count.toString();
|
|
5456
|
+
const labelY = barBottom - barHeightPx - 1;
|
|
5457
|
+
if (labelY >= barTop) {
|
|
5458
|
+
for (let ci = 0;ci < countStr.length; ci++) {
|
|
5459
|
+
canvas.drawChar(x - Math.floor(countStr.length / 2) + ci, labelY, countStr[ci], labelColor);
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
}
|
|
5463
|
+
const rowSpacing = Math.max(1, Math.floor(matrixHeight / setNames.length));
|
|
5464
|
+
if (showSetSizes) {
|
|
5465
|
+
for (let si = 0;si < setNames.length; si++) {
|
|
5466
|
+
const setName = setNames[si];
|
|
5467
|
+
const y = matrixTop + si * rowSpacing + 1;
|
|
5468
|
+
let setSize = 0;
|
|
5469
|
+
for (const row of data) {
|
|
5470
|
+
if (hasListFormat) {
|
|
5471
|
+
const val = row[setsField];
|
|
5472
|
+
if (typeof val === "string" && val.split(",").map((s) => s.trim()).includes(setName)) {
|
|
5473
|
+
setSize++;
|
|
5474
|
+
}
|
|
5475
|
+
} else {
|
|
5476
|
+
const v = row[setName];
|
|
5477
|
+
if (v === 1 || v === "1")
|
|
5478
|
+
setSize++;
|
|
5479
|
+
}
|
|
5480
|
+
}
|
|
5481
|
+
const label = `${setName.substring(0, 6)}`;
|
|
5482
|
+
for (let ci = 0;ci < label.length; ci++) {
|
|
5483
|
+
canvas.drawChar(plotLeft + ci, y, label[ci], labelColor);
|
|
5484
|
+
}
|
|
5485
|
+
const sizeBarLen = Math.max(1, Math.round(setSize / data.length * 5));
|
|
5486
|
+
for (let bi = 0;bi < sizeBarLen; bi++) {
|
|
5487
|
+
canvas.drawChar(plotLeft + label.length + 1 + bi, y, "▪", barColor);
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
}
|
|
5491
|
+
for (let i = 0;i < intersections.length; i++) {
|
|
5492
|
+
const inter = intersections[i];
|
|
5493
|
+
const x = plotLeft + setLabelWidth + i * colWidth + Math.floor(colWidth / 2);
|
|
5494
|
+
const activeRows = [];
|
|
5495
|
+
for (let si = 0;si < setNames.length; si++) {
|
|
5496
|
+
const setName = setNames[si];
|
|
5497
|
+
const y = matrixTop + si * rowSpacing + 1;
|
|
5498
|
+
const isActive = inter.sets.has(setName);
|
|
5499
|
+
if (isActive) {
|
|
5500
|
+
canvas.drawChar(x, y, dotChar, dotColor);
|
|
5501
|
+
activeRows.push(y);
|
|
5502
|
+
} else {
|
|
5503
|
+
canvas.drawChar(x, y, emptyChar, { r: 200, g: 200, b: 200, a: 1 });
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
5506
|
+
if (activeRows.length > 1) {
|
|
5507
|
+
const minY = Math.min(...activeRows);
|
|
5508
|
+
const maxY = Math.max(...activeRows);
|
|
5509
|
+
for (let y = minY + 1;y < maxY; y++) {
|
|
5510
|
+
if (!activeRows.includes(y)) {
|
|
5511
|
+
canvas.drawChar(x, y, lineChar, lineColor);
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
5517
|
+
function renderGeomDendrogram(data, geom, _aes, scales, canvas) {
|
|
5518
|
+
const params = geom.params || {};
|
|
5519
|
+
const orientation = params.orientation ?? "vertical";
|
|
5520
|
+
const labels = params.labels;
|
|
5521
|
+
const showLabels = params.show_labels ?? true;
|
|
5522
|
+
const hang = params.hang ?? false;
|
|
5523
|
+
const hConnector = params.h_connector ?? "─";
|
|
5524
|
+
const vConnector = params.v_connector ?? "│";
|
|
5525
|
+
const cornerTR = params.corner_tr ?? "┐";
|
|
5526
|
+
const cornerBL = params.corner_bl ?? "└";
|
|
5527
|
+
const cornerBR = params.corner_br ?? "┘";
|
|
5528
|
+
const leafChar = params.leaf_char ?? "○";
|
|
5529
|
+
const parentCol = params.parent_col ?? "parent";
|
|
5530
|
+
const heightCol = params.height_col ?? "height";
|
|
5531
|
+
const idCol = params.id_col ?? "id";
|
|
5532
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
5533
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
5534
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
5535
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
5536
|
+
const plotWidth = plotRight - plotLeft;
|
|
5537
|
+
const plotHeight = plotBottom - plotTop;
|
|
5538
|
+
const lineColor = { r: 50, g: 50, b: 50, a: 1 };
|
|
5539
|
+
const leafColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
5540
|
+
const labelColor = { r: 100, g: 100, b: 100, a: 1 };
|
|
5541
|
+
const hasLinkageFormat = data.length > 0 && (("merge1" in data[0]) || ("merge_1" in data[0]));
|
|
5542
|
+
if (hasLinkageFormat) {
|
|
5543
|
+
const linkage = data.map((row) => ({
|
|
5544
|
+
merge1: Number(row["merge1"] ?? row["merge_1"]),
|
|
5545
|
+
merge2: Number(row["merge2"] ?? row["merge_2"]),
|
|
5546
|
+
height: Number(row[heightCol] ?? row["height"]),
|
|
5547
|
+
size: Number(row["size"] ?? 2)
|
|
5548
|
+
}));
|
|
5549
|
+
if (linkage.length === 0)
|
|
5550
|
+
return;
|
|
5551
|
+
const n = linkage.length + 1;
|
|
5552
|
+
const nodes = new Map;
|
|
5553
|
+
for (let i = 0;i < n; i++) {
|
|
5554
|
+
nodes.set(i, {
|
|
5555
|
+
id: i,
|
|
5556
|
+
height: 0,
|
|
5557
|
+
label: labels?.[i] ?? `${i}`
|
|
5558
|
+
});
|
|
5559
|
+
}
|
|
5560
|
+
for (let i = 0;i < linkage.length; i++) {
|
|
5561
|
+
const row = linkage[i];
|
|
5562
|
+
const newId = n + i;
|
|
5563
|
+
const leftNode = nodes.get(row.merge1 < n ? row.merge1 : row.merge1);
|
|
5564
|
+
const rightNode = nodes.get(row.merge2 < n ? row.merge2 : row.merge2);
|
|
5565
|
+
nodes.set(newId, {
|
|
5566
|
+
id: newId,
|
|
5567
|
+
left: leftNode,
|
|
5568
|
+
right: rightNode,
|
|
5569
|
+
height: row.height
|
|
5570
|
+
});
|
|
5571
|
+
}
|
|
5572
|
+
const root = nodes.get(n + linkage.length - 1);
|
|
5573
|
+
if (!root)
|
|
5574
|
+
return;
|
|
5575
|
+
let xPos = 0;
|
|
5576
|
+
const assignX = (node) => {
|
|
5577
|
+
if (!node.left && !node.right) {
|
|
5578
|
+
node.x = xPos++;
|
|
5579
|
+
} else {
|
|
5580
|
+
if (node.left)
|
|
5581
|
+
assignX(node.left);
|
|
5582
|
+
if (node.right)
|
|
5583
|
+
assignX(node.right);
|
|
5584
|
+
const leftX = node.left?.x ?? 0;
|
|
5585
|
+
const rightX = node.right?.x ?? 0;
|
|
5586
|
+
node.x = (leftX + rightX) / 2;
|
|
5587
|
+
}
|
|
5588
|
+
};
|
|
5589
|
+
assignX(root);
|
|
5590
|
+
const maxHeight = root.height;
|
|
5591
|
+
const leafCount = xPos;
|
|
5592
|
+
const mapX = (x) => {
|
|
5593
|
+
if (orientation === "vertical") {
|
|
5594
|
+
return plotLeft + x / (leafCount - 1 || 1) * plotWidth;
|
|
5595
|
+
} else {
|
|
5596
|
+
return plotBottom - x / (leafCount - 1 || 1) * plotHeight;
|
|
5597
|
+
}
|
|
5598
|
+
};
|
|
5599
|
+
const mapY = (h) => {
|
|
5600
|
+
if (orientation === "vertical") {
|
|
5601
|
+
return plotTop + (1 - h / maxHeight) * (plotHeight - 3);
|
|
5602
|
+
} else {
|
|
5603
|
+
return plotLeft + h / maxHeight * plotWidth;
|
|
5604
|
+
}
|
|
5605
|
+
};
|
|
5606
|
+
const drawNode = (node) => {
|
|
5607
|
+
if (node.x === undefined)
|
|
5608
|
+
return;
|
|
5609
|
+
if (node.left && node.right) {
|
|
5610
|
+
const nodeY = mapY(node.height);
|
|
5611
|
+
const leftX = mapX(node.left.x);
|
|
5612
|
+
const leftY = mapY(node.left.height);
|
|
5613
|
+
const rightX = mapX(node.right.x);
|
|
5614
|
+
const rightY = mapY(node.right.height);
|
|
5615
|
+
if (orientation === "vertical") {
|
|
5616
|
+
const hLineY = Math.round(nodeY);
|
|
5617
|
+
const leftXRound = Math.round(leftX);
|
|
5618
|
+
const rightXRound = Math.round(rightX);
|
|
5619
|
+
for (let x = Math.min(leftXRound, rightXRound);x <= Math.max(leftXRound, rightXRound); x++) {
|
|
5620
|
+
canvas.drawChar(x, hLineY, hConnector, lineColor);
|
|
5621
|
+
}
|
|
5622
|
+
canvas.drawChar(leftXRound, hLineY, cornerBL, lineColor);
|
|
5623
|
+
canvas.drawChar(rightXRound, hLineY, cornerBR, lineColor);
|
|
5624
|
+
const leftYRound = Math.round(leftY);
|
|
5625
|
+
const rightYRound = Math.round(rightY);
|
|
5626
|
+
for (let y = hLineY + 1;y < leftYRound; y++) {
|
|
5627
|
+
canvas.drawChar(leftXRound, y, vConnector, lineColor);
|
|
5628
|
+
}
|
|
5629
|
+
for (let y = hLineY + 1;y < rightYRound; y++) {
|
|
5630
|
+
canvas.drawChar(rightXRound, y, vConnector, lineColor);
|
|
5631
|
+
}
|
|
5632
|
+
} else {
|
|
5633
|
+
const hLineX = Math.round(nodeY);
|
|
5634
|
+
const leftYRound = Math.round(leftX);
|
|
5635
|
+
const rightYRound = Math.round(rightX);
|
|
5636
|
+
for (let y = Math.min(leftYRound, rightYRound);y <= Math.max(leftYRound, rightYRound); y++) {
|
|
5637
|
+
canvas.drawChar(hLineX, y, vConnector, lineColor);
|
|
5638
|
+
}
|
|
5639
|
+
canvas.drawChar(hLineX, leftYRound, cornerTR, lineColor);
|
|
5640
|
+
canvas.drawChar(hLineX, rightYRound, cornerBR, lineColor);
|
|
5641
|
+
const leftXRound = Math.round(mapY(node.left.height));
|
|
5642
|
+
const rightXRound = Math.round(mapY(node.right.height));
|
|
5643
|
+
for (let x = hLineX + 1;x < leftXRound; x++) {
|
|
5644
|
+
canvas.drawChar(x, leftYRound, hConnector, lineColor);
|
|
5645
|
+
}
|
|
5646
|
+
for (let x = hLineX + 1;x < rightXRound; x++) {
|
|
5647
|
+
canvas.drawChar(x, rightYRound, hConnector, lineColor);
|
|
5648
|
+
}
|
|
5649
|
+
}
|
|
5650
|
+
drawNode(node.left);
|
|
5651
|
+
drawNode(node.right);
|
|
5652
|
+
} else {
|
|
5653
|
+
if (orientation === "vertical") {
|
|
5654
|
+
const x = Math.round(mapX(node.x));
|
|
5655
|
+
const y = hang ? plotBottom - 2 : Math.round(mapY(0));
|
|
5656
|
+
canvas.drawChar(x, y, leafChar, leafColor);
|
|
5657
|
+
if (showLabels && node.label) {
|
|
5658
|
+
const label = node.label.substring(0, 4);
|
|
5659
|
+
for (let ci = 0;ci < label.length; ci++) {
|
|
5660
|
+
canvas.drawChar(x - Math.floor(label.length / 2) + ci, y + 1, label[ci], labelColor);
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
5663
|
+
} else {
|
|
5664
|
+
const y = Math.round(mapX(node.x));
|
|
5665
|
+
const x = Math.round(mapY(0));
|
|
5666
|
+
canvas.drawChar(x, y, leafChar, leafColor);
|
|
5667
|
+
if (showLabels && node.label) {
|
|
5668
|
+
const label = node.label.substring(0, 6);
|
|
5669
|
+
for (let ci = 0;ci < label.length; ci++) {
|
|
5670
|
+
canvas.drawChar(x + 2 + ci, y, label[ci], labelColor);
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
};
|
|
5676
|
+
drawNode(root);
|
|
5677
|
+
} else {
|
|
5678
|
+
const nodeMap = new Map;
|
|
5679
|
+
for (const row of data) {
|
|
5680
|
+
const id = String(row[idCol] ?? "");
|
|
5681
|
+
const parent = row[parentCol];
|
|
5682
|
+
const height = Number(row[heightCol] ?? 0);
|
|
5683
|
+
nodeMap.set(id, {
|
|
5684
|
+
id,
|
|
5685
|
+
parent: parent === null || parent === "" || parent === "null" ? null : String(parent),
|
|
5686
|
+
height,
|
|
5687
|
+
children: []
|
|
5688
|
+
});
|
|
5689
|
+
}
|
|
5690
|
+
let root = null;
|
|
5691
|
+
for (const node of nodeMap.values()) {
|
|
5692
|
+
if (node.parent === null) {
|
|
5693
|
+
root = node;
|
|
5694
|
+
} else {
|
|
5695
|
+
const parentNode = nodeMap.get(node.parent);
|
|
5696
|
+
if (parentNode) {
|
|
5697
|
+
parentNode.children.push(node);
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5701
|
+
if (!root)
|
|
5702
|
+
return;
|
|
5703
|
+
let xPos = 0;
|
|
5704
|
+
const assignX = (node) => {
|
|
5705
|
+
if (node.children.length === 0) {
|
|
5706
|
+
node.x = xPos++;
|
|
5707
|
+
} else {
|
|
5708
|
+
for (const child of node.children) {
|
|
5709
|
+
assignX(child);
|
|
5710
|
+
}
|
|
5711
|
+
const childXs = node.children.map((c) => c.x ?? 0);
|
|
5712
|
+
node.x = childXs.reduce((a, b) => a + b, 0) / childXs.length;
|
|
5713
|
+
}
|
|
5714
|
+
};
|
|
5715
|
+
assignX(root);
|
|
5716
|
+
const findMaxHeight = (node) => {
|
|
5717
|
+
if (node.children.length === 0)
|
|
5718
|
+
return node.height;
|
|
5719
|
+
return Math.max(node.height, ...node.children.map(findMaxHeight));
|
|
5720
|
+
};
|
|
5721
|
+
const maxHeight = findMaxHeight(root) || 1;
|
|
5722
|
+
const leafCount = xPos || 1;
|
|
5723
|
+
const mapX = (x) => plotLeft + x / (leafCount - 1 || 1) * plotWidth;
|
|
5724
|
+
const mapY = (h) => plotTop + (1 - h / maxHeight) * (plotHeight - 3);
|
|
5725
|
+
const drawNode = (node) => {
|
|
5726
|
+
if (node.x === undefined)
|
|
5727
|
+
return;
|
|
5728
|
+
if (node.children.length > 0) {
|
|
5729
|
+
const nodeY = Math.round(mapY(node.height));
|
|
5730
|
+
const childXs = node.children.map((c) => Math.round(mapX(c.x ?? 0)));
|
|
5731
|
+
const minX = Math.min(...childXs);
|
|
5732
|
+
const maxX = Math.max(...childXs);
|
|
5733
|
+
for (let x = minX;x <= maxX; x++) {
|
|
5734
|
+
canvas.drawChar(x, nodeY, hConnector, lineColor);
|
|
5735
|
+
}
|
|
5736
|
+
for (const child of node.children) {
|
|
5737
|
+
const childX = Math.round(mapX(child.x ?? 0));
|
|
5738
|
+
const childY = Math.round(mapY(child.height));
|
|
5739
|
+
canvas.drawChar(childX, nodeY, child === node.children[0] ? cornerBL : child === node.children[node.children.length - 1] ? cornerBR : "┴", lineColor);
|
|
5740
|
+
for (let y = nodeY + 1;y < childY; y++) {
|
|
5741
|
+
canvas.drawChar(childX, y, vConnector, lineColor);
|
|
5742
|
+
}
|
|
5743
|
+
drawNode(child);
|
|
5744
|
+
}
|
|
5745
|
+
} else {
|
|
5746
|
+
const x = Math.round(mapX(node.x));
|
|
5747
|
+
const y = hang ? plotBottom - 2 : Math.round(mapY(node.height));
|
|
5748
|
+
canvas.drawChar(x, y, leafChar, leafColor);
|
|
5749
|
+
if (showLabels) {
|
|
5750
|
+
const label = node.id.substring(0, 4);
|
|
5751
|
+
for (let ci = 0;ci < label.length; ci++) {
|
|
5752
|
+
canvas.drawChar(x - Math.floor(label.length / 2) + ci, y + 1, label[ci], labelColor);
|
|
5753
|
+
}
|
|
5754
|
+
}
|
|
5755
|
+
}
|
|
5756
|
+
};
|
|
5757
|
+
drawNode(root);
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
3768
5760
|
var POINT_SHAPES, SIZE_CHARS;
|
|
3769
5761
|
var init_render_geoms = __esm(() => {
|
|
3770
5762
|
init_scales();
|
|
@@ -7077,28 +9069,17 @@ function geom_abline(options = {}) {
|
|
|
7077
9069
|
// src/geoms/qq.ts
|
|
7078
9070
|
function geom_qq(options = {}) {
|
|
7079
9071
|
return {
|
|
7080
|
-
type: "
|
|
7081
|
-
stat: "
|
|
7082
|
-
|
|
7083
|
-
distribution: options.distribution ?? "norm",
|
|
7084
|
-
dparams: options.dparams,
|
|
7085
|
-
size: options.size ?? 1,
|
|
7086
|
-
shape: options.shape ?? "●",
|
|
7087
|
-
color: options.color,
|
|
7088
|
-
alpha: options.alpha ?? 1
|
|
7089
|
-
}
|
|
7090
|
-
};
|
|
7091
|
-
}
|
|
7092
|
-
function geom_qq_line(options = {}) {
|
|
7093
|
-
return {
|
|
7094
|
-
type: "segment",
|
|
7095
|
-
stat: "qq_line",
|
|
9072
|
+
type: "qq",
|
|
9073
|
+
stat: "identity",
|
|
9074
|
+
position: "identity",
|
|
7096
9075
|
params: {
|
|
7097
|
-
distribution: options.distribution ?? "
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
9076
|
+
distribution: options.distribution ?? "normal",
|
|
9077
|
+
show_line: options.show_line ?? true,
|
|
9078
|
+
show_ci: options.show_ci ?? false,
|
|
9079
|
+
conf_level: options.conf_level ?? 0.95,
|
|
9080
|
+
line_color: options.line_color ?? "#ff0000",
|
|
9081
|
+
point_char: options.point_char ?? "●",
|
|
9082
|
+
standardize: options.standardize ?? true
|
|
7102
9083
|
}
|
|
7103
9084
|
};
|
|
7104
9085
|
}
|
|
@@ -7377,11 +9358,383 @@ function geom_treemap(options = {}) {
|
|
|
7377
9358
|
};
|
|
7378
9359
|
}
|
|
7379
9360
|
|
|
9361
|
+
// src/geoms/volcano.ts
|
|
9362
|
+
function geom_volcano(options = {}) {
|
|
9363
|
+
return {
|
|
9364
|
+
type: "volcano",
|
|
9365
|
+
stat: "identity",
|
|
9366
|
+
position: "identity",
|
|
9367
|
+
params: {
|
|
9368
|
+
fc_threshold: options.fc_threshold ?? 1,
|
|
9369
|
+
p_threshold: options.p_threshold ?? 0.05,
|
|
9370
|
+
y_is_neglog10: options.y_is_neglog10 ?? false,
|
|
9371
|
+
up_color: options.up_color ?? "#e41a1c",
|
|
9372
|
+
down_color: options.down_color ?? "#377eb8",
|
|
9373
|
+
ns_color: options.ns_color ?? "#999999",
|
|
9374
|
+
show_thresholds: options.show_thresholds ?? true,
|
|
9375
|
+
threshold_linetype: options.threshold_linetype ?? "dashed",
|
|
9376
|
+
n_labels: options.n_labels ?? 0,
|
|
9377
|
+
size: options.size ?? 1,
|
|
9378
|
+
alpha: options.alpha ?? 0.6,
|
|
9379
|
+
point_char: options.point_char ?? "●",
|
|
9380
|
+
show_legend: options.show_legend ?? true,
|
|
9381
|
+
classify: options.classify
|
|
9382
|
+
}
|
|
9383
|
+
};
|
|
9384
|
+
}
|
|
9385
|
+
|
|
9386
|
+
// src/geoms/ma.ts
|
|
9387
|
+
function geom_ma(options = {}) {
|
|
9388
|
+
return {
|
|
9389
|
+
type: "ma",
|
|
9390
|
+
stat: "identity",
|
|
9391
|
+
position: "identity",
|
|
9392
|
+
params: {
|
|
9393
|
+
fc_threshold: options.fc_threshold ?? 1,
|
|
9394
|
+
p_threshold: options.p_threshold ?? 0.05,
|
|
9395
|
+
p_col: options.p_col,
|
|
9396
|
+
x_is_log2: options.x_is_log2 ?? false,
|
|
9397
|
+
up_color: options.up_color ?? "#e41a1c",
|
|
9398
|
+
down_color: options.down_color ?? "#377eb8",
|
|
9399
|
+
ns_color: options.ns_color ?? "#999999",
|
|
9400
|
+
show_baseline: options.show_baseline ?? true,
|
|
9401
|
+
show_thresholds: options.show_thresholds ?? true,
|
|
9402
|
+
linetype: options.linetype ?? "dashed",
|
|
9403
|
+
n_labels: options.n_labels ?? 0,
|
|
9404
|
+
size: options.size ?? 1,
|
|
9405
|
+
alpha: options.alpha ?? 0.6,
|
|
9406
|
+
point_char: options.point_char ?? "●",
|
|
9407
|
+
show_smooth: options.show_smooth ?? false
|
|
9408
|
+
}
|
|
9409
|
+
};
|
|
9410
|
+
}
|
|
9411
|
+
|
|
9412
|
+
// src/geoms/manhattan.ts
|
|
9413
|
+
function geom_manhattan(options = {}) {
|
|
9414
|
+
return {
|
|
9415
|
+
type: "manhattan",
|
|
9416
|
+
stat: "identity",
|
|
9417
|
+
position: "identity",
|
|
9418
|
+
params: {
|
|
9419
|
+
suggestive_threshold: options.suggestive_threshold ?? 0.00001,
|
|
9420
|
+
genome_wide_threshold: options.genome_wide_threshold ?? 0.00000005,
|
|
9421
|
+
chr_col: options.chr_col,
|
|
9422
|
+
pos_col: options.pos_col,
|
|
9423
|
+
p_col: options.p_col,
|
|
9424
|
+
y_is_neglog10: options.y_is_neglog10 ?? false,
|
|
9425
|
+
chr_colors: options.chr_colors ?? DEFAULT_CHR_COLORS,
|
|
9426
|
+
highlight_color: options.highlight_color ?? "#e41a1c",
|
|
9427
|
+
suggestive_color: options.suggestive_color ?? "#ff7f00",
|
|
9428
|
+
show_thresholds: options.show_thresholds ?? true,
|
|
9429
|
+
threshold_linetype: options.threshold_linetype ?? "dashed",
|
|
9430
|
+
n_labels: options.n_labels ?? 0,
|
|
9431
|
+
label_col: options.label_col,
|
|
9432
|
+
size: options.size ?? 1,
|
|
9433
|
+
alpha: options.alpha ?? 0.6,
|
|
9434
|
+
point_char: options.point_char ?? "●",
|
|
9435
|
+
chr_gap: options.chr_gap ?? 0.02
|
|
9436
|
+
}
|
|
9437
|
+
};
|
|
9438
|
+
}
|
|
9439
|
+
var DEFAULT_CHR_COLORS;
|
|
9440
|
+
var init_manhattan = __esm(() => {
|
|
9441
|
+
DEFAULT_CHR_COLORS = ["#1f78b4", "#a6cee3"];
|
|
9442
|
+
});
|
|
9443
|
+
|
|
9444
|
+
// src/geoms/heatmap.ts
|
|
9445
|
+
function geom_heatmap(options = {}) {
|
|
9446
|
+
return {
|
|
9447
|
+
type: "heatmap",
|
|
9448
|
+
stat: "identity",
|
|
9449
|
+
position: "identity",
|
|
9450
|
+
params: {
|
|
9451
|
+
x_col: options.x_col,
|
|
9452
|
+
y_col: options.y_col,
|
|
9453
|
+
value_col: options.value_col ?? "value",
|
|
9454
|
+
low_color: options.low_color ?? "#313695",
|
|
9455
|
+
mid_color: options.mid_color ?? "#ffffbf",
|
|
9456
|
+
high_color: options.high_color ?? "#a50026",
|
|
9457
|
+
na_color: options.na_color ?? "#808080",
|
|
9458
|
+
midpoint: options.midpoint,
|
|
9459
|
+
cluster_rows: options.cluster_rows ?? false,
|
|
9460
|
+
cluster_cols: options.cluster_cols ?? false,
|
|
9461
|
+
clustering_method: options.clustering_method ?? "complete",
|
|
9462
|
+
clustering_distance: options.clustering_distance ?? "euclidean",
|
|
9463
|
+
show_row_dendrogram: options.show_row_dendrogram ?? true,
|
|
9464
|
+
show_col_dendrogram: options.show_col_dendrogram ?? true,
|
|
9465
|
+
dendrogram_ratio: options.dendrogram_ratio ?? 0.15,
|
|
9466
|
+
show_row_labels: options.show_row_labels ?? true,
|
|
9467
|
+
show_col_labels: options.show_col_labels ?? true,
|
|
9468
|
+
show_values: options.show_values ?? false,
|
|
9469
|
+
value_format: options.value_format ?? ".2f",
|
|
9470
|
+
cell_char: options.cell_char ?? "█",
|
|
9471
|
+
border: options.border ?? false,
|
|
9472
|
+
scale: options.scale ?? "none"
|
|
9473
|
+
}
|
|
9474
|
+
};
|
|
9475
|
+
}
|
|
9476
|
+
|
|
9477
|
+
// src/geoms/biplot.ts
|
|
9478
|
+
function geom_biplot(options = {}) {
|
|
9479
|
+
return {
|
|
9480
|
+
type: "biplot",
|
|
9481
|
+
stat: "identity",
|
|
9482
|
+
position: "identity",
|
|
9483
|
+
params: {
|
|
9484
|
+
pc1_col: options.pc1_col ?? "PC1",
|
|
9485
|
+
pc2_col: options.pc2_col ?? "PC2",
|
|
9486
|
+
loadings: options.loadings,
|
|
9487
|
+
var_explained: options.var_explained,
|
|
9488
|
+
show_scores: options.show_scores ?? true,
|
|
9489
|
+
score_color: options.score_color,
|
|
9490
|
+
score_size: options.score_size ?? 1,
|
|
9491
|
+
score_alpha: options.score_alpha ?? 0.8,
|
|
9492
|
+
score_char: options.score_char ?? "●",
|
|
9493
|
+
show_score_labels: options.show_score_labels ?? false,
|
|
9494
|
+
show_loadings: options.show_loadings ?? true,
|
|
9495
|
+
loading_color: options.loading_color ?? "#e41a1c",
|
|
9496
|
+
loading_scale: options.loading_scale,
|
|
9497
|
+
arrow_char: options.arrow_char ?? "→",
|
|
9498
|
+
show_loading_labels: options.show_loading_labels ?? true,
|
|
9499
|
+
show_origin: options.show_origin ?? true,
|
|
9500
|
+
origin_color: options.origin_color ?? "#999999",
|
|
9501
|
+
show_circle: options.show_circle ?? false,
|
|
9502
|
+
circle_color: options.circle_color ?? "#cccccc"
|
|
9503
|
+
}
|
|
9504
|
+
};
|
|
9505
|
+
}
|
|
9506
|
+
|
|
9507
|
+
// src/geoms/kaplan-meier.ts
|
|
9508
|
+
function geom_kaplan_meier(options = {}) {
|
|
9509
|
+
return {
|
|
9510
|
+
type: "kaplan_meier",
|
|
9511
|
+
stat: "identity",
|
|
9512
|
+
position: "identity",
|
|
9513
|
+
params: {
|
|
9514
|
+
show_ci: options.show_ci ?? false,
|
|
9515
|
+
conf_level: options.conf_level ?? 0.95,
|
|
9516
|
+
show_censored: options.show_censored ?? true,
|
|
9517
|
+
censor_char: options.censor_char ?? "+",
|
|
9518
|
+
show_risk_table: options.show_risk_table ?? false,
|
|
9519
|
+
linetype: options.linetype ?? "solid",
|
|
9520
|
+
show_median: options.show_median ?? false,
|
|
9521
|
+
step_type: options.step_type ?? "post"
|
|
9522
|
+
}
|
|
9523
|
+
};
|
|
9524
|
+
}
|
|
9525
|
+
|
|
9526
|
+
// src/geoms/forest.ts
|
|
9527
|
+
function geom_forest(options = {}) {
|
|
9528
|
+
return {
|
|
9529
|
+
type: "forest",
|
|
9530
|
+
stat: "identity",
|
|
9531
|
+
position: "identity",
|
|
9532
|
+
params: {
|
|
9533
|
+
null_line: options.null_line ?? 1,
|
|
9534
|
+
log_scale: options.log_scale ?? false,
|
|
9535
|
+
show_summary: options.show_summary ?? false,
|
|
9536
|
+
summary_row: options.summary_row,
|
|
9537
|
+
null_line_color: options.null_line_color ?? "#888888",
|
|
9538
|
+
null_line_type: options.null_line_type ?? "dashed",
|
|
9539
|
+
point_char: options.point_char ?? "■",
|
|
9540
|
+
show_weights: options.show_weights ?? false,
|
|
9541
|
+
min_size: options.min_size ?? 1,
|
|
9542
|
+
max_size: options.max_size ?? 3
|
|
9543
|
+
}
|
|
9544
|
+
};
|
|
9545
|
+
}
|
|
9546
|
+
|
|
9547
|
+
// src/geoms/roc.ts
|
|
9548
|
+
function geom_roc(options = {}) {
|
|
9549
|
+
return {
|
|
9550
|
+
type: "roc",
|
|
9551
|
+
stat: "identity",
|
|
9552
|
+
position: "identity",
|
|
9553
|
+
params: {
|
|
9554
|
+
show_diagonal: options.show_diagonal ?? true,
|
|
9555
|
+
diagonal_color: options.diagonal_color ?? "#888888",
|
|
9556
|
+
diagonal_type: options.diagonal_type ?? "dashed",
|
|
9557
|
+
show_auc: options.show_auc ?? true,
|
|
9558
|
+
show_optimal: options.show_optimal ?? false,
|
|
9559
|
+
optimal_char: options.optimal_char ?? "●",
|
|
9560
|
+
show_ci: options.show_ci ?? false,
|
|
9561
|
+
conf_level: options.conf_level ?? 0.95,
|
|
9562
|
+
fill_auc: options.fill_auc ?? false,
|
|
9563
|
+
fill_alpha: options.fill_alpha ?? 0.3
|
|
9564
|
+
}
|
|
9565
|
+
};
|
|
9566
|
+
}
|
|
9567
|
+
|
|
9568
|
+
// src/geoms/bland-altman.ts
|
|
9569
|
+
function geom_bland_altman(options = {}) {
|
|
9570
|
+
return {
|
|
9571
|
+
type: "bland_altman",
|
|
9572
|
+
stat: "identity",
|
|
9573
|
+
position: "identity",
|
|
9574
|
+
params: {
|
|
9575
|
+
show_limits: options.show_limits ?? true,
|
|
9576
|
+
show_bias: options.show_bias ?? true,
|
|
9577
|
+
limit_multiplier: options.limit_multiplier ?? 1.96,
|
|
9578
|
+
bias_color: options.bias_color ?? "#0000ff",
|
|
9579
|
+
limit_color: options.limit_color ?? "#ff0000",
|
|
9580
|
+
linetype: options.linetype ?? "dashed",
|
|
9581
|
+
show_ci: options.show_ci ?? false,
|
|
9582
|
+
conf_level: options.conf_level ?? 0.95,
|
|
9583
|
+
point_char: options.point_char ?? "●",
|
|
9584
|
+
percent_diff: options.percent_diff ?? false,
|
|
9585
|
+
precomputed: options.precomputed ?? false
|
|
9586
|
+
}
|
|
9587
|
+
};
|
|
9588
|
+
}
|
|
9589
|
+
|
|
9590
|
+
// src/geoms/ecdf.ts
|
|
9591
|
+
function geom_ecdf(options = {}) {
|
|
9592
|
+
return {
|
|
9593
|
+
type: "ecdf",
|
|
9594
|
+
stat: "identity",
|
|
9595
|
+
position: "identity",
|
|
9596
|
+
params: {
|
|
9597
|
+
pad: options.pad ?? true,
|
|
9598
|
+
show_ci: options.show_ci ?? false,
|
|
9599
|
+
conf_level: options.conf_level ?? 0.95,
|
|
9600
|
+
step_type: options.step_type ?? "post",
|
|
9601
|
+
show_points: options.show_points ?? false,
|
|
9602
|
+
line_char: options.line_char ?? "─",
|
|
9603
|
+
complement: options.complement ?? false
|
|
9604
|
+
}
|
|
9605
|
+
};
|
|
9606
|
+
}
|
|
9607
|
+
|
|
9608
|
+
// src/geoms/funnel.ts
|
|
9609
|
+
function geom_funnel(options = {}) {
|
|
9610
|
+
return {
|
|
9611
|
+
type: "funnel",
|
|
9612
|
+
stat: "identity",
|
|
9613
|
+
position: "identity",
|
|
9614
|
+
params: {
|
|
9615
|
+
show_contours: options.show_contours ?? true,
|
|
9616
|
+
contour_levels: options.contour_levels ?? [0.95],
|
|
9617
|
+
show_significance: options.show_significance ?? false,
|
|
9618
|
+
summary_effect: options.summary_effect,
|
|
9619
|
+
show_summary_line: options.show_summary_line ?? true,
|
|
9620
|
+
y_is_se: options.y_is_se ?? true,
|
|
9621
|
+
invert_y: options.invert_y ?? true,
|
|
9622
|
+
point_char: options.point_char ?? "●",
|
|
9623
|
+
contour_color: options.contour_color ?? "#888888"
|
|
9624
|
+
}
|
|
9625
|
+
};
|
|
9626
|
+
}
|
|
9627
|
+
|
|
9628
|
+
// src/geoms/control.ts
|
|
9629
|
+
function geom_control(options = {}) {
|
|
9630
|
+
return {
|
|
9631
|
+
type: "control",
|
|
9632
|
+
stat: "identity",
|
|
9633
|
+
position: "identity",
|
|
9634
|
+
params: {
|
|
9635
|
+
chart_type: options.chart_type ?? "i",
|
|
9636
|
+
sigma: options.sigma ?? 3,
|
|
9637
|
+
show_center: options.show_center ?? true,
|
|
9638
|
+
show_ucl: options.show_ucl ?? true,
|
|
9639
|
+
show_lcl: options.show_lcl ?? true,
|
|
9640
|
+
show_warning: options.show_warning ?? false,
|
|
9641
|
+
center: options.center,
|
|
9642
|
+
ucl: options.ucl,
|
|
9643
|
+
lcl: options.lcl,
|
|
9644
|
+
center_color: options.center_color ?? "#0000ff",
|
|
9645
|
+
limit_color: options.limit_color ?? "#ff0000",
|
|
9646
|
+
warning_color: options.warning_color ?? "#ffa500",
|
|
9647
|
+
connect_points: options.connect_points ?? true,
|
|
9648
|
+
highlight_ooc: options.highlight_ooc ?? true,
|
|
9649
|
+
ooc_char: options.ooc_char ?? "◆",
|
|
9650
|
+
point_char: options.point_char ?? "●"
|
|
9651
|
+
}
|
|
9652
|
+
};
|
|
9653
|
+
}
|
|
9654
|
+
|
|
9655
|
+
// src/geoms/scree.ts
|
|
9656
|
+
function geom_scree(options = {}) {
|
|
9657
|
+
return {
|
|
9658
|
+
type: "scree",
|
|
9659
|
+
stat: "identity",
|
|
9660
|
+
position: "identity",
|
|
9661
|
+
params: {
|
|
9662
|
+
show_cumulative: options.show_cumulative ?? false,
|
|
9663
|
+
show_kaiser: options.show_kaiser ?? false,
|
|
9664
|
+
show_elbow: options.show_elbow ?? false,
|
|
9665
|
+
show_broken_stick: options.show_broken_stick ?? false,
|
|
9666
|
+
connect_points: options.connect_points ?? true,
|
|
9667
|
+
show_bars: options.show_bars ?? false,
|
|
9668
|
+
point_char: options.point_char ?? "●",
|
|
9669
|
+
color: options.color,
|
|
9670
|
+
cumulative_color: options.cumulative_color ?? "#ff0000",
|
|
9671
|
+
kaiser_color: options.kaiser_color ?? "#888888",
|
|
9672
|
+
y_format: options.y_format ?? "percentage",
|
|
9673
|
+
threshold: options.threshold,
|
|
9674
|
+
threshold_color: options.threshold_color ?? "#00aa00"
|
|
9675
|
+
}
|
|
9676
|
+
};
|
|
9677
|
+
}
|
|
9678
|
+
|
|
9679
|
+
// src/geoms/upset.ts
|
|
9680
|
+
function geom_upset(options = {}) {
|
|
9681
|
+
return {
|
|
9682
|
+
type: "upset",
|
|
9683
|
+
stat: "identity",
|
|
9684
|
+
position: "identity",
|
|
9685
|
+
params: {
|
|
9686
|
+
sets: options.sets,
|
|
9687
|
+
min_size: options.min_size ?? 1,
|
|
9688
|
+
max_intersections: options.max_intersections ?? 20,
|
|
9689
|
+
sort_by: options.sort_by ?? "size",
|
|
9690
|
+
sort_order: options.sort_order ?? "desc",
|
|
9691
|
+
show_set_sizes: options.show_set_sizes ?? true,
|
|
9692
|
+
dot_char: options.dot_char ?? "●",
|
|
9693
|
+
empty_char: options.empty_char ?? "○",
|
|
9694
|
+
line_char: options.line_char ?? "│",
|
|
9695
|
+
bar_char: options.bar_char ?? "█",
|
|
9696
|
+
color: options.color,
|
|
9697
|
+
show_degree: options.show_degree ?? false
|
|
9698
|
+
}
|
|
9699
|
+
};
|
|
9700
|
+
}
|
|
9701
|
+
|
|
9702
|
+
// src/geoms/dendrogram.ts
|
|
9703
|
+
function geom_dendrogram(options = {}) {
|
|
9704
|
+
return {
|
|
9705
|
+
type: "dendrogram",
|
|
9706
|
+
stat: "identity",
|
|
9707
|
+
position: "identity",
|
|
9708
|
+
params: {
|
|
9709
|
+
orientation: options.orientation ?? "vertical",
|
|
9710
|
+
labels: options.labels,
|
|
9711
|
+
show_labels: options.show_labels ?? true,
|
|
9712
|
+
hang: options.hang ?? false,
|
|
9713
|
+
cut_height: options.cut_height,
|
|
9714
|
+
k: options.k,
|
|
9715
|
+
branch_char: options.branch_char ?? "│",
|
|
9716
|
+
h_connector: options.h_connector ?? "─",
|
|
9717
|
+
v_connector: options.v_connector ?? "│",
|
|
9718
|
+
corner_tl: options.corner_tl ?? "┌",
|
|
9719
|
+
corner_tr: options.corner_tr ?? "┐",
|
|
9720
|
+
corner_bl: options.corner_bl ?? "└",
|
|
9721
|
+
corner_br: options.corner_br ?? "┘",
|
|
9722
|
+
leaf_char: options.leaf_char ?? "○",
|
|
9723
|
+
cluster_colors: options.cluster_colors,
|
|
9724
|
+
line_style: options.line_style ?? "square",
|
|
9725
|
+
parent_col: options.parent_col ?? "parent",
|
|
9726
|
+
height_col: options.height_col ?? "height",
|
|
9727
|
+
id_col: options.id_col ?? "id"
|
|
9728
|
+
}
|
|
9729
|
+
};
|
|
9730
|
+
}
|
|
9731
|
+
|
|
7380
9732
|
// src/geoms/index.ts
|
|
7381
9733
|
var init_geoms = __esm(() => {
|
|
7382
9734
|
init_ridgeline();
|
|
7383
9735
|
init_sparkline();
|
|
7384
9736
|
init_braille();
|
|
9737
|
+
init_manhattan();
|
|
7385
9738
|
});
|
|
7386
9739
|
|
|
7387
9740
|
// src/scales/continuous.ts
|