@ggterm/core 0.2.11 → 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 +1683 -45
- package/dist/cli.js +1611 -45
- package/dist/geoms/braille.d.ts +39 -0
- package/dist/geoms/braille.d.ts.map +1 -0
- package/dist/geoms/bullet.d.ts +37 -0
- package/dist/geoms/bullet.d.ts.map +1 -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/dumbbell.d.ts +36 -0
- package/dist/geoms/dumbbell.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 +12 -0
- package/dist/geoms/index.d.ts.map +1 -1
- package/dist/geoms/lollipop.d.ts +35 -0
- package/dist/geoms/lollipop.d.ts.map +1 -0
- package/dist/geoms/sankey.d.ts +57 -0
- package/dist/geoms/sankey.d.ts.map +1 -0
- package/dist/geoms/sparkline.d.ts +36 -0
- package/dist/geoms/sparkline.d.ts.map +1 -0
- package/dist/geoms/treemap.d.ts +70 -0
- package/dist/geoms/treemap.d.ts.map +1 -0
- package/dist/geoms/waffle.d.ts +36 -0
- package/dist/geoms/waffle.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1650 -50
- package/dist/pipeline/render-geoms.d.ts +60 -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]);
|
|
@@ -2300,66 +2448,1193 @@ function parseColor(color) {
|
|
|
2300
2448
|
};
|
|
2301
2449
|
} else if (hex.length === 6) {
|
|
2302
2450
|
return {
|
|
2303
|
-
r: parseInt(hex.slice(0, 2), 16),
|
|
2304
|
-
g: parseInt(hex.slice(2, 4), 16),
|
|
2305
|
-
b: parseInt(hex.slice(4, 6), 16),
|
|
2451
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
2452
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
2453
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
2454
|
+
a: 1
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
return { r: 128, g: 128, b: 128, a: 1 };
|
|
2459
|
+
}
|
|
2460
|
+
function renderGeomBeeswarm(data, geom, aes, scales, canvas) {
|
|
2461
|
+
const alpha = geom.params.alpha ?? 1;
|
|
2462
|
+
const fixedColor = geom.params.color;
|
|
2463
|
+
const shape = getPointShape(geom.params.shape);
|
|
2464
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
2465
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
2466
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
2467
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
2468
|
+
const defaultColors = [
|
|
2469
|
+
{ r: 79, g: 169, b: 238, a: 1 },
|
|
2470
|
+
{ r: 238, g: 136, b: 102, a: 1 },
|
|
2471
|
+
{ r: 102, g: 204, b: 153, a: 1 },
|
|
2472
|
+
{ r: 204, g: 102, b: 204, a: 1 },
|
|
2473
|
+
{ r: 255, g: 200, b: 87, a: 1 },
|
|
2474
|
+
{ r: 138, g: 201, b: 222, a: 1 },
|
|
2475
|
+
{ r: 255, g: 153, b: 153, a: 1 },
|
|
2476
|
+
{ r: 170, g: 170, b: 170, a: 1 }
|
|
2477
|
+
];
|
|
2478
|
+
const categories = new Set;
|
|
2479
|
+
for (const row of data) {
|
|
2480
|
+
categories.add(String(row.xOriginal ?? row[aes.x] ?? "default"));
|
|
2481
|
+
}
|
|
2482
|
+
const categoryList = [...categories];
|
|
2483
|
+
for (const row of data) {
|
|
2484
|
+
const xVal = row.x;
|
|
2485
|
+
const yVal = row.y;
|
|
2486
|
+
if (xVal === null || xVal === undefined || yVal === null || yVal === undefined) {
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
const numGroups = categoryList.length;
|
|
2490
|
+
const xRange = plotRight - plotLeft;
|
|
2491
|
+
const xNormalized = (Number(xVal) + 0.5) / numGroups;
|
|
2492
|
+
const cx = Math.round(plotLeft + xNormalized * xRange);
|
|
2493
|
+
const cy = Math.round(scales.y.map(yVal));
|
|
2494
|
+
let color;
|
|
2495
|
+
if (fixedColor) {
|
|
2496
|
+
color = parseColorToRgba(fixedColor);
|
|
2497
|
+
} else if (scales.color && aes.color) {
|
|
2498
|
+
color = getPointColor(row, aes, scales.color);
|
|
2499
|
+
} else {
|
|
2500
|
+
const category = String(row.xOriginal ?? row[aes.x] ?? "default");
|
|
2501
|
+
const categoryIdx = categoryList.indexOf(category);
|
|
2502
|
+
color = defaultColors[categoryIdx % defaultColors.length];
|
|
2503
|
+
}
|
|
2504
|
+
if (alpha < 1) {
|
|
2505
|
+
color = { ...color, a: alpha };
|
|
2506
|
+
}
|
|
2507
|
+
if (cx >= plotLeft && cx <= plotRight && cy >= plotTop && cy <= plotBottom) {
|
|
2508
|
+
canvas.drawChar(cx, cy, shape, color);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
function renderGeomDumbbell(data, geom, aes, scales, canvas) {
|
|
2513
|
+
const lineColor = parseColorToRgba(geom.params.lineColor ?? "#666666");
|
|
2514
|
+
const alpha = geom.params.alpha ?? 1;
|
|
2515
|
+
const shape = getPointShape(geom.params.shape);
|
|
2516
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
2517
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
2518
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
2519
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
2520
|
+
const defaultColors = [
|
|
2521
|
+
{ r: 79, g: 169, b: 238, a: 1 },
|
|
2522
|
+
{ r: 238, g: 136, b: 102, a: 1 },
|
|
2523
|
+
{ r: 102, g: 204, b: 153, a: 1 },
|
|
2524
|
+
{ r: 204, g: 102, b: 204, a: 1 }
|
|
2525
|
+
];
|
|
2526
|
+
for (let i = 0;i < data.length; i++) {
|
|
2527
|
+
const row = data[i];
|
|
2528
|
+
const xVal = row[aes.x];
|
|
2529
|
+
const xendVal = row["xend"] ?? row[aes.x];
|
|
2530
|
+
const yVal = row[aes.y];
|
|
2531
|
+
if (xVal === null || xVal === undefined || yVal === null || yVal === undefined) {
|
|
2532
|
+
continue;
|
|
2533
|
+
}
|
|
2534
|
+
const x1 = Math.round(scales.x.map(xVal));
|
|
2535
|
+
const x2 = Math.round(scales.x.map(xendVal));
|
|
2536
|
+
const cy = Math.round(scales.y.map(yVal));
|
|
2537
|
+
let startColor;
|
|
2538
|
+
let endColor;
|
|
2539
|
+
if (geom.params.color) {
|
|
2540
|
+
startColor = parseColorToRgba(geom.params.color);
|
|
2541
|
+
} else if (scales.color && aes.color) {
|
|
2542
|
+
startColor = getPointColor(row, aes, scales.color);
|
|
2543
|
+
} else {
|
|
2544
|
+
startColor = defaultColors[0];
|
|
2545
|
+
}
|
|
2546
|
+
if (geom.params.colorEnd) {
|
|
2547
|
+
endColor = parseColorToRgba(geom.params.colorEnd);
|
|
2548
|
+
} else {
|
|
2549
|
+
endColor = geom.params.color ? startColor : defaultColors[1];
|
|
2550
|
+
}
|
|
2551
|
+
if (alpha < 1) {
|
|
2552
|
+
startColor = { ...startColor, a: alpha };
|
|
2553
|
+
endColor = { ...endColor, a: alpha };
|
|
2554
|
+
}
|
|
2555
|
+
if (cy >= plotTop && cy <= plotBottom) {
|
|
2556
|
+
const left = Math.max(plotLeft, Math.min(x1, x2));
|
|
2557
|
+
const right = Math.min(plotRight, Math.max(x1, x2));
|
|
2558
|
+
for (let x = left;x <= right; x++) {
|
|
2559
|
+
canvas.drawChar(x, cy, "─", lineColor);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
if (x1 >= plotLeft && x1 <= plotRight && cy >= plotTop && cy <= plotBottom) {
|
|
2563
|
+
canvas.drawChar(x1, cy, shape, startColor);
|
|
2564
|
+
}
|
|
2565
|
+
if (x2 >= plotLeft && x2 <= plotRight && cy >= plotTop && cy <= plotBottom) {
|
|
2566
|
+
canvas.drawChar(x2, cy, shape, endColor);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
function renderGeomLollipop(data, geom, aes, scales, canvas) {
|
|
2571
|
+
const alpha = geom.params.alpha ?? 1;
|
|
2572
|
+
const baseline = geom.params.baseline ?? 0;
|
|
2573
|
+
const direction = geom.params.direction ?? "vertical";
|
|
2574
|
+
const shape = getPointShape(geom.params.shape);
|
|
2575
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
2576
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
2577
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
2578
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
2579
|
+
const defaultColors = [
|
|
2580
|
+
{ r: 79, g: 169, b: 238, a: 1 },
|
|
2581
|
+
{ r: 238, g: 136, b: 102, a: 1 },
|
|
2582
|
+
{ r: 102, g: 204, b: 153, a: 1 },
|
|
2583
|
+
{ r: 204, g: 102, b: 204, a: 1 },
|
|
2584
|
+
{ r: 255, g: 200, b: 87, a: 1 },
|
|
2585
|
+
{ r: 138, g: 201, b: 222, a: 1 }
|
|
2586
|
+
];
|
|
2587
|
+
const xValues = [...new Set(data.map((row) => row[aes.x]))];
|
|
2588
|
+
for (let i = 0;i < data.length; i++) {
|
|
2589
|
+
const row = data[i];
|
|
2590
|
+
const xVal = row[aes.x];
|
|
2591
|
+
const yVal = row[aes.y];
|
|
2592
|
+
if (xVal === null || xVal === undefined || yVal === null || yVal === undefined) {
|
|
2593
|
+
continue;
|
|
2594
|
+
}
|
|
2595
|
+
const cx = Math.round(scales.x.map(xVal));
|
|
2596
|
+
const cy = Math.round(scales.y.map(yVal));
|
|
2597
|
+
let color;
|
|
2598
|
+
if (geom.params.color) {
|
|
2599
|
+
color = parseColorToRgba(geom.params.color);
|
|
2600
|
+
} else if (scales.color && aes.color) {
|
|
2601
|
+
color = getPointColor(row, aes, scales.color);
|
|
2602
|
+
} else {
|
|
2603
|
+
const categoryIdx = xValues.indexOf(xVal);
|
|
2604
|
+
color = defaultColors[categoryIdx % defaultColors.length];
|
|
2605
|
+
}
|
|
2606
|
+
let lineColor;
|
|
2607
|
+
if (geom.params.lineColor) {
|
|
2608
|
+
lineColor = parseColorToRgba(geom.params.lineColor);
|
|
2609
|
+
} else {
|
|
2610
|
+
lineColor = {
|
|
2611
|
+
r: Math.round(color.r * 0.7),
|
|
2612
|
+
g: Math.round(color.g * 0.7),
|
|
2613
|
+
b: Math.round(color.b * 0.7),
|
|
2614
|
+
a: color.a
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
if (alpha < 1) {
|
|
2618
|
+
color = { ...color, a: alpha };
|
|
2619
|
+
lineColor = { ...lineColor, a: alpha };
|
|
2620
|
+
}
|
|
2621
|
+
if (direction === "vertical") {
|
|
2622
|
+
let baselineY = Math.round(scales.y.map(baseline));
|
|
2623
|
+
baselineY = Math.max(plotTop, Math.min(plotBottom, baselineY));
|
|
2624
|
+
if (cx >= plotLeft && cx <= plotRight) {
|
|
2625
|
+
const top = Math.min(cy, baselineY);
|
|
2626
|
+
const bottom = Math.max(cy, baselineY);
|
|
2627
|
+
for (let y = top;y <= bottom; y++) {
|
|
2628
|
+
if (y >= plotTop && y <= plotBottom) {
|
|
2629
|
+
canvas.drawChar(cx, y, "│", lineColor);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
if (cx >= plotLeft && cx <= plotRight && cy >= plotTop && cy <= plotBottom) {
|
|
2634
|
+
canvas.drawChar(cx, cy, shape, color);
|
|
2635
|
+
}
|
|
2636
|
+
} else {
|
|
2637
|
+
let baselineX = Math.round(scales.x.map(baseline));
|
|
2638
|
+
baselineX = Math.max(plotLeft, Math.min(plotRight, baselineX));
|
|
2639
|
+
if (cy >= plotTop && cy <= plotBottom) {
|
|
2640
|
+
const left = Math.min(cx, baselineX);
|
|
2641
|
+
const right = Math.max(cx, baselineX);
|
|
2642
|
+
for (let x = left;x <= right; x++) {
|
|
2643
|
+
if (x >= plotLeft && x <= plotRight) {
|
|
2644
|
+
canvas.drawChar(x, cy, "─", lineColor);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
if (cx >= plotLeft && cx <= plotRight && cy >= plotTop && cy <= plotBottom) {
|
|
2649
|
+
canvas.drawChar(cx, cy, shape, color);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
function renderGeomWaffle(data, geom, aes, scales, canvas) {
|
|
2655
|
+
const rows = geom.params.rows ?? 10;
|
|
2656
|
+
const cols = geom.params.cols ?? 10;
|
|
2657
|
+
const fillChar = geom.params.fill_char ?? "█";
|
|
2658
|
+
const emptyChar = geom.params.empty_char ?? "░";
|
|
2659
|
+
const showLegend = geom.params.show_legend ?? true;
|
|
2660
|
+
const flip = geom.params.flip ?? false;
|
|
2661
|
+
const gap = geom.params.gap ?? 0;
|
|
2662
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
2663
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
2664
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
2665
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
2666
|
+
const defaultColors = [
|
|
2667
|
+
{ r: 79, g: 169, b: 238, a: 1 },
|
|
2668
|
+
{ r: 238, g: 136, b: 102, a: 1 },
|
|
2669
|
+
{ r: 102, g: 204, b: 153, a: 1 },
|
|
2670
|
+
{ r: 204, g: 102, b: 204, a: 1 },
|
|
2671
|
+
{ r: 255, g: 200, b: 87, a: 1 },
|
|
2672
|
+
{ r: 138, g: 201, b: 222, a: 1 },
|
|
2673
|
+
{ r: 255, g: 153, b: 153, a: 1 },
|
|
2674
|
+
{ r: 170, g: 170, b: 170, a: 1 }
|
|
2675
|
+
];
|
|
2676
|
+
const fillField = aes.fill || aes.color || "category";
|
|
2677
|
+
const valueField = aes.y || "value";
|
|
2678
|
+
const categories = new Map;
|
|
2679
|
+
let totalValue = 0;
|
|
2680
|
+
for (const row of data) {
|
|
2681
|
+
const cat = String(row[fillField] ?? "default");
|
|
2682
|
+
const val = Number(row[valueField]) || 1;
|
|
2683
|
+
categories.set(cat, (categories.get(cat) ?? 0) + val);
|
|
2684
|
+
totalValue += val;
|
|
2685
|
+
}
|
|
2686
|
+
const cellsPerCategory = [];
|
|
2687
|
+
const categoryList = [...categories.keys()];
|
|
2688
|
+
let cellsAssigned = 0;
|
|
2689
|
+
for (let i = 0;i < categoryList.length; i++) {
|
|
2690
|
+
const cat = categoryList[i];
|
|
2691
|
+
const val = categories.get(cat);
|
|
2692
|
+
const proportion = val / totalValue;
|
|
2693
|
+
const cells = Math.round(proportion * rows * cols);
|
|
2694
|
+
const color = scales.color?.map(cat) ?? defaultColors[i % defaultColors.length];
|
|
2695
|
+
cellsPerCategory.push({ category: cat, cells, color });
|
|
2696
|
+
cellsAssigned += cells;
|
|
2697
|
+
}
|
|
2698
|
+
if (cellsAssigned < rows * cols && cellsPerCategory.length > 0) {
|
|
2699
|
+
cellsPerCategory[0].cells += rows * cols - cellsAssigned;
|
|
2700
|
+
}
|
|
2701
|
+
const grid = [];
|
|
2702
|
+
for (const { cells, color } of cellsPerCategory) {
|
|
2703
|
+
for (let i = 0;i < cells; i++) {
|
|
2704
|
+
grid.push({ char: fillChar, color });
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
const emptyColor = { r: 80, g: 80, b: 80, a: 0.3 };
|
|
2708
|
+
while (grid.length < rows * cols) {
|
|
2709
|
+
grid.push({ char: emptyChar, color: emptyColor });
|
|
2710
|
+
}
|
|
2711
|
+
const availableWidth = plotRight - plotLeft - (showLegend ? 15 : 0);
|
|
2712
|
+
const availableHeight = plotBottom - plotTop;
|
|
2713
|
+
const cellWidth = Math.max(1, Math.floor(availableWidth / cols)) + gap;
|
|
2714
|
+
const cellHeight = Math.max(1, Math.floor(availableHeight / rows)) + gap;
|
|
2715
|
+
for (let row = 0;row < rows; row++) {
|
|
2716
|
+
for (let col = 0;col < cols; col++) {
|
|
2717
|
+
let idx;
|
|
2718
|
+
if (flip) {
|
|
2719
|
+
idx = row * cols + col;
|
|
2720
|
+
} else {
|
|
2721
|
+
idx = col * rows + (rows - 1 - row);
|
|
2722
|
+
}
|
|
2723
|
+
if (idx >= grid.length)
|
|
2724
|
+
continue;
|
|
2725
|
+
const cell = grid[idx];
|
|
2726
|
+
const x = plotLeft + col * cellWidth;
|
|
2727
|
+
const y = plotTop + row * cellHeight;
|
|
2728
|
+
if (x >= plotLeft && x < plotRight - (showLegend ? 15 : 0) && y >= plotTop && y <= plotBottom) {
|
|
2729
|
+
canvas.drawChar(x, y, cell.char, cell.color);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
if (showLegend) {
|
|
2734
|
+
const legendX = plotRight - 12;
|
|
2735
|
+
let legendY = plotTop;
|
|
2736
|
+
for (let i = 0;i < cellsPerCategory.length && legendY < plotBottom; i++) {
|
|
2737
|
+
const { category, cells, color } = cellsPerCategory[i];
|
|
2738
|
+
const pct = Math.round(cells / (rows * cols) * 100);
|
|
2739
|
+
const label = `${category.slice(0, 6)} ${pct}%`;
|
|
2740
|
+
canvas.drawChar(legendX, legendY, "█", color);
|
|
2741
|
+
canvas.drawString(legendX + 2, legendY, label, { r: 180, g: 180, b: 180, a: 1 });
|
|
2742
|
+
legendY += 2;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
function renderGeomSparkline(data, geom, aes, scales, canvas) {
|
|
2747
|
+
const sparkType = geom.params.sparkType ?? "bar";
|
|
2748
|
+
const width = geom.params.width ?? 20;
|
|
2749
|
+
const showMinmax = geom.params.show_minmax ?? false;
|
|
2750
|
+
const normalize = geom.params.normalize ?? true;
|
|
2751
|
+
const minColor = parseColorToRgba(geom.params.min_color ?? "#e74c3c");
|
|
2752
|
+
const maxColor = parseColorToRgba(geom.params.max_color ?? "#2ecc71");
|
|
2753
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
2754
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
2755
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
2756
|
+
const SPARK_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
2757
|
+
const defaultColor = { r: 79, g: 169, b: 238, a: 1 };
|
|
2758
|
+
const groupField = aes.group || aes.color;
|
|
2759
|
+
const groups = new Map;
|
|
2760
|
+
if (groupField) {
|
|
2761
|
+
for (const row of data) {
|
|
2762
|
+
const key = String(row[groupField] ?? "default");
|
|
2763
|
+
if (!groups.has(key))
|
|
2764
|
+
groups.set(key, []);
|
|
2765
|
+
groups.get(key).push(row);
|
|
2766
|
+
}
|
|
2767
|
+
} else {
|
|
2768
|
+
groups.set("default", [...data]);
|
|
2769
|
+
}
|
|
2770
|
+
let currentY = plotTop;
|
|
2771
|
+
for (const [groupKey, groupData] of groups) {
|
|
2772
|
+
const sorted = aes.x ? [...groupData].sort((a, b) => Number(a[aes.x]) - Number(b[aes.x])) : groupData;
|
|
2773
|
+
const values = sorted.map((row) => Number(row[aes.y]) || 0);
|
|
2774
|
+
if (values.length === 0)
|
|
2775
|
+
continue;
|
|
2776
|
+
const minVal = Math.min(...values);
|
|
2777
|
+
const maxVal = Math.max(...values);
|
|
2778
|
+
const minIdx = values.indexOf(minVal);
|
|
2779
|
+
const maxIdx = values.indexOf(maxVal);
|
|
2780
|
+
const range = maxVal - minVal || 1;
|
|
2781
|
+
const color = scales.color?.map(groupKey) ?? defaultColor;
|
|
2782
|
+
const sparkValues = [];
|
|
2783
|
+
if (values.length <= width) {
|
|
2784
|
+
sparkValues.push(...values);
|
|
2785
|
+
} else {
|
|
2786
|
+
for (let i = 0;i < width; i++) {
|
|
2787
|
+
const idx = Math.floor(i * values.length / width);
|
|
2788
|
+
sparkValues.push(values[idx]);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
for (let i = 0;i < sparkValues.length; i++) {
|
|
2792
|
+
const val = sparkValues[i];
|
|
2793
|
+
const normalized = normalize ? (val - minVal) / range : val / (maxVal || 1);
|
|
2794
|
+
const charIdx = Math.min(7, Math.max(0, Math.floor(normalized * 8)));
|
|
2795
|
+
const char = sparkType === "dot" ? "•" : SPARK_CHARS[charIdx];
|
|
2796
|
+
const x = plotLeft + i;
|
|
2797
|
+
const y = currentY;
|
|
2798
|
+
let pointColor = color;
|
|
2799
|
+
if (showMinmax) {
|
|
2800
|
+
const origIdx = Math.floor(i * values.length / sparkValues.length);
|
|
2801
|
+
if (origIdx === minIdx)
|
|
2802
|
+
pointColor = minColor;
|
|
2803
|
+
else if (origIdx === maxIdx)
|
|
2804
|
+
pointColor = maxColor;
|
|
2805
|
+
}
|
|
2806
|
+
if (x < plotLeft + width && y >= plotTop && y <= plotBottom) {
|
|
2807
|
+
canvas.drawChar(x, y, char, pointColor);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
if (groupField && groups.size > 1) {
|
|
2811
|
+
const labelX = plotLeft + width + 1;
|
|
2812
|
+
canvas.drawString(labelX, currentY, groupKey.slice(0, 8), { r: 180, g: 180, b: 180, a: 1 });
|
|
2813
|
+
}
|
|
2814
|
+
currentY += 2;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
function renderGeomBullet(data, geom, aes, scales, canvas) {
|
|
2818
|
+
const width = geom.params.width ?? 40;
|
|
2819
|
+
const targetChar = geom.params.target_char ?? "│";
|
|
2820
|
+
const barChar = geom.params.bar_char ?? "█";
|
|
2821
|
+
const rangeChars = geom.params.range_chars ?? ["░", "▒", "▓"];
|
|
2822
|
+
const showValues = geom.params.show_values ?? true;
|
|
2823
|
+
const targetColor = parseColorToRgba(geom.params.target_color ?? "#e74c3c");
|
|
2824
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
2825
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
2826
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
2827
|
+
const defaultColors = [
|
|
2828
|
+
{ r: 79, g: 169, b: 238, a: 1 },
|
|
2829
|
+
{ r: 238, g: 136, b: 102, a: 1 },
|
|
2830
|
+
{ r: 102, g: 204, b: 153, a: 1 }
|
|
2831
|
+
];
|
|
2832
|
+
const rangeColors = [
|
|
2833
|
+
{ r: 60, g: 60, b: 60, a: 1 },
|
|
2834
|
+
{ r: 90, g: 90, b: 90, a: 1 },
|
|
2835
|
+
{ r: 120, g: 120, b: 120, a: 1 }
|
|
2836
|
+
];
|
|
2837
|
+
let currentY = plotTop;
|
|
2838
|
+
for (let i = 0;i < data.length; i++) {
|
|
2839
|
+
const row = data[i];
|
|
2840
|
+
const label = aes.x ? String(row[aes.x]).slice(0, 10) : `Item ${i + 1}`;
|
|
2841
|
+
const value = Number(row[aes.y]) || 0;
|
|
2842
|
+
const target = Number(row["target"]) || null;
|
|
2843
|
+
const maxValue = Number(row["max"]) || Math.max(value, target || 0) * 1.2;
|
|
2844
|
+
const ranges = row["ranges"] ?? [maxValue * 0.6, maxValue * 0.8, maxValue];
|
|
2845
|
+
const color = scales.color?.map(label) ?? defaultColors[i % defaultColors.length];
|
|
2846
|
+
const labelWidth = 12;
|
|
2847
|
+
canvas.drawString(plotLeft, currentY, label.padEnd(labelWidth), { r: 180, g: 180, b: 180, a: 1 });
|
|
2848
|
+
const barStart = plotLeft + labelWidth;
|
|
2849
|
+
const barWidth = Math.min(width, scales.x.range[1] - barStart - (showValues ? 8 : 0));
|
|
2850
|
+
for (let r = ranges.length - 1;r >= 0; r--) {
|
|
2851
|
+
const rangeWidth = Math.round(ranges[r] / maxValue * barWidth);
|
|
2852
|
+
for (let x = 0;x < rangeWidth; x++) {
|
|
2853
|
+
if (barStart + x <= scales.x.range[1]) {
|
|
2854
|
+
canvas.drawChar(barStart + x, currentY, rangeChars[r], rangeColors[r]);
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
const valueWidth = Math.round(value / maxValue * barWidth);
|
|
2859
|
+
for (let x = 0;x < valueWidth; x++) {
|
|
2860
|
+
if (barStart + x <= scales.x.range[1]) {
|
|
2861
|
+
canvas.drawChar(barStart + x, currentY, barChar, color);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
if (target !== null) {
|
|
2865
|
+
const targetX = barStart + Math.round(target / maxValue * barWidth);
|
|
2866
|
+
if (targetX >= barStart && targetX <= barStart + barWidth) {
|
|
2867
|
+
canvas.drawChar(targetX, currentY, targetChar, targetColor);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
if (showValues) {
|
|
2871
|
+
const valueStr = value.toFixed(0);
|
|
2872
|
+
canvas.drawString(barStart + barWidth + 2, currentY, valueStr, { r: 180, g: 180, b: 180, a: 1 });
|
|
2873
|
+
}
|
|
2874
|
+
currentY += 2;
|
|
2875
|
+
if (currentY > plotBottom)
|
|
2876
|
+
break;
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
function renderGeomBraille(data, geom, aes, scales, canvas) {
|
|
2880
|
+
const brailleType = geom.params.brailleType ?? "point";
|
|
2881
|
+
const fill = geom.params.fill ?? false;
|
|
2882
|
+
const alpha = geom.params.alpha ?? 1;
|
|
2883
|
+
const BRAILLE_BASE = 10240;
|
|
2884
|
+
const DOTS = [
|
|
2885
|
+
[1, 2, 4, 64],
|
|
2886
|
+
[8, 16, 32, 128]
|
|
2887
|
+
];
|
|
2888
|
+
const plotLeft = Math.round(scales.x.range[0]);
|
|
2889
|
+
const plotRight = Math.round(scales.x.range[1]);
|
|
2890
|
+
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
2891
|
+
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
2892
|
+
const plotWidth = plotRight - plotLeft;
|
|
2893
|
+
const plotHeight = plotBottom - plotTop;
|
|
2894
|
+
const brailleWidth = plotWidth;
|
|
2895
|
+
const brailleHeight = plotHeight;
|
|
2896
|
+
const buffer = [];
|
|
2897
|
+
for (let y = 0;y < brailleHeight; y++) {
|
|
2898
|
+
buffer[y] = new Array(brailleWidth).fill(0);
|
|
2899
|
+
}
|
|
2900
|
+
const defaultColor = { r: 79, g: 169, b: 238, a: 1 };
|
|
2901
|
+
const color = geom.params.color ? parseColorToRgba(geom.params.color) : defaultColor;
|
|
2902
|
+
const finalColor = alpha < 1 ? { ...color, a: alpha } : color;
|
|
2903
|
+
const sorted = aes.x ? [...data].sort((a, b) => Number(a[aes.x]) - Number(b[aes.x])) : data;
|
|
2904
|
+
const setDot = (canvasX, canvasY) => {
|
|
2905
|
+
const subX = (canvasX - plotLeft) * 2;
|
|
2906
|
+
const subY = (canvasY - plotTop) * 4;
|
|
2907
|
+
const cellX = Math.floor(subX / 2);
|
|
2908
|
+
const cellY = Math.floor(subY / 4);
|
|
2909
|
+
const dotCol = subX % 2;
|
|
2910
|
+
const dotRow = subY % 4;
|
|
2911
|
+
if (cellX >= 0 && cellX < brailleWidth && cellY >= 0 && cellY < brailleHeight) {
|
|
2912
|
+
if (dotCol >= 0 && dotCol < 2 && dotRow >= 0 && dotRow < 4) {
|
|
2913
|
+
buffer[cellY][cellX] |= DOTS[dotCol][dotRow];
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
let prevCx = null;
|
|
2918
|
+
let prevCy = null;
|
|
2919
|
+
for (const row of sorted) {
|
|
2920
|
+
const xVal = row[aes.x];
|
|
2921
|
+
const yVal = row[aes.y];
|
|
2922
|
+
if (xVal === null || xVal === undefined || yVal === null || yVal === undefined) {
|
|
2923
|
+
prevCx = null;
|
|
2924
|
+
prevCy = null;
|
|
2925
|
+
continue;
|
|
2926
|
+
}
|
|
2927
|
+
const cx = Math.round(scales.x.map(xVal));
|
|
2928
|
+
const cy = Math.round(scales.y.map(yVal));
|
|
2929
|
+
if (brailleType === "line" && prevCx !== null && prevCy !== null) {
|
|
2930
|
+
const dx = Math.abs(cx - prevCx);
|
|
2931
|
+
const dy = Math.abs(cy - prevCy);
|
|
2932
|
+
const sx = prevCx < cx ? 1 : -1;
|
|
2933
|
+
const sy = prevCy < cy ? 1 : -1;
|
|
2934
|
+
let err = dx - dy;
|
|
2935
|
+
let x = prevCx;
|
|
2936
|
+
let y = prevCy;
|
|
2937
|
+
while (true) {
|
|
2938
|
+
if (x >= plotLeft && x < plotRight && y >= plotTop && y < plotBottom) {
|
|
2939
|
+
setDot(x, y);
|
|
2940
|
+
if (fill) {
|
|
2941
|
+
for (let fy = y;fy < plotBottom; fy++) {
|
|
2942
|
+
setDot(x, fy);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
if (x === cx && y === cy)
|
|
2947
|
+
break;
|
|
2948
|
+
const e2 = 2 * err;
|
|
2949
|
+
if (e2 > -dy) {
|
|
2950
|
+
err -= dy;
|
|
2951
|
+
x += sx;
|
|
2952
|
+
}
|
|
2953
|
+
if (e2 < dx) {
|
|
2954
|
+
err += dx;
|
|
2955
|
+
y += sy;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
} else {
|
|
2959
|
+
if (cx >= plotLeft && cx < plotRight && cy >= plotTop && cy < plotBottom) {
|
|
2960
|
+
setDot(cx, cy);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
prevCx = cx;
|
|
2964
|
+
prevCy = cy;
|
|
2965
|
+
}
|
|
2966
|
+
for (let y = 0;y < brailleHeight; y++) {
|
|
2967
|
+
for (let x = 0;x < brailleWidth; x++) {
|
|
2968
|
+
if (buffer[y][x] > 0) {
|
|
2969
|
+
const char = String.fromCharCode(BRAILLE_BASE + buffer[y][x]);
|
|
2970
|
+
canvas.drawChar(plotLeft + x, plotTop + y, char, finalColor);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
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),
|
|
2306
3264
|
a: 1
|
|
2307
3265
|
};
|
|
2308
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
|
+
}
|
|
2309
3301
|
}
|
|
2310
|
-
return { r: 128, g: 128, b: 128, a: 1 };
|
|
2311
3302
|
}
|
|
2312
|
-
function
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
const
|
|
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;
|
|
2316
3312
|
const plotLeft = Math.round(scales.x.range[0]);
|
|
2317
3313
|
const plotRight = Math.round(scales.x.range[1]);
|
|
2318
3314
|
const plotTop = Math.round(Math.min(scales.y.range[0], scales.y.range[1]));
|
|
2319
3315
|
const plotBottom = Math.round(Math.max(scales.y.range[0], scales.y.range[1]));
|
|
2320
|
-
const
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
{ r: 170, g: 170, b: 170, a: 1 }
|
|
2329
|
-
];
|
|
2330
|
-
const categories = new Set;
|
|
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;
|
|
2331
3324
|
for (const row of data) {
|
|
2332
|
-
|
|
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
|
+
}
|
|
2333
3350
|
}
|
|
2334
|
-
const
|
|
2335
|
-
for (const
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
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])
|
|
2339
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;
|
|
2340
3370
|
}
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
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);
|
|
2351
3487
|
} else {
|
|
2352
|
-
|
|
2353
|
-
const categoryIdx = categoryList.indexOf(category);
|
|
2354
|
-
color = defaultColors[categoryIdx % defaultColors.length];
|
|
3488
|
+
roots.push(node);
|
|
2355
3489
|
}
|
|
2356
|
-
|
|
2357
|
-
|
|
3490
|
+
}
|
|
3491
|
+
if (roots.length === 0) {
|
|
3492
|
+
for (const [, node] of nodeMap) {
|
|
3493
|
+
roots.push(node);
|
|
2358
3494
|
}
|
|
2359
|
-
|
|
2360
|
-
|
|
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
|
+
}
|
|
2361
3633
|
}
|
|
2362
3634
|
}
|
|
3635
|
+
for (let i = 0;i < validRoots.length; i++) {
|
|
3636
|
+
renderNode(validRoots[i], i, 0);
|
|
3637
|
+
}
|
|
2363
3638
|
}
|
|
2364
3639
|
function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
2365
3640
|
switch (geom.type) {
|
|
@@ -2398,6 +3673,9 @@ function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
|
2398
3673
|
case "freqpoly":
|
|
2399
3674
|
renderGeomFreqpoly(data, geom, aes, scales, canvas);
|
|
2400
3675
|
break;
|
|
3676
|
+
case "density":
|
|
3677
|
+
renderGeomDensity(data, geom, aes, scales, canvas);
|
|
3678
|
+
break;
|
|
2401
3679
|
case "boxplot":
|
|
2402
3680
|
renderGeomBoxplot(data, geom, aes, scales, canvas);
|
|
2403
3681
|
break;
|
|
@@ -2436,6 +3714,9 @@ function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
|
2436
3714
|
case "linerange":
|
|
2437
3715
|
renderGeomLinerange(data, geom, aes, scales, canvas);
|
|
2438
3716
|
break;
|
|
3717
|
+
case "crossbar":
|
|
3718
|
+
renderGeomCrossbar(data, geom, aes, scales, canvas);
|
|
3719
|
+
break;
|
|
2439
3720
|
case "pointrange":
|
|
2440
3721
|
renderGeomPointrange(data, geom, aes, scales, canvas);
|
|
2441
3722
|
break;
|
|
@@ -2446,6 +3727,40 @@ function renderGeom(data, geom, aes, scales, canvas, coordType) {
|
|
|
2446
3727
|
case "quasirandom":
|
|
2447
3728
|
renderGeomBeeswarm(data, geom, aes, scales, canvas);
|
|
2448
3729
|
break;
|
|
3730
|
+
case "dumbbell":
|
|
3731
|
+
renderGeomDumbbell(data, geom, aes, scales, canvas);
|
|
3732
|
+
break;
|
|
3733
|
+
case "lollipop":
|
|
3734
|
+
renderGeomLollipop(data, geom, aes, scales, canvas);
|
|
3735
|
+
break;
|
|
3736
|
+
case "waffle":
|
|
3737
|
+
renderGeomWaffle(data, geom, aes, scales, canvas);
|
|
3738
|
+
break;
|
|
3739
|
+
case "sparkline":
|
|
3740
|
+
renderGeomSparkline(data, geom, aes, scales, canvas);
|
|
3741
|
+
break;
|
|
3742
|
+
case "bullet":
|
|
3743
|
+
renderGeomBullet(data, geom, aes, scales, canvas);
|
|
3744
|
+
break;
|
|
3745
|
+
case "braille":
|
|
3746
|
+
renderGeomBraille(data, geom, aes, scales, canvas);
|
|
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;
|
|
2449
3764
|
default:
|
|
2450
3765
|
break;
|
|
2451
3766
|
}
|
|
@@ -5451,6 +6766,26 @@ function geom_freqpoly(options = {}) {
|
|
|
5451
6766
|
};
|
|
5452
6767
|
}
|
|
5453
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
|
+
|
|
5454
6789
|
// src/geoms/boxplot.ts
|
|
5455
6790
|
function geom_boxplot(options = {}) {
|
|
5456
6791
|
return {
|
|
@@ -5818,9 +7153,240 @@ function geom_quasirandom(options = {}) {
|
|
|
5818
7153
|
return geom_beeswarm({ ...options, method: "center" });
|
|
5819
7154
|
}
|
|
5820
7155
|
|
|
7156
|
+
// src/geoms/dumbbell.ts
|
|
7157
|
+
function geom_dumbbell(options = {}) {
|
|
7158
|
+
return {
|
|
7159
|
+
type: "dumbbell",
|
|
7160
|
+
stat: "identity",
|
|
7161
|
+
position: "identity",
|
|
7162
|
+
params: {
|
|
7163
|
+
size: options.size ?? 2,
|
|
7164
|
+
sizeEnd: options.sizeEnd ?? options.size ?? 2,
|
|
7165
|
+
color: options.color,
|
|
7166
|
+
colorEnd: options.colorEnd ?? options.color,
|
|
7167
|
+
lineColor: options.lineColor ?? "#666666",
|
|
7168
|
+
lineWidth: options.lineWidth ?? 1,
|
|
7169
|
+
alpha: options.alpha ?? 1,
|
|
7170
|
+
shape: options.shape ?? "circle"
|
|
7171
|
+
}
|
|
7172
|
+
};
|
|
7173
|
+
}
|
|
7174
|
+
|
|
7175
|
+
// src/geoms/lollipop.ts
|
|
7176
|
+
function geom_lollipop(options = {}) {
|
|
7177
|
+
return {
|
|
7178
|
+
type: "lollipop",
|
|
7179
|
+
stat: "identity",
|
|
7180
|
+
position: "identity",
|
|
7181
|
+
params: {
|
|
7182
|
+
size: options.size ?? 2,
|
|
7183
|
+
color: options.color,
|
|
7184
|
+
lineColor: options.lineColor,
|
|
7185
|
+
lineWidth: options.lineWidth ?? 1,
|
|
7186
|
+
alpha: options.alpha ?? 1,
|
|
7187
|
+
shape: options.shape ?? "circle",
|
|
7188
|
+
direction: options.direction ?? "vertical",
|
|
7189
|
+
baseline: options.baseline ?? 0
|
|
7190
|
+
}
|
|
7191
|
+
};
|
|
7192
|
+
}
|
|
7193
|
+
|
|
7194
|
+
// src/geoms/waffle.ts
|
|
7195
|
+
function geom_waffle(options = {}) {
|
|
7196
|
+
return {
|
|
7197
|
+
type: "waffle",
|
|
7198
|
+
stat: "identity",
|
|
7199
|
+
position: "identity",
|
|
7200
|
+
params: {
|
|
7201
|
+
rows: options.rows ?? 10,
|
|
7202
|
+
cols: options.cols ?? 10,
|
|
7203
|
+
n_total: options.n_total ?? 100,
|
|
7204
|
+
fill_char: options.fill_char ?? "█",
|
|
7205
|
+
empty_char: options.empty_char ?? "░",
|
|
7206
|
+
alpha: options.alpha ?? 1,
|
|
7207
|
+
show_legend: options.show_legend ?? true,
|
|
7208
|
+
flip: options.flip ?? false,
|
|
7209
|
+
gap: options.gap ?? 0
|
|
7210
|
+
}
|
|
7211
|
+
};
|
|
7212
|
+
}
|
|
7213
|
+
|
|
7214
|
+
// src/geoms/sparkline.ts
|
|
7215
|
+
function geom_sparkline(options = {}) {
|
|
7216
|
+
return {
|
|
7217
|
+
type: "sparkline",
|
|
7218
|
+
stat: "identity",
|
|
7219
|
+
position: "identity",
|
|
7220
|
+
params: {
|
|
7221
|
+
sparkType: options.type ?? "bar",
|
|
7222
|
+
width: options.width ?? 20,
|
|
7223
|
+
height: options.height ?? 1,
|
|
7224
|
+
show_minmax: options.show_minmax ?? false,
|
|
7225
|
+
color: options.color,
|
|
7226
|
+
min_color: options.min_color ?? "#e74c3c",
|
|
7227
|
+
max_color: options.max_color ?? "#2ecc71",
|
|
7228
|
+
normalize: options.normalize ?? true
|
|
7229
|
+
}
|
|
7230
|
+
};
|
|
7231
|
+
}
|
|
7232
|
+
var SPARK_BARS, SPARK_DOTS;
|
|
7233
|
+
var init_sparkline = __esm(() => {
|
|
7234
|
+
SPARK_BARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
7235
|
+
SPARK_DOTS = ["⠀", "⢀", "⢠", "⢰", "⢸", "⣸", "⣾", "⣿"];
|
|
7236
|
+
});
|
|
7237
|
+
|
|
7238
|
+
// src/geoms/bullet.ts
|
|
7239
|
+
function geom_bullet(options = {}) {
|
|
7240
|
+
return {
|
|
7241
|
+
type: "bullet",
|
|
7242
|
+
stat: "identity",
|
|
7243
|
+
position: "identity",
|
|
7244
|
+
params: {
|
|
7245
|
+
width: options.width ?? 40,
|
|
7246
|
+
height: options.height ?? 1,
|
|
7247
|
+
target_char: options.target_char ?? "│",
|
|
7248
|
+
bar_char: options.bar_char ?? "█",
|
|
7249
|
+
range_chars: options.range_chars ?? ["░", "▒", "▓"],
|
|
7250
|
+
show_values: options.show_values ?? true,
|
|
7251
|
+
color: options.color,
|
|
7252
|
+
target_color: options.target_color ?? "#e74c3c",
|
|
7253
|
+
orientation: options.orientation ?? "horizontal"
|
|
7254
|
+
}
|
|
7255
|
+
};
|
|
7256
|
+
}
|
|
7257
|
+
|
|
7258
|
+
// src/geoms/braille.ts
|
|
7259
|
+
function geom_braille(options = {}) {
|
|
7260
|
+
return {
|
|
7261
|
+
type: "braille",
|
|
7262
|
+
stat: "identity",
|
|
7263
|
+
position: "identity",
|
|
7264
|
+
params: {
|
|
7265
|
+
brailleType: options.type ?? "point",
|
|
7266
|
+
color: options.color,
|
|
7267
|
+
fill: options.fill ?? false,
|
|
7268
|
+
alpha: options.alpha ?? 1,
|
|
7269
|
+
dot_size: options.dot_size ?? 1
|
|
7270
|
+
}
|
|
7271
|
+
};
|
|
7272
|
+
}
|
|
7273
|
+
var BRAILLE_BASE = 10240, BRAILLE_DOTS;
|
|
7274
|
+
var init_braille = __esm(() => {
|
|
7275
|
+
BRAILLE_DOTS = [
|
|
7276
|
+
[1, 2, 4, 64],
|
|
7277
|
+
[8, 16, 32, 128]
|
|
7278
|
+
];
|
|
7279
|
+
});
|
|
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
|
+
|
|
5821
7385
|
// src/geoms/index.ts
|
|
5822
7386
|
var init_geoms = __esm(() => {
|
|
5823
7387
|
init_ridgeline();
|
|
7388
|
+
init_sparkline();
|
|
7389
|
+
init_braille();
|
|
5824
7390
|
});
|
|
5825
7391
|
|
|
5826
7392
|
// src/stats/index.ts
|
|
@@ -10323,13 +11889,17 @@ __export(exports_src, {
|
|
|
10323
11889
|
getColorEscape: () => getColorEscape,
|
|
10324
11890
|
getCapabilities: () => getCapabilities,
|
|
10325
11891
|
getAvailablePalettes: () => getAvailablePalettes,
|
|
11892
|
+
geom_waffle: () => geom_waffle,
|
|
10326
11893
|
geom_vline: () => geom_vline,
|
|
10327
11894
|
geom_violin: () => geom_violin,
|
|
11895
|
+
geom_treemap: () => geom_treemap,
|
|
10328
11896
|
geom_tile: () => geom_tile,
|
|
10329
11897
|
geom_text: () => geom_text,
|
|
10330
11898
|
geom_step: () => geom_step,
|
|
11899
|
+
geom_sparkline: () => geom_sparkline,
|
|
10331
11900
|
geom_smooth: () => geom_smooth,
|
|
10332
11901
|
geom_segment: () => geom_segment,
|
|
11902
|
+
geom_sankey: () => geom_sankey,
|
|
10333
11903
|
geom_rug: () => geom_rug,
|
|
10334
11904
|
geom_ridgeline: () => geom_ridgeline,
|
|
10335
11905
|
geom_ribbon: () => geom_ribbon,
|
|
@@ -10341,21 +11911,30 @@ __export(exports_src, {
|
|
|
10341
11911
|
geom_pointrange: () => geom_pointrange,
|
|
10342
11912
|
geom_point: () => geom_point,
|
|
10343
11913
|
geom_path: () => geom_path,
|
|
11914
|
+
geom_lollipop: () => geom_lollipop,
|
|
10344
11915
|
geom_linerange: () => geom_linerange,
|
|
10345
11916
|
geom_line: () => geom_line,
|
|
10346
11917
|
geom_label: () => geom_label,
|
|
10347
11918
|
geom_joy: () => geom_joy,
|
|
11919
|
+
geom_icicle: () => geom_icicle,
|
|
10348
11920
|
geom_hline: () => geom_hline,
|
|
10349
11921
|
geom_histogram: () => geom_histogram,
|
|
10350
11922
|
geom_freqpoly: () => geom_freqpoly,
|
|
11923
|
+
geom_flame: () => geom_flame,
|
|
10351
11924
|
geom_errorbarh: () => geom_errorbarh,
|
|
10352
11925
|
geom_errorbar: () => geom_errorbar,
|
|
11926
|
+
geom_dumbbell: () => geom_dumbbell,
|
|
10353
11927
|
geom_density_2d: () => geom_density_2d,
|
|
11928
|
+
geom_density: () => geom_density,
|
|
10354
11929
|
geom_curve: () => geom_curve,
|
|
10355
11930
|
geom_crossbar: () => geom_crossbar,
|
|
11931
|
+
geom_corrmat: () => geom_corrmat,
|
|
10356
11932
|
geom_contour_filled: () => geom_contour_filled,
|
|
10357
11933
|
geom_contour: () => geom_contour,
|
|
10358
11934
|
geom_col: () => geom_col,
|
|
11935
|
+
geom_calendar: () => geom_calendar,
|
|
11936
|
+
geom_bullet: () => geom_bullet,
|
|
11937
|
+
geom_braille: () => geom_braille,
|
|
10359
11938
|
geom_boxplot: () => geom_boxplot,
|
|
10360
11939
|
geom_bin2d: () => geom_bin2d,
|
|
10361
11940
|
geom_beeswarm: () => geom_beeswarm,
|
|
@@ -10427,6 +12006,8 @@ __export(exports_src, {
|
|
|
10427
12006
|
annotate: () => annotate,
|
|
10428
12007
|
TerminalCanvas: () => TerminalCanvas,
|
|
10429
12008
|
StreamingPlot: () => StreamingPlot,
|
|
12009
|
+
SPARK_DOTS: () => SPARK_DOTS,
|
|
12010
|
+
SPARK_BARS: () => SPARK_BARS,
|
|
10430
12011
|
SHAPE_CHARS: () => SHAPE_CHARS,
|
|
10431
12012
|
RollingAggregator: () => RollingAggregator,
|
|
10432
12013
|
RendererChain: () => RendererChain,
|
|
@@ -10446,6 +12027,8 @@ __export(exports_src, {
|
|
|
10446
12027
|
DEFAULT_BG: () => DEFAULT_BG,
|
|
10447
12028
|
CanvasDiff: () => CanvasDiff,
|
|
10448
12029
|
Binner: () => Binner,
|
|
12030
|
+
BRAILLE_DOTS: () => BRAILLE_DOTS,
|
|
12031
|
+
BRAILLE_BASE: () => BRAILLE_BASE,
|
|
10449
12032
|
ANSI_16_COLORS: () => ANSI_16_COLORS
|
|
10450
12033
|
});
|
|
10451
12034
|
var init_src = __esm(() => {
|
|
@@ -11253,12 +12836,19 @@ var GEOM_TYPES = [
|
|
|
11253
12836
|
"col",
|
|
11254
12837
|
"histogram",
|
|
11255
12838
|
"freqpoly",
|
|
12839
|
+
"density",
|
|
11256
12840
|
"boxplot",
|
|
11257
12841
|
"violin",
|
|
11258
12842
|
"ridgeline",
|
|
11259
12843
|
"joy",
|
|
11260
12844
|
"beeswarm",
|
|
11261
12845
|
"quasirandom",
|
|
12846
|
+
"dumbbell",
|
|
12847
|
+
"lollipop",
|
|
12848
|
+
"waffle",
|
|
12849
|
+
"sparkline",
|
|
12850
|
+
"bullet",
|
|
12851
|
+
"braille",
|
|
11262
12852
|
"area",
|
|
11263
12853
|
"ribbon",
|
|
11264
12854
|
"rug",
|
|
@@ -11278,7 +12868,13 @@ var GEOM_TYPES = [
|
|
|
11278
12868
|
"contour",
|
|
11279
12869
|
"contour_filled",
|
|
11280
12870
|
"density_2d",
|
|
11281
|
-
"qq"
|
|
12871
|
+
"qq",
|
|
12872
|
+
"calendar",
|
|
12873
|
+
"flame",
|
|
12874
|
+
"icicle",
|
|
12875
|
+
"corrmat",
|
|
12876
|
+
"sankey",
|
|
12877
|
+
"treemap"
|
|
11282
12878
|
];
|
|
11283
12879
|
var datePattern = /^\d{4}-\d{2}-\d{2}/;
|
|
11284
12880
|
function fileExists(path) {
|
|
@@ -11653,7 +13249,7 @@ Error: Unknown geometry type "${geomType}"`);
|
|
|
11653
13249
|
Available geom types:`);
|
|
11654
13250
|
console.error(` Points/Lines: point, line, path, step, smooth, segment`);
|
|
11655
13251
|
console.error(` Bars/Areas: bar, col, histogram, freqpoly, area, ribbon`);
|
|
11656
|
-
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`);
|
|
11657
13253
|
console.error(` Uncertainty: errorbar, errorbarh, crossbar, linerange, pointrange`);
|
|
11658
13254
|
console.error(` 2D: tile, rect, raster, bin2d, contour, contour_filled`);
|
|
11659
13255
|
console.error(` Text: text, label`);
|
|
@@ -11762,7 +13358,7 @@ Error: Geometry type "${geomType}" requires a y column`);
|
|
|
11762
13358
|
console.error(`
|
|
11763
13359
|
Usage: cli-plot.ts ${dataFile} <x> <y> [color] [title] ${geomType}`);
|
|
11764
13360
|
console.error(`
|
|
11765
|
-
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`);
|
|
11766
13362
|
process.exit(1);
|
|
11767
13363
|
}
|
|
11768
13364
|
const aes = { x };
|
|
@@ -11792,6 +13388,9 @@ If you want a univariate plot, try: histogram, bar, qq, or freqpoly`);
|
|
|
11792
13388
|
case "freqpoly":
|
|
11793
13389
|
plot = plot.geom(geom_freqpoly({ bins: 20 }));
|
|
11794
13390
|
break;
|
|
13391
|
+
case "density":
|
|
13392
|
+
plot = plot.geom(geom_density());
|
|
13393
|
+
break;
|
|
11795
13394
|
case "boxplot":
|
|
11796
13395
|
plot = plot.geom(geom_boxplot());
|
|
11797
13396
|
break;
|
|
@@ -11806,6 +13405,24 @@ If you want a univariate plot, try: histogram, bar, qq, or freqpoly`);
|
|
|
11806
13405
|
case "quasirandom":
|
|
11807
13406
|
plot = plot.geom(geom_beeswarm());
|
|
11808
13407
|
break;
|
|
13408
|
+
case "dumbbell":
|
|
13409
|
+
plot = plot.geom(geom_dumbbell());
|
|
13410
|
+
break;
|
|
13411
|
+
case "lollipop":
|
|
13412
|
+
plot = plot.geom(geom_lollipop());
|
|
13413
|
+
break;
|
|
13414
|
+
case "waffle":
|
|
13415
|
+
plot = plot.geom(geom_waffle());
|
|
13416
|
+
break;
|
|
13417
|
+
case "sparkline":
|
|
13418
|
+
plot = plot.geom(geom_sparkline());
|
|
13419
|
+
break;
|
|
13420
|
+
case "bullet":
|
|
13421
|
+
plot = plot.geom(geom_bullet());
|
|
13422
|
+
break;
|
|
13423
|
+
case "braille":
|
|
13424
|
+
plot = plot.geom(geom_braille());
|
|
13425
|
+
break;
|
|
11809
13426
|
case "bar":
|
|
11810
13427
|
plot = plot.geom(geom_bar());
|
|
11811
13428
|
break;
|
|
@@ -11873,6 +13490,24 @@ If you want a univariate plot, try: histogram, bar, qq, or freqpoly`);
|
|
|
11873
13490
|
plot = plot.geom(geom_qq());
|
|
11874
13491
|
plot = plot.geom(geom_qq_line());
|
|
11875
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;
|
|
11876
13511
|
case "point":
|
|
11877
13512
|
default:
|
|
11878
13513
|
plot = plot.geom(geom_point());
|
|
@@ -11890,6 +13525,9 @@ If you want a univariate plot, try: histogram, bar, qq, or freqpoly`);
|
|
|
11890
13525
|
if ((geomType === "histogram" || geomType === "bar" || geomType === "freqpoly") && !yLabel) {
|
|
11891
13526
|
yLabel = "count";
|
|
11892
13527
|
}
|
|
13528
|
+
if (geomType === "density" && !yLabel) {
|
|
13529
|
+
yLabel = "density";
|
|
13530
|
+
}
|
|
11893
13531
|
if (geomType === "qq") {
|
|
11894
13532
|
yLabel = "Sample Quantiles";
|
|
11895
13533
|
}
|