@ggterm/core 0.2.17 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-plot.js +3128 -118
- package/dist/cli.js +2287 -23
- 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 +15 -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/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2315 -25
- package/dist/pipeline/pipeline.d.ts.map +1 -1
- package/dist/pipeline/render-geoms.d.ts +4 -0
- package/dist/pipeline/render-geoms.d.ts.map +1 -1
- package/dist/serve.d.ts +8 -0
- package/dist/serve.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/cli-plot.js
CHANGED
|
@@ -3708,6 +3708,96 @@ function renderGeomVolcano(data, geom, aes, scales, canvas) {
|
|
|
3708
3708
|
}
|
|
3709
3709
|
if (nLabels > 0 && aes.label) {
|
|
3710
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: 255, g: 255, b: 255, 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);
|
|
3711
3801
|
const labelColor = { r: 50, g: 50, b: 50, a: 1 };
|
|
3712
3802
|
for (const point of significantPoints) {
|
|
3713
3803
|
const cx = Math.round(scales.x.map(point.x));
|
|
@@ -3721,6 +3811,1385 @@ function renderGeomVolcano(data, geom, aes, scales, canvas) {
|
|
|
3721
3811
|
}
|
|
3722
3812
|
}
|
|
3723
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
|
+
const label = row.study;
|
|
4490
|
+
const labelColor = { r: 200, g: 200, b: 200, a: 1 };
|
|
4491
|
+
const labelEnd = plotLeft - 1;
|
|
4492
|
+
const labelStart = labelEnd - label.length;
|
|
4493
|
+
for (let c = 0;c < label.length; c++) {
|
|
4494
|
+
const charX = labelStart + c;
|
|
4495
|
+
if (charX >= 0) {
|
|
4496
|
+
canvas.drawChar(charX, y, label[c], labelColor);
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
function renderGeomRoc(data, geom, aes, scales, canvas) {
|
|
4502
|
+
const params = geom.params || {};
|
|
4503
|
+
const showDiagonal = Boolean(params.show_diagonal ?? true);
|
|
4504
|
+
const diagonalColor = String(params.diagonal_color ?? "#888888");
|
|
4505
|
+
const showAuc = Boolean(params.show_auc ?? true);
|
|
4506
|
+
const showOptimal = Boolean(params.show_optimal ?? false);
|
|
4507
|
+
const optimalChar = String(params.optimal_char ?? "●");
|
|
4508
|
+
if (!Array.isArray(data) || data.length === 0)
|
|
4509
|
+
return;
|
|
4510
|
+
const parseHex = (hex) => {
|
|
4511
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
4512
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
4513
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
4514
|
+
return { r, g, b, a: 1 };
|
|
4515
|
+
};
|
|
4516
|
+
const diagColor = parseHex(diagonalColor);
|
|
4517
|
+
const xField = typeof aes.x === "string" ? aes.x : "fpr";
|
|
4518
|
+
const yField = typeof aes.y === "string" ? aes.y : "tpr";
|
|
4519
|
+
const colorField = typeof aes.color === "string" ? aes.color : undefined;
|
|
4520
|
+
const groups = new Map;
|
|
4521
|
+
for (const row of data) {
|
|
4522
|
+
const fpr = Number(row[xField] ?? 0);
|
|
4523
|
+
const tpr = Number(row[yField] ?? 0);
|
|
4524
|
+
const group = colorField ? String(row[colorField] ?? "default") : "default";
|
|
4525
|
+
if (!groups.has(group))
|
|
4526
|
+
groups.set(group, []);
|
|
4527
|
+
groups.get(group).push({ fpr, tpr });
|
|
4528
|
+
}
|
|
4529
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4530
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4531
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4532
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4533
|
+
const mapX = (v) => plotLeft + v * (plotRight - plotLeft);
|
|
4534
|
+
const mapY = (v) => plotBottom - v * (plotBottom - plotTop);
|
|
4535
|
+
if (showDiagonal) {
|
|
4536
|
+
const steps = plotRight - plotLeft;
|
|
4537
|
+
for (let i = 0;i <= steps; i += 2) {
|
|
4538
|
+
const t = i / steps;
|
|
4539
|
+
const x = Math.round(mapX(t));
|
|
4540
|
+
const y = Math.round(mapY(t));
|
|
4541
|
+
canvas.drawChar(x, y, "·", diagColor);
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
const colors = [
|
|
4545
|
+
{ r: 31, g: 119, b: 180, a: 1 },
|
|
4546
|
+
{ r: 255, g: 127, b: 14, a: 1 },
|
|
4547
|
+
{ r: 44, g: 160, b: 44, a: 1 },
|
|
4548
|
+
{ r: 214, g: 39, b: 40, a: 1 }
|
|
4549
|
+
];
|
|
4550
|
+
let colorIndex = 0;
|
|
4551
|
+
for (const [, points] of groups) {
|
|
4552
|
+
const color = colors[colorIndex % colors.length];
|
|
4553
|
+
colorIndex++;
|
|
4554
|
+
points.sort((a, b) => a.fpr - b.fpr);
|
|
4555
|
+
let auc = 0;
|
|
4556
|
+
for (let i = 1;i < points.length; i++) {
|
|
4557
|
+
const dx = points[i].fpr - points[i - 1].fpr;
|
|
4558
|
+
const avgY = (points[i].tpr + points[i - 1].tpr) / 2;
|
|
4559
|
+
auc += dx * avgY;
|
|
4560
|
+
}
|
|
4561
|
+
let optimalPoint = points[0];
|
|
4562
|
+
let maxJ = -Infinity;
|
|
4563
|
+
for (const p of points) {
|
|
4564
|
+
const j = p.tpr - p.fpr;
|
|
4565
|
+
if (j > maxJ) {
|
|
4566
|
+
maxJ = j;
|
|
4567
|
+
optimalPoint = p;
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
for (let i = 0;i < points.length; i++) {
|
|
4571
|
+
const p = points[i];
|
|
4572
|
+
const x = Math.round(mapX(p.fpr));
|
|
4573
|
+
const y = Math.round(mapY(p.tpr));
|
|
4574
|
+
if (i > 0) {
|
|
4575
|
+
const prev = points[i - 1];
|
|
4576
|
+
const px = Math.round(mapX(prev.fpr));
|
|
4577
|
+
const py = Math.round(mapY(prev.tpr));
|
|
4578
|
+
const steps = Math.max(Math.abs(x - px), Math.abs(y - py));
|
|
4579
|
+
for (let s = 0;s <= steps; s++) {
|
|
4580
|
+
const t = steps > 0 ? s / steps : 0;
|
|
4581
|
+
const lx = Math.round(px + (x - px) * t);
|
|
4582
|
+
const ly = Math.round(py + (y - py) * t);
|
|
4583
|
+
canvas.drawChar(lx, ly, "─", color);
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
canvas.drawChar(x, y, "●", color);
|
|
4587
|
+
}
|
|
4588
|
+
if (showOptimal) {
|
|
4589
|
+
const ox = Math.round(mapX(optimalPoint.fpr));
|
|
4590
|
+
const oy = Math.round(mapY(optimalPoint.tpr));
|
|
4591
|
+
canvas.drawChar(ox, oy, optimalChar, { r: 255, g: 0, b: 0, a: 1 });
|
|
4592
|
+
}
|
|
4593
|
+
if (showAuc && colorIndex === 1) {
|
|
4594
|
+
const aucText = `AUC=${auc.toFixed(3)}`;
|
|
4595
|
+
const labelColor = { r: 100, g: 100, b: 100, a: 1 };
|
|
4596
|
+
for (let i = 0;i < aucText.length; i++) {
|
|
4597
|
+
canvas.drawChar(plotRight - aucText.length + i, plotTop + 1, aucText[i], labelColor);
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
function renderGeomBlandAltman(data, geom, aes, scales, canvas) {
|
|
4603
|
+
const params = geom.params || {};
|
|
4604
|
+
const showLimits = Boolean(params.show_limits ?? true);
|
|
4605
|
+
const showBias = Boolean(params.show_bias ?? true);
|
|
4606
|
+
const limitMultiplier = Number(params.limit_multiplier ?? 1.96);
|
|
4607
|
+
const biasColor = String(params.bias_color ?? "#0000ff");
|
|
4608
|
+
const limitColor = String(params.limit_color ?? "#ff0000");
|
|
4609
|
+
const pointChar = String(params.point_char ?? "●");
|
|
4610
|
+
const precomputed = Boolean(params.precomputed ?? false);
|
|
4611
|
+
if (!Array.isArray(data) || data.length === 0)
|
|
4612
|
+
return;
|
|
4613
|
+
const parseHex = (hex) => {
|
|
4614
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
4615
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
4616
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
4617
|
+
return { r, g, b, a: 1 };
|
|
4618
|
+
};
|
|
4619
|
+
const biasColorParsed = parseHex(biasColor);
|
|
4620
|
+
const limitColorParsed = parseHex(limitColor);
|
|
4621
|
+
const xField = typeof aes.x === "string" ? aes.x : "method1";
|
|
4622
|
+
const yField = typeof aes.y === "string" ? aes.y : "method2";
|
|
4623
|
+
const points = [];
|
|
4624
|
+
if (precomputed) {
|
|
4625
|
+
for (const row of data) {
|
|
4626
|
+
const mean = Number(row[xField] ?? 0);
|
|
4627
|
+
const diff = Number(row[yField] ?? 0);
|
|
4628
|
+
points.push({ mean, diff });
|
|
4629
|
+
}
|
|
4630
|
+
} else {
|
|
4631
|
+
for (const row of data) {
|
|
4632
|
+
const m1 = Number(row[xField] ?? 0);
|
|
4633
|
+
const m2 = Number(row[yField] ?? 0);
|
|
4634
|
+
const mean = (m1 + m2) / 2;
|
|
4635
|
+
const diff = m1 - m2;
|
|
4636
|
+
points.push({ mean, diff });
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4639
|
+
if (points.length === 0)
|
|
4640
|
+
return;
|
|
4641
|
+
const diffs = points.map((p) => p.diff);
|
|
4642
|
+
const bias = diffs.reduce((a, b) => a + b, 0) / diffs.length;
|
|
4643
|
+
const variance = diffs.reduce((a, b) => a + Math.pow(b - bias, 2), 0) / (diffs.length - 1);
|
|
4644
|
+
const sd = Math.sqrt(variance);
|
|
4645
|
+
const upperLimit = bias + limitMultiplier * sd;
|
|
4646
|
+
const lowerLimit = bias - limitMultiplier * sd;
|
|
4647
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4648
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4649
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4650
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4651
|
+
const minMean = Math.min(...points.map((p) => p.mean));
|
|
4652
|
+
const maxMean = Math.max(...points.map((p) => p.mean));
|
|
4653
|
+
const minDiff = Math.min(...points.map((p) => p.diff), lowerLimit);
|
|
4654
|
+
const maxDiff = Math.max(...points.map((p) => p.diff), upperLimit);
|
|
4655
|
+
const mapX = (v) => plotLeft + (v - minMean) / (maxMean - minMean) * (plotRight - plotLeft);
|
|
4656
|
+
const mapY = (v) => plotBottom - (v - minDiff) / (maxDiff - minDiff) * (plotBottom - plotTop);
|
|
4657
|
+
if (showBias) {
|
|
4658
|
+
const biasY = Math.round(mapY(bias));
|
|
4659
|
+
for (let x = plotLeft;x <= plotRight; x++) {
|
|
4660
|
+
canvas.drawChar(x, biasY, "─", biasColorParsed);
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
if (showLimits) {
|
|
4664
|
+
const upperY = Math.round(mapY(upperLimit));
|
|
4665
|
+
const lowerY = Math.round(mapY(lowerLimit));
|
|
4666
|
+
for (let x = plotLeft;x <= plotRight; x += 2) {
|
|
4667
|
+
canvas.drawChar(x, upperY, "─", limitColorParsed);
|
|
4668
|
+
canvas.drawChar(x, lowerY, "─", limitColorParsed);
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
const pointColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
4672
|
+
for (const p of points) {
|
|
4673
|
+
const x = Math.round(mapX(p.mean));
|
|
4674
|
+
const y = Math.round(mapY(p.diff));
|
|
4675
|
+
canvas.drawChar(x, y, pointChar, pointColor);
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
function renderGeomQQ(data, geom, aes, scales, canvas) {
|
|
4679
|
+
const params = geom.params || {};
|
|
4680
|
+
const showLine = params.show_line ?? true;
|
|
4681
|
+
const lineColor = params.line_color ?? "#ff0000";
|
|
4682
|
+
const pointChar = params.point_char ?? "●";
|
|
4683
|
+
const standardize = params.standardize ?? true;
|
|
4684
|
+
const parseHex = (hex) => {
|
|
4685
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
4686
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
4687
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
4688
|
+
return { r, g, b, a: 1 };
|
|
4689
|
+
};
|
|
4690
|
+
const lineColorParsed = parseHex(lineColor);
|
|
4691
|
+
const sampleField = typeof aes.x === "string" ? aes.x : "x";
|
|
4692
|
+
const values = [];
|
|
4693
|
+
for (const row of data) {
|
|
4694
|
+
const v = Number(row[sampleField]);
|
|
4695
|
+
if (!isNaN(v))
|
|
4696
|
+
values.push(v);
|
|
4697
|
+
}
|
|
4698
|
+
if (values.length === 0)
|
|
4699
|
+
return;
|
|
4700
|
+
values.sort((a, b) => a - b);
|
|
4701
|
+
const n = values.length;
|
|
4702
|
+
const mean = values.reduce((a, b) => a + b, 0) / n;
|
|
4703
|
+
const variance = values.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / (n - 1);
|
|
4704
|
+
const sd = Math.sqrt(variance);
|
|
4705
|
+
const qnorm = (p) => {
|
|
4706
|
+
if (p <= 0)
|
|
4707
|
+
return -Infinity;
|
|
4708
|
+
if (p >= 1)
|
|
4709
|
+
return Infinity;
|
|
4710
|
+
if (p === 0.5)
|
|
4711
|
+
return 0;
|
|
4712
|
+
const a = [
|
|
4713
|
+
-39.69683028665376,
|
|
4714
|
+
220.9460984245205,
|
|
4715
|
+
-275.9285104469687,
|
|
4716
|
+
138.357751867269,
|
|
4717
|
+
-30.66479806614716,
|
|
4718
|
+
2.506628277459239
|
|
4719
|
+
];
|
|
4720
|
+
const b = [
|
|
4721
|
+
-54.47609879822406,
|
|
4722
|
+
161.5858368580409,
|
|
4723
|
+
-155.6989798598866,
|
|
4724
|
+
66.80131188771972,
|
|
4725
|
+
-13.28068155288572
|
|
4726
|
+
];
|
|
4727
|
+
const c = [
|
|
4728
|
+
-0.007784894002430293,
|
|
4729
|
+
-0.3223964580411365,
|
|
4730
|
+
-2.400758277161838,
|
|
4731
|
+
-2.549732539343734,
|
|
4732
|
+
4.374664141464968,
|
|
4733
|
+
2.938163982698783
|
|
4734
|
+
];
|
|
4735
|
+
const d = [
|
|
4736
|
+
0.007784695709041462,
|
|
4737
|
+
0.3224671290700398,
|
|
4738
|
+
2.445134137142996,
|
|
4739
|
+
3.754408661907416
|
|
4740
|
+
];
|
|
4741
|
+
const pLow = 0.02425;
|
|
4742
|
+
const pHigh = 1 - pLow;
|
|
4743
|
+
let q;
|
|
4744
|
+
if (p < pLow) {
|
|
4745
|
+
q = Math.sqrt(-2 * Math.log(p));
|
|
4746
|
+
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);
|
|
4747
|
+
} else if (p <= pHigh) {
|
|
4748
|
+
q = p - 0.5;
|
|
4749
|
+
const r = q * q;
|
|
4750
|
+
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);
|
|
4751
|
+
} else {
|
|
4752
|
+
q = Math.sqrt(-2 * Math.log(1 - p));
|
|
4753
|
+
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);
|
|
4754
|
+
}
|
|
4755
|
+
};
|
|
4756
|
+
const points = [];
|
|
4757
|
+
for (let i = 0;i < n; i++) {
|
|
4758
|
+
const p = (i + 0.5) / n;
|
|
4759
|
+
const theoretical = qnorm(p);
|
|
4760
|
+
const sample = standardize ? (values[i] - mean) / sd : values[i];
|
|
4761
|
+
points.push({ theoretical, sample });
|
|
4762
|
+
}
|
|
4763
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4764
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4765
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4766
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4767
|
+
const minT = Math.min(...points.map((p) => p.theoretical));
|
|
4768
|
+
const maxT = Math.max(...points.map((p) => p.theoretical));
|
|
4769
|
+
const minS = Math.min(...points.map((p) => p.sample));
|
|
4770
|
+
const maxS = Math.max(...points.map((p) => p.sample));
|
|
4771
|
+
const minVal = Math.min(minT, minS);
|
|
4772
|
+
const maxVal = Math.max(maxT, maxS);
|
|
4773
|
+
const mapX = (v) => plotLeft + (v - minVal) / (maxVal - minVal) * (plotRight - plotLeft);
|
|
4774
|
+
const mapY = (v) => plotBottom - (v - minVal) / (maxVal - minVal) * (plotBottom - plotTop);
|
|
4775
|
+
if (showLine) {
|
|
4776
|
+
const steps = plotRight - plotLeft;
|
|
4777
|
+
for (let i = 0;i <= steps; i++) {
|
|
4778
|
+
const v = minVal + i / steps * (maxVal - minVal);
|
|
4779
|
+
const x = Math.round(mapX(v));
|
|
4780
|
+
const y = Math.round(mapY(v));
|
|
4781
|
+
if (y >= plotTop && y <= plotBottom) {
|
|
4782
|
+
canvas.drawChar(x, y, "─", lineColorParsed);
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
const pointColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
4787
|
+
for (const p of points) {
|
|
4788
|
+
const x = Math.round(mapX(p.theoretical));
|
|
4789
|
+
const y = Math.round(mapY(p.sample));
|
|
4790
|
+
canvas.drawChar(x, y, pointChar, pointColor);
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4793
|
+
function renderGeomECDF(data, geom, aes, scales, canvas) {
|
|
4794
|
+
const params = geom.params || {};
|
|
4795
|
+
const complement = params.complement ?? false;
|
|
4796
|
+
const showPoints = params.show_points ?? false;
|
|
4797
|
+
const xField = typeof aes.x === "string" ? aes.x : "x";
|
|
4798
|
+
const colorField = typeof aes.color === "string" ? aes.color : null;
|
|
4799
|
+
const groups = new Map;
|
|
4800
|
+
for (const row of data) {
|
|
4801
|
+
const v = Number(row[xField]);
|
|
4802
|
+
if (isNaN(v))
|
|
4803
|
+
continue;
|
|
4804
|
+
const groupKey = colorField ? String(row[colorField] ?? "default") : "default";
|
|
4805
|
+
if (!groups.has(groupKey))
|
|
4806
|
+
groups.set(groupKey, []);
|
|
4807
|
+
groups.get(groupKey).push(v);
|
|
4808
|
+
}
|
|
4809
|
+
if (groups.size === 0)
|
|
4810
|
+
return;
|
|
4811
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4812
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4813
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4814
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4815
|
+
let globalMin = Infinity;
|
|
4816
|
+
let globalMax = -Infinity;
|
|
4817
|
+
for (const values of groups.values()) {
|
|
4818
|
+
globalMin = Math.min(globalMin, ...values);
|
|
4819
|
+
globalMax = Math.max(globalMax, ...values);
|
|
4820
|
+
}
|
|
4821
|
+
const mapX = (v) => plotLeft + (v - globalMin) / (globalMax - globalMin) * (plotRight - plotLeft);
|
|
4822
|
+
const mapY = (v) => {
|
|
4823
|
+
const ecdf = complement ? 1 - v : v;
|
|
4824
|
+
return plotBottom - ecdf * (plotBottom - plotTop);
|
|
4825
|
+
};
|
|
4826
|
+
const colors = [
|
|
4827
|
+
{ r: 31, g: 119, b: 180, a: 1 },
|
|
4828
|
+
{ r: 255, g: 127, b: 14, a: 1 },
|
|
4829
|
+
{ r: 44, g: 160, b: 44, a: 1 },
|
|
4830
|
+
{ r: 214, g: 39, b: 40, a: 1 },
|
|
4831
|
+
{ r: 148, g: 103, b: 189, a: 1 }
|
|
4832
|
+
];
|
|
4833
|
+
let colorIdx = 0;
|
|
4834
|
+
for (const [, values] of groups) {
|
|
4835
|
+
const color = colors[colorIdx % colors.length];
|
|
4836
|
+
colorIdx++;
|
|
4837
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4838
|
+
const n = sorted.length;
|
|
4839
|
+
let prevX = plotLeft;
|
|
4840
|
+
let prevY = Math.round(mapY(0));
|
|
4841
|
+
for (let i = 0;i < n; i++) {
|
|
4842
|
+
const ecdfVal = (i + 1) / n;
|
|
4843
|
+
const x = Math.round(mapX(sorted[i]));
|
|
4844
|
+
const y = Math.round(mapY(ecdfVal));
|
|
4845
|
+
for (let px = prevX;px <= x; px++) {
|
|
4846
|
+
canvas.drawChar(px, prevY, "─", color);
|
|
4847
|
+
}
|
|
4848
|
+
const stepDir = y < prevY ? -1 : 1;
|
|
4849
|
+
for (let py = prevY;stepDir > 0 ? py <= y : py >= y; py += stepDir) {
|
|
4850
|
+
canvas.drawChar(x, py, "│", color);
|
|
4851
|
+
}
|
|
4852
|
+
if (showPoints) {
|
|
4853
|
+
canvas.drawChar(x, y, "●", color);
|
|
4854
|
+
}
|
|
4855
|
+
prevX = x;
|
|
4856
|
+
prevY = y;
|
|
4857
|
+
}
|
|
4858
|
+
for (let px = prevX;px <= plotRight; px++) {
|
|
4859
|
+
canvas.drawChar(px, prevY, "─", color);
|
|
4860
|
+
}
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
function renderGeomFunnel(data, geom, aes, scales, canvas) {
|
|
4864
|
+
const params = geom.params || {};
|
|
4865
|
+
const showContours = params.show_contours ?? true;
|
|
4866
|
+
const showSummaryLine = params.show_summary_line ?? true;
|
|
4867
|
+
const summaryEffect = params.summary_effect;
|
|
4868
|
+
const pointChar = params.point_char ?? "●";
|
|
4869
|
+
const contourColor = params.contour_color ?? "#888888";
|
|
4870
|
+
const invertY = params.invert_y ?? true;
|
|
4871
|
+
const parseHex = (hex) => {
|
|
4872
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
4873
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
4874
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
4875
|
+
return { r, g, b, a: 1 };
|
|
4876
|
+
};
|
|
4877
|
+
const contourColorParsed = parseHex(contourColor);
|
|
4878
|
+
const xField = typeof aes.x === "string" ? aes.x : "effect";
|
|
4879
|
+
const yField = typeof aes.y === "string" ? aes.y : "se";
|
|
4880
|
+
const points = [];
|
|
4881
|
+
for (const row of data) {
|
|
4882
|
+
const effect = Number(row[xField]);
|
|
4883
|
+
const se = Number(row[yField]);
|
|
4884
|
+
if (!isNaN(effect) && !isNaN(se)) {
|
|
4885
|
+
points.push({ effect, se });
|
|
4886
|
+
}
|
|
4887
|
+
}
|
|
4888
|
+
if (points.length === 0)
|
|
4889
|
+
return;
|
|
4890
|
+
const summary = summaryEffect ?? points.reduce((a, b) => a + b.effect, 0) / points.length;
|
|
4891
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4892
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4893
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4894
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4895
|
+
const minEffect = Math.min(...points.map((p) => p.effect));
|
|
4896
|
+
const maxEffect = Math.max(...points.map((p) => p.effect));
|
|
4897
|
+
const maxSE = Math.max(...points.map((p) => p.se));
|
|
4898
|
+
const effectPad = (maxEffect - minEffect) * 0.2;
|
|
4899
|
+
const effectMin = minEffect - effectPad;
|
|
4900
|
+
const effectMax = maxEffect + effectPad;
|
|
4901
|
+
const mapX = (v) => plotLeft + (v - effectMin) / (effectMax - effectMin) * (plotRight - plotLeft);
|
|
4902
|
+
const mapY = (v) => {
|
|
4903
|
+
if (invertY) {
|
|
4904
|
+
return plotTop + v / maxSE * (plotBottom - plotTop);
|
|
4905
|
+
}
|
|
4906
|
+
return plotBottom - v / maxSE * (plotBottom - plotTop);
|
|
4907
|
+
};
|
|
4908
|
+
if (showContours) {
|
|
4909
|
+
const z = 1.96;
|
|
4910
|
+
for (let se = 0;se <= maxSE; se += maxSE / 40) {
|
|
4911
|
+
const leftBound = summary - z * se;
|
|
4912
|
+
const rightBound = summary + z * se;
|
|
4913
|
+
const y = Math.round(mapY(se));
|
|
4914
|
+
const leftX = Math.round(mapX(leftBound));
|
|
4915
|
+
const rightX = Math.round(mapX(rightBound));
|
|
4916
|
+
if (leftX >= plotLeft && leftX <= plotRight) {
|
|
4917
|
+
canvas.drawChar(leftX, y, "·", contourColorParsed);
|
|
4918
|
+
}
|
|
4919
|
+
if (rightX >= plotLeft && rightX <= plotRight) {
|
|
4920
|
+
canvas.drawChar(rightX, y, "·", contourColorParsed);
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
if (showSummaryLine) {
|
|
4925
|
+
const summaryX = Math.round(mapX(summary));
|
|
4926
|
+
for (let y = plotTop;y <= plotBottom; y += 2) {
|
|
4927
|
+
canvas.drawChar(summaryX, y, "│", contourColorParsed);
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
const pointColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
4931
|
+
for (const p of points) {
|
|
4932
|
+
const x = Math.round(mapX(p.effect));
|
|
4933
|
+
const y = Math.round(mapY(p.se));
|
|
4934
|
+
canvas.drawChar(x, y, pointChar, pointColor);
|
|
4935
|
+
}
|
|
4936
|
+
}
|
|
4937
|
+
function renderGeomControl(data, geom, aes, scales, canvas) {
|
|
4938
|
+
const params = geom.params || {};
|
|
4939
|
+
const sigma = params.sigma ?? 3;
|
|
4940
|
+
const showCenter = params.show_center ?? true;
|
|
4941
|
+
const showUCL = params.show_ucl ?? true;
|
|
4942
|
+
const showLCL = params.show_lcl ?? true;
|
|
4943
|
+
const showWarning = params.show_warning ?? false;
|
|
4944
|
+
const customCenter = params.center;
|
|
4945
|
+
const customUCL = params.ucl;
|
|
4946
|
+
const customLCL = params.lcl;
|
|
4947
|
+
const centerColor = params.center_color ?? "#0000ff";
|
|
4948
|
+
const limitColor = params.limit_color ?? "#ff0000";
|
|
4949
|
+
const warningColor = params.warning_color ?? "#ffa500";
|
|
4950
|
+
const connectPoints = params.connect_points ?? true;
|
|
4951
|
+
const highlightOOC = params.highlight_ooc ?? true;
|
|
4952
|
+
const oocChar = params.ooc_char ?? "◆";
|
|
4953
|
+
const pointChar = params.point_char ?? "●";
|
|
4954
|
+
const parseHex = (hex) => {
|
|
4955
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
4956
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
4957
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
4958
|
+
return { r, g, b, a: 1 };
|
|
4959
|
+
};
|
|
4960
|
+
const centerColorParsed = parseHex(centerColor);
|
|
4961
|
+
const limitColorParsed = parseHex(limitColor);
|
|
4962
|
+
const warningColorParsed = parseHex(warningColor);
|
|
4963
|
+
const xField = typeof aes.x === "string" ? aes.x : "x";
|
|
4964
|
+
const yField = typeof aes.y === "string" ? aes.y : "y";
|
|
4965
|
+
const points = [];
|
|
4966
|
+
for (const row of data) {
|
|
4967
|
+
const x = Number(row[xField]);
|
|
4968
|
+
const y = Number(row[yField]);
|
|
4969
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
4970
|
+
points.push({ x, y });
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4973
|
+
if (points.length === 0)
|
|
4974
|
+
return;
|
|
4975
|
+
points.sort((a, b) => a.x - b.x);
|
|
4976
|
+
const yValues = points.map((p) => p.y);
|
|
4977
|
+
const mean = customCenter ?? yValues.reduce((a, b) => a + b, 0) / yValues.length;
|
|
4978
|
+
let sigmaEst;
|
|
4979
|
+
if (points.length > 1) {
|
|
4980
|
+
const movingRanges = [];
|
|
4981
|
+
for (let i = 1;i < points.length; i++) {
|
|
4982
|
+
movingRanges.push(Math.abs(points[i].y - points[i - 1].y));
|
|
4983
|
+
}
|
|
4984
|
+
const avgMR = movingRanges.reduce((a, b) => a + b, 0) / movingRanges.length;
|
|
4985
|
+
sigmaEst = avgMR / 1.128;
|
|
4986
|
+
} else {
|
|
4987
|
+
const variance = yValues.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / (yValues.length - 1);
|
|
4988
|
+
sigmaEst = Math.sqrt(variance);
|
|
4989
|
+
}
|
|
4990
|
+
const ucl = customUCL ?? mean + sigma * sigmaEst;
|
|
4991
|
+
const lcl = customLCL ?? mean - sigma * sigmaEst;
|
|
4992
|
+
const uwl = mean + 2 * sigmaEst;
|
|
4993
|
+
const lwl = mean - 2 * sigmaEst;
|
|
4994
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
4995
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
4996
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
4997
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
4998
|
+
const minX = Math.min(...points.map((p) => p.x));
|
|
4999
|
+
const maxX = Math.max(...points.map((p) => p.x));
|
|
5000
|
+
const minY = Math.min(...points.map((p) => p.y), lcl);
|
|
5001
|
+
const maxY = Math.max(...points.map((p) => p.y), ucl);
|
|
5002
|
+
const mapX = (v) => plotLeft + (v - minX) / (maxX - minX) * (plotRight - plotLeft);
|
|
5003
|
+
const mapY = (v) => plotBottom - (v - minY) / (maxY - minY) * (plotBottom - plotTop);
|
|
5004
|
+
if (showCenter) {
|
|
5005
|
+
const centerY = Math.round(mapY(mean));
|
|
5006
|
+
for (let x = plotLeft;x <= plotRight; x++) {
|
|
5007
|
+
canvas.drawChar(x, centerY, "─", centerColorParsed);
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
5010
|
+
if (showUCL) {
|
|
5011
|
+
const uclY = Math.round(mapY(ucl));
|
|
5012
|
+
for (let x = plotLeft;x <= plotRight; x += 2) {
|
|
5013
|
+
canvas.drawChar(x, uclY, "─", limitColorParsed);
|
|
5014
|
+
}
|
|
5015
|
+
}
|
|
5016
|
+
if (showLCL) {
|
|
5017
|
+
const lclY = Math.round(mapY(lcl));
|
|
5018
|
+
for (let x = plotLeft;x <= plotRight; x += 2) {
|
|
5019
|
+
canvas.drawChar(x, lclY, "─", limitColorParsed);
|
|
5020
|
+
}
|
|
5021
|
+
}
|
|
5022
|
+
if (showWarning) {
|
|
5023
|
+
const uwlY = Math.round(mapY(uwl));
|
|
5024
|
+
const lwlY = Math.round(mapY(lwl));
|
|
5025
|
+
for (let x = plotLeft;x <= plotRight; x += 3) {
|
|
5026
|
+
canvas.drawChar(x, uwlY, "·", warningColorParsed);
|
|
5027
|
+
canvas.drawChar(x, lwlY, "·", warningColorParsed);
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
if (connectPoints && points.length > 1) {
|
|
5031
|
+
const lineColor = { r: 100, g: 100, b: 100, a: 1 };
|
|
5032
|
+
for (let i = 1;i < points.length; i++) {
|
|
5033
|
+
const x1 = Math.round(mapX(points[i - 1].x));
|
|
5034
|
+
const y1 = Math.round(mapY(points[i - 1].y));
|
|
5035
|
+
const x2 = Math.round(mapX(points[i].x));
|
|
5036
|
+
const y2 = Math.round(mapY(points[i].y));
|
|
5037
|
+
const dx = Math.abs(x2 - x1);
|
|
5038
|
+
const dy = Math.abs(y2 - y1);
|
|
5039
|
+
const sx = x1 < x2 ? 1 : -1;
|
|
5040
|
+
const sy = y1 < y2 ? 1 : -1;
|
|
5041
|
+
let err = dx - dy;
|
|
5042
|
+
let x = x1;
|
|
5043
|
+
let y = y1;
|
|
5044
|
+
while (true) {
|
|
5045
|
+
canvas.drawChar(x, y, "·", lineColor);
|
|
5046
|
+
if (x === x2 && y === y2)
|
|
5047
|
+
break;
|
|
5048
|
+
const e2 = 2 * err;
|
|
5049
|
+
if (e2 > -dy) {
|
|
5050
|
+
err -= dy;
|
|
5051
|
+
x += sx;
|
|
5052
|
+
}
|
|
5053
|
+
if (e2 < dx) {
|
|
5054
|
+
err += dx;
|
|
5055
|
+
y += sy;
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
const inControlColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
5061
|
+
const oocColor = { r: 214, g: 39, b: 40, a: 1 };
|
|
5062
|
+
for (const p of points) {
|
|
5063
|
+
const x = Math.round(mapX(p.x));
|
|
5064
|
+
const y = Math.round(mapY(p.y));
|
|
5065
|
+
const isOOC = p.y > ucl || p.y < lcl;
|
|
5066
|
+
if (highlightOOC && isOOC) {
|
|
5067
|
+
canvas.drawChar(x, y, oocChar, oocColor);
|
|
5068
|
+
} else {
|
|
5069
|
+
canvas.drawChar(x, y, pointChar, inControlColor);
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
function renderGeomScree(data, geom, aes, scales, canvas) {
|
|
5074
|
+
const params = geom.params || {};
|
|
5075
|
+
const showCumulative = params.show_cumulative ?? false;
|
|
5076
|
+
const showKaiser = params.show_kaiser ?? false;
|
|
5077
|
+
const connectPoints = params.connect_points ?? true;
|
|
5078
|
+
const showBars = params.show_bars ?? false;
|
|
5079
|
+
const pointChar = params.point_char ?? "●";
|
|
5080
|
+
const cumulativeColor = params.cumulative_color ?? "#ff0000";
|
|
5081
|
+
const kaiserColor = params.kaiser_color ?? "#888888";
|
|
5082
|
+
const threshold = params.threshold;
|
|
5083
|
+
const thresholdColor = params.threshold_color ?? "#00aa00";
|
|
5084
|
+
const parseHex = (hex) => {
|
|
5085
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
5086
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
5087
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
5088
|
+
return { r, g, b, a: 1 };
|
|
5089
|
+
};
|
|
5090
|
+
const cumulativeColorParsed = parseHex(cumulativeColor);
|
|
5091
|
+
const kaiserColorParsed = parseHex(kaiserColor);
|
|
5092
|
+
const thresholdColorParsed = parseHex(thresholdColor);
|
|
5093
|
+
const xField = typeof aes.x === "string" ? aes.x : "component";
|
|
5094
|
+
const yField = typeof aes.y === "string" ? aes.y : "variance";
|
|
5095
|
+
const points = [];
|
|
5096
|
+
for (const row of data) {
|
|
5097
|
+
const component = Number(row[xField]);
|
|
5098
|
+
const variance = Number(row[yField]);
|
|
5099
|
+
if (!isNaN(component) && !isNaN(variance)) {
|
|
5100
|
+
points.push({ component, variance });
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
if (points.length === 0)
|
|
5104
|
+
return;
|
|
5105
|
+
points.sort((a, b) => a.component - b.component);
|
|
5106
|
+
const total = points.reduce((a, b) => a + b.variance, 0);
|
|
5107
|
+
let cumSum = 0;
|
|
5108
|
+
const cumulativePoints = points.map((p) => {
|
|
5109
|
+
cumSum += p.variance;
|
|
5110
|
+
return { component: p.component, cumulative: cumSum / total };
|
|
5111
|
+
});
|
|
5112
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
5113
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
5114
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
5115
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
5116
|
+
const minX = Math.min(...points.map((p) => p.component));
|
|
5117
|
+
const maxX = Math.max(...points.map((p) => p.component));
|
|
5118
|
+
const maxY = Math.max(...points.map((p) => p.variance));
|
|
5119
|
+
const yMax = showCumulative ? Math.max(maxY, total) : maxY;
|
|
5120
|
+
const mapX = (v) => plotLeft + (v - minX) / (maxX - minX) * (plotRight - plotLeft);
|
|
5121
|
+
const mapY = (v) => plotBottom - v / yMax * (plotBottom - plotTop);
|
|
5122
|
+
const mapYCumulative = (v) => plotBottom - v * (plotBottom - plotTop);
|
|
5123
|
+
if (showKaiser) {
|
|
5124
|
+
const kaiserY = Math.round(mapY(1));
|
|
5125
|
+
if (kaiserY >= plotTop && kaiserY <= plotBottom) {
|
|
5126
|
+
for (let x = plotLeft;x <= plotRight; x += 2) {
|
|
5127
|
+
canvas.drawChar(x, kaiserY, "─", kaiserColorParsed);
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
if (threshold !== undefined) {
|
|
5132
|
+
const thresholdY = Math.round(mapYCumulative(threshold));
|
|
5133
|
+
for (let x = plotLeft;x <= plotRight; x += 2) {
|
|
5134
|
+
canvas.drawChar(x, thresholdY, "─", thresholdColorParsed);
|
|
5135
|
+
}
|
|
5136
|
+
}
|
|
5137
|
+
if (showBars) {
|
|
5138
|
+
const barColor = { r: 180, g: 180, b: 180, a: 1 };
|
|
5139
|
+
const barWidth = Math.max(1, Math.floor((plotRight - plotLeft) / points.length / 2));
|
|
5140
|
+
for (const p of points) {
|
|
5141
|
+
const x = Math.round(mapX(p.component));
|
|
5142
|
+
const y = Math.round(mapY(p.variance));
|
|
5143
|
+
for (let bx = x - barWidth;bx <= x + barWidth; bx++) {
|
|
5144
|
+
for (let by = y;by <= plotBottom; by++) {
|
|
5145
|
+
canvas.drawChar(bx, by, "░", barColor);
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
if (connectPoints && points.length > 1) {
|
|
5151
|
+
const lineColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
5152
|
+
for (let i = 1;i < points.length; i++) {
|
|
5153
|
+
const x1 = Math.round(mapX(points[i - 1].component));
|
|
5154
|
+
const y1 = Math.round(mapY(points[i - 1].variance));
|
|
5155
|
+
const x2 = Math.round(mapX(points[i].component));
|
|
5156
|
+
const y2 = Math.round(mapY(points[i].variance));
|
|
5157
|
+
const steps = Math.max(Math.abs(x2 - x1), 1);
|
|
5158
|
+
for (let s = 0;s <= steps; s++) {
|
|
5159
|
+
const t = s / steps;
|
|
5160
|
+
const x = Math.round(x1 + t * (x2 - x1));
|
|
5161
|
+
const y = Math.round(y1 + t * (y2 - y1));
|
|
5162
|
+
canvas.drawChar(x, y, "─", lineColor);
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
if (showCumulative && cumulativePoints.length > 1) {
|
|
5167
|
+
for (let i = 1;i < cumulativePoints.length; i++) {
|
|
5168
|
+
const x1 = Math.round(mapX(cumulativePoints[i - 1].component));
|
|
5169
|
+
const y1 = Math.round(mapYCumulative(cumulativePoints[i - 1].cumulative));
|
|
5170
|
+
const x2 = Math.round(mapX(cumulativePoints[i].component));
|
|
5171
|
+
const y2 = Math.round(mapYCumulative(cumulativePoints[i].cumulative));
|
|
5172
|
+
const steps = Math.max(Math.abs(x2 - x1), 1);
|
|
5173
|
+
for (let s = 0;s <= steps; s++) {
|
|
5174
|
+
const t = s / steps;
|
|
5175
|
+
const x = Math.round(x1 + t * (x2 - x1));
|
|
5176
|
+
const y = Math.round(y1 + t * (y2 - y1));
|
|
5177
|
+
canvas.drawChar(x, y, "─", cumulativeColorParsed);
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
for (const p of cumulativePoints) {
|
|
5181
|
+
const x = Math.round(mapX(p.component));
|
|
5182
|
+
const y = Math.round(mapYCumulative(p.cumulative));
|
|
5183
|
+
canvas.drawChar(x, y, "○", cumulativeColorParsed);
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
const pointColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
5187
|
+
for (const p of points) {
|
|
5188
|
+
const x = Math.round(mapX(p.component));
|
|
5189
|
+
const y = Math.round(mapY(p.variance));
|
|
5190
|
+
canvas.drawChar(x, y, pointChar, pointColor);
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
3724
5193
|
function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
3725
5194
|
switch (geom.type) {
|
|
3726
5195
|
case "point":
|
|
@@ -3849,10 +5318,455 @@ function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
|
3849
5318
|
case "volcano":
|
|
3850
5319
|
renderGeomVolcano(data, geom, aes, scales, canvas);
|
|
3851
5320
|
break;
|
|
5321
|
+
case "ma":
|
|
5322
|
+
renderGeomMA(data, geom, aes, scales, canvas);
|
|
5323
|
+
break;
|
|
5324
|
+
case "manhattan":
|
|
5325
|
+
renderGeomManhattan(data, geom, aes, scales, canvas);
|
|
5326
|
+
break;
|
|
5327
|
+
case "heatmap":
|
|
5328
|
+
renderGeomHeatmap(data, geom, aes, scales, canvas);
|
|
5329
|
+
break;
|
|
5330
|
+
case "biplot":
|
|
5331
|
+
renderGeomBiplot(data, geom, aes, scales, canvas);
|
|
5332
|
+
break;
|
|
5333
|
+
case "kaplan_meier":
|
|
5334
|
+
renderGeomKaplanMeier(data, geom, aes, scales, canvas);
|
|
5335
|
+
break;
|
|
5336
|
+
case "forest":
|
|
5337
|
+
renderGeomForest(data, geom, aes, scales, canvas);
|
|
5338
|
+
break;
|
|
5339
|
+
case "roc":
|
|
5340
|
+
renderGeomRoc(data, geom, aes, scales, canvas);
|
|
5341
|
+
break;
|
|
5342
|
+
case "bland_altman":
|
|
5343
|
+
renderGeomBlandAltman(data, geom, aes, scales, canvas);
|
|
5344
|
+
break;
|
|
5345
|
+
case "qq":
|
|
5346
|
+
renderGeomQQ(data, geom, aes, scales, canvas);
|
|
5347
|
+
break;
|
|
5348
|
+
case "ecdf":
|
|
5349
|
+
renderGeomECDF(data, geom, aes, scales, canvas);
|
|
5350
|
+
break;
|
|
5351
|
+
case "funnel":
|
|
5352
|
+
renderGeomFunnel(data, geom, aes, scales, canvas);
|
|
5353
|
+
break;
|
|
5354
|
+
case "control":
|
|
5355
|
+
renderGeomControl(data, geom, aes, scales, canvas);
|
|
5356
|
+
break;
|
|
5357
|
+
case "scree":
|
|
5358
|
+
renderGeomScree(data, geom, aes, scales, canvas);
|
|
5359
|
+
break;
|
|
5360
|
+
case "upset":
|
|
5361
|
+
renderGeomUpset(data, geom, aes, scales, canvas);
|
|
5362
|
+
break;
|
|
5363
|
+
case "dendrogram":
|
|
5364
|
+
renderGeomDendrogram(data, geom, aes, scales, canvas);
|
|
5365
|
+
break;
|
|
3852
5366
|
default:
|
|
3853
5367
|
break;
|
|
3854
5368
|
}
|
|
3855
5369
|
}
|
|
5370
|
+
function renderGeomUpset(data, geom, aes, scales, canvas) {
|
|
5371
|
+
const params = geom.params || {};
|
|
5372
|
+
const sets = params.sets;
|
|
5373
|
+
const minSize = params.min_size ?? 1;
|
|
5374
|
+
const maxIntersections = params.max_intersections ?? 20;
|
|
5375
|
+
const sortBy = params.sort_by ?? "size";
|
|
5376
|
+
const sortOrder = params.sort_order ?? "desc";
|
|
5377
|
+
const showSetSizes = params.show_set_sizes ?? true;
|
|
5378
|
+
const dotChar = params.dot_char ?? "●";
|
|
5379
|
+
const emptyChar = params.empty_char ?? "○";
|
|
5380
|
+
const lineChar = params.line_char ?? "│";
|
|
5381
|
+
const barChar = params.bar_char ?? "█";
|
|
5382
|
+
let setNames = [];
|
|
5383
|
+
if (sets && sets.length > 0) {
|
|
5384
|
+
setNames = sets;
|
|
5385
|
+
} else if (data.length > 0) {
|
|
5386
|
+
const firstRow = data[0];
|
|
5387
|
+
for (const key of Object.keys(firstRow)) {
|
|
5388
|
+
const values = data.map((row) => row[key]);
|
|
5389
|
+
const isBinary = values.every((v) => v === 0 || v === 1 || v === "0" || v === "1");
|
|
5390
|
+
if (isBinary && key !== "id" && key !== "name" && key !== "element") {
|
|
5391
|
+
setNames.push(key);
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5394
|
+
if (setNames.length === 0) {
|
|
5395
|
+
const setsField2 = typeof aes.x === "string" ? aes.x : "sets";
|
|
5396
|
+
const allSets = new Set;
|
|
5397
|
+
for (const row of data) {
|
|
5398
|
+
const val = row[setsField2];
|
|
5399
|
+
if (typeof val === "string") {
|
|
5400
|
+
val.split(",").forEach((s) => allSets.add(s.trim()));
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
setNames = Array.from(allSets).sort();
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
if (setNames.length === 0)
|
|
5407
|
+
return;
|
|
5408
|
+
const intersectionMap = new Map;
|
|
5409
|
+
const setsField = typeof aes.x === "string" ? aes.x : "sets";
|
|
5410
|
+
const hasListFormat = data.length > 0 && typeof data[0][setsField] === "string";
|
|
5411
|
+
for (const row of data) {
|
|
5412
|
+
let memberSets;
|
|
5413
|
+
if (hasListFormat) {
|
|
5414
|
+
const val = row[setsField];
|
|
5415
|
+
memberSets = typeof val === "string" ? val.split(",").map((s) => s.trim()).filter((s) => setNames.includes(s)) : [];
|
|
5416
|
+
} else {
|
|
5417
|
+
memberSets = setNames.filter((s) => {
|
|
5418
|
+
const v = row[s];
|
|
5419
|
+
return v === 1 || v === "1";
|
|
5420
|
+
});
|
|
5421
|
+
}
|
|
5422
|
+
if (memberSets.length > 0) {
|
|
5423
|
+
const key = memberSets.sort().join("|");
|
|
5424
|
+
intersectionMap.set(key, (intersectionMap.get(key) || 0) + 1);
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
let intersections = Array.from(intersectionMap.entries()).map(([key, count]) => ({
|
|
5428
|
+
sets: new Set(key.split("|")),
|
|
5429
|
+
count,
|
|
5430
|
+
key
|
|
5431
|
+
})).filter((i) => i.count >= minSize);
|
|
5432
|
+
if (sortBy === "size") {
|
|
5433
|
+
intersections.sort((a, b) => sortOrder === "desc" ? b.count - a.count : a.count - b.count);
|
|
5434
|
+
} else if (sortBy === "degree") {
|
|
5435
|
+
intersections.sort((a, b) => sortOrder === "desc" ? b.sets.size - a.sets.size : a.sets.size - b.sets.size);
|
|
5436
|
+
}
|
|
5437
|
+
intersections = intersections.slice(0, maxIntersections);
|
|
5438
|
+
if (intersections.length === 0)
|
|
5439
|
+
return;
|
|
5440
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
5441
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
5442
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
5443
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
5444
|
+
const plotWidth = plotRight - plotLeft;
|
|
5445
|
+
const plotHeight = plotBottom - plotTop;
|
|
5446
|
+
const matrixHeight = Math.min(setNames.length * 2 + 2, Math.floor(plotHeight * 0.4));
|
|
5447
|
+
const barHeight = plotHeight - matrixHeight - 2;
|
|
5448
|
+
const barTop = plotTop;
|
|
5449
|
+
const barBottom = plotTop + barHeight;
|
|
5450
|
+
const matrixTop = barBottom + 2;
|
|
5451
|
+
const setLabelWidth = showSetSizes ? Math.max(...setNames.map((s) => s.length)) + 8 : 0;
|
|
5452
|
+
const colWidth = Math.max(2, Math.floor((plotWidth - setLabelWidth) / intersections.length));
|
|
5453
|
+
const maxCount = Math.max(...intersections.map((i) => i.count));
|
|
5454
|
+
const barColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
5455
|
+
const dotColor = { r: 50, g: 50, b: 50, a: 1 };
|
|
5456
|
+
const lineColor = { r: 100, g: 100, b: 100, a: 1 };
|
|
5457
|
+
const labelColor = { r: 150, g: 150, b: 150, a: 1 };
|
|
5458
|
+
for (let i = 0;i < intersections.length; i++) {
|
|
5459
|
+
const inter = intersections[i];
|
|
5460
|
+
const x = plotLeft + setLabelWidth + i * colWidth + Math.floor(colWidth / 2);
|
|
5461
|
+
const barHeightPx = Math.round(inter.count / maxCount * barHeight);
|
|
5462
|
+
for (let y = barBottom - barHeightPx;y <= barBottom; y++) {
|
|
5463
|
+
canvas.drawChar(x, y, barChar, barColor);
|
|
5464
|
+
}
|
|
5465
|
+
const countStr = inter.count.toString();
|
|
5466
|
+
const labelY = barBottom - barHeightPx - 1;
|
|
5467
|
+
if (labelY >= barTop) {
|
|
5468
|
+
for (let ci = 0;ci < countStr.length; ci++) {
|
|
5469
|
+
canvas.drawChar(x - Math.floor(countStr.length / 2) + ci, labelY, countStr[ci], labelColor);
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
const rowSpacing = Math.max(1, Math.floor(matrixHeight / setNames.length));
|
|
5474
|
+
if (showSetSizes) {
|
|
5475
|
+
for (let si = 0;si < setNames.length; si++) {
|
|
5476
|
+
const setName = setNames[si];
|
|
5477
|
+
const y = matrixTop + si * rowSpacing + 1;
|
|
5478
|
+
let setSize = 0;
|
|
5479
|
+
for (const row of data) {
|
|
5480
|
+
if (hasListFormat) {
|
|
5481
|
+
const val = row[setsField];
|
|
5482
|
+
if (typeof val === "string" && val.split(",").map((s) => s.trim()).includes(setName)) {
|
|
5483
|
+
setSize++;
|
|
5484
|
+
}
|
|
5485
|
+
} else {
|
|
5486
|
+
const v = row[setName];
|
|
5487
|
+
if (v === 1 || v === "1")
|
|
5488
|
+
setSize++;
|
|
5489
|
+
}
|
|
5490
|
+
}
|
|
5491
|
+
const label = `${setName.substring(0, 6)}`;
|
|
5492
|
+
for (let ci = 0;ci < label.length; ci++) {
|
|
5493
|
+
canvas.drawChar(plotLeft + ci, y, label[ci], labelColor);
|
|
5494
|
+
}
|
|
5495
|
+
const sizeBarLen = Math.max(1, Math.round(setSize / data.length * 5));
|
|
5496
|
+
for (let bi = 0;bi < sizeBarLen; bi++) {
|
|
5497
|
+
canvas.drawChar(plotLeft + label.length + 1 + bi, y, "▪", barColor);
|
|
5498
|
+
}
|
|
5499
|
+
}
|
|
5500
|
+
}
|
|
5501
|
+
for (let i = 0;i < intersections.length; i++) {
|
|
5502
|
+
const inter = intersections[i];
|
|
5503
|
+
const x = plotLeft + setLabelWidth + i * colWidth + Math.floor(colWidth / 2);
|
|
5504
|
+
const activeRows = [];
|
|
5505
|
+
for (let si = 0;si < setNames.length; si++) {
|
|
5506
|
+
const setName = setNames[si];
|
|
5507
|
+
const y = matrixTop + si * rowSpacing + 1;
|
|
5508
|
+
const isActive = inter.sets.has(setName);
|
|
5509
|
+
if (isActive) {
|
|
5510
|
+
canvas.drawChar(x, y, dotChar, dotColor);
|
|
5511
|
+
activeRows.push(y);
|
|
5512
|
+
} else {
|
|
5513
|
+
canvas.drawChar(x, y, emptyChar, { r: 200, g: 200, b: 200, a: 1 });
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
if (activeRows.length > 1) {
|
|
5517
|
+
const minY = Math.min(...activeRows);
|
|
5518
|
+
const maxY = Math.max(...activeRows);
|
|
5519
|
+
for (let y = minY + 1;y < maxY; y++) {
|
|
5520
|
+
if (!activeRows.includes(y)) {
|
|
5521
|
+
canvas.drawChar(x, y, lineChar, lineColor);
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
}
|
|
5526
|
+
}
|
|
5527
|
+
function renderGeomDendrogram(data, geom, _aes, scales, canvas) {
|
|
5528
|
+
const params = geom.params || {};
|
|
5529
|
+
const orientation = params.orientation ?? "vertical";
|
|
5530
|
+
const labels = params.labels;
|
|
5531
|
+
const showLabels = params.show_labels ?? true;
|
|
5532
|
+
const hang = params.hang ?? false;
|
|
5533
|
+
const hConnector = params.h_connector ?? "─";
|
|
5534
|
+
const vConnector = params.v_connector ?? "│";
|
|
5535
|
+
const cornerTR = params.corner_tr ?? "┐";
|
|
5536
|
+
const cornerBL = params.corner_bl ?? "└";
|
|
5537
|
+
const cornerBR = params.corner_br ?? "┘";
|
|
5538
|
+
const leafChar = params.leaf_char ?? "○";
|
|
5539
|
+
const parentCol = params.parent_col ?? "parent";
|
|
5540
|
+
const heightCol = params.height_col ?? "height";
|
|
5541
|
+
const idCol = params.id_col ?? "id";
|
|
5542
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
5543
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
5544
|
+
const plotTop = Math.round(scales.y.range[1]);
|
|
5545
|
+
const plotBottom = Math.round(scales.y.range[0]);
|
|
5546
|
+
const plotWidth = plotRight - plotLeft;
|
|
5547
|
+
const plotHeight = plotBottom - plotTop;
|
|
5548
|
+
const lineColor = { r: 50, g: 50, b: 50, a: 1 };
|
|
5549
|
+
const leafColor = { r: 31, g: 119, b: 180, a: 1 };
|
|
5550
|
+
const labelColor = { r: 100, g: 100, b: 100, a: 1 };
|
|
5551
|
+
const hasLinkageFormat = data.length > 0 && (("merge1" in data[0]) || ("merge_1" in data[0]));
|
|
5552
|
+
if (hasLinkageFormat) {
|
|
5553
|
+
const linkage = data.map((row) => ({
|
|
5554
|
+
merge1: Number(row["merge1"] ?? row["merge_1"]),
|
|
5555
|
+
merge2: Number(row["merge2"] ?? row["merge_2"]),
|
|
5556
|
+
height: Number(row[heightCol] ?? row["height"]),
|
|
5557
|
+
size: Number(row["size"] ?? 2)
|
|
5558
|
+
}));
|
|
5559
|
+
if (linkage.length === 0)
|
|
5560
|
+
return;
|
|
5561
|
+
const n = linkage.length + 1;
|
|
5562
|
+
const nodes = new Map;
|
|
5563
|
+
for (let i = 0;i < n; i++) {
|
|
5564
|
+
nodes.set(i, {
|
|
5565
|
+
id: i,
|
|
5566
|
+
height: 0,
|
|
5567
|
+
label: labels?.[i] ?? `${i}`
|
|
5568
|
+
});
|
|
5569
|
+
}
|
|
5570
|
+
for (let i = 0;i < linkage.length; i++) {
|
|
5571
|
+
const row = linkage[i];
|
|
5572
|
+
const newId = n + i;
|
|
5573
|
+
const leftNode = nodes.get(row.merge1 < n ? row.merge1 : row.merge1);
|
|
5574
|
+
const rightNode = nodes.get(row.merge2 < n ? row.merge2 : row.merge2);
|
|
5575
|
+
nodes.set(newId, {
|
|
5576
|
+
id: newId,
|
|
5577
|
+
left: leftNode,
|
|
5578
|
+
right: rightNode,
|
|
5579
|
+
height: row.height
|
|
5580
|
+
});
|
|
5581
|
+
}
|
|
5582
|
+
const root = nodes.get(n + linkage.length - 1);
|
|
5583
|
+
if (!root)
|
|
5584
|
+
return;
|
|
5585
|
+
let xPos = 0;
|
|
5586
|
+
const assignX = (node) => {
|
|
5587
|
+
if (!node.left && !node.right) {
|
|
5588
|
+
node.x = xPos++;
|
|
5589
|
+
} else {
|
|
5590
|
+
if (node.left)
|
|
5591
|
+
assignX(node.left);
|
|
5592
|
+
if (node.right)
|
|
5593
|
+
assignX(node.right);
|
|
5594
|
+
const leftX = node.left?.x ?? 0;
|
|
5595
|
+
const rightX = node.right?.x ?? 0;
|
|
5596
|
+
node.x = (leftX + rightX) / 2;
|
|
5597
|
+
}
|
|
5598
|
+
};
|
|
5599
|
+
assignX(root);
|
|
5600
|
+
const maxHeight = root.height;
|
|
5601
|
+
const leafCount = xPos;
|
|
5602
|
+
const mapX = (x) => {
|
|
5603
|
+
if (orientation === "vertical") {
|
|
5604
|
+
return plotLeft + x / (leafCount - 1 || 1) * plotWidth;
|
|
5605
|
+
} else {
|
|
5606
|
+
return plotBottom - x / (leafCount - 1 || 1) * plotHeight;
|
|
5607
|
+
}
|
|
5608
|
+
};
|
|
5609
|
+
const mapY = (h) => {
|
|
5610
|
+
if (orientation === "vertical") {
|
|
5611
|
+
return plotTop + (1 - h / maxHeight) * (plotHeight - 3);
|
|
5612
|
+
} else {
|
|
5613
|
+
return plotLeft + h / maxHeight * plotWidth;
|
|
5614
|
+
}
|
|
5615
|
+
};
|
|
5616
|
+
const drawNode = (node) => {
|
|
5617
|
+
if (node.x === undefined)
|
|
5618
|
+
return;
|
|
5619
|
+
if (node.left && node.right) {
|
|
5620
|
+
const nodeY = mapY(node.height);
|
|
5621
|
+
const leftX = mapX(node.left.x);
|
|
5622
|
+
const leftY = mapY(node.left.height);
|
|
5623
|
+
const rightX = mapX(node.right.x);
|
|
5624
|
+
const rightY = mapY(node.right.height);
|
|
5625
|
+
if (orientation === "vertical") {
|
|
5626
|
+
const hLineY = Math.round(nodeY);
|
|
5627
|
+
const leftXRound = Math.round(leftX);
|
|
5628
|
+
const rightXRound = Math.round(rightX);
|
|
5629
|
+
for (let x = Math.min(leftXRound, rightXRound);x <= Math.max(leftXRound, rightXRound); x++) {
|
|
5630
|
+
canvas.drawChar(x, hLineY, hConnector, lineColor);
|
|
5631
|
+
}
|
|
5632
|
+
canvas.drawChar(leftXRound, hLineY, cornerBL, lineColor);
|
|
5633
|
+
canvas.drawChar(rightXRound, hLineY, cornerBR, lineColor);
|
|
5634
|
+
const leftYRound = Math.round(leftY);
|
|
5635
|
+
const rightYRound = Math.round(rightY);
|
|
5636
|
+
for (let y = hLineY + 1;y < leftYRound; y++) {
|
|
5637
|
+
canvas.drawChar(leftXRound, y, vConnector, lineColor);
|
|
5638
|
+
}
|
|
5639
|
+
for (let y = hLineY + 1;y < rightYRound; y++) {
|
|
5640
|
+
canvas.drawChar(rightXRound, y, vConnector, lineColor);
|
|
5641
|
+
}
|
|
5642
|
+
} else {
|
|
5643
|
+
const hLineX = Math.round(nodeY);
|
|
5644
|
+
const leftYRound = Math.round(leftX);
|
|
5645
|
+
const rightYRound = Math.round(rightX);
|
|
5646
|
+
for (let y = Math.min(leftYRound, rightYRound);y <= Math.max(leftYRound, rightYRound); y++) {
|
|
5647
|
+
canvas.drawChar(hLineX, y, vConnector, lineColor);
|
|
5648
|
+
}
|
|
5649
|
+
canvas.drawChar(hLineX, leftYRound, cornerTR, lineColor);
|
|
5650
|
+
canvas.drawChar(hLineX, rightYRound, cornerBR, lineColor);
|
|
5651
|
+
const leftXRound = Math.round(mapY(node.left.height));
|
|
5652
|
+
const rightXRound = Math.round(mapY(node.right.height));
|
|
5653
|
+
for (let x = hLineX + 1;x < leftXRound; x++) {
|
|
5654
|
+
canvas.drawChar(x, leftYRound, hConnector, lineColor);
|
|
5655
|
+
}
|
|
5656
|
+
for (let x = hLineX + 1;x < rightXRound; x++) {
|
|
5657
|
+
canvas.drawChar(x, rightYRound, hConnector, lineColor);
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
drawNode(node.left);
|
|
5661
|
+
drawNode(node.right);
|
|
5662
|
+
} else {
|
|
5663
|
+
if (orientation === "vertical") {
|
|
5664
|
+
const x = Math.round(mapX(node.x));
|
|
5665
|
+
const y = hang ? plotBottom - 2 : Math.round(mapY(0));
|
|
5666
|
+
canvas.drawChar(x, y, leafChar, leafColor);
|
|
5667
|
+
if (showLabels && node.label) {
|
|
5668
|
+
const label = node.label.substring(0, 4);
|
|
5669
|
+
for (let ci = 0;ci < label.length; ci++) {
|
|
5670
|
+
canvas.drawChar(x - Math.floor(label.length / 2) + ci, y + 1, label[ci], labelColor);
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
} else {
|
|
5674
|
+
const y = Math.round(mapX(node.x));
|
|
5675
|
+
const x = Math.round(mapY(0));
|
|
5676
|
+
canvas.drawChar(x, y, leafChar, leafColor);
|
|
5677
|
+
if (showLabels && node.label) {
|
|
5678
|
+
const label = node.label.substring(0, 6);
|
|
5679
|
+
for (let ci = 0;ci < label.length; ci++) {
|
|
5680
|
+
canvas.drawChar(x + 2 + ci, y, label[ci], labelColor);
|
|
5681
|
+
}
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
};
|
|
5686
|
+
drawNode(root);
|
|
5687
|
+
} else {
|
|
5688
|
+
const nodeMap = new Map;
|
|
5689
|
+
for (const row of data) {
|
|
5690
|
+
const id = String(row[idCol] ?? "");
|
|
5691
|
+
const parent = row[parentCol];
|
|
5692
|
+
const height = Number(row[heightCol] ?? 0);
|
|
5693
|
+
nodeMap.set(id, {
|
|
5694
|
+
id,
|
|
5695
|
+
parent: parent === null || parent === "" || parent === "null" ? null : String(parent),
|
|
5696
|
+
height,
|
|
5697
|
+
children: []
|
|
5698
|
+
});
|
|
5699
|
+
}
|
|
5700
|
+
let root = null;
|
|
5701
|
+
for (const node of nodeMap.values()) {
|
|
5702
|
+
if (node.parent === null) {
|
|
5703
|
+
root = node;
|
|
5704
|
+
} else {
|
|
5705
|
+
const parentNode = nodeMap.get(node.parent);
|
|
5706
|
+
if (parentNode) {
|
|
5707
|
+
parentNode.children.push(node);
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
if (!root)
|
|
5712
|
+
return;
|
|
5713
|
+
let xPos = 0;
|
|
5714
|
+
const assignX = (node) => {
|
|
5715
|
+
if (node.children.length === 0) {
|
|
5716
|
+
node.x = xPos++;
|
|
5717
|
+
} else {
|
|
5718
|
+
for (const child of node.children) {
|
|
5719
|
+
assignX(child);
|
|
5720
|
+
}
|
|
5721
|
+
const childXs = node.children.map((c) => c.x ?? 0);
|
|
5722
|
+
node.x = childXs.reduce((a, b) => a + b, 0) / childXs.length;
|
|
5723
|
+
}
|
|
5724
|
+
};
|
|
5725
|
+
assignX(root);
|
|
5726
|
+
const findMaxHeight = (node) => {
|
|
5727
|
+
if (node.children.length === 0)
|
|
5728
|
+
return node.height;
|
|
5729
|
+
return Math.max(node.height, ...node.children.map(findMaxHeight));
|
|
5730
|
+
};
|
|
5731
|
+
const maxHeight = findMaxHeight(root) || 1;
|
|
5732
|
+
const leafCount = xPos || 1;
|
|
5733
|
+
const mapX = (x) => plotLeft + x / (leafCount - 1 || 1) * plotWidth;
|
|
5734
|
+
const mapY = (h) => plotTop + (1 - h / maxHeight) * (plotHeight - 3);
|
|
5735
|
+
const drawNode = (node) => {
|
|
5736
|
+
if (node.x === undefined)
|
|
5737
|
+
return;
|
|
5738
|
+
if (node.children.length > 0) {
|
|
5739
|
+
const nodeY = Math.round(mapY(node.height));
|
|
5740
|
+
const childXs = node.children.map((c) => Math.round(mapX(c.x ?? 0)));
|
|
5741
|
+
const minX = Math.min(...childXs);
|
|
5742
|
+
const maxX = Math.max(...childXs);
|
|
5743
|
+
for (let x = minX;x <= maxX; x++) {
|
|
5744
|
+
canvas.drawChar(x, nodeY, hConnector, lineColor);
|
|
5745
|
+
}
|
|
5746
|
+
for (const child of node.children) {
|
|
5747
|
+
const childX = Math.round(mapX(child.x ?? 0));
|
|
5748
|
+
const childY = Math.round(mapY(child.height));
|
|
5749
|
+
canvas.drawChar(childX, nodeY, child === node.children[0] ? cornerBL : child === node.children[node.children.length - 1] ? cornerBR : "┴", lineColor);
|
|
5750
|
+
for (let y = nodeY + 1;y < childY; y++) {
|
|
5751
|
+
canvas.drawChar(childX, y, vConnector, lineColor);
|
|
5752
|
+
}
|
|
5753
|
+
drawNode(child);
|
|
5754
|
+
}
|
|
5755
|
+
} else {
|
|
5756
|
+
const x = Math.round(mapX(node.x));
|
|
5757
|
+
const y = hang ? plotBottom - 2 : Math.round(mapY(node.height));
|
|
5758
|
+
canvas.drawChar(x, y, leafChar, leafColor);
|
|
5759
|
+
if (showLabels) {
|
|
5760
|
+
const label = node.id.substring(0, 4);
|
|
5761
|
+
for (let ci = 0;ci < label.length; ci++) {
|
|
5762
|
+
canvas.drawChar(x - Math.floor(label.length / 2) + ci, y + 1, label[ci], labelColor);
|
|
5763
|
+
}
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
};
|
|
5767
|
+
drawNode(root);
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
3856
5770
|
var POINT_SHAPES, SIZE_CHARS;
|
|
3857
5771
|
var init_render_geoms = __esm(() => {
|
|
3858
5772
|
init_scales();
|
|
@@ -6003,11 +7917,23 @@ function calculateLayout(spec, options) {
|
|
|
6003
7917
|
} else if (hasY2) {
|
|
6004
7918
|
rightMargin = 8 + (hasY2Label ? 2 : 0);
|
|
6005
7919
|
}
|
|
7920
|
+
let forestLabelWidth = 0;
|
|
7921
|
+
const isForestPlot = spec.geoms.some((g) => g.type === "forest");
|
|
7922
|
+
if (isForestPlot && Array.isArray(spec.data) && spec.data.length > 0) {
|
|
7923
|
+
const yField = typeof spec.aes.y === "string" ? spec.aes.y : "study";
|
|
7924
|
+
for (const row of spec.data) {
|
|
7925
|
+
const label = String(row[yField] ?? "");
|
|
7926
|
+
if (label.length > forestLabelWidth)
|
|
7927
|
+
forestLabelWidth = label.length;
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7930
|
+
const defaultLeft = 8 + (hasYLabel ? 2 : 0);
|
|
7931
|
+
const neededLeft = forestLabelWidth > 0 ? forestLabelWidth + 2 : defaultLeft;
|
|
6006
7932
|
const margins = {
|
|
6007
7933
|
top: hasTitle ? 2 : 1,
|
|
6008
7934
|
right: rightMargin,
|
|
6009
7935
|
bottom: 2 + (hasXLabel ? 1 : 0) + (hasLegend && legendPosition === "bottom" ? 2 : 0),
|
|
6010
|
-
left:
|
|
7936
|
+
left: Math.max(defaultLeft, neededLeft)
|
|
6011
7937
|
};
|
|
6012
7938
|
const plotArea = {
|
|
6013
7939
|
x: margins.left,
|
|
@@ -6221,7 +8147,9 @@ function renderToCanvas(spec, options) {
|
|
|
6221
8147
|
renderTitle(canvas, spec.labels.title, layout.width, spec.theme);
|
|
6222
8148
|
}
|
|
6223
8149
|
renderGridLines(canvas, scales, layout.plotArea, spec.theme);
|
|
6224
|
-
|
|
8150
|
+
const isForest = spec.geoms.some((g) => g.type === "forest");
|
|
8151
|
+
const axisLabels = isForest ? { ...spec.labels, y: undefined } : spec.labels;
|
|
8152
|
+
renderAxes(canvas, scales, layout.plotArea, axisLabels, spec.theme);
|
|
6225
8153
|
for (const geom of spec.geoms) {
|
|
6226
8154
|
let geomData;
|
|
6227
8155
|
let geomAes = spec.aes;
|
|
@@ -7170,28 +9098,17 @@ function geom_abline(options = {}) {
|
|
|
7170
9098
|
// src/geoms/qq.ts
|
|
7171
9099
|
function geom_qq(options = {}) {
|
|
7172
9100
|
return {
|
|
7173
|
-
type: "
|
|
7174
|
-
stat: "
|
|
7175
|
-
|
|
7176
|
-
distribution: options.distribution ?? "norm",
|
|
7177
|
-
dparams: options.dparams,
|
|
7178
|
-
size: options.size ?? 1,
|
|
7179
|
-
shape: options.shape ?? "●",
|
|
7180
|
-
color: options.color,
|
|
7181
|
-
alpha: options.alpha ?? 1
|
|
7182
|
-
}
|
|
7183
|
-
};
|
|
7184
|
-
}
|
|
7185
|
-
function geom_qq_line(options = {}) {
|
|
7186
|
-
return {
|
|
7187
|
-
type: "segment",
|
|
7188
|
-
stat: "qq_line",
|
|
9101
|
+
type: "qq",
|
|
9102
|
+
stat: "identity",
|
|
9103
|
+
position: "identity",
|
|
7189
9104
|
params: {
|
|
7190
|
-
distribution: options.distribution ?? "
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
9105
|
+
distribution: options.distribution ?? "normal",
|
|
9106
|
+
show_line: options.show_line ?? true,
|
|
9107
|
+
show_ci: options.show_ci ?? false,
|
|
9108
|
+
conf_level: options.conf_level ?? 0.95,
|
|
9109
|
+
line_color: options.line_color ?? "#ff0000",
|
|
9110
|
+
point_char: options.point_char ?? "●",
|
|
9111
|
+
standardize: options.standardize ?? true
|
|
7195
9112
|
}
|
|
7196
9113
|
};
|
|
7197
9114
|
}
|
|
@@ -7366,131 +9283,477 @@ var init_braille = __esm(() => {
|
|
|
7366
9283
|
];
|
|
7367
9284
|
});
|
|
7368
9285
|
|
|
7369
|
-
// src/geoms/calendar.ts
|
|
7370
|
-
function geom_calendar(options = {}) {
|
|
9286
|
+
// src/geoms/calendar.ts
|
|
9287
|
+
function geom_calendar(options = {}) {
|
|
9288
|
+
return {
|
|
9289
|
+
type: "calendar",
|
|
9290
|
+
stat: "identity",
|
|
9291
|
+
position: "identity",
|
|
9292
|
+
params: {
|
|
9293
|
+
cell_char: options.cell_char ?? "█",
|
|
9294
|
+
empty_char: options.empty_char ?? "░",
|
|
9295
|
+
empty_color: options.empty_color ?? "#161b22",
|
|
9296
|
+
fill_color: options.fill_color ?? "#39d353",
|
|
9297
|
+
show_months: options.show_months ?? true,
|
|
9298
|
+
show_days: options.show_days ?? true,
|
|
9299
|
+
week_start: options.week_start ?? 0,
|
|
9300
|
+
levels: options.levels ?? 5
|
|
9301
|
+
}
|
|
9302
|
+
};
|
|
9303
|
+
}
|
|
9304
|
+
|
|
9305
|
+
// src/geoms/flame.ts
|
|
9306
|
+
function geom_flame(options = {}) {
|
|
9307
|
+
return {
|
|
9308
|
+
type: "flame",
|
|
9309
|
+
stat: "identity",
|
|
9310
|
+
position: "identity",
|
|
9311
|
+
params: {
|
|
9312
|
+
style: options.style ?? "flame",
|
|
9313
|
+
palette: options.palette ?? "warm",
|
|
9314
|
+
show_labels: options.show_labels ?? true,
|
|
9315
|
+
min_label_width: options.min_label_width ?? 10,
|
|
9316
|
+
sort: options.sort ?? "alpha",
|
|
9317
|
+
bar_char: options.bar_char ?? "█"
|
|
9318
|
+
}
|
|
9319
|
+
};
|
|
9320
|
+
}
|
|
9321
|
+
function geom_icicle(options = {}) {
|
|
9322
|
+
return geom_flame({ ...options, style: "icicle" });
|
|
9323
|
+
}
|
|
9324
|
+
|
|
9325
|
+
// src/geoms/corrmat.ts
|
|
9326
|
+
function geom_corrmat(options = {}) {
|
|
9327
|
+
return {
|
|
9328
|
+
type: "corrmat",
|
|
9329
|
+
stat: "identity",
|
|
9330
|
+
position: "identity",
|
|
9331
|
+
params: {
|
|
9332
|
+
show_values: options.show_values ?? true,
|
|
9333
|
+
decimals: options.decimals ?? 2,
|
|
9334
|
+
show_significance: options.show_significance ?? false,
|
|
9335
|
+
sig_threshold: options.sig_threshold ?? 0.05,
|
|
9336
|
+
sig_marker: options.sig_marker ?? "*",
|
|
9337
|
+
positive_color: options.positive_color ?? "#2166ac",
|
|
9338
|
+
negative_color: options.negative_color ?? "#b2182b",
|
|
9339
|
+
neutral_color: options.neutral_color ?? "#f7f7f7",
|
|
9340
|
+
lower_triangle: options.lower_triangle ?? false,
|
|
9341
|
+
upper_triangle: options.upper_triangle ?? false,
|
|
9342
|
+
show_diagonal: options.show_diagonal ?? true,
|
|
9343
|
+
method: options.method ?? "pearson"
|
|
9344
|
+
}
|
|
9345
|
+
};
|
|
9346
|
+
}
|
|
9347
|
+
|
|
9348
|
+
// src/geoms/sankey.ts
|
|
9349
|
+
function geom_sankey(options = {}) {
|
|
9350
|
+
return {
|
|
9351
|
+
type: "sankey",
|
|
9352
|
+
stat: "identity",
|
|
9353
|
+
position: "identity",
|
|
9354
|
+
params: {
|
|
9355
|
+
node_width: options.node_width ?? 3,
|
|
9356
|
+
node_padding: options.node_padding ?? 2,
|
|
9357
|
+
node_char: options.node_char ?? "█",
|
|
9358
|
+
flow_char: options.flow_char ?? "─",
|
|
9359
|
+
show_labels: options.show_labels ?? true,
|
|
9360
|
+
show_values: options.show_values ?? false,
|
|
9361
|
+
align: options.align ?? "justify",
|
|
9362
|
+
color_by: options.color_by ?? "auto",
|
|
9363
|
+
min_flow_width: options.min_flow_width ?? 1,
|
|
9364
|
+
flow_gap: options.flow_gap ?? 0
|
|
9365
|
+
}
|
|
9366
|
+
};
|
|
9367
|
+
}
|
|
9368
|
+
|
|
9369
|
+
// src/geoms/treemap.ts
|
|
9370
|
+
function geom_treemap(options = {}) {
|
|
9371
|
+
return {
|
|
9372
|
+
type: "treemap",
|
|
9373
|
+
stat: "identity",
|
|
9374
|
+
position: "identity",
|
|
9375
|
+
params: {
|
|
9376
|
+
algorithm: options.algorithm ?? "squarify",
|
|
9377
|
+
show_labels: options.show_labels ?? true,
|
|
9378
|
+
show_values: options.show_values ?? false,
|
|
9379
|
+
border: options.border ?? true,
|
|
9380
|
+
padding: options.padding ?? 0,
|
|
9381
|
+
min_label_size: options.min_label_size ?? 4,
|
|
9382
|
+
color_by: options.color_by ?? "value",
|
|
9383
|
+
fill_char: options.fill_char ?? "█",
|
|
9384
|
+
max_depth: options.max_depth,
|
|
9385
|
+
aspect_ratio: options.aspect_ratio ?? 1.618
|
|
9386
|
+
}
|
|
9387
|
+
};
|
|
9388
|
+
}
|
|
9389
|
+
|
|
9390
|
+
// src/geoms/volcano.ts
|
|
9391
|
+
function geom_volcano(options = {}) {
|
|
9392
|
+
return {
|
|
9393
|
+
type: "volcano",
|
|
9394
|
+
stat: "identity",
|
|
9395
|
+
position: "identity",
|
|
9396
|
+
params: {
|
|
9397
|
+
fc_threshold: options.fc_threshold ?? 1,
|
|
9398
|
+
p_threshold: options.p_threshold ?? 0.05,
|
|
9399
|
+
y_is_neglog10: options.y_is_neglog10 ?? false,
|
|
9400
|
+
up_color: options.up_color ?? "#e41a1c",
|
|
9401
|
+
down_color: options.down_color ?? "#377eb8",
|
|
9402
|
+
ns_color: options.ns_color ?? "#999999",
|
|
9403
|
+
show_thresholds: options.show_thresholds ?? true,
|
|
9404
|
+
threshold_linetype: options.threshold_linetype ?? "dashed",
|
|
9405
|
+
n_labels: options.n_labels ?? 0,
|
|
9406
|
+
size: options.size ?? 1,
|
|
9407
|
+
alpha: options.alpha ?? 0.6,
|
|
9408
|
+
point_char: options.point_char ?? "●",
|
|
9409
|
+
show_legend: options.show_legend ?? true,
|
|
9410
|
+
classify: options.classify
|
|
9411
|
+
}
|
|
9412
|
+
};
|
|
9413
|
+
}
|
|
9414
|
+
|
|
9415
|
+
// src/geoms/ma.ts
|
|
9416
|
+
function geom_ma(options = {}) {
|
|
9417
|
+
return {
|
|
9418
|
+
type: "ma",
|
|
9419
|
+
stat: "identity",
|
|
9420
|
+
position: "identity",
|
|
9421
|
+
params: {
|
|
9422
|
+
fc_threshold: options.fc_threshold ?? 1,
|
|
9423
|
+
p_threshold: options.p_threshold ?? 0.05,
|
|
9424
|
+
p_col: options.p_col,
|
|
9425
|
+
x_is_log2: options.x_is_log2 ?? false,
|
|
9426
|
+
up_color: options.up_color ?? "#e41a1c",
|
|
9427
|
+
down_color: options.down_color ?? "#377eb8",
|
|
9428
|
+
ns_color: options.ns_color ?? "#999999",
|
|
9429
|
+
show_baseline: options.show_baseline ?? true,
|
|
9430
|
+
show_thresholds: options.show_thresholds ?? true,
|
|
9431
|
+
linetype: options.linetype ?? "dashed",
|
|
9432
|
+
n_labels: options.n_labels ?? 0,
|
|
9433
|
+
size: options.size ?? 1,
|
|
9434
|
+
alpha: options.alpha ?? 0.6,
|
|
9435
|
+
point_char: options.point_char ?? "●",
|
|
9436
|
+
show_smooth: options.show_smooth ?? false
|
|
9437
|
+
}
|
|
9438
|
+
};
|
|
9439
|
+
}
|
|
9440
|
+
|
|
9441
|
+
// src/geoms/manhattan.ts
|
|
9442
|
+
function geom_manhattan(options = {}) {
|
|
9443
|
+
return {
|
|
9444
|
+
type: "manhattan",
|
|
9445
|
+
stat: "identity",
|
|
9446
|
+
position: "identity",
|
|
9447
|
+
params: {
|
|
9448
|
+
suggestive_threshold: options.suggestive_threshold ?? 0.00001,
|
|
9449
|
+
genome_wide_threshold: options.genome_wide_threshold ?? 0.00000005,
|
|
9450
|
+
chr_col: options.chr_col,
|
|
9451
|
+
pos_col: options.pos_col,
|
|
9452
|
+
p_col: options.p_col,
|
|
9453
|
+
y_is_neglog10: options.y_is_neglog10 ?? false,
|
|
9454
|
+
chr_colors: options.chr_colors ?? DEFAULT_CHR_COLORS,
|
|
9455
|
+
highlight_color: options.highlight_color ?? "#e41a1c",
|
|
9456
|
+
suggestive_color: options.suggestive_color ?? "#ff7f00",
|
|
9457
|
+
show_thresholds: options.show_thresholds ?? true,
|
|
9458
|
+
threshold_linetype: options.threshold_linetype ?? "dashed",
|
|
9459
|
+
n_labels: options.n_labels ?? 0,
|
|
9460
|
+
label_col: options.label_col,
|
|
9461
|
+
size: options.size ?? 1,
|
|
9462
|
+
alpha: options.alpha ?? 0.6,
|
|
9463
|
+
point_char: options.point_char ?? "●",
|
|
9464
|
+
chr_gap: options.chr_gap ?? 0.02
|
|
9465
|
+
}
|
|
9466
|
+
};
|
|
9467
|
+
}
|
|
9468
|
+
var DEFAULT_CHR_COLORS;
|
|
9469
|
+
var init_manhattan = __esm(() => {
|
|
9470
|
+
DEFAULT_CHR_COLORS = ["#1f78b4", "#a6cee3"];
|
|
9471
|
+
});
|
|
9472
|
+
|
|
9473
|
+
// src/geoms/heatmap.ts
|
|
9474
|
+
function geom_heatmap(options = {}) {
|
|
9475
|
+
return {
|
|
9476
|
+
type: "heatmap",
|
|
9477
|
+
stat: "identity",
|
|
9478
|
+
position: "identity",
|
|
9479
|
+
params: {
|
|
9480
|
+
x_col: options.x_col,
|
|
9481
|
+
y_col: options.y_col,
|
|
9482
|
+
value_col: options.value_col ?? "value",
|
|
9483
|
+
low_color: options.low_color ?? "#313695",
|
|
9484
|
+
mid_color: options.mid_color ?? "#ffffbf",
|
|
9485
|
+
high_color: options.high_color ?? "#a50026",
|
|
9486
|
+
na_color: options.na_color ?? "#808080",
|
|
9487
|
+
midpoint: options.midpoint,
|
|
9488
|
+
cluster_rows: options.cluster_rows ?? false,
|
|
9489
|
+
cluster_cols: options.cluster_cols ?? false,
|
|
9490
|
+
clustering_method: options.clustering_method ?? "complete",
|
|
9491
|
+
clustering_distance: options.clustering_distance ?? "euclidean",
|
|
9492
|
+
show_row_dendrogram: options.show_row_dendrogram ?? true,
|
|
9493
|
+
show_col_dendrogram: options.show_col_dendrogram ?? true,
|
|
9494
|
+
dendrogram_ratio: options.dendrogram_ratio ?? 0.15,
|
|
9495
|
+
show_row_labels: options.show_row_labels ?? true,
|
|
9496
|
+
show_col_labels: options.show_col_labels ?? true,
|
|
9497
|
+
show_values: options.show_values ?? false,
|
|
9498
|
+
value_format: options.value_format ?? ".2f",
|
|
9499
|
+
cell_char: options.cell_char ?? "█",
|
|
9500
|
+
border: options.border ?? false,
|
|
9501
|
+
scale: options.scale ?? "none"
|
|
9502
|
+
}
|
|
9503
|
+
};
|
|
9504
|
+
}
|
|
9505
|
+
|
|
9506
|
+
// src/geoms/biplot.ts
|
|
9507
|
+
function geom_biplot(options = {}) {
|
|
9508
|
+
return {
|
|
9509
|
+
type: "biplot",
|
|
9510
|
+
stat: "identity",
|
|
9511
|
+
position: "identity",
|
|
9512
|
+
params: {
|
|
9513
|
+
pc1_col: options.pc1_col ?? "PC1",
|
|
9514
|
+
pc2_col: options.pc2_col ?? "PC2",
|
|
9515
|
+
loadings: options.loadings,
|
|
9516
|
+
var_explained: options.var_explained,
|
|
9517
|
+
show_scores: options.show_scores ?? true,
|
|
9518
|
+
score_color: options.score_color,
|
|
9519
|
+
score_size: options.score_size ?? 1,
|
|
9520
|
+
score_alpha: options.score_alpha ?? 0.8,
|
|
9521
|
+
score_char: options.score_char ?? "●",
|
|
9522
|
+
show_score_labels: options.show_score_labels ?? false,
|
|
9523
|
+
show_loadings: options.show_loadings ?? true,
|
|
9524
|
+
loading_color: options.loading_color ?? "#e41a1c",
|
|
9525
|
+
loading_scale: options.loading_scale,
|
|
9526
|
+
arrow_char: options.arrow_char ?? "→",
|
|
9527
|
+
show_loading_labels: options.show_loading_labels ?? true,
|
|
9528
|
+
show_origin: options.show_origin ?? true,
|
|
9529
|
+
origin_color: options.origin_color ?? "#999999",
|
|
9530
|
+
show_circle: options.show_circle ?? false,
|
|
9531
|
+
circle_color: options.circle_color ?? "#cccccc"
|
|
9532
|
+
}
|
|
9533
|
+
};
|
|
9534
|
+
}
|
|
9535
|
+
|
|
9536
|
+
// src/geoms/kaplan-meier.ts
|
|
9537
|
+
function geom_kaplan_meier(options = {}) {
|
|
9538
|
+
return {
|
|
9539
|
+
type: "kaplan_meier",
|
|
9540
|
+
stat: "identity",
|
|
9541
|
+
position: "identity",
|
|
9542
|
+
params: {
|
|
9543
|
+
show_ci: options.show_ci ?? false,
|
|
9544
|
+
conf_level: options.conf_level ?? 0.95,
|
|
9545
|
+
show_censored: options.show_censored ?? true,
|
|
9546
|
+
censor_char: options.censor_char ?? "+",
|
|
9547
|
+
show_risk_table: options.show_risk_table ?? false,
|
|
9548
|
+
linetype: options.linetype ?? "solid",
|
|
9549
|
+
show_median: options.show_median ?? false,
|
|
9550
|
+
step_type: options.step_type ?? "post"
|
|
9551
|
+
}
|
|
9552
|
+
};
|
|
9553
|
+
}
|
|
9554
|
+
|
|
9555
|
+
// src/geoms/forest.ts
|
|
9556
|
+
function geom_forest(options = {}) {
|
|
9557
|
+
return {
|
|
9558
|
+
type: "forest",
|
|
9559
|
+
stat: "identity",
|
|
9560
|
+
position: "identity",
|
|
9561
|
+
params: {
|
|
9562
|
+
null_line: options.null_line ?? 1,
|
|
9563
|
+
log_scale: options.log_scale ?? false,
|
|
9564
|
+
show_summary: options.show_summary ?? false,
|
|
9565
|
+
summary_row: options.summary_row,
|
|
9566
|
+
null_line_color: options.null_line_color ?? "#888888",
|
|
9567
|
+
null_line_type: options.null_line_type ?? "dashed",
|
|
9568
|
+
point_char: options.point_char ?? "■",
|
|
9569
|
+
show_weights: options.show_weights ?? false,
|
|
9570
|
+
min_size: options.min_size ?? 1,
|
|
9571
|
+
max_size: options.max_size ?? 3
|
|
9572
|
+
}
|
|
9573
|
+
};
|
|
9574
|
+
}
|
|
9575
|
+
|
|
9576
|
+
// src/geoms/roc.ts
|
|
9577
|
+
function geom_roc(options = {}) {
|
|
9578
|
+
return {
|
|
9579
|
+
type: "roc",
|
|
9580
|
+
stat: "identity",
|
|
9581
|
+
position: "identity",
|
|
9582
|
+
params: {
|
|
9583
|
+
show_diagonal: options.show_diagonal ?? true,
|
|
9584
|
+
diagonal_color: options.diagonal_color ?? "#888888",
|
|
9585
|
+
diagonal_type: options.diagonal_type ?? "dashed",
|
|
9586
|
+
show_auc: options.show_auc ?? true,
|
|
9587
|
+
show_optimal: options.show_optimal ?? false,
|
|
9588
|
+
optimal_char: options.optimal_char ?? "●",
|
|
9589
|
+
show_ci: options.show_ci ?? false,
|
|
9590
|
+
conf_level: options.conf_level ?? 0.95,
|
|
9591
|
+
fill_auc: options.fill_auc ?? false,
|
|
9592
|
+
fill_alpha: options.fill_alpha ?? 0.3
|
|
9593
|
+
}
|
|
9594
|
+
};
|
|
9595
|
+
}
|
|
9596
|
+
|
|
9597
|
+
// src/geoms/bland-altman.ts
|
|
9598
|
+
function geom_bland_altman(options = {}) {
|
|
9599
|
+
return {
|
|
9600
|
+
type: "bland_altman",
|
|
9601
|
+
stat: "identity",
|
|
9602
|
+
position: "identity",
|
|
9603
|
+
params: {
|
|
9604
|
+
show_limits: options.show_limits ?? true,
|
|
9605
|
+
show_bias: options.show_bias ?? true,
|
|
9606
|
+
limit_multiplier: options.limit_multiplier ?? 1.96,
|
|
9607
|
+
bias_color: options.bias_color ?? "#0000ff",
|
|
9608
|
+
limit_color: options.limit_color ?? "#ff0000",
|
|
9609
|
+
linetype: options.linetype ?? "dashed",
|
|
9610
|
+
show_ci: options.show_ci ?? false,
|
|
9611
|
+
conf_level: options.conf_level ?? 0.95,
|
|
9612
|
+
point_char: options.point_char ?? "●",
|
|
9613
|
+
percent_diff: options.percent_diff ?? false,
|
|
9614
|
+
precomputed: options.precomputed ?? false
|
|
9615
|
+
}
|
|
9616
|
+
};
|
|
9617
|
+
}
|
|
9618
|
+
|
|
9619
|
+
// src/geoms/ecdf.ts
|
|
9620
|
+
function geom_ecdf(options = {}) {
|
|
7371
9621
|
return {
|
|
7372
|
-
type: "
|
|
9622
|
+
type: "ecdf",
|
|
7373
9623
|
stat: "identity",
|
|
7374
9624
|
position: "identity",
|
|
7375
9625
|
params: {
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
levels: options.levels ?? 5
|
|
9626
|
+
pad: options.pad ?? true,
|
|
9627
|
+
show_ci: options.show_ci ?? false,
|
|
9628
|
+
conf_level: options.conf_level ?? 0.95,
|
|
9629
|
+
step_type: options.step_type ?? "post",
|
|
9630
|
+
show_points: options.show_points ?? false,
|
|
9631
|
+
line_char: options.line_char ?? "─",
|
|
9632
|
+
complement: options.complement ?? false
|
|
7384
9633
|
}
|
|
7385
9634
|
};
|
|
7386
9635
|
}
|
|
7387
9636
|
|
|
7388
|
-
// src/geoms/
|
|
7389
|
-
function
|
|
9637
|
+
// src/geoms/funnel.ts
|
|
9638
|
+
function geom_funnel(options = {}) {
|
|
7390
9639
|
return {
|
|
7391
|
-
type: "
|
|
9640
|
+
type: "funnel",
|
|
7392
9641
|
stat: "identity",
|
|
7393
9642
|
position: "identity",
|
|
7394
9643
|
params: {
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
9644
|
+
show_contours: options.show_contours ?? true,
|
|
9645
|
+
contour_levels: options.contour_levels ?? [0.95],
|
|
9646
|
+
show_significance: options.show_significance ?? false,
|
|
9647
|
+
summary_effect: options.summary_effect,
|
|
9648
|
+
show_summary_line: options.show_summary_line ?? true,
|
|
9649
|
+
y_is_se: options.y_is_se ?? true,
|
|
9650
|
+
invert_y: options.invert_y ?? true,
|
|
9651
|
+
point_char: options.point_char ?? "●",
|
|
9652
|
+
contour_color: options.contour_color ?? "#888888"
|
|
7401
9653
|
}
|
|
7402
9654
|
};
|
|
7403
9655
|
}
|
|
7404
|
-
function geom_icicle(options = {}) {
|
|
7405
|
-
return geom_flame({ ...options, style: "icicle" });
|
|
7406
|
-
}
|
|
7407
9656
|
|
|
7408
|
-
// src/geoms/
|
|
7409
|
-
function
|
|
9657
|
+
// src/geoms/control.ts
|
|
9658
|
+
function geom_control(options = {}) {
|
|
7410
9659
|
return {
|
|
7411
|
-
type: "
|
|
9660
|
+
type: "control",
|
|
7412
9661
|
stat: "identity",
|
|
7413
9662
|
position: "identity",
|
|
7414
9663
|
params: {
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
9664
|
+
chart_type: options.chart_type ?? "i",
|
|
9665
|
+
sigma: options.sigma ?? 3,
|
|
9666
|
+
show_center: options.show_center ?? true,
|
|
9667
|
+
show_ucl: options.show_ucl ?? true,
|
|
9668
|
+
show_lcl: options.show_lcl ?? true,
|
|
9669
|
+
show_warning: options.show_warning ?? false,
|
|
9670
|
+
center: options.center,
|
|
9671
|
+
ucl: options.ucl,
|
|
9672
|
+
lcl: options.lcl,
|
|
9673
|
+
center_color: options.center_color ?? "#0000ff",
|
|
9674
|
+
limit_color: options.limit_color ?? "#ff0000",
|
|
9675
|
+
warning_color: options.warning_color ?? "#ffa500",
|
|
9676
|
+
connect_points: options.connect_points ?? true,
|
|
9677
|
+
highlight_ooc: options.highlight_ooc ?? true,
|
|
9678
|
+
ooc_char: options.ooc_char ?? "◆",
|
|
9679
|
+
point_char: options.point_char ?? "●"
|
|
7427
9680
|
}
|
|
7428
9681
|
};
|
|
7429
9682
|
}
|
|
7430
9683
|
|
|
7431
|
-
// src/geoms/
|
|
7432
|
-
function
|
|
9684
|
+
// src/geoms/scree.ts
|
|
9685
|
+
function geom_scree(options = {}) {
|
|
7433
9686
|
return {
|
|
7434
|
-
type: "
|
|
9687
|
+
type: "scree",
|
|
7435
9688
|
stat: "identity",
|
|
7436
9689
|
position: "identity",
|
|
7437
9690
|
params: {
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
9691
|
+
show_cumulative: options.show_cumulative ?? false,
|
|
9692
|
+
show_kaiser: options.show_kaiser ?? false,
|
|
9693
|
+
show_elbow: options.show_elbow ?? false,
|
|
9694
|
+
show_broken_stick: options.show_broken_stick ?? false,
|
|
9695
|
+
connect_points: options.connect_points ?? true,
|
|
9696
|
+
show_bars: options.show_bars ?? false,
|
|
9697
|
+
point_char: options.point_char ?? "●",
|
|
9698
|
+
color: options.color,
|
|
9699
|
+
cumulative_color: options.cumulative_color ?? "#ff0000",
|
|
9700
|
+
kaiser_color: options.kaiser_color ?? "#888888",
|
|
9701
|
+
y_format: options.y_format ?? "percentage",
|
|
9702
|
+
threshold: options.threshold,
|
|
9703
|
+
threshold_color: options.threshold_color ?? "#00aa00"
|
|
7448
9704
|
}
|
|
7449
9705
|
};
|
|
7450
9706
|
}
|
|
7451
9707
|
|
|
7452
|
-
// src/geoms/
|
|
7453
|
-
function
|
|
9708
|
+
// src/geoms/upset.ts
|
|
9709
|
+
function geom_upset(options = {}) {
|
|
7454
9710
|
return {
|
|
7455
|
-
type: "
|
|
9711
|
+
type: "upset",
|
|
7456
9712
|
stat: "identity",
|
|
7457
9713
|
position: "identity",
|
|
7458
9714
|
params: {
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
9715
|
+
sets: options.sets,
|
|
9716
|
+
min_size: options.min_size ?? 1,
|
|
9717
|
+
max_intersections: options.max_intersections ?? 20,
|
|
9718
|
+
sort_by: options.sort_by ?? "size",
|
|
9719
|
+
sort_order: options.sort_order ?? "desc",
|
|
9720
|
+
show_set_sizes: options.show_set_sizes ?? true,
|
|
9721
|
+
dot_char: options.dot_char ?? "●",
|
|
9722
|
+
empty_char: options.empty_char ?? "○",
|
|
9723
|
+
line_char: options.line_char ?? "│",
|
|
9724
|
+
bar_char: options.bar_char ?? "█",
|
|
9725
|
+
color: options.color,
|
|
9726
|
+
show_degree: options.show_degree ?? false
|
|
7469
9727
|
}
|
|
7470
9728
|
};
|
|
7471
9729
|
}
|
|
7472
9730
|
|
|
7473
|
-
// src/geoms/
|
|
7474
|
-
function
|
|
9731
|
+
// src/geoms/dendrogram.ts
|
|
9732
|
+
function geom_dendrogram(options = {}) {
|
|
7475
9733
|
return {
|
|
7476
|
-
type: "
|
|
9734
|
+
type: "dendrogram",
|
|
7477
9735
|
stat: "identity",
|
|
7478
9736
|
position: "identity",
|
|
7479
9737
|
params: {
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
9738
|
+
orientation: options.orientation ?? "vertical",
|
|
9739
|
+
labels: options.labels,
|
|
9740
|
+
show_labels: options.show_labels ?? true,
|
|
9741
|
+
hang: options.hang ?? false,
|
|
9742
|
+
cut_height: options.cut_height,
|
|
9743
|
+
k: options.k,
|
|
9744
|
+
branch_char: options.branch_char ?? "│",
|
|
9745
|
+
h_connector: options.h_connector ?? "─",
|
|
9746
|
+
v_connector: options.v_connector ?? "│",
|
|
9747
|
+
corner_tl: options.corner_tl ?? "┌",
|
|
9748
|
+
corner_tr: options.corner_tr ?? "┐",
|
|
9749
|
+
corner_bl: options.corner_bl ?? "└",
|
|
9750
|
+
corner_br: options.corner_br ?? "┘",
|
|
9751
|
+
leaf_char: options.leaf_char ?? "○",
|
|
9752
|
+
cluster_colors: options.cluster_colors,
|
|
9753
|
+
line_style: options.line_style ?? "square",
|
|
9754
|
+
parent_col: options.parent_col ?? "parent",
|
|
9755
|
+
height_col: options.height_col ?? "height",
|
|
9756
|
+
id_col: options.id_col ?? "id"
|
|
7494
9757
|
}
|
|
7495
9758
|
};
|
|
7496
9759
|
}
|
|
@@ -7500,6 +9763,7 @@ var init_geoms = __esm(() => {
|
|
|
7500
9763
|
init_ridgeline();
|
|
7501
9764
|
init_sparkline();
|
|
7502
9765
|
init_braille();
|
|
9766
|
+
init_manhattan();
|
|
7503
9767
|
});
|
|
7504
9768
|
|
|
7505
9769
|
// src/stats/index.ts
|
|
@@ -12006,6 +14270,7 @@ __export(exports_src, {
|
|
|
12006
14270
|
geom_volcano: () => geom_volcano,
|
|
12007
14271
|
geom_vline: () => geom_vline,
|
|
12008
14272
|
geom_violin: () => geom_violin,
|
|
14273
|
+
geom_upset: () => geom_upset,
|
|
12009
14274
|
geom_treemap: () => geom_treemap,
|
|
12010
14275
|
geom_tile: () => geom_tile,
|
|
12011
14276
|
geom_text: () => geom_text,
|
|
@@ -12013,36 +14278,46 @@ __export(exports_src, {
|
|
|
12013
14278
|
geom_sparkline: () => geom_sparkline,
|
|
12014
14279
|
geom_smooth: () => geom_smooth,
|
|
12015
14280
|
geom_segment: () => geom_segment,
|
|
14281
|
+
geom_scree: () => geom_scree,
|
|
12016
14282
|
geom_sankey: () => geom_sankey,
|
|
12017
14283
|
geom_rug: () => geom_rug,
|
|
14284
|
+
geom_roc: () => geom_roc,
|
|
12018
14285
|
geom_ridgeline: () => geom_ridgeline,
|
|
12019
14286
|
geom_ribbon: () => geom_ribbon,
|
|
12020
14287
|
geom_rect: () => geom_rect,
|
|
12021
14288
|
geom_raster: () => geom_raster,
|
|
12022
14289
|
geom_quasirandom: () => geom_quasirandom,
|
|
12023
|
-
geom_qq_line: () => geom_qq_line,
|
|
12024
14290
|
geom_qq: () => geom_qq,
|
|
12025
14291
|
geom_pointrange: () => geom_pointrange,
|
|
12026
14292
|
geom_point: () => geom_point,
|
|
12027
14293
|
geom_path: () => geom_path,
|
|
14294
|
+
geom_manhattan: () => geom_manhattan,
|
|
14295
|
+
geom_ma: () => geom_ma,
|
|
12028
14296
|
geom_lollipop: () => geom_lollipop,
|
|
12029
14297
|
geom_linerange: () => geom_linerange,
|
|
12030
14298
|
geom_line: () => geom_line,
|
|
12031
14299
|
geom_label: () => geom_label,
|
|
14300
|
+
geom_kaplan_meier: () => geom_kaplan_meier,
|
|
12032
14301
|
geom_joy: () => geom_joy,
|
|
12033
14302
|
geom_icicle: () => geom_icicle,
|
|
12034
14303
|
geom_hline: () => geom_hline,
|
|
12035
14304
|
geom_histogram: () => geom_histogram,
|
|
14305
|
+
geom_heatmap: () => geom_heatmap,
|
|
14306
|
+
geom_funnel: () => geom_funnel,
|
|
12036
14307
|
geom_freqpoly: () => geom_freqpoly,
|
|
14308
|
+
geom_forest: () => geom_forest,
|
|
12037
14309
|
geom_flame: () => geom_flame,
|
|
12038
14310
|
geom_errorbarh: () => geom_errorbarh,
|
|
12039
14311
|
geom_errorbar: () => geom_errorbar,
|
|
14312
|
+
geom_ecdf: () => geom_ecdf,
|
|
12040
14313
|
geom_dumbbell: () => geom_dumbbell,
|
|
12041
14314
|
geom_density_2d: () => geom_density_2d,
|
|
12042
14315
|
geom_density: () => geom_density,
|
|
14316
|
+
geom_dendrogram: () => geom_dendrogram,
|
|
12043
14317
|
geom_curve: () => geom_curve,
|
|
12044
14318
|
geom_crossbar: () => geom_crossbar,
|
|
12045
14319
|
geom_corrmat: () => geom_corrmat,
|
|
14320
|
+
geom_control: () => geom_control,
|
|
12046
14321
|
geom_contour_filled: () => geom_contour_filled,
|
|
12047
14322
|
geom_contour: () => geom_contour,
|
|
12048
14323
|
geom_col: () => geom_col,
|
|
@@ -12050,6 +14325,8 @@ __export(exports_src, {
|
|
|
12050
14325
|
geom_bullet: () => geom_bullet,
|
|
12051
14326
|
geom_braille: () => geom_braille,
|
|
12052
14327
|
geom_boxplot: () => geom_boxplot,
|
|
14328
|
+
geom_bland_altman: () => geom_bland_altman,
|
|
14329
|
+
geom_biplot: () => geom_biplot,
|
|
12053
14330
|
geom_bin2d: () => geom_bin2d,
|
|
12054
14331
|
geom_beeswarm: () => geom_beeswarm,
|
|
12055
14332
|
geom_bar: () => geom_bar,
|
|
@@ -12159,11 +14436,6 @@ var init_src = __esm(() => {
|
|
|
12159
14436
|
init_export();
|
|
12160
14437
|
});
|
|
12161
14438
|
|
|
12162
|
-
// src/cli-plot.ts
|
|
12163
|
-
init_src();
|
|
12164
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
12165
|
-
import { join as join3 } from "path";
|
|
12166
|
-
|
|
12167
14439
|
// src/history/index.ts
|
|
12168
14440
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, readdirSync } from "fs";
|
|
12169
14441
|
import { join } from "path";
|
|
@@ -12320,6 +14592,589 @@ function getLatestPlotId() {
|
|
|
12320
14592
|
}
|
|
12321
14593
|
return history[history.length - 1].id;
|
|
12322
14594
|
}
|
|
14595
|
+
var init_history = () => {};
|
|
14596
|
+
|
|
14597
|
+
// src/serve.ts
|
|
14598
|
+
var exports_serve = {};
|
|
14599
|
+
__export(exports_serve, {
|
|
14600
|
+
handleServe: () => handleServe
|
|
14601
|
+
});
|
|
14602
|
+
import { watch, writeFileSync as writeFileSync3, unlinkSync } from "fs";
|
|
14603
|
+
import { join as join3 } from "path";
|
|
14604
|
+
function plotToVegaLite(plot) {
|
|
14605
|
+
const geomTypes = plot._provenance.geomTypes;
|
|
14606
|
+
const hasCompositeMark = geomTypes.some((t) => COMPOSITE_MARKS.has(t));
|
|
14607
|
+
const spec = plotSpecToVegaLite(plot.spec, { interactive: !hasCompositeMark });
|
|
14608
|
+
return { spec, provenance: plot._provenance };
|
|
14609
|
+
}
|
|
14610
|
+
function getLatestPayload() {
|
|
14611
|
+
const id = getLatestPlotId();
|
|
14612
|
+
if (!id)
|
|
14613
|
+
return null;
|
|
14614
|
+
const plot = loadPlotFromHistory(id);
|
|
14615
|
+
if (!plot)
|
|
14616
|
+
return null;
|
|
14617
|
+
const { spec, provenance } = plotToVegaLite(plot);
|
|
14618
|
+
return JSON.stringify({ type: "plot", spec, provenance });
|
|
14619
|
+
}
|
|
14620
|
+
function handleServe(port) {
|
|
14621
|
+
const p = port || 4242;
|
|
14622
|
+
ensureHistoryDirs();
|
|
14623
|
+
const clients = new Set;
|
|
14624
|
+
let debounceTimer = null;
|
|
14625
|
+
const plotsDir = getPlotsDir();
|
|
14626
|
+
watch(plotsDir, (_event, filename) => {
|
|
14627
|
+
if (!filename || !filename.endsWith(".json"))
|
|
14628
|
+
return;
|
|
14629
|
+
if (debounceTimer)
|
|
14630
|
+
clearTimeout(debounceTimer);
|
|
14631
|
+
debounceTimer = setTimeout(() => {
|
|
14632
|
+
const payload = getLatestPayload();
|
|
14633
|
+
if (!payload)
|
|
14634
|
+
return;
|
|
14635
|
+
for (const client of clients) {
|
|
14636
|
+
try {
|
|
14637
|
+
client.send(payload);
|
|
14638
|
+
} catch {
|
|
14639
|
+
clients.delete(client);
|
|
14640
|
+
}
|
|
14641
|
+
}
|
|
14642
|
+
}, 150);
|
|
14643
|
+
});
|
|
14644
|
+
const server = Bun.serve({
|
|
14645
|
+
port: p,
|
|
14646
|
+
fetch(req, server2) {
|
|
14647
|
+
const url2 = new URL(req.url);
|
|
14648
|
+
if (url2.pathname === "/ws") {
|
|
14649
|
+
if (server2.upgrade(req))
|
|
14650
|
+
return;
|
|
14651
|
+
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
14652
|
+
}
|
|
14653
|
+
if (url2.pathname === "/api/latest") {
|
|
14654
|
+
const payload = getLatestPayload();
|
|
14655
|
+
if (!payload)
|
|
14656
|
+
return Response.json({ type: "empty" });
|
|
14657
|
+
return new Response(payload, { headers: { "content-type": "application/json" } });
|
|
14658
|
+
}
|
|
14659
|
+
if (url2.pathname === "/api/history") {
|
|
14660
|
+
const entries = getHistory().slice(-50);
|
|
14661
|
+
return Response.json(entries);
|
|
14662
|
+
}
|
|
14663
|
+
if (url2.pathname.startsWith("/api/plot/")) {
|
|
14664
|
+
const id = url2.pathname.slice("/api/plot/".length);
|
|
14665
|
+
const plot = loadPlotFromHistory(id);
|
|
14666
|
+
if (!plot)
|
|
14667
|
+
return Response.json({ error: "not found" }, { status: 404 });
|
|
14668
|
+
const { spec, provenance } = plotToVegaLite(plot);
|
|
14669
|
+
return Response.json({ type: "plot", spec, provenance });
|
|
14670
|
+
}
|
|
14671
|
+
return new Response(CLIENT_HTML, {
|
|
14672
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
14673
|
+
});
|
|
14674
|
+
},
|
|
14675
|
+
websocket: {
|
|
14676
|
+
open(ws) {
|
|
14677
|
+
clients.add(ws);
|
|
14678
|
+
const payload = getLatestPayload();
|
|
14679
|
+
if (payload)
|
|
14680
|
+
ws.send(payload);
|
|
14681
|
+
},
|
|
14682
|
+
close(ws) {
|
|
14683
|
+
clients.delete(ws);
|
|
14684
|
+
},
|
|
14685
|
+
message() {}
|
|
14686
|
+
}
|
|
14687
|
+
});
|
|
14688
|
+
const url = `http://localhost:${server.port}`;
|
|
14689
|
+
console.log(`ggterm live viewer running at ${url}`);
|
|
14690
|
+
const markerPath = join3(getGGTermDir(), "serve.json");
|
|
14691
|
+
writeFileSync3(markerPath, JSON.stringify({ port: server.port, pid: process.pid }));
|
|
14692
|
+
const cleanup = () => {
|
|
14693
|
+
try {
|
|
14694
|
+
unlinkSync(markerPath);
|
|
14695
|
+
} catch {}
|
|
14696
|
+
};
|
|
14697
|
+
process.on("SIGINT", () => {
|
|
14698
|
+
cleanup();
|
|
14699
|
+
process.exit(0);
|
|
14700
|
+
});
|
|
14701
|
+
process.on("SIGTERM", () => {
|
|
14702
|
+
cleanup();
|
|
14703
|
+
process.exit(0);
|
|
14704
|
+
});
|
|
14705
|
+
process.on("exit", cleanup);
|
|
14706
|
+
if (process.env.TERM_PROGRAM === "waveterm") {
|
|
14707
|
+
Bun.spawn(["wsh", "web", "open", url]);
|
|
14708
|
+
console.log(`Opened Wave panel`);
|
|
14709
|
+
} else {
|
|
14710
|
+
console.log(`Open in browser or Wave panel: wsh web open ${url}`);
|
|
14711
|
+
}
|
|
14712
|
+
console.log(`Watching ${plotsDir} for new plots...`);
|
|
14713
|
+
console.log(`Press Ctrl+C to stop`);
|
|
14714
|
+
}
|
|
14715
|
+
var COMPOSITE_MARKS, CLIENT_HTML = `<!DOCTYPE html>
|
|
14716
|
+
<html lang="en">
|
|
14717
|
+
<head>
|
|
14718
|
+
<meta charset="utf-8">
|
|
14719
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
14720
|
+
<title>ggterm</title>
|
|
14721
|
+
<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
|
|
14722
|
+
<script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
|
|
14723
|
+
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
|
|
14724
|
+
<style>
|
|
14725
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
14726
|
+
body {
|
|
14727
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Mono', Menlo, monospace;
|
|
14728
|
+
background: #0d1117;
|
|
14729
|
+
color: #c9d1d9;
|
|
14730
|
+
height: 100vh;
|
|
14731
|
+
display: flex;
|
|
14732
|
+
overflow: hidden;
|
|
14733
|
+
}
|
|
14734
|
+
#sidebar {
|
|
14735
|
+
width: 260px;
|
|
14736
|
+
background: #161b22;
|
|
14737
|
+
border-right: 1px solid #30363d;
|
|
14738
|
+
display: flex;
|
|
14739
|
+
flex-direction: column;
|
|
14740
|
+
flex-shrink: 0;
|
|
14741
|
+
transform: translateX(-260px);
|
|
14742
|
+
transition: transform 0.2s ease;
|
|
14743
|
+
position: absolute;
|
|
14744
|
+
top: 0;
|
|
14745
|
+
left: 0;
|
|
14746
|
+
bottom: 0;
|
|
14747
|
+
z-index: 10;
|
|
14748
|
+
}
|
|
14749
|
+
#sidebar.open { transform: translateX(0); }
|
|
14750
|
+
#sidebar-header {
|
|
14751
|
+
padding: 12px;
|
|
14752
|
+
border-bottom: 1px solid #30363d;
|
|
14753
|
+
display: flex;
|
|
14754
|
+
align-items: center;
|
|
14755
|
+
justify-content: space-between;
|
|
14756
|
+
font-size: 12px;
|
|
14757
|
+
font-weight: 600;
|
|
14758
|
+
color: #c9d1d9;
|
|
14759
|
+
}
|
|
14760
|
+
#sidebar-header button {
|
|
14761
|
+
background: none;
|
|
14762
|
+
border: none;
|
|
14763
|
+
color: #8b949e;
|
|
14764
|
+
cursor: pointer;
|
|
14765
|
+
font-size: 14px;
|
|
14766
|
+
padding: 2px 6px;
|
|
14767
|
+
}
|
|
14768
|
+
#sidebar-header button:hover { color: #c9d1d9; }
|
|
14769
|
+
#history-list {
|
|
14770
|
+
flex: 1;
|
|
14771
|
+
overflow-y: auto;
|
|
14772
|
+
padding: 4px 0;
|
|
14773
|
+
}
|
|
14774
|
+
#history-list::-webkit-scrollbar { width: 6px; }
|
|
14775
|
+
#history-list::-webkit-scrollbar-track { background: transparent; }
|
|
14776
|
+
#history-list::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
|
|
14777
|
+
.history-item {
|
|
14778
|
+
padding: 8px 12px;
|
|
14779
|
+
cursor: pointer;
|
|
14780
|
+
border-left: 2px solid transparent;
|
|
14781
|
+
transition: background 0.1s;
|
|
14782
|
+
}
|
|
14783
|
+
.history-item:hover { background: #21262d; }
|
|
14784
|
+
.history-item.active { background: #1c2128; border-left-color: #58a6ff; }
|
|
14785
|
+
.history-item .hi-id { font-size: 11px; color: #58a6ff; font-weight: 600; }
|
|
14786
|
+
.history-item .hi-desc { font-size: 11px; color: #8b949e; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
14787
|
+
.history-item .hi-meta { font-size: 10px; color: #484f58; margin-top: 2px; display: flex; gap: 8px; }
|
|
14788
|
+
.history-item .hi-geom {
|
|
14789
|
+
background: #21262d;
|
|
14790
|
+
border: 1px solid #30363d;
|
|
14791
|
+
border-radius: 3px;
|
|
14792
|
+
padding: 0 4px;
|
|
14793
|
+
font-size: 10px;
|
|
14794
|
+
color: #8b949e;
|
|
14795
|
+
}
|
|
14796
|
+
#main {
|
|
14797
|
+
flex: 1;
|
|
14798
|
+
display: flex;
|
|
14799
|
+
flex-direction: column;
|
|
14800
|
+
min-width: 0;
|
|
14801
|
+
}
|
|
14802
|
+
#vis {
|
|
14803
|
+
flex: 1;
|
|
14804
|
+
display: flex;
|
|
14805
|
+
align-items: center;
|
|
14806
|
+
justify-content: center;
|
|
14807
|
+
padding: 16px;
|
|
14808
|
+
}
|
|
14809
|
+
#vis .vega-embed { width: 100%; }
|
|
14810
|
+
#vis .vega-embed canvas,
|
|
14811
|
+
#vis .vega-embed svg {
|
|
14812
|
+
max-width: 100%;
|
|
14813
|
+
max-height: calc(100vh - 80px);
|
|
14814
|
+
}
|
|
14815
|
+
#bar {
|
|
14816
|
+
display: flex;
|
|
14817
|
+
align-items: center;
|
|
14818
|
+
justify-content: space-between;
|
|
14819
|
+
padding: 8px 16px;
|
|
14820
|
+
background: #161b22;
|
|
14821
|
+
border-top: 1px solid #30363d;
|
|
14822
|
+
font-size: 12px;
|
|
14823
|
+
gap: 12px;
|
|
14824
|
+
min-height: 40px;
|
|
14825
|
+
}
|
|
14826
|
+
#meta { display: flex; gap: 16px; align-items: center; flex: 1; min-width: 0; }
|
|
14827
|
+
#plot-id { color: #58a6ff; font-weight: 600; }
|
|
14828
|
+
#plot-desc { color: #8b949e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
14829
|
+
#plot-time { color: #484f58; }
|
|
14830
|
+
#nav { display: flex; gap: 4px; align-items: center; }
|
|
14831
|
+
.bar-btn {
|
|
14832
|
+
background: #21262d;
|
|
14833
|
+
border: 1px solid #30363d;
|
|
14834
|
+
color: #c9d1d9;
|
|
14835
|
+
border-radius: 4px;
|
|
14836
|
+
padding: 4px 10px;
|
|
14837
|
+
cursor: pointer;
|
|
14838
|
+
font-size: 12px;
|
|
14839
|
+
font-family: inherit;
|
|
14840
|
+
}
|
|
14841
|
+
.bar-btn:hover { background: #30363d; }
|
|
14842
|
+
.bar-btn:disabled { opacity: 0.3; cursor: default; }
|
|
14843
|
+
.bar-btn:disabled:hover { background: #21262d; }
|
|
14844
|
+
.bar-btn.active { background: #30363d; border-color: #58a6ff; }
|
|
14845
|
+
#actions { display: flex; gap: 4px; align-items: center; }
|
|
14846
|
+
#actions button {
|
|
14847
|
+
background: none;
|
|
14848
|
+
border: 1px solid #30363d;
|
|
14849
|
+
color: #8b949e;
|
|
14850
|
+
border-radius: 4px;
|
|
14851
|
+
padding: 4px 8px;
|
|
14852
|
+
cursor: pointer;
|
|
14853
|
+
font-size: 11px;
|
|
14854
|
+
font-family: inherit;
|
|
14855
|
+
}
|
|
14856
|
+
#actions button:hover { color: #c9d1d9; border-color: #58a6ff; }
|
|
14857
|
+
#status {
|
|
14858
|
+
width: 8px; height: 8px;
|
|
14859
|
+
border-radius: 50%;
|
|
14860
|
+
background: #f85149;
|
|
14861
|
+
flex-shrink: 0;
|
|
14862
|
+
}
|
|
14863
|
+
#status.connected { background: #3fb950; }
|
|
14864
|
+
.waiting {
|
|
14865
|
+
color: #484f58;
|
|
14866
|
+
font-size: 14px;
|
|
14867
|
+
text-align: center;
|
|
14868
|
+
}
|
|
14869
|
+
.waiting .hint { font-size: 12px; margin-top: 8px; color: #30363d; }
|
|
14870
|
+
#shortcuts {
|
|
14871
|
+
display: none;
|
|
14872
|
+
position: fixed;
|
|
14873
|
+
top: 50%;
|
|
14874
|
+
left: 50%;
|
|
14875
|
+
transform: translate(-50%, -50%);
|
|
14876
|
+
background: #161b22;
|
|
14877
|
+
border: 1px solid #30363d;
|
|
14878
|
+
border-radius: 8px;
|
|
14879
|
+
padding: 20px 24px;
|
|
14880
|
+
z-index: 20;
|
|
14881
|
+
font-size: 12px;
|
|
14882
|
+
min-width: 220px;
|
|
14883
|
+
}
|
|
14884
|
+
#shortcuts.open { display: block; }
|
|
14885
|
+
#shortcuts h3 { font-size: 13px; margin-bottom: 12px; color: #c9d1d9; }
|
|
14886
|
+
.shortcut-row { display: flex; justify-content: space-between; padding: 4px 0; }
|
|
14887
|
+
.shortcut-row kbd {
|
|
14888
|
+
background: #21262d;
|
|
14889
|
+
border: 1px solid #30363d;
|
|
14890
|
+
border-radius: 3px;
|
|
14891
|
+
padding: 1px 6px;
|
|
14892
|
+
font-family: inherit;
|
|
14893
|
+
font-size: 11px;
|
|
14894
|
+
color: #c9d1d9;
|
|
14895
|
+
}
|
|
14896
|
+
.shortcut-row span { color: #8b949e; }
|
|
14897
|
+
#overlay {
|
|
14898
|
+
display: none;
|
|
14899
|
+
position: fixed;
|
|
14900
|
+
inset: 0;
|
|
14901
|
+
background: rgba(0,0,0,0.5);
|
|
14902
|
+
z-index: 15;
|
|
14903
|
+
}
|
|
14904
|
+
#overlay.open { display: block; }
|
|
14905
|
+
</style>
|
|
14906
|
+
</head>
|
|
14907
|
+
<body>
|
|
14908
|
+
<div id="sidebar">
|
|
14909
|
+
<div id="sidebar-header">
|
|
14910
|
+
<span>History</span>
|
|
14911
|
+
<button onclick="toggleHistory()">×</button>
|
|
14912
|
+
</div>
|
|
14913
|
+
<div id="history-list"></div>
|
|
14914
|
+
</div>
|
|
14915
|
+
<div id="main">
|
|
14916
|
+
<div id="vis">
|
|
14917
|
+
<div class="waiting">
|
|
14918
|
+
<div>waiting for plots...</div>
|
|
14919
|
+
<div class="hint">create a plot in ggterm and it will appear here</div>
|
|
14920
|
+
</div>
|
|
14921
|
+
</div>
|
|
14922
|
+
<div id="bar">
|
|
14923
|
+
<div id="status"></div>
|
|
14924
|
+
<div id="meta">
|
|
14925
|
+
<span id="plot-id"></span>
|
|
14926
|
+
<span id="plot-desc"></span>
|
|
14927
|
+
<span id="plot-time"></span>
|
|
14928
|
+
</div>
|
|
14929
|
+
<div id="nav">
|
|
14930
|
+
<button id="hist-btn" class="bar-btn" onclick="toggleHistory()" title="History (h)">H</button>
|
|
14931
|
+
<button id="prev" class="bar-btn" disabled title="Previous plot (←)">←</button>
|
|
14932
|
+
<button id="next" class="bar-btn" disabled title="Next plot (→)">→</button>
|
|
14933
|
+
</div>
|
|
14934
|
+
<div id="actions">
|
|
14935
|
+
<button onclick="downloadSVG()" title="Download SVG (s)">SVG</button>
|
|
14936
|
+
<button onclick="downloadPNG()" title="Download PNG (p)">PNG</button>
|
|
14937
|
+
<button onclick="toggleShortcuts()" title="Keyboard shortcuts (?)">?</button>
|
|
14938
|
+
</div>
|
|
14939
|
+
</div>
|
|
14940
|
+
</div>
|
|
14941
|
+
<div id="overlay" onclick="closeOverlays()"></div>
|
|
14942
|
+
<div id="shortcuts">
|
|
14943
|
+
<h3>Keyboard Shortcuts</h3>
|
|
14944
|
+
<div class="shortcut-row"><span>Previous plot</span><kbd>←</kbd></div>
|
|
14945
|
+
<div class="shortcut-row"><span>Next plot</span><kbd>→</kbd></div>
|
|
14946
|
+
<div class="shortcut-row"><span>Latest plot</span><kbd>End</kbd></div>
|
|
14947
|
+
<div class="shortcut-row"><span>First plot</span><kbd>Home</kbd></div>
|
|
14948
|
+
<div class="shortcut-row"><span>Toggle history</span><kbd>h</kbd></div>
|
|
14949
|
+
<div class="shortcut-row"><span>Download SVG</span><kbd>s</kbd></div>
|
|
14950
|
+
<div class="shortcut-row"><span>Download PNG</span><kbd>p</kbd></div>
|
|
14951
|
+
<div class="shortcut-row"><span>Fullscreen</span><kbd>f</kbd></div>
|
|
14952
|
+
<div class="shortcut-row"><span>Show shortcuts</span><kbd>?</kbd></div>
|
|
14953
|
+
<div class="shortcut-row"><span>Close panel</span><kbd>Esc</kbd></div>
|
|
14954
|
+
</div>
|
|
14955
|
+
<script>
|
|
14956
|
+
const vis = document.getElementById('vis');
|
|
14957
|
+
const main = document.getElementById('main');
|
|
14958
|
+
const statusEl = document.getElementById('status');
|
|
14959
|
+
const idEl = document.getElementById('plot-id');
|
|
14960
|
+
const descEl = document.getElementById('plot-desc');
|
|
14961
|
+
const timeEl = document.getElementById('plot-time');
|
|
14962
|
+
const prevBtn = document.getElementById('prev');
|
|
14963
|
+
const nextBtn = document.getElementById('next');
|
|
14964
|
+
const histBtn = document.getElementById('hist-btn');
|
|
14965
|
+
const sidebar = document.getElementById('sidebar');
|
|
14966
|
+
const historyList = document.getElementById('history-list');
|
|
14967
|
+
const shortcutsEl = document.getElementById('shortcuts');
|
|
14968
|
+
const overlayEl = document.getElementById('overlay');
|
|
14969
|
+
|
|
14970
|
+
let history = [];
|
|
14971
|
+
let historyIndex = {};
|
|
14972
|
+
let currentIdx = -1;
|
|
14973
|
+
let view = null;
|
|
14974
|
+
let ws = null;
|
|
14975
|
+
|
|
14976
|
+
function updateMeta(prov) {
|
|
14977
|
+
if (!prov) return;
|
|
14978
|
+
idEl.textContent = prov.id;
|
|
14979
|
+
descEl.textContent = prov.description || '';
|
|
14980
|
+
timeEl.textContent = prov.timestamp ? new Date(prov.timestamp).toLocaleTimeString() : '';
|
|
14981
|
+
}
|
|
14982
|
+
|
|
14983
|
+
function updateNav() {
|
|
14984
|
+
prevBtn.disabled = currentIdx <= 0;
|
|
14985
|
+
nextBtn.disabled = currentIdx >= history.length - 1;
|
|
14986
|
+
}
|
|
14987
|
+
|
|
14988
|
+
function updateHistoryHighlight() {
|
|
14989
|
+
historyList.querySelectorAll('.history-item').forEach((el, i) => {
|
|
14990
|
+
el.classList.toggle('active', i === currentIdx);
|
|
14991
|
+
});
|
|
14992
|
+
const active = historyList.querySelector('.active');
|
|
14993
|
+
if (active) active.scrollIntoView({ block: 'nearest' });
|
|
14994
|
+
}
|
|
14995
|
+
|
|
14996
|
+
function addHistoryItem(data, idx) {
|
|
14997
|
+
const prov = data.provenance;
|
|
14998
|
+
if (!prov) return;
|
|
14999
|
+
const div = document.createElement('div');
|
|
15000
|
+
div.className = 'history-item' + (idx === currentIdx ? ' active' : '');
|
|
15001
|
+
div.innerHTML =
|
|
15002
|
+
'<div class="hi-id">' + prov.id + '</div>' +
|
|
15003
|
+
'<div class="hi-desc">' + (prov.description || '') + '</div>' +
|
|
15004
|
+
'<div class="hi-meta">' +
|
|
15005
|
+
'<span class="hi-geom">' + (prov.geomTypes ? prov.geomTypes.join('+') : '') + '</span>' +
|
|
15006
|
+
'<span>' + (prov.timestamp ? new Date(prov.timestamp).toLocaleTimeString() : '') + '</span>' +
|
|
15007
|
+
'</div>';
|
|
15008
|
+
div.onclick = () => navigate(idx);
|
|
15009
|
+
historyList.appendChild(div);
|
|
15010
|
+
}
|
|
15011
|
+
|
|
15012
|
+
function rebuildHistoryList() {
|
|
15013
|
+
historyList.innerHTML = '';
|
|
15014
|
+
history.forEach((data, i) => addHistoryItem(data, i));
|
|
15015
|
+
}
|
|
15016
|
+
|
|
15017
|
+
const embedOpts = {
|
|
15018
|
+
actions: false,
|
|
15019
|
+
theme: 'dark',
|
|
15020
|
+
renderer: 'svg',
|
|
15021
|
+
config: {
|
|
15022
|
+
background: '#0d1117',
|
|
15023
|
+
axis: { domainColor: '#30363d', gridColor: '#21262d', tickColor: '#30363d', labelColor: '#8b949e', titleColor: '#c9d1d9' },
|
|
15024
|
+
legend: { labelColor: '#c9d1d9', titleColor: '#c9d1d9' },
|
|
15025
|
+
title: { color: '#c9d1d9', subtitleColor: '#8b949e' },
|
|
15026
|
+
view: { stroke: null }
|
|
15027
|
+
}
|
|
15028
|
+
};
|
|
15029
|
+
|
|
15030
|
+
async function renderSpec(spec) {
|
|
15031
|
+
vis.innerHTML = '';
|
|
15032
|
+
const vegaSpec = { ...spec, width: 'container', height: 'container', autosize: { type: 'fit', contains: 'padding' } };
|
|
15033
|
+
try {
|
|
15034
|
+
const result = await vegaEmbed(vis, vegaSpec, embedOpts);
|
|
15035
|
+
view = result.view;
|
|
15036
|
+
} catch (e) {
|
|
15037
|
+
// Retry without interactive params (composite marks like boxplot don't support selections)
|
|
15038
|
+
console.warn('Render failed, retrying without params:', e.message);
|
|
15039
|
+
const { params, ...cleanSpec } = vegaSpec;
|
|
15040
|
+
const result = await vegaEmbed(vis, cleanSpec, embedOpts);
|
|
15041
|
+
view = result.view;
|
|
15042
|
+
}
|
|
15043
|
+
}
|
|
15044
|
+
|
|
15045
|
+
async function showPlot(data) {
|
|
15046
|
+
await renderSpec(data.spec);
|
|
15047
|
+
updateMeta(data.provenance);
|
|
15048
|
+
}
|
|
15049
|
+
|
|
15050
|
+
function navigate(idx) {
|
|
15051
|
+
if (idx < 0 || idx >= history.length) return;
|
|
15052
|
+
currentIdx = idx;
|
|
15053
|
+
const data = history[idx];
|
|
15054
|
+
if (data.spec) {
|
|
15055
|
+
showPlot(data);
|
|
15056
|
+
} else {
|
|
15057
|
+
// Lazy-load full spec from server
|
|
15058
|
+
fetch('/api/plot/' + data.provenance.id)
|
|
15059
|
+
.then(r => r.json())
|
|
15060
|
+
.then(full => {
|
|
15061
|
+
history[idx] = full;
|
|
15062
|
+
showPlot(full);
|
|
15063
|
+
});
|
|
15064
|
+
}
|
|
15065
|
+
updateNav();
|
|
15066
|
+
updateHistoryHighlight();
|
|
15067
|
+
}
|
|
15068
|
+
|
|
15069
|
+
prevBtn.onclick = () => navigate(currentIdx - 1);
|
|
15070
|
+
nextBtn.onclick = () => navigate(currentIdx + 1);
|
|
15071
|
+
|
|
15072
|
+
function toggleHistory() {
|
|
15073
|
+
const open = sidebar.classList.toggle('open');
|
|
15074
|
+
histBtn.classList.toggle('active', open);
|
|
15075
|
+
}
|
|
15076
|
+
|
|
15077
|
+
function toggleShortcuts() {
|
|
15078
|
+
const open = shortcutsEl.classList.toggle('open');
|
|
15079
|
+
overlayEl.classList.toggle('open', open);
|
|
15080
|
+
}
|
|
15081
|
+
|
|
15082
|
+
function closeOverlays() {
|
|
15083
|
+
shortcutsEl.classList.remove('open');
|
|
15084
|
+
overlayEl.classList.remove('open');
|
|
15085
|
+
}
|
|
15086
|
+
|
|
15087
|
+
document.addEventListener('keydown', (e) => {
|
|
15088
|
+
// Ignore when typing in an input
|
|
15089
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
15090
|
+
|
|
15091
|
+
switch (e.key) {
|
|
15092
|
+
case 'ArrowLeft': navigate(currentIdx - 1); break;
|
|
15093
|
+
case 'ArrowRight': navigate(currentIdx + 1); break;
|
|
15094
|
+
case 'Home': e.preventDefault(); navigate(0); break;
|
|
15095
|
+
case 'End': e.preventDefault(); navigate(history.length - 1); break;
|
|
15096
|
+
case 'h': toggleHistory(); break;
|
|
15097
|
+
case 's': downloadSVG(); break;
|
|
15098
|
+
case 'p': downloadPNG(); break;
|
|
15099
|
+
case 'f':
|
|
15100
|
+
if (!document.fullscreenElement) {
|
|
15101
|
+
document.documentElement.requestFullscreen();
|
|
15102
|
+
} else {
|
|
15103
|
+
document.exitFullscreen();
|
|
15104
|
+
}
|
|
15105
|
+
break;
|
|
15106
|
+
case '?': toggleShortcuts(); break;
|
|
15107
|
+
case 'Escape': closeOverlays(); if (sidebar.classList.contains('open')) toggleHistory(); break;
|
|
15108
|
+
}
|
|
15109
|
+
});
|
|
15110
|
+
|
|
15111
|
+
function downloadSVG() {
|
|
15112
|
+
if (!view) return;
|
|
15113
|
+
view.toSVG().then(svg => {
|
|
15114
|
+
const a = document.createElement('a');
|
|
15115
|
+
a.href = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }));
|
|
15116
|
+
a.download = (idEl.textContent || 'plot') + '.svg';
|
|
15117
|
+
a.click();
|
|
15118
|
+
});
|
|
15119
|
+
}
|
|
15120
|
+
|
|
15121
|
+
function downloadPNG() {
|
|
15122
|
+
if (!view) return;
|
|
15123
|
+
view.toCanvas(2).then(canvas => {
|
|
15124
|
+
const a = document.createElement('a');
|
|
15125
|
+
a.href = canvas.toDataURL('image/png');
|
|
15126
|
+
a.download = (idEl.textContent || 'plot') + '.png';
|
|
15127
|
+
a.click();
|
|
15128
|
+
});
|
|
15129
|
+
}
|
|
15130
|
+
|
|
15131
|
+
function connect() {
|
|
15132
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
15133
|
+
ws = new WebSocket(proto + '//' + location.host + '/ws');
|
|
15134
|
+
|
|
15135
|
+
ws.onopen = () => { statusEl.classList.add('connected'); };
|
|
15136
|
+
ws.onclose = () => {
|
|
15137
|
+
statusEl.classList.remove('connected');
|
|
15138
|
+
setTimeout(connect, 2000);
|
|
15139
|
+
};
|
|
15140
|
+
ws.onerror = () => ws.close();
|
|
15141
|
+
|
|
15142
|
+
ws.onmessage = (e) => {
|
|
15143
|
+
const data = JSON.parse(e.data);
|
|
15144
|
+
if (data.type === 'plot') {
|
|
15145
|
+
history.push(data);
|
|
15146
|
+
currentIdx = history.length - 1;
|
|
15147
|
+
addHistoryItem(data, history.length - 1);
|
|
15148
|
+
showPlot(data);
|
|
15149
|
+
updateNav();
|
|
15150
|
+
updateHistoryHighlight();
|
|
15151
|
+
}
|
|
15152
|
+
};
|
|
15153
|
+
}
|
|
15154
|
+
|
|
15155
|
+
// Load initial history then connect
|
|
15156
|
+
fetch('/api/history')
|
|
15157
|
+
.then(r => r.json())
|
|
15158
|
+
.then(entries => {
|
|
15159
|
+
// Populate history with provenance-only stubs (lazy-load specs on navigate)
|
|
15160
|
+
history = entries.map(e => ({ provenance: e, spec: null }));
|
|
15161
|
+
rebuildHistoryList();
|
|
15162
|
+
})
|
|
15163
|
+
.then(() => connect());
|
|
15164
|
+
</script>
|
|
15165
|
+
</body>
|
|
15166
|
+
</html>`;
|
|
15167
|
+
var init_serve = __esm(() => {
|
|
15168
|
+
init_history();
|
|
15169
|
+
init_export();
|
|
15170
|
+
COMPOSITE_MARKS = new Set(["boxplot", "violin", "errorband", "errorbar"]);
|
|
15171
|
+
});
|
|
15172
|
+
|
|
15173
|
+
// src/cli-plot.ts
|
|
15174
|
+
init_src();
|
|
15175
|
+
init_history();
|
|
15176
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, existsSync as existsSync3 } from "fs";
|
|
15177
|
+
import { join as join4 } from "path";
|
|
12323
15178
|
|
|
12324
15179
|
// src/init.ts
|
|
12325
15180
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, existsSync as existsSync2, readdirSync as readdirSync2, statSync } from "fs";
|
|
@@ -12941,6 +15796,18 @@ function handleInit() {
|
|
|
12941
15796
|
}
|
|
12942
15797
|
|
|
12943
15798
|
// src/cli-plot.ts
|
|
15799
|
+
function isServeRunning() {
|
|
15800
|
+
const markerPath = join4(getGGTermDir(), "serve.json");
|
|
15801
|
+
if (!existsSync3(markerPath))
|
|
15802
|
+
return false;
|
|
15803
|
+
try {
|
|
15804
|
+
const info = JSON.parse(readFileSync3(markerPath, "utf-8"));
|
|
15805
|
+
process.kill(info.pid, 0);
|
|
15806
|
+
return true;
|
|
15807
|
+
} catch {
|
|
15808
|
+
return false;
|
|
15809
|
+
}
|
|
15810
|
+
}
|
|
12944
15811
|
var GEOM_TYPES = [
|
|
12945
15812
|
"point",
|
|
12946
15813
|
"line",
|
|
@@ -12989,7 +15856,22 @@ var GEOM_TYPES = [
|
|
|
12989
15856
|
"corrmat",
|
|
12990
15857
|
"sankey",
|
|
12991
15858
|
"treemap",
|
|
12992
|
-
"volcano"
|
|
15859
|
+
"volcano",
|
|
15860
|
+
"ma",
|
|
15861
|
+
"manhattan",
|
|
15862
|
+
"heatmap",
|
|
15863
|
+
"biplot",
|
|
15864
|
+
"kaplan_meier",
|
|
15865
|
+
"forest",
|
|
15866
|
+
"roc",
|
|
15867
|
+
"bland_altman",
|
|
15868
|
+
"qq",
|
|
15869
|
+
"ecdf",
|
|
15870
|
+
"funnel",
|
|
15871
|
+
"control",
|
|
15872
|
+
"scree",
|
|
15873
|
+
"upset",
|
|
15874
|
+
"dendrogram"
|
|
12993
15875
|
];
|
|
12994
15876
|
var datePattern = /^\d{4}-\d{2}-\d{2}/;
|
|
12995
15877
|
function fileExists(path) {
|
|
@@ -13456,8 +16338,15 @@ Did you mean one of these commands?`);
|
|
|
13456
16338
|
process.exit(1);
|
|
13457
16339
|
}
|
|
13458
16340
|
const [dataFile, x, y, color, title, geomSpec = "point", facetVar] = args;
|
|
13459
|
-
|
|
16341
|
+
let { headers, data } = loadData(dataFile);
|
|
13460
16342
|
const { baseGeom: geomType, refLines } = parseGeomSpec(geomSpec);
|
|
16343
|
+
if (geomType === "manhattan" && y && y !== "-") {
|
|
16344
|
+
data = data.map((row) => {
|
|
16345
|
+
const pval = Number(row[y]);
|
|
16346
|
+
const negLogP = pval > 0 ? -Math.log10(pval) : 0;
|
|
16347
|
+
return { ...row, [`_neglog10_${y}`]: negLogP };
|
|
16348
|
+
});
|
|
16349
|
+
}
|
|
13461
16350
|
validateGeomType(geomType);
|
|
13462
16351
|
validateColumn(x, headers, "x");
|
|
13463
16352
|
if (y && y !== "-")
|
|
@@ -13477,8 +16366,11 @@ If you want a univariate plot, try: histogram, density, bar, qq, or freqpoly`);
|
|
|
13477
16366
|
process.exit(1);
|
|
13478
16367
|
}
|
|
13479
16368
|
const aes = { x };
|
|
13480
|
-
if (y && y !== "-")
|
|
16369
|
+
if (geomType === "manhattan" && y && y !== "-") {
|
|
16370
|
+
aes.y = `_neglog10_${y}`;
|
|
16371
|
+
} else if (y && y !== "-") {
|
|
13481
16372
|
aes.y = y;
|
|
16373
|
+
}
|
|
13482
16374
|
if (color && color !== "-") {
|
|
13483
16375
|
if (geomType === "tile" || geomType === "raster" || geomType === "bin2d") {
|
|
13484
16376
|
aes.fill = color;
|
|
@@ -13603,7 +16495,6 @@ If you want a univariate plot, try: histogram, density, bar, qq, or freqpoly`);
|
|
|
13603
16495
|
break;
|
|
13604
16496
|
case "qq":
|
|
13605
16497
|
plot = plot.geom(geom_qq());
|
|
13606
|
-
plot = plot.geom(geom_qq_line());
|
|
13607
16498
|
break;
|
|
13608
16499
|
case "calendar":
|
|
13609
16500
|
plot = plot.geom(geom_calendar());
|
|
@@ -13626,6 +16517,51 @@ If you want a univariate plot, try: histogram, density, bar, qq, or freqpoly`);
|
|
|
13626
16517
|
case "volcano":
|
|
13627
16518
|
plot = plot.geom(geom_volcano());
|
|
13628
16519
|
break;
|
|
16520
|
+
case "ma":
|
|
16521
|
+
plot = plot.geom(geom_ma());
|
|
16522
|
+
break;
|
|
16523
|
+
case "manhattan":
|
|
16524
|
+
plot = plot.geom(geom_manhattan({ y_is_neglog10: true }));
|
|
16525
|
+
break;
|
|
16526
|
+
case "heatmap":
|
|
16527
|
+
plot = plot.geom(geom_heatmap());
|
|
16528
|
+
break;
|
|
16529
|
+
case "biplot":
|
|
16530
|
+
plot = plot.geom(geom_biplot());
|
|
16531
|
+
break;
|
|
16532
|
+
case "kaplan_meier":
|
|
16533
|
+
plot = plot.geom(geom_kaplan_meier());
|
|
16534
|
+
break;
|
|
16535
|
+
case "forest":
|
|
16536
|
+
plot = plot.geom(geom_forest());
|
|
16537
|
+
break;
|
|
16538
|
+
case "roc":
|
|
16539
|
+
plot = plot.geom(geom_roc());
|
|
16540
|
+
break;
|
|
16541
|
+
case "bland_altman":
|
|
16542
|
+
plot = plot.geom(geom_bland_altman());
|
|
16543
|
+
break;
|
|
16544
|
+
case "qq":
|
|
16545
|
+
plot = plot.geom(geom_qq());
|
|
16546
|
+
break;
|
|
16547
|
+
case "ecdf":
|
|
16548
|
+
plot = plot.geom(geom_ecdf());
|
|
16549
|
+
break;
|
|
16550
|
+
case "funnel":
|
|
16551
|
+
plot = plot.geom(geom_funnel());
|
|
16552
|
+
break;
|
|
16553
|
+
case "control":
|
|
16554
|
+
plot = plot.geom(geom_control());
|
|
16555
|
+
break;
|
|
16556
|
+
case "scree":
|
|
16557
|
+
plot = plot.geom(geom_scree());
|
|
16558
|
+
break;
|
|
16559
|
+
case "upset":
|
|
16560
|
+
plot = plot.geom(geom_upset());
|
|
16561
|
+
break;
|
|
16562
|
+
case "dendrogram":
|
|
16563
|
+
plot = plot.geom(geom_dendrogram());
|
|
16564
|
+
break;
|
|
13629
16565
|
case "point":
|
|
13630
16566
|
default:
|
|
13631
16567
|
plot = plot.geom(geom_point());
|
|
@@ -13649,10 +16585,70 @@ If you want a univariate plot, try: histogram, density, bar, qq, or freqpoly`);
|
|
|
13649
16585
|
if (geomType === "qq") {
|
|
13650
16586
|
yLabel = "Sample Quantiles";
|
|
13651
16587
|
}
|
|
16588
|
+
if (geomType === "manhattan") {
|
|
16589
|
+
yLabel = "-log10(p-value)";
|
|
16590
|
+
}
|
|
16591
|
+
if (geomType === "kaplan_meier") {
|
|
16592
|
+
yLabel = "Survival Probability";
|
|
16593
|
+
}
|
|
16594
|
+
if (geomType === "roc") {
|
|
16595
|
+
yLabel = "True Positive Rate (Sensitivity)";
|
|
16596
|
+
}
|
|
16597
|
+
if (geomType === "bland_altman") {
|
|
16598
|
+
yLabel = "Difference (Method1 - Method2)";
|
|
16599
|
+
}
|
|
16600
|
+
if (geomType === "ecdf") {
|
|
16601
|
+
yLabel = "Cumulative Probability";
|
|
16602
|
+
}
|
|
16603
|
+
if (geomType === "funnel") {
|
|
16604
|
+
yLabel = "Standard Error";
|
|
16605
|
+
}
|
|
16606
|
+
if (geomType === "control") {
|
|
16607
|
+
yLabel = "Measurement";
|
|
16608
|
+
}
|
|
16609
|
+
if (geomType === "scree") {
|
|
16610
|
+
yLabel = "Variance Explained";
|
|
16611
|
+
}
|
|
16612
|
+
if (geomType === "upset") {
|
|
16613
|
+
yLabel = "Intersection Size";
|
|
16614
|
+
}
|
|
16615
|
+
if (geomType === "dendrogram") {
|
|
16616
|
+
yLabel = "Height";
|
|
16617
|
+
}
|
|
13652
16618
|
let xLabel = x;
|
|
13653
16619
|
if (geomType === "qq") {
|
|
13654
16620
|
xLabel = "Theoretical Quantiles";
|
|
13655
16621
|
}
|
|
16622
|
+
if (geomType === "manhattan") {
|
|
16623
|
+
xLabel = "Chromosome";
|
|
16624
|
+
}
|
|
16625
|
+
if (geomType === "kaplan_meier") {
|
|
16626
|
+
xLabel = "Time";
|
|
16627
|
+
}
|
|
16628
|
+
if (geomType === "roc") {
|
|
16629
|
+
xLabel = "False Positive Rate (1 - Specificity)";
|
|
16630
|
+
}
|
|
16631
|
+
if (geomType === "bland_altman") {
|
|
16632
|
+
xLabel = "Mean of Methods";
|
|
16633
|
+
}
|
|
16634
|
+
if (geomType === "forest") {
|
|
16635
|
+
xLabel = "Effect Size";
|
|
16636
|
+
}
|
|
16637
|
+
if (geomType === "funnel") {
|
|
16638
|
+
xLabel = "Effect Size";
|
|
16639
|
+
}
|
|
16640
|
+
if (geomType === "control") {
|
|
16641
|
+
xLabel = "Sample Number";
|
|
16642
|
+
}
|
|
16643
|
+
if (geomType === "scree") {
|
|
16644
|
+
xLabel = "Component";
|
|
16645
|
+
}
|
|
16646
|
+
if (geomType === "upset") {
|
|
16647
|
+
xLabel = "Set Intersection";
|
|
16648
|
+
}
|
|
16649
|
+
if (geomType === "dendrogram") {
|
|
16650
|
+
xLabel = "Cluster";
|
|
16651
|
+
}
|
|
13656
16652
|
if (title && title !== "-") {
|
|
13657
16653
|
plot = plot.labs({ title, x: xLabel, y: yLabel });
|
|
13658
16654
|
} else {
|
|
@@ -13661,15 +16657,22 @@ If you want a univariate plot, try: histogram, density, bar, qq, or freqpoly`);
|
|
|
13661
16657
|
if (facetVar && facetVar !== "-") {
|
|
13662
16658
|
plot = plot.facet(facet_wrap(facetVar));
|
|
13663
16659
|
}
|
|
13664
|
-
|
|
16660
|
+
const serveActive = isServeRunning();
|
|
16661
|
+
if (!serveActive) {
|
|
16662
|
+
console.log(plot.render({ width: 70, height: 20, colorMode: "truecolor" }));
|
|
16663
|
+
}
|
|
13665
16664
|
const spec = plot.spec();
|
|
13666
16665
|
const commandStr = `cli-plot.ts ${args.join(" ")}`;
|
|
13667
16666
|
const plotId = savePlotToHistory(spec, {
|
|
13668
16667
|
dataFile,
|
|
13669
16668
|
command: commandStr
|
|
13670
16669
|
});
|
|
13671
|
-
|
|
16670
|
+
if (serveActive) {
|
|
16671
|
+
console.log(`[${plotId}] → live viewer`);
|
|
16672
|
+
} else {
|
|
16673
|
+
console.log(`
|
|
13672
16674
|
[Saved as ${plotId}]`);
|
|
16675
|
+
}
|
|
13673
16676
|
}
|
|
13674
16677
|
function handleHistory(searchQuery) {
|
|
13675
16678
|
const entries = searchQuery ? searchHistory(searchQuery) : getHistory();
|
|
@@ -13748,7 +16751,7 @@ function handleExport(idOrFile, outputFile) {
|
|
|
13748
16751
|
const spec = plotSpecToVegaLite2(plotSpec, {
|
|
13749
16752
|
interactive: true
|
|
13750
16753
|
});
|
|
13751
|
-
|
|
16754
|
+
writeFileSync4(join4(getGGTermDir(), "last-plot-vegalite.json"), JSON.stringify(spec, null, 2));
|
|
13752
16755
|
const title = typeof spec.title === "string" ? spec.title : spec.title?.text || "Plot";
|
|
13753
16756
|
const output = outputFile || `${plotId}.html`;
|
|
13754
16757
|
const html = `<!DOCTYPE html>
|
|
@@ -13805,7 +16808,7 @@ function handleExport(idOrFile, outputFile) {
|
|
|
13805
16808
|
</script>
|
|
13806
16809
|
</body>
|
|
13807
16810
|
</html>`;
|
|
13808
|
-
|
|
16811
|
+
writeFileSync4(output, html);
|
|
13809
16812
|
console.log(`Created ${output} - open in browser to view and export`);
|
|
13810
16813
|
}
|
|
13811
16814
|
function printUsage() {
|
|
@@ -13819,6 +16822,7 @@ Commands:
|
|
|
13819
16822
|
history [search] List all plots (optionally filter by search)
|
|
13820
16823
|
show <id> Re-render a plot from history
|
|
13821
16824
|
export [id] [output.html] Export plot to HTML (latest or by ID)
|
|
16825
|
+
serve [port] Start live plot viewer (default: 4242)
|
|
13822
16826
|
<file> <x> <y> [color] [title] [geom] [facet] Create a plot
|
|
13823
16827
|
|
|
13824
16828
|
Supported formats: CSV, JSON, JSONL (auto-detected by extension)
|
|
@@ -13843,6 +16847,8 @@ Examples:
|
|
|
13843
16847
|
ggterm-plot history
|
|
13844
16848
|
ggterm-plot show 2024-01-26-001
|
|
13845
16849
|
ggterm-plot export 2024-01-26-001 figure.html
|
|
16850
|
+
ggterm-plot serve # Start live plot viewer
|
|
16851
|
+
ggterm-plot serve 8080 # Custom port
|
|
13846
16852
|
`);
|
|
13847
16853
|
}
|
|
13848
16854
|
var args = process.argv.slice(2);
|
|
@@ -13875,6 +16881,10 @@ if (command === "init") {
|
|
|
13875
16881
|
handleShow(args[1]);
|
|
13876
16882
|
} else if (command === "export") {
|
|
13877
16883
|
handleExport(args[1], args[2]);
|
|
16884
|
+
} else if (command === "serve") {
|
|
16885
|
+
Promise.resolve().then(() => (init_serve(), exports_serve)).then(({ handleServe: handleServe2 }) => {
|
|
16886
|
+
handleServe2(args[1] ? parseInt(args[1]) : undefined);
|
|
16887
|
+
});
|
|
13878
16888
|
} else if (command === "help" || command === "--help" || command === "-h") {
|
|
13879
16889
|
printUsage();
|
|
13880
16890
|
} else {
|