@ggterm/core 0.2.12 → 0.2.15
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 +999 -3
- package/dist/cli.js +958 -0
- package/dist/geoms/calendar.d.ts +48 -0
- package/dist/geoms/calendar.d.ts.map +1 -0
- package/dist/geoms/corrmat.d.ts +59 -0
- package/dist/geoms/corrmat.d.ts.map +1 -0
- package/dist/geoms/density.d.ts +60 -0
- package/dist/geoms/density.d.ts.map +1 -0
- package/dist/geoms/flame.d.ts +50 -0
- package/dist/geoms/flame.d.ts.map +1 -0
- package/dist/geoms/index.d.ts +6 -0
- package/dist/geoms/index.d.ts.map +1 -1
- package/dist/geoms/sankey.d.ts +57 -0
- package/dist/geoms/sankey.d.ts.map +1 -0
- package/dist/geoms/treemap.d.ts +70 -0
- package/dist/geoms/treemap.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +972 -0
- package/dist/pipeline/render-geoms.d.ts +29 -0
- package/dist/pipeline/render-geoms.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli-plot.js
CHANGED
|
@@ -1565,6 +1565,82 @@ function renderGeomFreqpoly(data, _geom, aes, scales, canvas) {
|
|
|
1565
1565
|
}
|
|
1566
1566
|
}
|
|
1567
1567
|
}
|
|
1568
|
+
function renderGeomDensity(data, geom, aes, scales, canvas) {
|
|
1569
|
+
if (data.length < 2)
|
|
1570
|
+
return;
|
|
1571
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
1572
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
1573
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
1574
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
1575
|
+
const groups = new Map;
|
|
1576
|
+
const groupField = aes.group || aes.color || aes.fill;
|
|
1577
|
+
if (groupField) {
|
|
1578
|
+
for (const row of data) {
|
|
1579
|
+
const key = String(row[groupField] ?? "default");
|
|
1580
|
+
if (!groups.has(key))
|
|
1581
|
+
groups.set(key, []);
|
|
1582
|
+
groups.get(key).push(row);
|
|
1583
|
+
}
|
|
1584
|
+
} else {
|
|
1585
|
+
groups.set("default", data);
|
|
1586
|
+
}
|
|
1587
|
+
let baseline = Math.round(scales.y.map(0));
|
|
1588
|
+
baseline = Math.max(plotTop, Math.min(plotBottom, baseline));
|
|
1589
|
+
const alpha = geom.params.alpha ?? 0.3;
|
|
1590
|
+
const fillChar = "░";
|
|
1591
|
+
for (const [groupKey, groupData] of groups) {
|
|
1592
|
+
if (groupData.length < 2)
|
|
1593
|
+
continue;
|
|
1594
|
+
const sorted = [...groupData].sort((a, b) => {
|
|
1595
|
+
const ax = Number(a.x) || 0;
|
|
1596
|
+
const bx = Number(b.x) || 0;
|
|
1597
|
+
return ax - bx;
|
|
1598
|
+
});
|
|
1599
|
+
let baseColor = scales.color?.map(groupKey) ?? DEFAULT_POINT_COLOR;
|
|
1600
|
+
const fillColor = {
|
|
1601
|
+
r: Math.round(baseColor.r * alpha),
|
|
1602
|
+
g: Math.round(baseColor.g * alpha),
|
|
1603
|
+
b: Math.round(baseColor.b * alpha),
|
|
1604
|
+
a: baseColor.a
|
|
1605
|
+
};
|
|
1606
|
+
for (let i = 0;i < sorted.length - 1; i++) {
|
|
1607
|
+
const row1 = sorted[i];
|
|
1608
|
+
const row2 = sorted[i + 1];
|
|
1609
|
+
const density1 = Number(row1.y ?? row1.density) || 0;
|
|
1610
|
+
const density2 = Number(row2.y ?? row2.density) || 0;
|
|
1611
|
+
const x1 = Math.round(scales.x.map(row1.x));
|
|
1612
|
+
const y1 = Math.round(scales.y.map(density1));
|
|
1613
|
+
const x2 = Math.round(scales.x.map(row2.x));
|
|
1614
|
+
const y2 = Math.round(scales.y.map(density2));
|
|
1615
|
+
for (let x = x1;x <= x2; x++) {
|
|
1616
|
+
if (x < plotLeft || x > plotRight)
|
|
1617
|
+
continue;
|
|
1618
|
+
const t = x2 !== x1 ? (x - x1) / (x2 - x1) : 0;
|
|
1619
|
+
const yInterp = Math.round(y1 + (y2 - y1) * t);
|
|
1620
|
+
const top = Math.min(yInterp, baseline);
|
|
1621
|
+
const bottom = Math.max(yInterp, baseline);
|
|
1622
|
+
for (let y = top;y <= bottom; y++) {
|
|
1623
|
+
if (y >= plotTop && y <= plotBottom) {
|
|
1624
|
+
canvas.drawChar(x, y, fillChar, fillColor);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
for (let i = 0;i < sorted.length - 1; i++) {
|
|
1630
|
+
const row1 = sorted[i];
|
|
1631
|
+
const row2 = sorted[i + 1];
|
|
1632
|
+
const density1 = Number(row1.y ?? row1.density) || 0;
|
|
1633
|
+
const density2 = Number(row2.y ?? row2.density) || 0;
|
|
1634
|
+
const x1 = Math.round(scales.x.map(row1.x));
|
|
1635
|
+
const y1 = Math.round(scales.y.map(density1));
|
|
1636
|
+
const x2 = Math.round(scales.x.map(row2.x));
|
|
1637
|
+
const y2 = Math.round(scales.y.map(density2));
|
|
1638
|
+
if (x1 >= plotLeft && x1 <= plotRight && x2 >= plotLeft && x2 <= plotRight && y1 >= plotTop && y1 <= plotBottom && y2 >= plotTop && y2 <= plotBottom) {
|
|
1639
|
+
drawLine(canvas, x1, y1, x2, y2, baseColor);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1568
1644
|
function renderGeomBoxplot(data, geom, _aes, scales, canvas) {
|
|
1569
1645
|
const widthFactor = geom.params.width ?? 0.75;
|
|
1570
1646
|
let boxWidth;
|
|
@@ -2209,6 +2285,78 @@ function renderGeomLinerange(data, geom, aes, scales, canvas) {
|
|
|
2209
2285
|
}
|
|
2210
2286
|
}
|
|
2211
2287
|
}
|
|
2288
|
+
function renderGeomCrossbar(data, geom, aes, scales, canvas) {
|
|
2289
|
+
const width = geom.params.width ?? 0.5;
|
|
2290
|
+
const fatten = geom.params.fatten ?? 2.5;
|
|
2291
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
2292
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
2293
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
2294
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
2295
|
+
const halfWidth = Math.max(2, Math.round(width * 4));
|
|
2296
|
+
for (const row of data) {
|
|
2297
|
+
const xVal = row[aes.x];
|
|
2298
|
+
const yVal = row[aes.y];
|
|
2299
|
+
const ymin = row["ymin"];
|
|
2300
|
+
const ymax = row["ymax"];
|
|
2301
|
+
if (xVal === null || xVal === undefined || ymin === undefined || ymax === undefined) {
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2304
|
+
const cx = Math.round(scales.x.map(xVal));
|
|
2305
|
+
const cy = yVal !== null && yVal !== undefined ? Math.round(scales.y.map(yVal)) : null;
|
|
2306
|
+
const cyMin = Math.round(scales.y.map(ymin));
|
|
2307
|
+
const cyMax = Math.round(scales.y.map(ymax));
|
|
2308
|
+
const color = getPointColor(row, aes, scales.color);
|
|
2309
|
+
const top = Math.min(cyMin, cyMax);
|
|
2310
|
+
const bottom = Math.max(cyMin, cyMax);
|
|
2311
|
+
const left = cx - halfWidth;
|
|
2312
|
+
const right = cx + halfWidth;
|
|
2313
|
+
for (let y = top;y <= bottom; y++) {
|
|
2314
|
+
if (y >= plotTop && y <= plotBottom) {
|
|
2315
|
+
if (left >= plotLeft && left <= plotRight) {
|
|
2316
|
+
canvas.drawChar(left, y, "│", color);
|
|
2317
|
+
}
|
|
2318
|
+
if (right >= plotLeft && right <= plotRight) {
|
|
2319
|
+
canvas.drawChar(right, y, "│", color);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
for (let x = left;x <= right; x++) {
|
|
2324
|
+
if (x >= plotLeft && x <= plotRight) {
|
|
2325
|
+
if (top >= plotTop && top <= plotBottom) {
|
|
2326
|
+
canvas.drawChar(x, top, "─", color);
|
|
2327
|
+
}
|
|
2328
|
+
if (bottom >= plotTop && bottom <= plotBottom) {
|
|
2329
|
+
canvas.drawChar(x, bottom, "─", color);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
if (left >= plotLeft && left <= plotRight) {
|
|
2334
|
+
if (top >= plotTop && top <= plotBottom)
|
|
2335
|
+
canvas.drawChar(left, top, "┌", color);
|
|
2336
|
+
if (bottom >= plotTop && bottom <= plotBottom)
|
|
2337
|
+
canvas.drawChar(left, bottom, "└", color);
|
|
2338
|
+
}
|
|
2339
|
+
if (right >= plotLeft && right <= plotRight) {
|
|
2340
|
+
if (top >= plotTop && top <= plotBottom)
|
|
2341
|
+
canvas.drawChar(right, top, "┐", color);
|
|
2342
|
+
if (bottom >= plotTop && bottom <= plotBottom)
|
|
2343
|
+
canvas.drawChar(right, bottom, "┘", color);
|
|
2344
|
+
}
|
|
2345
|
+
if (cy !== null && cy >= plotTop && cy <= plotBottom) {
|
|
2346
|
+
const fattenLines = Math.max(1, Math.round(fatten / 2));
|
|
2347
|
+
for (let dy = -fattenLines + 1;dy < fattenLines; dy++) {
|
|
2348
|
+
const lineY = cy + dy;
|
|
2349
|
+
if (lineY >= plotTop && lineY <= plotBottom && lineY >= top && lineY <= bottom) {
|
|
2350
|
+
for (let x = left + 1;x < right; x++) {
|
|
2351
|
+
if (x >= plotLeft && x <= plotRight) {
|
|
2352
|
+
canvas.drawChar(x, lineY, "━", color);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2212
2360
|
function renderGeomPointrange(data, geom, aes, scales, canvas) {
|
|
2213
2361
|
renderGeomLinerange(data, geom, aes, scales, canvas);
|
|
2214
2362
|
const plotLeft = Math.round(scales.x.range[0]);
|
|
@@ -2824,6 +2972,670 @@ function renderGeomBraille(data, geom, aes, scales, canvas) {
|
|
|
2824
2972
|
}
|
|
2825
2973
|
}
|
|
2826
2974
|
}
|
|
2975
|
+
function renderGeomCalendar(data, geom, aes, scales, canvas) {
|
|
2976
|
+
if (data.length === 0)
|
|
2977
|
+
return;
|
|
2978
|
+
const cellChar = geom.params.cell_char ?? "█";
|
|
2979
|
+
const emptyColor = parseColorToRgba(geom.params.empty_color ?? "#161b22");
|
|
2980
|
+
const fillColor = parseColorToRgba(geom.params.fill_color ?? "#39d353");
|
|
2981
|
+
const levels = geom.params.levels ?? 5;
|
|
2982
|
+
const showDays = geom.params.show_days ?? true;
|
|
2983
|
+
const showMonths = geom.params.show_months ?? true;
|
|
2984
|
+
const weekStart = geom.params.week_start ?? 0;
|
|
2985
|
+
const dateField = aes.x;
|
|
2986
|
+
const valueField = aes.fill || aes.y || "value";
|
|
2987
|
+
const entries = [];
|
|
2988
|
+
let minValue = Infinity;
|
|
2989
|
+
let maxValue = -Infinity;
|
|
2990
|
+
for (const row of data) {
|
|
2991
|
+
const dateVal = row[dateField];
|
|
2992
|
+
const val = Number(row[valueField]) || 0;
|
|
2993
|
+
let date;
|
|
2994
|
+
if (dateVal instanceof Date) {
|
|
2995
|
+
date = dateVal;
|
|
2996
|
+
} else if (typeof dateVal === "string" || typeof dateVal === "number") {
|
|
2997
|
+
date = new Date(dateVal);
|
|
2998
|
+
} else {
|
|
2999
|
+
continue;
|
|
3000
|
+
}
|
|
3001
|
+
if (isNaN(date.getTime()))
|
|
3002
|
+
continue;
|
|
3003
|
+
entries.push({ date, value: val });
|
|
3004
|
+
if (val < minValue)
|
|
3005
|
+
minValue = val;
|
|
3006
|
+
if (val > maxValue)
|
|
3007
|
+
maxValue = val;
|
|
3008
|
+
}
|
|
3009
|
+
if (entries.length === 0)
|
|
3010
|
+
return;
|
|
3011
|
+
entries.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
3012
|
+
const startDate = new Date(entries[0].date);
|
|
3013
|
+
const endDate = new Date(entries[entries.length - 1].date);
|
|
3014
|
+
startDate.setDate(startDate.getDate() - (startDate.getDay() - weekStart + 7) % 7);
|
|
3015
|
+
const valueMap = new Map;
|
|
3016
|
+
for (const entry of entries) {
|
|
3017
|
+
const key = entry.date.toISOString().slice(0, 10);
|
|
3018
|
+
valueMap.set(key, (valueMap.get(key) || 0) + entry.value);
|
|
3019
|
+
}
|
|
3020
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
3021
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
3022
|
+
const dayLabels = ["S", "M", "T", "W", "T", "F", "S"];
|
|
3023
|
+
const monthLabels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
3024
|
+
const msPerDay = 24 * 60 * 60 * 1000;
|
|
3025
|
+
const totalDays = Math.ceil((endDate.getTime() - startDate.getTime()) / msPerDay) + 7;
|
|
3026
|
+
const numWeeks = Math.ceil(totalDays / 7);
|
|
3027
|
+
const getColor = (value) => {
|
|
3028
|
+
if (value === 0 || maxValue === minValue)
|
|
3029
|
+
return emptyColor;
|
|
3030
|
+
const t = Math.min(1, Math.max(0, (value - minValue) / (maxValue - minValue)));
|
|
3031
|
+
const level = Math.floor(t * (levels - 1));
|
|
3032
|
+
const levelT = level / (levels - 1);
|
|
3033
|
+
return {
|
|
3034
|
+
r: Math.round(emptyColor.r + (fillColor.r - emptyColor.r) * levelT),
|
|
3035
|
+
g: Math.round(emptyColor.g + (fillColor.g - emptyColor.g) * levelT),
|
|
3036
|
+
b: Math.round(emptyColor.b + (fillColor.b - emptyColor.b) * levelT),
|
|
3037
|
+
a: 1
|
|
3038
|
+
};
|
|
3039
|
+
};
|
|
3040
|
+
const xOffset = showDays ? 3 : 0;
|
|
3041
|
+
const yOffset = showMonths ? 2 : 0;
|
|
3042
|
+
if (showDays) {
|
|
3043
|
+
for (let d = 0;d < 7; d++) {
|
|
3044
|
+
const dayIndex = (d + weekStart) % 7;
|
|
3045
|
+
const y = plotTop + yOffset + d;
|
|
3046
|
+
canvas.drawChar(plotLeft, y, dayLabels[dayIndex], { r: 128, g: 128, b: 128, a: 1 });
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
let lastMonth = -1;
|
|
3050
|
+
const currentDate = new Date(startDate);
|
|
3051
|
+
for (let week = 0;week < numWeeks; week++) {
|
|
3052
|
+
const x = plotLeft + xOffset + week * 2;
|
|
3053
|
+
if (showMonths && currentDate.getMonth() !== lastMonth) {
|
|
3054
|
+
const monthLabel = monthLabels[currentDate.getMonth()];
|
|
3055
|
+
for (let i = 0;i < monthLabel.length; i++) {
|
|
3056
|
+
canvas.drawChar(x + i, plotTop, monthLabel[i], { r: 128, g: 128, b: 128, a: 1 });
|
|
3057
|
+
}
|
|
3058
|
+
lastMonth = currentDate.getMonth();
|
|
3059
|
+
}
|
|
3060
|
+
for (let day = 0;day < 7; day++) {
|
|
3061
|
+
const dayDate = new Date(currentDate);
|
|
3062
|
+
dayDate.setDate(dayDate.getDate() + day);
|
|
3063
|
+
if (dayDate > endDate)
|
|
3064
|
+
break;
|
|
3065
|
+
const key = dayDate.toISOString().slice(0, 10);
|
|
3066
|
+
const value = valueMap.get(key) || 0;
|
|
3067
|
+
const color = getColor(value);
|
|
3068
|
+
const y = plotTop + yOffset + day;
|
|
3069
|
+
canvas.drawChar(x, y, cellChar, color);
|
|
3070
|
+
}
|
|
3071
|
+
currentDate.setDate(currentDate.getDate() + 7);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
function renderGeomFlame(data, geom, aes, scales, canvas) {
|
|
3075
|
+
if (data.length === 0)
|
|
3076
|
+
return;
|
|
3077
|
+
const style = geom.params.style ?? "flame";
|
|
3078
|
+
const palette = geom.params.palette ?? "warm";
|
|
3079
|
+
const showLabels = geom.params.show_labels ?? true;
|
|
3080
|
+
const minLabelWidth = geom.params.min_label_width ?? 10;
|
|
3081
|
+
const barChar = geom.params.bar_char ?? "█";
|
|
3082
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
3083
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
3084
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
3085
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
3086
|
+
const plotWidth = plotRight - plotLeft;
|
|
3087
|
+
const plotHeight = plotBottom - plotTop;
|
|
3088
|
+
const nameField = aes.x || "name";
|
|
3089
|
+
const valueField = aes.fill || "value";
|
|
3090
|
+
const depthField = aes.y || "depth";
|
|
3091
|
+
const startField = "start";
|
|
3092
|
+
const frames = [];
|
|
3093
|
+
let totalValue = 0;
|
|
3094
|
+
let maxDepth = 0;
|
|
3095
|
+
for (const row of data) {
|
|
3096
|
+
const name = String(row[nameField] || "");
|
|
3097
|
+
const value = Number(row[valueField]) || 0;
|
|
3098
|
+
const depth = Number(row[depthField]) || 0;
|
|
3099
|
+
const start = row[startField] !== undefined ? Number(row[startField]) : -1;
|
|
3100
|
+
frames.push({ name, value, depth, start, width: 0 });
|
|
3101
|
+
if (depth === 0)
|
|
3102
|
+
totalValue += value;
|
|
3103
|
+
if (depth > maxDepth)
|
|
3104
|
+
maxDepth = depth;
|
|
3105
|
+
}
|
|
3106
|
+
if (totalValue === 0)
|
|
3107
|
+
return;
|
|
3108
|
+
for (const frame of frames) {
|
|
3109
|
+
frame.width = frame.value / totalValue * plotWidth;
|
|
3110
|
+
}
|
|
3111
|
+
frames.sort((a, b) => {
|
|
3112
|
+
if (a.depth !== b.depth)
|
|
3113
|
+
return a.depth - b.depth;
|
|
3114
|
+
if (a.start !== b.start)
|
|
3115
|
+
return a.start - b.start;
|
|
3116
|
+
return a.name.localeCompare(b.name);
|
|
3117
|
+
});
|
|
3118
|
+
const depthOffsets = new Array(maxDepth + 1).fill(0);
|
|
3119
|
+
for (const frame of frames) {
|
|
3120
|
+
if (frame.start < 0) {
|
|
3121
|
+
frame.start = depthOffsets[frame.depth];
|
|
3122
|
+
}
|
|
3123
|
+
depthOffsets[frame.depth] = frame.start + frame.width;
|
|
3124
|
+
}
|
|
3125
|
+
const getPaletteColor = (name) => {
|
|
3126
|
+
let hash = 0;
|
|
3127
|
+
for (let i = 0;i < name.length; i++) {
|
|
3128
|
+
hash = (hash << 5) - hash + name.charCodeAt(i);
|
|
3129
|
+
hash = hash & hash;
|
|
3130
|
+
}
|
|
3131
|
+
const hue = Math.abs(hash) % 360;
|
|
3132
|
+
let h;
|
|
3133
|
+
if (palette === "warm") {
|
|
3134
|
+
h = hue % 60 + 0;
|
|
3135
|
+
} else if (palette === "cool") {
|
|
3136
|
+
h = hue % 60 + 180;
|
|
3137
|
+
} else {
|
|
3138
|
+
h = hue % 40 + 10;
|
|
3139
|
+
}
|
|
3140
|
+
const s = 0.7;
|
|
3141
|
+
const l = 0.5;
|
|
3142
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
3143
|
+
const x = c * (1 - Math.abs(h / 60 % 2 - 1));
|
|
3144
|
+
const m = l - c / 2;
|
|
3145
|
+
let r = 0, g = 0, b = 0;
|
|
3146
|
+
if (h < 60) {
|
|
3147
|
+
r = c;
|
|
3148
|
+
g = x;
|
|
3149
|
+
b = 0;
|
|
3150
|
+
} else if (h < 120) {
|
|
3151
|
+
r = x;
|
|
3152
|
+
g = c;
|
|
3153
|
+
b = 0;
|
|
3154
|
+
} else if (h < 180) {
|
|
3155
|
+
r = 0;
|
|
3156
|
+
g = c;
|
|
3157
|
+
b = x;
|
|
3158
|
+
} else if (h < 240) {
|
|
3159
|
+
r = 0;
|
|
3160
|
+
g = x;
|
|
3161
|
+
b = c;
|
|
3162
|
+
} else if (h < 300) {
|
|
3163
|
+
r = x;
|
|
3164
|
+
g = 0;
|
|
3165
|
+
b = c;
|
|
3166
|
+
} else {
|
|
3167
|
+
r = c;
|
|
3168
|
+
g = 0;
|
|
3169
|
+
b = x;
|
|
3170
|
+
}
|
|
3171
|
+
return {
|
|
3172
|
+
r: Math.round((r + m) * 255),
|
|
3173
|
+
g: Math.round((g + m) * 255),
|
|
3174
|
+
b: Math.round((b + m) * 255),
|
|
3175
|
+
a: 1
|
|
3176
|
+
};
|
|
3177
|
+
};
|
|
3178
|
+
const rowHeight = Math.max(1, Math.floor(plotHeight / (maxDepth + 1)));
|
|
3179
|
+
for (const frame of frames) {
|
|
3180
|
+
const x1 = plotLeft + Math.round(frame.start);
|
|
3181
|
+
const x2 = plotLeft + Math.round(frame.start + frame.width);
|
|
3182
|
+
const width = x2 - x1;
|
|
3183
|
+
if (width < 1)
|
|
3184
|
+
continue;
|
|
3185
|
+
let y;
|
|
3186
|
+
if (style === "icicle") {
|
|
3187
|
+
y = plotTop + frame.depth * rowHeight;
|
|
3188
|
+
} else {
|
|
3189
|
+
y = plotBottom - (frame.depth + 1) * rowHeight;
|
|
3190
|
+
}
|
|
3191
|
+
const color = getPaletteColor(frame.name);
|
|
3192
|
+
for (let row = 0;row < rowHeight; row++) {
|
|
3193
|
+
for (let col = x1;col < x2; col++) {
|
|
3194
|
+
if (col >= plotLeft && col < plotRight && y + row >= plotTop && y + row < plotBottom) {
|
|
3195
|
+
canvas.drawChar(col, y + row, barChar, color);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
if (showLabels && width >= minLabelWidth) {
|
|
3200
|
+
const label = frame.name.slice(0, width - 2);
|
|
3201
|
+
const labelX = x1 + 1;
|
|
3202
|
+
const labelY = y + Math.floor(rowHeight / 2);
|
|
3203
|
+
const textColor = { r: 255, g: 255, b: 255, a: 1 };
|
|
3204
|
+
for (let i = 0;i < label.length; i++) {
|
|
3205
|
+
if (labelX + i < x2 - 1) {
|
|
3206
|
+
canvas.drawChar(labelX + i, labelY, label[i], textColor);
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
function renderGeomCorrmat(data, geom, aes, scales, canvas) {
|
|
3213
|
+
if (data.length === 0)
|
|
3214
|
+
return;
|
|
3215
|
+
const showValues = geom.params.show_values ?? true;
|
|
3216
|
+
const decimals = geom.params.decimals ?? 2;
|
|
3217
|
+
const positiveColor = parseColorToRgba(geom.params.positive_color ?? "#2166ac");
|
|
3218
|
+
const negativeColor = parseColorToRgba(geom.params.negative_color ?? "#b2182b");
|
|
3219
|
+
const neutralColor = parseColorToRgba(geom.params.neutral_color ?? "#f7f7f7");
|
|
3220
|
+
const lowerTriangle = geom.params.lower_triangle ?? false;
|
|
3221
|
+
const upperTriangle = geom.params.upper_triangle ?? false;
|
|
3222
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
3223
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
3224
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
3225
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
3226
|
+
const xField = aes.x || "var1";
|
|
3227
|
+
const yField = aes.y || "var2";
|
|
3228
|
+
const valueField = aes.fill || "correlation";
|
|
3229
|
+
const xVars = new Set;
|
|
3230
|
+
const yVars = new Set;
|
|
3231
|
+
const corrMap = new Map;
|
|
3232
|
+
for (const row of data) {
|
|
3233
|
+
const x = String(row[xField] || "");
|
|
3234
|
+
const y = String(row[yField] || "");
|
|
3235
|
+
const r = Number(row[valueField]) || 0;
|
|
3236
|
+
xVars.add(x);
|
|
3237
|
+
yVars.add(y);
|
|
3238
|
+
corrMap.set(`${x}|${y}`, r);
|
|
3239
|
+
}
|
|
3240
|
+
const xList = [...xVars];
|
|
3241
|
+
const yList = [...yVars];
|
|
3242
|
+
const numX = xList.length;
|
|
3243
|
+
const numY = yList.length;
|
|
3244
|
+
if (numX === 0 || numY === 0)
|
|
3245
|
+
return;
|
|
3246
|
+
const cellWidth = Math.max(4, Math.floor((plotRight - plotLeft) / numX));
|
|
3247
|
+
const cellHeight = Math.max(2, Math.floor((plotBottom - plotTop) / numY));
|
|
3248
|
+
const getColor = (r) => {
|
|
3249
|
+
const t = (r + 1) / 2;
|
|
3250
|
+
if (t < 0.5) {
|
|
3251
|
+
const s = t * 2;
|
|
3252
|
+
return {
|
|
3253
|
+
r: Math.round(negativeColor.r + (neutralColor.r - negativeColor.r) * s),
|
|
3254
|
+
g: Math.round(negativeColor.g + (neutralColor.g - negativeColor.g) * s),
|
|
3255
|
+
b: Math.round(negativeColor.b + (neutralColor.b - negativeColor.b) * s),
|
|
3256
|
+
a: 1
|
|
3257
|
+
};
|
|
3258
|
+
} else {
|
|
3259
|
+
const s = (t - 0.5) * 2;
|
|
3260
|
+
return {
|
|
3261
|
+
r: Math.round(neutralColor.r + (positiveColor.r - neutralColor.r) * s),
|
|
3262
|
+
g: Math.round(neutralColor.g + (positiveColor.g - neutralColor.g) * s),
|
|
3263
|
+
b: Math.round(neutralColor.b + (positiveColor.b - neutralColor.b) * s),
|
|
3264
|
+
a: 1
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
for (let i = 0;i < numX; i++) {
|
|
3269
|
+
for (let j = 0;j < numY; j++) {
|
|
3270
|
+
if (lowerTriangle && i < j)
|
|
3271
|
+
continue;
|
|
3272
|
+
if (upperTriangle && i > j)
|
|
3273
|
+
continue;
|
|
3274
|
+
const x = xList[i];
|
|
3275
|
+
const y = yList[j];
|
|
3276
|
+
const key = `${x}|${y}`;
|
|
3277
|
+
const r = corrMap.get(key) ?? corrMap.get(`${y}|${x}`) ?? (i === j ? 1 : 0);
|
|
3278
|
+
const color = getColor(r);
|
|
3279
|
+
const cellX = plotLeft + i * cellWidth;
|
|
3280
|
+
const cellY = plotTop + j * cellHeight;
|
|
3281
|
+
for (let cy = 0;cy < cellHeight; cy++) {
|
|
3282
|
+
for (let cx = 0;cx < cellWidth; cx++) {
|
|
3283
|
+
if (cellX + cx < plotRight && cellY + cy < plotBottom) {
|
|
3284
|
+
canvas.drawChar(cellX + cx, cellY + cy, "█", color);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
if (showValues && cellWidth >= 4) {
|
|
3289
|
+
const valueStr = r.toFixed(decimals);
|
|
3290
|
+
const textX = cellX + Math.floor((cellWidth - valueStr.length) / 2);
|
|
3291
|
+
const textY = cellY + Math.floor(cellHeight / 2);
|
|
3292
|
+
const brightness = (color.r * 299 + color.g * 587 + color.b * 114) / 1000;
|
|
3293
|
+
const textColor = brightness > 128 ? { r: 0, g: 0, b: 0, a: 1 } : { r: 255, g: 255, b: 255, a: 1 };
|
|
3294
|
+
for (let k = 0;k < valueStr.length; k++) {
|
|
3295
|
+
if (textX + k < cellX + cellWidth && textX + k < plotRight) {
|
|
3296
|
+
canvas.drawChar(textX + k, textY, valueStr[k], textColor);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
function renderGeomSankey(data, geom, aes, scales, canvas) {
|
|
3304
|
+
if (data.length === 0)
|
|
3305
|
+
return;
|
|
3306
|
+
const nodeWidth = geom.params.node_width ?? 3;
|
|
3307
|
+
const nodePadding = geom.params.node_padding ?? 2;
|
|
3308
|
+
const nodeChar = geom.params.node_char ?? "█";
|
|
3309
|
+
const showLabels = geom.params.show_labels ?? true;
|
|
3310
|
+
const showValues = geom.params.show_values ?? false;
|
|
3311
|
+
const minFlowWidth = geom.params.min_flow_width ?? 1;
|
|
3312
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
3313
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
3314
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
3315
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
3316
|
+
const plotHeight = plotBottom - plotTop;
|
|
3317
|
+
const sourceField = aes.x || "source";
|
|
3318
|
+
const targetField = aes.y || "target";
|
|
3319
|
+
const valueField = aes.fill || "value";
|
|
3320
|
+
const nodes = new Map;
|
|
3321
|
+
const links = [];
|
|
3322
|
+
const sourceNodes = new Set;
|
|
3323
|
+
const targetNodes = new Set;
|
|
3324
|
+
for (const row of data) {
|
|
3325
|
+
const source = String(row[sourceField] ?? "");
|
|
3326
|
+
const target = String(row[targetField] ?? "");
|
|
3327
|
+
const value = Number(row[valueField]) || 0;
|
|
3328
|
+
if (!source || !target)
|
|
3329
|
+
continue;
|
|
3330
|
+
sourceNodes.add(source);
|
|
3331
|
+
targetNodes.add(target);
|
|
3332
|
+
links.push({ source, target, value });
|
|
3333
|
+
if (!nodes.has(source)) {
|
|
3334
|
+
nodes.set(source, { name: source, value: 0, column: 0, y: 0, height: 0 });
|
|
3335
|
+
}
|
|
3336
|
+
if (!nodes.has(target)) {
|
|
3337
|
+
nodes.set(target, { name: target, value: 0, column: 1, y: 0, height: 0 });
|
|
3338
|
+
}
|
|
3339
|
+
nodes.get(source).value += value;
|
|
3340
|
+
nodes.get(target).value += value;
|
|
3341
|
+
}
|
|
3342
|
+
for (const [name, node] of nodes) {
|
|
3343
|
+
if (sourceNodes.has(name) && !targetNodes.has(name)) {
|
|
3344
|
+
node.column = 0;
|
|
3345
|
+
} else if (targetNodes.has(name) && !sourceNodes.has(name)) {
|
|
3346
|
+
node.column = 1;
|
|
3347
|
+
} else {
|
|
3348
|
+
node.column = 0;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
const columns = [[], []];
|
|
3352
|
+
for (const [, node] of nodes) {
|
|
3353
|
+
if (!columns[node.column])
|
|
3354
|
+
columns[node.column] = [];
|
|
3355
|
+
columns[node.column].push(node);
|
|
3356
|
+
}
|
|
3357
|
+
const maxColumnValue = Math.max(columns[0]?.reduce((sum, n) => sum + n.value, 0) || 0, columns[1]?.reduce((sum, n) => sum + n.value, 0) || 0);
|
|
3358
|
+
if (maxColumnValue === 0)
|
|
3359
|
+
return;
|
|
3360
|
+
const availableHeight = plotHeight - nodePadding * Math.max(columns[0]?.length || 0, columns[1]?.length || 0);
|
|
3361
|
+
for (let col = 0;col < 2; col++) {
|
|
3362
|
+
if (!columns[col])
|
|
3363
|
+
continue;
|
|
3364
|
+
let currentY = plotTop;
|
|
3365
|
+
columns[col].sort((a, b) => b.value - a.value);
|
|
3366
|
+
for (const node of columns[col]) {
|
|
3367
|
+
node.height = Math.max(1, Math.round(node.value / maxColumnValue * availableHeight));
|
|
3368
|
+
node.y = currentY;
|
|
3369
|
+
currentY += node.height + nodePadding;
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
const colorPalette = [
|
|
3373
|
+
{ r: 79, g: 169, b: 238, a: 1 },
|
|
3374
|
+
{ r: 240, g: 128, b: 60, a: 1 },
|
|
3375
|
+
{ r: 102, g: 194, b: 114, a: 1 },
|
|
3376
|
+
{ r: 218, g: 98, b: 125, a: 1 },
|
|
3377
|
+
{ r: 169, g: 140, b: 204, a: 1 },
|
|
3378
|
+
{ r: 255, g: 204, b: 102, a: 1 }
|
|
3379
|
+
];
|
|
3380
|
+
const nodeColors = new Map;
|
|
3381
|
+
let colorIdx = 0;
|
|
3382
|
+
for (const [name] of nodes) {
|
|
3383
|
+
nodeColors.set(name, colorPalette[colorIdx % colorPalette.length]);
|
|
3384
|
+
colorIdx++;
|
|
3385
|
+
}
|
|
3386
|
+
const labelSpace = showLabels ? 8 : 0;
|
|
3387
|
+
const col0X = plotLeft + labelSpace;
|
|
3388
|
+
const col1X = plotRight - nodeWidth - labelSpace;
|
|
3389
|
+
for (const [name, node] of nodes) {
|
|
3390
|
+
const x = node.column === 0 ? col0X : col1X;
|
|
3391
|
+
const color = nodeColors.get(name);
|
|
3392
|
+
for (let dy = 0;dy < node.height; dy++) {
|
|
3393
|
+
for (let dx = 0;dx < nodeWidth; dx++) {
|
|
3394
|
+
canvas.drawChar(x + dx, node.y + dy, nodeChar, color);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
if (showLabels) {
|
|
3398
|
+
const labelX = node.column === 0 ? x - name.length - 1 : x + nodeWidth + 1;
|
|
3399
|
+
const labelY = node.y + Math.floor(node.height / 2);
|
|
3400
|
+
const labelColor = { r: 180, g: 180, b: 180, a: 1 };
|
|
3401
|
+
for (let i = 0;i < name.length && labelX + i >= plotLeft && labelX + i < plotRight; i++) {
|
|
3402
|
+
canvas.drawChar(labelX + i, labelY, name[i], labelColor);
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
for (const link of links) {
|
|
3407
|
+
const sourceNode = nodes.get(link.source);
|
|
3408
|
+
const targetNode = nodes.get(link.target);
|
|
3409
|
+
if (!sourceNode || !targetNode)
|
|
3410
|
+
continue;
|
|
3411
|
+
const flowWidth = Math.max(minFlowWidth, Math.round(link.value / maxColumnValue * (plotHeight / 4)));
|
|
3412
|
+
const sourceColor = nodeColors.get(link.source);
|
|
3413
|
+
const x1 = col0X + nodeWidth;
|
|
3414
|
+
const x2 = col1X;
|
|
3415
|
+
const y1 = sourceNode.y + Math.floor(sourceNode.height / 2);
|
|
3416
|
+
const y2 = targetNode.y + Math.floor(targetNode.height / 2);
|
|
3417
|
+
const steps = Math.abs(x2 - x1);
|
|
3418
|
+
for (let i = 0;i <= steps; i++) {
|
|
3419
|
+
const t = i / steps;
|
|
3420
|
+
const x = Math.round(x1 + (x2 - x1) * t);
|
|
3421
|
+
const y = Math.round(y1 + (y2 - y1) * (3 * t * t - 2 * t * t * t));
|
|
3422
|
+
const halfWidth = Math.floor(flowWidth / 2);
|
|
3423
|
+
for (let dy = -halfWidth;dy <= halfWidth; dy++) {
|
|
3424
|
+
const flowY = y + dy;
|
|
3425
|
+
if (flowY >= plotTop && flowY < plotBottom && x >= plotLeft && x < plotRight) {
|
|
3426
|
+
const flowColor = {
|
|
3427
|
+
r: sourceColor.r,
|
|
3428
|
+
g: sourceColor.g,
|
|
3429
|
+
b: sourceColor.b,
|
|
3430
|
+
a: 0.4
|
|
3431
|
+
};
|
|
3432
|
+
const char = dy === 0 ? "─" : "░";
|
|
3433
|
+
canvas.drawChar(x, flowY, char, flowColor);
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
if (showValues) {
|
|
3438
|
+
const midX = Math.round((x1 + x2) / 2);
|
|
3439
|
+
const midY = Math.round((y1 + y2) / 2);
|
|
3440
|
+
const valueStr = String(link.value);
|
|
3441
|
+
const textColor = { r: 200, g: 200, b: 200, a: 1 };
|
|
3442
|
+
for (let i = 0;i < valueStr.length; i++) {
|
|
3443
|
+
canvas.drawChar(midX - Math.floor(valueStr.length / 2) + i, midY, valueStr[i], textColor);
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
function renderGeomTreemap(data, geom, aes, scales, canvas) {
|
|
3449
|
+
if (data.length === 0)
|
|
3450
|
+
return;
|
|
3451
|
+
const showLabels = geom.params.show_labels ?? true;
|
|
3452
|
+
const showValues = geom.params.show_values ?? false;
|
|
3453
|
+
const border = geom.params.border ?? true;
|
|
3454
|
+
const minLabelSize = geom.params.min_label_size ?? 4;
|
|
3455
|
+
const fillChar = geom.params.fill_char ?? "█";
|
|
3456
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
3457
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
3458
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
3459
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
3460
|
+
const labelField = aes.x || "name";
|
|
3461
|
+
const valueField = aes.fill || aes.y || "value";
|
|
3462
|
+
const parentField = aes.group || "parent";
|
|
3463
|
+
const idField = aes.x || "id";
|
|
3464
|
+
const nodeMap = new Map;
|
|
3465
|
+
const roots = [];
|
|
3466
|
+
for (const row of data) {
|
|
3467
|
+
const id = String(row[idField] ?? row[labelField] ?? "");
|
|
3468
|
+
const label = String(row[labelField] ?? id);
|
|
3469
|
+
const value = Number(row[valueField]) || 0;
|
|
3470
|
+
const parent = row[parentField] ? String(row[parentField]) : undefined;
|
|
3471
|
+
const node = {
|
|
3472
|
+
id,
|
|
3473
|
+
label,
|
|
3474
|
+
value,
|
|
3475
|
+
parent,
|
|
3476
|
+
children: [],
|
|
3477
|
+
x: 0,
|
|
3478
|
+
y: 0,
|
|
3479
|
+
width: 0,
|
|
3480
|
+
height: 0
|
|
3481
|
+
};
|
|
3482
|
+
nodeMap.set(id, node);
|
|
3483
|
+
}
|
|
3484
|
+
for (const [, node] of nodeMap) {
|
|
3485
|
+
if (node.parent && nodeMap.has(node.parent)) {
|
|
3486
|
+
nodeMap.get(node.parent).children.push(node);
|
|
3487
|
+
} else {
|
|
3488
|
+
roots.push(node);
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
if (roots.length === 0) {
|
|
3492
|
+
for (const [, node] of nodeMap) {
|
|
3493
|
+
roots.push(node);
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
function calculateValue(node) {
|
|
3497
|
+
if (node.children.length === 0) {
|
|
3498
|
+
return node.value;
|
|
3499
|
+
}
|
|
3500
|
+
const childSum = node.children.reduce((sum, child) => sum + calculateValue(child), 0);
|
|
3501
|
+
return node.value || childSum;
|
|
3502
|
+
}
|
|
3503
|
+
for (const root of roots) {
|
|
3504
|
+
root.value = calculateValue(root);
|
|
3505
|
+
}
|
|
3506
|
+
const validRoots = roots.filter((n) => n.value > 0).sort((a, b) => b.value - a.value);
|
|
3507
|
+
if (validRoots.length === 0)
|
|
3508
|
+
return;
|
|
3509
|
+
function squarify(nodes, x, y, width, height) {
|
|
3510
|
+
if (nodes.length === 0 || width <= 0 || height <= 0)
|
|
3511
|
+
return;
|
|
3512
|
+
const totalValue = nodes.reduce((sum, n) => sum + n.value, 0);
|
|
3513
|
+
if (totalValue === 0)
|
|
3514
|
+
return;
|
|
3515
|
+
if (nodes.length === 1) {
|
|
3516
|
+
const node = nodes[0];
|
|
3517
|
+
node.x = x;
|
|
3518
|
+
node.y = y;
|
|
3519
|
+
node.width = width;
|
|
3520
|
+
node.height = height;
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
3523
|
+
const isHorizontal = width > height;
|
|
3524
|
+
let currentPos = isHorizontal ? x : y;
|
|
3525
|
+
const totalSize = isHorizontal ? width : height;
|
|
3526
|
+
for (const node of nodes) {
|
|
3527
|
+
const fraction = node.value / totalValue;
|
|
3528
|
+
const size = Math.round(totalSize * fraction);
|
|
3529
|
+
if (isHorizontal) {
|
|
3530
|
+
node.x = currentPos;
|
|
3531
|
+
node.y = y;
|
|
3532
|
+
node.width = Math.max(1, size);
|
|
3533
|
+
node.height = height;
|
|
3534
|
+
currentPos += node.width;
|
|
3535
|
+
} else {
|
|
3536
|
+
node.x = x;
|
|
3537
|
+
node.y = currentPos;
|
|
3538
|
+
node.width = width;
|
|
3539
|
+
node.height = Math.max(1, size);
|
|
3540
|
+
currentPos += node.height;
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
squarify(validRoots, plotLeft, plotTop, plotRight - plotLeft, plotBottom - plotTop);
|
|
3545
|
+
const colorPalette = [
|
|
3546
|
+
{ r: 66, g: 133, b: 244, a: 1 },
|
|
3547
|
+
{ r: 234, g: 67, b: 53, a: 1 },
|
|
3548
|
+
{ r: 251, g: 188, b: 5, a: 1 },
|
|
3549
|
+
{ r: 52, g: 168, b: 83, a: 1 },
|
|
3550
|
+
{ r: 154, g: 102, b: 255, a: 1 },
|
|
3551
|
+
{ r: 255, g: 109, b: 0, a: 1 },
|
|
3552
|
+
{ r: 0, g: 188, b: 212, a: 1 },
|
|
3553
|
+
{ r: 233, g: 30, b: 99, a: 1 }
|
|
3554
|
+
];
|
|
3555
|
+
function renderNode(node, colorIndex, depth) {
|
|
3556
|
+
if (node.width <= 0 || node.height <= 0)
|
|
3557
|
+
return;
|
|
3558
|
+
const baseColor = colorPalette[colorIndex % colorPalette.length];
|
|
3559
|
+
const depthFactor = Math.max(0.5, 1 - depth * 0.15);
|
|
3560
|
+
const color = {
|
|
3561
|
+
r: Math.round(baseColor.r * depthFactor),
|
|
3562
|
+
g: Math.round(baseColor.g * depthFactor),
|
|
3563
|
+
b: Math.round(baseColor.b * depthFactor),
|
|
3564
|
+
a: 1
|
|
3565
|
+
};
|
|
3566
|
+
for (let dy = 0;dy < node.height; dy++) {
|
|
3567
|
+
for (let dx = 0;dx < node.width; dx++) {
|
|
3568
|
+
const px = node.x + dx;
|
|
3569
|
+
const py = node.y + dy;
|
|
3570
|
+
if (px >= plotLeft && px < plotRight && py >= plotTop && py < plotBottom) {
|
|
3571
|
+
canvas.drawChar(px, py, fillChar, color);
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
if (border && node.width >= 2 && node.height >= 2) {
|
|
3576
|
+
const borderColor = { r: 40, g: 40, b: 40, a: 1 };
|
|
3577
|
+
for (let dx = 0;dx < node.width; dx++) {
|
|
3578
|
+
const px = node.x + dx;
|
|
3579
|
+
if (px >= plotLeft && px < plotRight) {
|
|
3580
|
+
if (node.y >= plotTop)
|
|
3581
|
+
canvas.drawChar(px, node.y, "─", borderColor);
|
|
3582
|
+
if (node.y + node.height - 1 < plotBottom)
|
|
3583
|
+
canvas.drawChar(px, node.y + node.height - 1, "─", borderColor);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
for (let dy = 0;dy < node.height; dy++) {
|
|
3587
|
+
const py = node.y + dy;
|
|
3588
|
+
if (py >= plotTop && py < plotBottom) {
|
|
3589
|
+
if (node.x >= plotLeft)
|
|
3590
|
+
canvas.drawChar(node.x, py, "│", borderColor);
|
|
3591
|
+
if (node.x + node.width - 1 < plotRight)
|
|
3592
|
+
canvas.drawChar(node.x + node.width - 1, py, "│", borderColor);
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
if (node.x >= plotLeft && node.y >= plotTop)
|
|
3596
|
+
canvas.drawChar(node.x, node.y, "┌", borderColor);
|
|
3597
|
+
if (node.x + node.width - 1 < plotRight && node.y >= plotTop)
|
|
3598
|
+
canvas.drawChar(node.x + node.width - 1, node.y, "┐", borderColor);
|
|
3599
|
+
if (node.x >= plotLeft && node.y + node.height - 1 < plotBottom)
|
|
3600
|
+
canvas.drawChar(node.x, node.y + node.height - 1, "└", borderColor);
|
|
3601
|
+
if (node.x + node.width - 1 < plotRight && node.y + node.height - 1 < plotBottom)
|
|
3602
|
+
canvas.drawChar(node.x + node.width - 1, node.y + node.height - 1, "┘", borderColor);
|
|
3603
|
+
}
|
|
3604
|
+
if (showLabels && node.width >= minLabelSize && node.height >= 1) {
|
|
3605
|
+
const label = node.label.substring(0, node.width - 2);
|
|
3606
|
+
const labelX = node.x + 1;
|
|
3607
|
+
const labelY = node.y + Math.floor(node.height / 2);
|
|
3608
|
+
const brightness = (color.r * 299 + color.g * 587 + color.b * 114) / 1000;
|
|
3609
|
+
const textColor = brightness > 128 ? { r: 0, g: 0, b: 0, a: 1 } : { r: 255, g: 255, b: 255, a: 1 };
|
|
3610
|
+
for (let i = 0;i < label.length; i++) {
|
|
3611
|
+
const px = labelX + i;
|
|
3612
|
+
if (px >= plotLeft && px < node.x + node.width - 1 && px < plotRight) {
|
|
3613
|
+
canvas.drawChar(px, labelY, label[i], textColor);
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
if (showValues && node.height >= 3) {
|
|
3617
|
+
const valueStr = String(node.value);
|
|
3618
|
+
const valueY = labelY + 1;
|
|
3619
|
+
for (let i = 0;i < valueStr.length; i++) {
|
|
3620
|
+
const px = labelX + i;
|
|
3621
|
+
if (px >= plotLeft && px < node.x + node.width - 1 && px < plotRight && valueY < plotBottom) {
|
|
3622
|
+
canvas.drawChar(px, valueY, valueStr[i], textColor);
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
if (node.children.length > 0) {
|
|
3628
|
+
const childrenSorted = [...node.children].sort((a, b) => b.value - a.value);
|
|
3629
|
+
squarify(childrenSorted, node.x + (border ? 1 : 0), node.y + (border ? 1 : 0), node.width - (border ? 2 : 0), node.height - (border ? 2 : 0));
|
|
3630
|
+
for (let i = 0;i < childrenSorted.length; i++) {
|
|
3631
|
+
renderNode(childrenSorted[i], colorIndex, depth + 1);
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
for (let i = 0;i < validRoots.length; i++) {
|
|
3636
|
+
renderNode(validRoots[i], i, 0);
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
2827
3639
|
function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
2828
3640
|
switch (geom.type) {
|
|
2829
3641
|
case "point":
|
|
@@ -2861,6 +3673,9 @@ function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
|
2861
3673
|
case "freqpoly":
|
|
2862
3674
|
renderGeomFreqpoly(data, geom, aes, scales, canvas);
|
|
2863
3675
|
break;
|
|
3676
|
+
case "density":
|
|
3677
|
+
renderGeomDensity(data, geom, aes, scales, canvas);
|
|
3678
|
+
break;
|
|
2864
3679
|
case "boxplot":
|
|
2865
3680
|
renderGeomBoxplot(data, geom, aes, scales, canvas);
|
|
2866
3681
|
break;
|
|
@@ -2899,6 +3714,9 @@ function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
|
2899
3714
|
case "linerange":
|
|
2900
3715
|
renderGeomLinerange(data, geom, aes, scales, canvas);
|
|
2901
3716
|
break;
|
|
3717
|
+
case "crossbar":
|
|
3718
|
+
renderGeomCrossbar(data, geom, aes, scales, canvas);
|
|
3719
|
+
break;
|
|
2902
3720
|
case "pointrange":
|
|
2903
3721
|
renderGeomPointrange(data, geom, aes, scales, canvas);
|
|
2904
3722
|
break;
|
|
@@ -2927,6 +3745,22 @@ function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
|
2927
3745
|
case "braille":
|
|
2928
3746
|
renderGeomBraille(data, geom, aes, scales, canvas);
|
|
2929
3747
|
break;
|
|
3748
|
+
case "calendar":
|
|
3749
|
+
renderGeomCalendar(data, geom, aes, scales, canvas);
|
|
3750
|
+
break;
|
|
3751
|
+
case "flame":
|
|
3752
|
+
case "icicle":
|
|
3753
|
+
renderGeomFlame(data, geom, aes, scales, canvas);
|
|
3754
|
+
break;
|
|
3755
|
+
case "corrmat":
|
|
3756
|
+
renderGeomCorrmat(data, geom, aes, scales, canvas);
|
|
3757
|
+
break;
|
|
3758
|
+
case "sankey":
|
|
3759
|
+
renderGeomSankey(data, geom, aes, scales, canvas);
|
|
3760
|
+
break;
|
|
3761
|
+
case "treemap":
|
|
3762
|
+
renderGeomTreemap(data, geom, aes, scales, canvas);
|
|
3763
|
+
break;
|
|
2930
3764
|
default:
|
|
2931
3765
|
break;
|
|
2932
3766
|
}
|
|
@@ -5932,6 +6766,26 @@ function geom_freqpoly(options = {}) {
|
|
|
5932
6766
|
};
|
|
5933
6767
|
}
|
|
5934
6768
|
|
|
6769
|
+
// src/geoms/density.ts
|
|
6770
|
+
function geom_density(options = {}) {
|
|
6771
|
+
return {
|
|
6772
|
+
type: "density",
|
|
6773
|
+
stat: "density",
|
|
6774
|
+
position: "identity",
|
|
6775
|
+
params: {
|
|
6776
|
+
n: options.n ?? 512,
|
|
6777
|
+
bw: options.bw,
|
|
6778
|
+
kernel: options.kernel ?? "gaussian",
|
|
6779
|
+
adjust: options.adjust ?? 1,
|
|
6780
|
+
alpha: options.alpha ?? 0.3,
|
|
6781
|
+
color: options.color,
|
|
6782
|
+
fill: options.fill,
|
|
6783
|
+
linewidth: options.linewidth ?? 1,
|
|
6784
|
+
linetype: options.linetype ?? "solid"
|
|
6785
|
+
}
|
|
6786
|
+
};
|
|
6787
|
+
}
|
|
6788
|
+
|
|
5935
6789
|
// src/geoms/boxplot.ts
|
|
5936
6790
|
function geom_boxplot(options = {}) {
|
|
5937
6791
|
return {
|
|
@@ -6424,6 +7278,110 @@ var init_braille = __esm(() => {
|
|
|
6424
7278
|
];
|
|
6425
7279
|
});
|
|
6426
7280
|
|
|
7281
|
+
// src/geoms/calendar.ts
|
|
7282
|
+
function geom_calendar(options = {}) {
|
|
7283
|
+
return {
|
|
7284
|
+
type: "calendar",
|
|
7285
|
+
stat: "identity",
|
|
7286
|
+
position: "identity",
|
|
7287
|
+
params: {
|
|
7288
|
+
cell_char: options.cell_char ?? "█",
|
|
7289
|
+
empty_char: options.empty_char ?? "░",
|
|
7290
|
+
empty_color: options.empty_color ?? "#161b22",
|
|
7291
|
+
fill_color: options.fill_color ?? "#39d353",
|
|
7292
|
+
show_months: options.show_months ?? true,
|
|
7293
|
+
show_days: options.show_days ?? true,
|
|
7294
|
+
week_start: options.week_start ?? 0,
|
|
7295
|
+
levels: options.levels ?? 5
|
|
7296
|
+
}
|
|
7297
|
+
};
|
|
7298
|
+
}
|
|
7299
|
+
|
|
7300
|
+
// src/geoms/flame.ts
|
|
7301
|
+
function geom_flame(options = {}) {
|
|
7302
|
+
return {
|
|
7303
|
+
type: "flame",
|
|
7304
|
+
stat: "identity",
|
|
7305
|
+
position: "identity",
|
|
7306
|
+
params: {
|
|
7307
|
+
style: options.style ?? "flame",
|
|
7308
|
+
palette: options.palette ?? "warm",
|
|
7309
|
+
show_labels: options.show_labels ?? true,
|
|
7310
|
+
min_label_width: options.min_label_width ?? 10,
|
|
7311
|
+
sort: options.sort ?? "alpha",
|
|
7312
|
+
bar_char: options.bar_char ?? "█"
|
|
7313
|
+
}
|
|
7314
|
+
};
|
|
7315
|
+
}
|
|
7316
|
+
function geom_icicle(options = {}) {
|
|
7317
|
+
return geom_flame({ ...options, style: "icicle" });
|
|
7318
|
+
}
|
|
7319
|
+
|
|
7320
|
+
// src/geoms/corrmat.ts
|
|
7321
|
+
function geom_corrmat(options = {}) {
|
|
7322
|
+
return {
|
|
7323
|
+
type: "corrmat",
|
|
7324
|
+
stat: "identity",
|
|
7325
|
+
position: "identity",
|
|
7326
|
+
params: {
|
|
7327
|
+
show_values: options.show_values ?? true,
|
|
7328
|
+
decimals: options.decimals ?? 2,
|
|
7329
|
+
show_significance: options.show_significance ?? false,
|
|
7330
|
+
sig_threshold: options.sig_threshold ?? 0.05,
|
|
7331
|
+
sig_marker: options.sig_marker ?? "*",
|
|
7332
|
+
positive_color: options.positive_color ?? "#2166ac",
|
|
7333
|
+
negative_color: options.negative_color ?? "#b2182b",
|
|
7334
|
+
neutral_color: options.neutral_color ?? "#f7f7f7",
|
|
7335
|
+
lower_triangle: options.lower_triangle ?? false,
|
|
7336
|
+
upper_triangle: options.upper_triangle ?? false,
|
|
7337
|
+
show_diagonal: options.show_diagonal ?? true,
|
|
7338
|
+
method: options.method ?? "pearson"
|
|
7339
|
+
}
|
|
7340
|
+
};
|
|
7341
|
+
}
|
|
7342
|
+
|
|
7343
|
+
// src/geoms/sankey.ts
|
|
7344
|
+
function geom_sankey(options = {}) {
|
|
7345
|
+
return {
|
|
7346
|
+
type: "sankey",
|
|
7347
|
+
stat: "identity",
|
|
7348
|
+
position: "identity",
|
|
7349
|
+
params: {
|
|
7350
|
+
node_width: options.node_width ?? 3,
|
|
7351
|
+
node_padding: options.node_padding ?? 2,
|
|
7352
|
+
node_char: options.node_char ?? "█",
|
|
7353
|
+
flow_char: options.flow_char ?? "─",
|
|
7354
|
+
show_labels: options.show_labels ?? true,
|
|
7355
|
+
show_values: options.show_values ?? false,
|
|
7356
|
+
align: options.align ?? "justify",
|
|
7357
|
+
color_by: options.color_by ?? "auto",
|
|
7358
|
+
min_flow_width: options.min_flow_width ?? 1,
|
|
7359
|
+
flow_gap: options.flow_gap ?? 0
|
|
7360
|
+
}
|
|
7361
|
+
};
|
|
7362
|
+
}
|
|
7363
|
+
|
|
7364
|
+
// src/geoms/treemap.ts
|
|
7365
|
+
function geom_treemap(options = {}) {
|
|
7366
|
+
return {
|
|
7367
|
+
type: "treemap",
|
|
7368
|
+
stat: "identity",
|
|
7369
|
+
position: "identity",
|
|
7370
|
+
params: {
|
|
7371
|
+
algorithm: options.algorithm ?? "squarify",
|
|
7372
|
+
show_labels: options.show_labels ?? true,
|
|
7373
|
+
show_values: options.show_values ?? false,
|
|
7374
|
+
border: options.border ?? true,
|
|
7375
|
+
padding: options.padding ?? 0,
|
|
7376
|
+
min_label_size: options.min_label_size ?? 4,
|
|
7377
|
+
color_by: options.color_by ?? "value",
|
|
7378
|
+
fill_char: options.fill_char ?? "█",
|
|
7379
|
+
max_depth: options.max_depth,
|
|
7380
|
+
aspect_ratio: options.aspect_ratio ?? 1.618
|
|
7381
|
+
}
|
|
7382
|
+
};
|
|
7383
|
+
}
|
|
7384
|
+
|
|
6427
7385
|
// src/geoms/index.ts
|
|
6428
7386
|
var init_geoms = __esm(() => {
|
|
6429
7387
|
init_ridgeline();
|
|
@@ -10934,12 +11892,14 @@ __export(exports_src, {
|
|
|
10934
11892
|
geom_waffle: () => geom_waffle,
|
|
10935
11893
|
geom_vline: () => geom_vline,
|
|
10936
11894
|
geom_violin: () => geom_violin,
|
|
11895
|
+
geom_treemap: () => geom_treemap,
|
|
10937
11896
|
geom_tile: () => geom_tile,
|
|
10938
11897
|
geom_text: () => geom_text,
|
|
10939
11898
|
geom_step: () => geom_step,
|
|
10940
11899
|
geom_sparkline: () => geom_sparkline,
|
|
10941
11900
|
geom_smooth: () => geom_smooth,
|
|
10942
11901
|
geom_segment: () => geom_segment,
|
|
11902
|
+
geom_sankey: () => geom_sankey,
|
|
10943
11903
|
geom_rug: () => geom_rug,
|
|
10944
11904
|
geom_ridgeline: () => geom_ridgeline,
|
|
10945
11905
|
geom_ribbon: () => geom_ribbon,
|
|
@@ -10956,18 +11916,23 @@ __export(exports_src, {
|
|
|
10956
11916
|
geom_line: () => geom_line,
|
|
10957
11917
|
geom_label: () => geom_label,
|
|
10958
11918
|
geom_joy: () => geom_joy,
|
|
11919
|
+
geom_icicle: () => geom_icicle,
|
|
10959
11920
|
geom_hline: () => geom_hline,
|
|
10960
11921
|
geom_histogram: () => geom_histogram,
|
|
10961
11922
|
geom_freqpoly: () => geom_freqpoly,
|
|
11923
|
+
geom_flame: () => geom_flame,
|
|
10962
11924
|
geom_errorbarh: () => geom_errorbarh,
|
|
10963
11925
|
geom_errorbar: () => geom_errorbar,
|
|
10964
11926
|
geom_dumbbell: () => geom_dumbbell,
|
|
10965
11927
|
geom_density_2d: () => geom_density_2d,
|
|
11928
|
+
geom_density: () => geom_density,
|
|
10966
11929
|
geom_curve: () => geom_curve,
|
|
10967
11930
|
geom_crossbar: () => geom_crossbar,
|
|
11931
|
+
geom_corrmat: () => geom_corrmat,
|
|
10968
11932
|
geom_contour_filled: () => geom_contour_filled,
|
|
10969
11933
|
geom_contour: () => geom_contour,
|
|
10970
11934
|
geom_col: () => geom_col,
|
|
11935
|
+
geom_calendar: () => geom_calendar,
|
|
10971
11936
|
geom_bullet: () => geom_bullet,
|
|
10972
11937
|
geom_braille: () => geom_braille,
|
|
10973
11938
|
geom_boxplot: () => geom_boxplot,
|
|
@@ -11871,6 +12836,7 @@ var GEOM_TYPES = [
|
|
|
11871
12836
|
"col",
|
|
11872
12837
|
"histogram",
|
|
11873
12838
|
"freqpoly",
|
|
12839
|
+
"density",
|
|
11874
12840
|
"boxplot",
|
|
11875
12841
|
"violin",
|
|
11876
12842
|
"ridgeline",
|
|
@@ -11902,7 +12868,13 @@ var GEOM_TYPES = [
|
|
|
11902
12868
|
"contour",
|
|
11903
12869
|
"contour_filled",
|
|
11904
12870
|
"density_2d",
|
|
11905
|
-
"qq"
|
|
12871
|
+
"qq",
|
|
12872
|
+
"calendar",
|
|
12873
|
+
"flame",
|
|
12874
|
+
"icicle",
|
|
12875
|
+
"corrmat",
|
|
12876
|
+
"sankey",
|
|
12877
|
+
"treemap"
|
|
11906
12878
|
];
|
|
11907
12879
|
var datePattern = /^\d{4}-\d{2}-\d{2}/;
|
|
11908
12880
|
function fileExists(path) {
|
|
@@ -12277,7 +13249,7 @@ Error: Unknown geometry type "${geomType}"`);
|
|
|
12277
13249
|
Available geom types:`);
|
|
12278
13250
|
console.error(` Points/Lines: point, line, path, step, smooth, segment`);
|
|
12279
13251
|
console.error(` Bars/Areas: bar, col, histogram, freqpoly, area, ribbon`);
|
|
12280
|
-
console.error(` Distributions: boxplot, violin, ridgeline, joy, beeswarm, quasirandom, qq, density_2d`);
|
|
13252
|
+
console.error(` Distributions: boxplot, violin, ridgeline, joy, beeswarm, quasirandom, qq, density, density_2d`);
|
|
12281
13253
|
console.error(` Uncertainty: errorbar, errorbarh, crossbar, linerange, pointrange`);
|
|
12282
13254
|
console.error(` 2D: tile, rect, raster, bin2d, contour, contour_filled`);
|
|
12283
13255
|
console.error(` Text: text, label`);
|
|
@@ -12386,7 +13358,7 @@ Error: Geometry type "${geomType}" requires a y column`);
|
|
|
12386
13358
|
console.error(`
|
|
12387
13359
|
Usage: cli-plot.ts ${dataFile} <x> <y> [color] [title] ${geomType}`);
|
|
12388
13360
|
console.error(`
|
|
12389
|
-
If you want a univariate plot, try: histogram, bar, qq, or freqpoly`);
|
|
13361
|
+
If you want a univariate plot, try: histogram, density, bar, qq, or freqpoly`);
|
|
12390
13362
|
process.exit(1);
|
|
12391
13363
|
}
|
|
12392
13364
|
const aes = { x };
|
|
@@ -12416,6 +13388,9 @@ If you want a univariate plot, try: histogram, bar, qq, or freqpoly`);
|
|
|
12416
13388
|
case "freqpoly":
|
|
12417
13389
|
plot = plot.geom(geom_freqpoly({ bins: 20 }));
|
|
12418
13390
|
break;
|
|
13391
|
+
case "density":
|
|
13392
|
+
plot = plot.geom(geom_density());
|
|
13393
|
+
break;
|
|
12419
13394
|
case "boxplot":
|
|
12420
13395
|
plot = plot.geom(geom_boxplot());
|
|
12421
13396
|
break;
|
|
@@ -12515,6 +13490,24 @@ If you want a univariate plot, try: histogram, bar, qq, or freqpoly`);
|
|
|
12515
13490
|
plot = plot.geom(geom_qq());
|
|
12516
13491
|
plot = plot.geom(geom_qq_line());
|
|
12517
13492
|
break;
|
|
13493
|
+
case "calendar":
|
|
13494
|
+
plot = plot.geom(geom_calendar());
|
|
13495
|
+
break;
|
|
13496
|
+
case "flame":
|
|
13497
|
+
plot = plot.geom(geom_flame());
|
|
13498
|
+
break;
|
|
13499
|
+
case "icicle":
|
|
13500
|
+
plot = plot.geom(geom_icicle());
|
|
13501
|
+
break;
|
|
13502
|
+
case "corrmat":
|
|
13503
|
+
plot = plot.geom(geom_corrmat());
|
|
13504
|
+
break;
|
|
13505
|
+
case "sankey":
|
|
13506
|
+
plot = plot.geom(geom_sankey());
|
|
13507
|
+
break;
|
|
13508
|
+
case "treemap":
|
|
13509
|
+
plot = plot.geom(geom_treemap());
|
|
13510
|
+
break;
|
|
12518
13511
|
case "point":
|
|
12519
13512
|
default:
|
|
12520
13513
|
plot = plot.geom(geom_point());
|
|
@@ -12532,6 +13525,9 @@ If you want a univariate plot, try: histogram, bar, qq, or freqpoly`);
|
|
|
12532
13525
|
if ((geomType === "histogram" || geomType === "bar" || geomType === "freqpoly") && !yLabel) {
|
|
12533
13526
|
yLabel = "count";
|
|
12534
13527
|
}
|
|
13528
|
+
if (geomType === "density" && !yLabel) {
|
|
13529
|
+
yLabel = "density";
|
|
13530
|
+
}
|
|
12535
13531
|
if (geomType === "qq") {
|
|
12536
13532
|
yLabel = "Sample Quantiles";
|
|
12537
13533
|
}
|