td-plots 1.7.2 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/BoxPlot.d.ts +32 -0
- package/dist/components/LegendUtils.d.ts +10 -0
- package/dist/components/LineWithHistogram.d.ts +32 -0
- package/dist/components/PairedComparisonsBoxPlot.d.ts +22 -0
- package/dist/components/StatsDonut.d.ts +2 -1
- package/dist/components/SummaryComparisonPlot.d.ts +30 -0
- package/dist/components/Utils.d.ts +3 -0
- package/dist/index.d.ts +12 -4
- package/dist/index.esm.js +877 -22
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +881 -22
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/components/TestPlot.d.ts +0 -6
package/dist/index.esm.js
CHANGED
|
@@ -6,6 +6,8 @@ import Button from '@mui/material/Button';
|
|
|
6
6
|
import Tooltip from '@mui/material/Tooltip';
|
|
7
7
|
import { createSvgIcon } from '@mui/material/utils';
|
|
8
8
|
import IconButton from '@mui/material/IconButton';
|
|
9
|
+
import { Box as Box$1 } from '@mui/material';
|
|
10
|
+
import Box from '@mui/material/Box';
|
|
9
11
|
|
|
10
12
|
function styleInject(css, ref) {
|
|
11
13
|
if ( ref === void 0 ) ref = {};
|
|
@@ -34,7 +36,7 @@ function styleInject(css, ref) {
|
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
var css_248z = ".plot-container{height:100%;max-width:100%;min-height:300px;overflow:hidden!important;position:relative;width:100%}.plot-container>div{flex:1;
|
|
39
|
+
var css_248z = ".plot-container{height:100%;max-width:100%;min-height:300px;overflow:hidden!important;position:relative;width:100%}.plot-container>div{flex:1;width:100%!important}.plot-container .main-svg{max-height:100%!important;max-width:100%!important}.plot-container .main-svg,.plot-container .plotly-graph-div,.plot-container svg.main-svg[height],.plot-container svg.main-svg[width]{height:100%!important;width:100%!important}.plot-container .point{border-radius:5px!important;overflow:hidden!important}.plot-container .cursor-ns-resize{height:0;width:0}.plot-container .cursor-ew-resize{fill:var(--selection-color,blue)!important;stroke:var(--selection-color,blue)!important}.plot-container .selectionlayer>path{stroke:var(--selection-color,blue)!important;stroke-dasharray:0!important;stroke-width:1px!important;opacity:.5!important}.plot-container .zoomlayer>path{stroke-dasharray:0!important;stroke:var(--selection-color,blue)!important;fill:var(--selection-color,blue)!important}.radial-histogram-container{aspect-ratio:1}.loading-overlay{align-items:center;background-color:hsla(0,0%,100%,.8);display:flex;height:100%;justify-content:center;left:0;position:absolute;top:0;width:100%;z-index:300}.histogram-controls{pointer-events:auto}.histogram-controls.show-always{opacity:1;visibility:visible}.histogram-controls.show-on-hover{opacity:0;transition:opacity .2s ease-in-out,visibility .2s ease-in-out;visibility:hidden}.plot-container.histogram:hover .histogram-controls.show-on-hover{opacity:1;visibility:visible}";
|
|
38
40
|
styleInject(css_248z);
|
|
39
41
|
|
|
40
42
|
function formatDecimal(x) {
|
|
@@ -422,6 +424,47 @@ const plotlyMToMilliseconds = (mString) => {
|
|
|
422
424
|
}
|
|
423
425
|
return 0;
|
|
424
426
|
};
|
|
427
|
+
// Helper function to convert color to rgba with specified opacity
|
|
428
|
+
const colorToRGBA = (color, opacity) => {
|
|
429
|
+
// For named colors, create a temporary element to get computed rgb
|
|
430
|
+
// This is a fallback that works in browser environments
|
|
431
|
+
const temp = document.createElement("div");
|
|
432
|
+
temp.style.color = color;
|
|
433
|
+
document.body.appendChild(temp);
|
|
434
|
+
const computed = window.getComputedStyle(temp).color;
|
|
435
|
+
document.body.removeChild(temp);
|
|
436
|
+
const rgbMatch = computed.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
437
|
+
if (rgbMatch) {
|
|
438
|
+
return `rgba(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]}, ${opacity})`;
|
|
439
|
+
}
|
|
440
|
+
// Fallback: return original color
|
|
441
|
+
return color;
|
|
442
|
+
};
|
|
443
|
+
const computeMedian = (data) => {
|
|
444
|
+
if (data.length === 0) {
|
|
445
|
+
return 0;
|
|
446
|
+
}
|
|
447
|
+
const values = data.sort((a, b) => a - b);
|
|
448
|
+
const mid = Math.floor(values.length / 2);
|
|
449
|
+
return values.length % 2 !== 0
|
|
450
|
+
? values[mid]
|
|
451
|
+
: (values[mid - 1] + values[mid]) / 2;
|
|
452
|
+
};
|
|
453
|
+
const computeQuartile = (data, quartile) => {
|
|
454
|
+
if (data.length === 0) {
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
const values = data.sort((a, b) => a - b);
|
|
458
|
+
const pos = (values.length - 1) * (quartile / 4);
|
|
459
|
+
const base = Math.floor(pos);
|
|
460
|
+
const rest = pos - base;
|
|
461
|
+
if (values[base + 1] !== undefined) {
|
|
462
|
+
return values[base] + rest * (values[base + 1] - values[base]);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
return values[base];
|
|
466
|
+
}
|
|
467
|
+
};
|
|
425
468
|
|
|
426
469
|
// Loading component renders a circular spinner above an element (usually a plot)
|
|
427
470
|
const Loading = () => {
|
|
@@ -496,7 +539,7 @@ var SettingsIcon = createSvgIcon(/*#__PURE__*/jsx("path", {
|
|
|
496
539
|
d: "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6"
|
|
497
540
|
}), 'Settings');
|
|
498
541
|
|
|
499
|
-
const Plot$
|
|
542
|
+
const Plot$5 = lazy(() => import('react-plotly.js'));
|
|
500
543
|
const HistogramPlot = (props) => {
|
|
501
544
|
var _a, _b, _c, _d;
|
|
502
545
|
const { data, title, xAxisTitle, barColor = "rgb(72, 72, 74)", unselectedBarColor = "rgba(203, 195, 195, 0.88)", selectorsColor = "black", containerStyleOverrides, unselectedData = [], handleClickOrSelection = () => { }, onDeselect = () => { }, plotId, selectByBin = false, dateTickFormat, binSizeOverride, statsAnnotations = ["mean"], emptySelectedRange = false, d3FormatValueString = ".1f", showBinSizeControls = "always", onBinSizeCalculated, onBinSizeChange, showBinSizeControlValue = true, isMobile = false, settingsTitleStylingOverrides = {}, } = props;
|
|
@@ -1131,7 +1174,9 @@ const HistogramPlot = (props) => {
|
|
|
1131
1174
|
.map((n) => n * ONEAVGMONTH)
|
|
1132
1175
|
.concat([defaultBinSize])
|
|
1133
1176
|
: unitOfTime === "day"
|
|
1134
|
-
? [1, 2, 3, 5, 7, 30]
|
|
1177
|
+
? [1, 2, 3, 5, 7, 30]
|
|
1178
|
+
.map((n) => n * ONEDAY)
|
|
1179
|
+
.concat([defaultBinSize])
|
|
1135
1180
|
: unitOfTime === "hr"
|
|
1136
1181
|
? [1, 2, 3, 6, 12, 24]
|
|
1137
1182
|
.map((n) => n * ONEHOUR)
|
|
@@ -1193,7 +1238,7 @@ const HistogramPlot = (props) => {
|
|
|
1193
1238
|
return valueLabel;
|
|
1194
1239
|
};
|
|
1195
1240
|
}
|
|
1196
|
-
return (jsx("div", { ref: containerRef, className: `plot-container ${plotId}`, style: Object.assign({ "--selection-color": selectorsColor }, containerStyles), children: jsx(Suspense, { fallback: jsx(Loading, {}), children: jsxs("div", { style: {
|
|
1241
|
+
return (jsx("div", { ref: containerRef, className: `plot-container histogram ${plotId}`, style: Object.assign({ "--selection-color": selectorsColor }, containerStyles), children: jsx(Suspense, { fallback: jsx(Loading, {}), children: jsxs("div", { style: {
|
|
1197
1242
|
position: "relative",
|
|
1198
1243
|
width: "100%",
|
|
1199
1244
|
height: "100%",
|
|
@@ -1246,7 +1291,7 @@ const HistogramPlot = (props) => {
|
|
|
1246
1291
|
const newBinSize = newValue;
|
|
1247
1292
|
setBinSize(newBinSize);
|
|
1248
1293
|
onBinSizeChange === null || onBinSizeChange === void 0 ? void 0 : onBinSizeChange(newBinSize);
|
|
1249
|
-
}, colorOverride: barColor, valueLabelFormat: valueLabelFormat, titleStylingOverrides: settingsTitleStylingOverrides, disabledTooltipText: "Requires 10 or more data points" }) }) })] })), jsx(Plot$
|
|
1294
|
+
}, colorOverride: barColor, valueLabelFormat: valueLabelFormat, titleStylingOverrides: settingsTitleStylingOverrides, disabledTooltipText: "Requires 10 or more data points" }) }) })] })), jsx(Plot$5, { data: plotlyData, layout: layout, config: config, onSelected: handleSelection, onClick: handleClick, onDeselect: () => {
|
|
1250
1295
|
onDeselect();
|
|
1251
1296
|
setSelectedRange(null); // Remove selected box
|
|
1252
1297
|
}, onUpdate: handlePlotUpdate, useResizeHandler: true, style: {
|
|
@@ -1257,7 +1302,7 @@ const HistogramPlot = (props) => {
|
|
|
1257
1302
|
} }, `histogram-${plotId || "default"}`)] }) }) }));
|
|
1258
1303
|
};
|
|
1259
1304
|
|
|
1260
|
-
const Plot$
|
|
1305
|
+
const Plot$4 = lazy(() => import('react-plotly.js'));
|
|
1261
1306
|
const RadialHistogramPlot = (props) => {
|
|
1262
1307
|
const { data, barColor = 'rgb(72, 72, 74)', unselectedBarColor = 'rgba(203, 195, 195, 0.88)', selectorsColor = 'black', onSelected, onClick, containerStyleOverrides, barWidth = 20, // Default bar width in degrees
|
|
1263
1308
|
} = props;
|
|
@@ -1357,14 +1402,14 @@ const RadialHistogramPlot = (props) => {
|
|
|
1357
1402
|
staticPlot: false,
|
|
1358
1403
|
};
|
|
1359
1404
|
const containerStyles = Object.assign({ width: "100%", height: "100%", position: "relative" }, containerStyleOverrides);
|
|
1360
|
-
return (jsx("div", { ref: containerRef, className: "plot-container radial-histogram-container", style: Object.assign({ '--selection-color': selectorsColor }, containerStyles), children: jsx(Suspense, { fallback: jsx(Loading, {}), children: jsx(Plot$
|
|
1405
|
+
return (jsx("div", { ref: containerRef, className: "plot-container radial-histogram-container", style: Object.assign({ '--selection-color': selectorsColor }, containerStyles), children: jsx(Suspense, { fallback: jsx(Loading, {}), children: jsx(Plot$4, { data: plotlyData, layout: layout, config: config, onSelected: onSelected, onClick: onClick, useResizeHandler: true, style: {
|
|
1361
1406
|
width: "100%",
|
|
1362
1407
|
height: "100%",
|
|
1363
1408
|
display: "block"
|
|
1364
1409
|
} }) }) }));
|
|
1365
1410
|
};
|
|
1366
1411
|
|
|
1367
|
-
const Plot$
|
|
1412
|
+
const Plot$3 = lazy(() => import('react-plotly.js'));
|
|
1368
1413
|
const StatsDonut = (props) => {
|
|
1369
1414
|
const { withCenterLabel = false, centerLabel, centerValue, showLegend = true, legendPosition = "right", containerStyle = {}, } = props;
|
|
1370
1415
|
// Configure legend position based on prop
|
|
@@ -1412,7 +1457,7 @@ const StatsDonut = (props) => {
|
|
|
1412
1457
|
},
|
|
1413
1458
|
]
|
|
1414
1459
|
: [];
|
|
1415
|
-
return (jsx("div", { style: Object.assign({ width: "100%", height: "100%", minHeight: "300px", display: "flex", justifyContent: "center", alignItems: "center" }, containerStyle), children: jsx(Suspense, { fallback: jsx(Loading, {}), children: jsx(Plot$
|
|
1460
|
+
return (jsx("div", { style: Object.assign({ width: "100%", height: "100%", minHeight: "300px", display: "flex", justifyContent: "center", alignItems: "center" }, containerStyle), children: jsx(Suspense, { fallback: jsx(Loading, {}), children: jsx(Plot$3, { data: [
|
|
1416
1461
|
{
|
|
1417
1462
|
type: "pie",
|
|
1418
1463
|
values: props.values,
|
|
@@ -1445,24 +1490,834 @@ const StatsDonut = (props) => {
|
|
|
1445
1490
|
}, useResizeHandler: true }) }) }));
|
|
1446
1491
|
};
|
|
1447
1492
|
|
|
1493
|
+
const Plot$2 = lazy(() => import('react-plotly.js'));
|
|
1494
|
+
const BoxPlot = (props) => {
|
|
1495
|
+
const { data, width = 600, height = 400, title = "Box Plot", xAxisTitle, yAxisTitle, containerStyleOverrides, plotId = "boxplot", extraLayoutConfig = {}, } = props;
|
|
1496
|
+
// Ref for plot container
|
|
1497
|
+
const containerRef = useRef(null);
|
|
1498
|
+
// State for custom tooltip
|
|
1499
|
+
const [tooltip, setTooltip] = useState({ visible: false, x: 0, y: 0, content: "", color: "#1f77b4" });
|
|
1500
|
+
const plotlyData = data.map((trace) => {
|
|
1501
|
+
const boxDefinition = isBoxPlotDataSummary(trace.values)
|
|
1502
|
+
? {
|
|
1503
|
+
y0: trace.y !== undefined ? trace.y : [trace.label], // Position the box at this y-value
|
|
1504
|
+
lowerfence: [trace.values.lowerWhisker],
|
|
1505
|
+
q1: [trace.values.q1],
|
|
1506
|
+
median: [trace.values.median],
|
|
1507
|
+
q3: [trace.values.q3],
|
|
1508
|
+
upperfence: [trace.values.upperWhisker],
|
|
1509
|
+
mean: trace.values.mean ? [trace.values.mean] : undefined,
|
|
1510
|
+
}
|
|
1511
|
+
: {
|
|
1512
|
+
x: trace.values,
|
|
1513
|
+
y0: trace.y !== undefined ? trace.y : undefined,
|
|
1514
|
+
}; // Values map to x because the boxplot is horizontal
|
|
1515
|
+
return Object.assign({ type: "box", orientation: "h", name: trace.label, fillcolor: trace.fill === "none" ? "rgba(255, 255, 255, 0)" : undefined, line: {
|
|
1516
|
+
color: trace.color || "#1f77b4", // Default to Plotly's default blue if no color provided
|
|
1517
|
+
}, boxpoints: false, hoverinfo: "none" }, boxDefinition);
|
|
1518
|
+
});
|
|
1519
|
+
const layout = Object.assign(Object.assign({}, extraLayoutConfig), { title: {
|
|
1520
|
+
text: title,
|
|
1521
|
+
}, showlegend: false, autosize: true, width: undefined, height: undefined, margin: Object.assign({ l: 50, r: 35, t: 50, b: 50, pad: 4 }, extraLayoutConfig.margin), xaxis: {
|
|
1522
|
+
title: {
|
|
1523
|
+
text: xAxisTitle,
|
|
1524
|
+
},
|
|
1525
|
+
// range: displayXAxis, // Fixed range prevents axis shifting during interaction or data updates
|
|
1526
|
+
showgrid: true,
|
|
1527
|
+
zeroline: false,
|
|
1528
|
+
showline: true,
|
|
1529
|
+
mirror: "ticks",
|
|
1530
|
+
gridcolor: "#efefef",
|
|
1531
|
+
gridwidth: 0.2,
|
|
1532
|
+
zerolinecolor: "#969696",
|
|
1533
|
+
zerolinewidth: 1,
|
|
1534
|
+
linecolor: "#bababa",
|
|
1535
|
+
linewidth: 1,
|
|
1536
|
+
fixedrange: true, // Disable zooming
|
|
1537
|
+
ticklabelposition: "outside",
|
|
1538
|
+
// tickformat: isDateArray(data) ? dateTickFormat : d3FormatValueString, // Format ticks for dates
|
|
1539
|
+
// automargin: true, // Adjust margin if tick labels rotate
|
|
1540
|
+
// hoverformat: isNumberArray(allData) ? d3FormatValueString : undefined,
|
|
1541
|
+
}, yaxis: Object.assign({ title: {
|
|
1542
|
+
text: yAxisTitle || "", // Set default to empty string to prevent Plotly from adding its own default title
|
|
1543
|
+
standoff: 12, // Add space between title and axis
|
|
1544
|
+
}, automargin: true, showgrid: true, zeroline: false, showline: true, mirror: "ticks", gridcolor: "#efefef", gridwidth: 0.2, zerolinecolor: "#969696", zerolinewidth: 1, linecolor: "#bababa", linewidth: 1, fixedrange: true, tickcolor: "white", ticklen: 10, ticksuffix: " " }, extraLayoutConfig.yaxis) });
|
|
1545
|
+
const config = {
|
|
1546
|
+
responsive: true, // Make the plot responsive
|
|
1547
|
+
displayModeBar: false, // Hide the mode bar
|
|
1548
|
+
displaylogo: false, // Hide the Plotly logo
|
|
1549
|
+
scrollZoom: false, // Disable zooming with scroll
|
|
1550
|
+
staticPlot: false, // Enable interactivity
|
|
1551
|
+
};
|
|
1552
|
+
const containerStyles = Object.assign({ width: "100%", height: "100%", position: "relative" }, containerStyleOverrides);
|
|
1553
|
+
return (jsx("div", { ref: containerRef,
|
|
1554
|
+
// className={`plot-container ${plotId}`}
|
|
1555
|
+
style: Object.assign({}, containerStyles), children: jsx(Suspense, { fallback: jsx(Loading, {}), children: jsxs("div", { style: {
|
|
1556
|
+
position: "relative",
|
|
1557
|
+
width: "100%",
|
|
1558
|
+
height: "100%",
|
|
1559
|
+
}, children: [jsx(Plot$2, { data: plotlyData, layout: layout, config: config, useResizeHandler: true,
|
|
1560
|
+
// onHover={handleHover}
|
|
1561
|
+
// onUnhover={handleUnhover}
|
|
1562
|
+
style: {
|
|
1563
|
+
width: "100%",
|
|
1564
|
+
height: "100%",
|
|
1565
|
+
display: "block",
|
|
1566
|
+
transition: "opacity 0.15s ease-in-out",
|
|
1567
|
+
} }, `boxplot-${plotId || "default"}`), tooltip.visible && (jsx("div", { style: {
|
|
1568
|
+
position: "fixed",
|
|
1569
|
+
left: tooltip.x + 10,
|
|
1570
|
+
top: tooltip.y - 10,
|
|
1571
|
+
backgroundColor: "white",
|
|
1572
|
+
color: "#333",
|
|
1573
|
+
padding: "8px 12px",
|
|
1574
|
+
borderRadius: "4px",
|
|
1575
|
+
fontSize: "13px",
|
|
1576
|
+
lineHeight: "1.5",
|
|
1577
|
+
pointerEvents: "none",
|
|
1578
|
+
zIndex: 1000,
|
|
1579
|
+
whiteSpace: "nowrap",
|
|
1580
|
+
boxShadow: "0 2px 4px rgba(0,0,0,0.2)",
|
|
1581
|
+
border: `2px solid ${tooltip.color}`,
|
|
1582
|
+
fontFamily: '"Open Sans", verdana, arial, sans-serif',
|
|
1583
|
+
}, dangerouslySetInnerHTML: { __html: tooltip.content } }))] }) }) }));
|
|
1584
|
+
};
|
|
1585
|
+
function isBoxPlotDataSummary(value) {
|
|
1586
|
+
return (typeof value === "object" &&
|
|
1587
|
+
value !== null &&
|
|
1588
|
+
typeof value.lowerWhisker === "number" &&
|
|
1589
|
+
typeof value.q1 === "number" &&
|
|
1590
|
+
typeof value.median === "number" &&
|
|
1591
|
+
typeof value.q3 === "number" &&
|
|
1592
|
+
typeof value.upperWhisker === "number" &&
|
|
1593
|
+
(value.mean === undefined || typeof value.mean === "number"));
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
const SVGBoxWithLine = (props) => {
|
|
1597
|
+
const boxWidth = 35;
|
|
1598
|
+
const boxHeight = 15;
|
|
1599
|
+
const lineWidth = boxWidth * 2; // 90% width
|
|
1600
|
+
const lineY = boxHeight / 2; // Vertical center
|
|
1601
|
+
return (jsxs("svg", { width: lineWidth, height: boxHeight, viewBox: `0 0 ${lineWidth + 3} ${boxHeight + 3}`, children: [jsx("line", { x1: 0, y1: lineY, x2: (lineWidth - boxWidth) / 2, y2: lineY, stroke: props.color, strokeWidth: "2" }), jsx("line", { x1: (lineWidth + boxWidth) / 2, y1: lineY, x2: lineWidth, y2: lineY, stroke: props.color, strokeWidth: "2" }), jsx("line", { x1: 0, y1: lineY - 5, x2: 0, y2: lineY + 5, stroke: props.color, strokeWidth: "2" }), jsx("line", { x1: lineWidth, y1: lineY - 5, x2: lineWidth, y2: lineY + 5, stroke: props.color, strokeWidth: "2" }), jsx("rect", { x: (lineWidth - boxWidth) / 2, y: "0", width: boxWidth, height: boxHeight, fill: props.boxStyle !== "outlined" ? props.color : "none", opacity: props.boxStyle === "shaded" ? 0.3 : 1 }), jsx("rect", { x: (lineWidth - boxWidth) / 2, y: "0", width: boxWidth, height: boxHeight, stroke: props.color, strokeWidth: "2", fill: "none" }), jsx("line", { x1: (lineWidth - boxWidth) / 2 + boxWidth * 0.5, y1: 0, x2: (lineWidth - boxWidth) / 2 + boxWidth * 0.5, y2: boxHeight, stroke: props.color, strokeWidth: "2" })] }));
|
|
1602
|
+
};
|
|
1603
|
+
// export const OutlinedBox = (props: BoxProps) => {
|
|
1604
|
+
// return (
|
|
1605
|
+
// <Box
|
|
1606
|
+
// border={`2px solid ${props.color}`}
|
|
1607
|
+
// width={BOX_SIZE}
|
|
1608
|
+
// height={BOX_SIZE}
|
|
1609
|
+
// mr={1}
|
|
1610
|
+
// />
|
|
1611
|
+
// );
|
|
1612
|
+
// };
|
|
1613
|
+
// export const FilledBox = (props: BoxProps) => {
|
|
1614
|
+
// return (
|
|
1615
|
+
// <Box
|
|
1616
|
+
// width={BOX_SIZE}
|
|
1617
|
+
// height={BOX_SIZE}
|
|
1618
|
+
// sx={{
|
|
1619
|
+
// backgroundColor: props.color,
|
|
1620
|
+
// border: `2px solid ${props.color}`,
|
|
1621
|
+
// }}
|
|
1622
|
+
// mr={1}
|
|
1623
|
+
// />
|
|
1624
|
+
// );
|
|
1625
|
+
// };
|
|
1626
|
+
// export const ShadedBox = (props: BoxProps) => {
|
|
1627
|
+
// return (
|
|
1628
|
+
// <Box
|
|
1629
|
+
// width={BOX_SIZE}
|
|
1630
|
+
// height={BOX_SIZE}
|
|
1631
|
+
// sx={{
|
|
1632
|
+
// border: `2px solid ${props.color}`,
|
|
1633
|
+
// position: "relative",
|
|
1634
|
+
// "&::before": {
|
|
1635
|
+
// content: '""',
|
|
1636
|
+
// position: "absolute",
|
|
1637
|
+
// top: 0,
|
|
1638
|
+
// left: 0,
|
|
1639
|
+
// right: 0,
|
|
1640
|
+
// bottom: 0,
|
|
1641
|
+
// backgroundColor: `${props.color}`,
|
|
1642
|
+
// opacity: 0.5,
|
|
1643
|
+
// },
|
|
1644
|
+
// }}
|
|
1645
|
+
// mr={1}
|
|
1646
|
+
// />
|
|
1647
|
+
// );
|
|
1648
|
+
// };
|
|
1649
|
+
// export const LegendItem = (props: LegendItemProps) => {
|
|
1650
|
+
// return (
|
|
1651
|
+
// <Box
|
|
1652
|
+
// key={props.label}
|
|
1653
|
+
// display="flex"
|
|
1654
|
+
// alignItems="center"
|
|
1655
|
+
// mr={1}
|
|
1656
|
+
// sx={{
|
|
1657
|
+
// fontFamily: "Open Sans, verdana, arial, sans-serif",
|
|
1658
|
+
// }}
|
|
1659
|
+
// >
|
|
1660
|
+
// {props.boxStyle === "shaded" ? (
|
|
1661
|
+
// <ShadedBox color={props.color} />
|
|
1662
|
+
// ) : props.boxStyle === "outlined" ? (
|
|
1663
|
+
// <OutlinedBox color={props.color} />
|
|
1664
|
+
// ) : (
|
|
1665
|
+
// <FilledBox color={props.color} />
|
|
1666
|
+
// )}
|
|
1667
|
+
// {props.label}
|
|
1668
|
+
// </Box>
|
|
1669
|
+
// );
|
|
1670
|
+
// };
|
|
1671
|
+
const LegendBoxPlotItem = (props) => {
|
|
1672
|
+
return (jsxs(Box, { display: "flex", alignItems: "center", mr: 4, sx: {
|
|
1673
|
+
fontFamily: "Open Sans, verdana, arial, sans-serif",
|
|
1674
|
+
}, children: [jsx(SVGBoxWithLine, { color: props.color, boxStyle: props.boxStyle }), props.label] }, props.label));
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
// This component takes grouped data and transforms it for displaying with the BoxPlot component.
|
|
1678
|
+
// The highest level is the group (ex, rating bracket), and then within each group are individual boxes (ex, mine vs others' throws within that rating bracket)
|
|
1679
|
+
// Importantly, this component is currently optimized for two boxes per group. It could be extended in the future to allow for more within-group boxes if useful.
|
|
1680
|
+
const PairedComparisonsBoxPlot = (props) => {
|
|
1681
|
+
const { groups, pairLabels, width = 600, height = 400, title = "", xAxisTitle, yAxisTitle, containerStyleOverrides, showLegend = true, plotId = "paired-comparisons-boxplot", } = props;
|
|
1682
|
+
// Transform the grouped data into an array for BoxPlot
|
|
1683
|
+
const boxPlotData = groups.flatMap((group, groupIndex) => {
|
|
1684
|
+
const groupYPosition = groupIndex; // Position the group on the y-axis based on its index
|
|
1685
|
+
group.color || "orange";
|
|
1686
|
+
return group.boxes.map((box, boxIndex) => (Object.assign(Object.assign({}, box), {
|
|
1687
|
+
// color: box.color || groupColor, // Use box color if provided, otherwise use group color
|
|
1688
|
+
color: "#75757f", fill: boxIndex % 2 === 0 ? "none" : "auto", y: groupYPosition + 0.15 + (boxIndex - 1) * 0.3 })));
|
|
1689
|
+
});
|
|
1690
|
+
// We have to construct nice ticks. We can position them with the group indices.
|
|
1691
|
+
const tickvals = groups.map((_, index) => index);
|
|
1692
|
+
const ticktext = groups.map((group) => group.groupLabel);
|
|
1693
|
+
// Draw some gray rectangles to live behind the boxes to help separate the groups
|
|
1694
|
+
const separatorShapes = groups.map((group, groupIndex) => {
|
|
1695
|
+
return {
|
|
1696
|
+
type: "rect",
|
|
1697
|
+
x0: 0,
|
|
1698
|
+
x1: 1,
|
|
1699
|
+
xref: "paper",
|
|
1700
|
+
y0: groupIndex - 0.5,
|
|
1701
|
+
y1: groupIndex + 0.5,
|
|
1702
|
+
yref: "y",
|
|
1703
|
+
fillcolor: groupIndex % 2 === 0 ? "#ffffff" : "#f8f8f8",
|
|
1704
|
+
opacity: 0.05,
|
|
1705
|
+
layer: "below",
|
|
1706
|
+
line: {
|
|
1707
|
+
width: 0,
|
|
1708
|
+
},
|
|
1709
|
+
};
|
|
1710
|
+
});
|
|
1711
|
+
const groupAnnotations = groups.map((group, groupIndex) => ({
|
|
1712
|
+
type: "line",
|
|
1713
|
+
yref: "y",
|
|
1714
|
+
xref: "paper",
|
|
1715
|
+
x0: -0.01,
|
|
1716
|
+
x1: -0.01,
|
|
1717
|
+
y0: groupIndex - 0.4, // Align with the center of the group
|
|
1718
|
+
y1: groupIndex + 0.4,
|
|
1719
|
+
line: {
|
|
1720
|
+
color: group.color || "orange",
|
|
1721
|
+
width: 7,
|
|
1722
|
+
},
|
|
1723
|
+
}));
|
|
1724
|
+
const differenceAnnotations = [];
|
|
1725
|
+
const differenceBetweenMediansLines = [];
|
|
1726
|
+
groups.forEach((group, groupIndex) => {
|
|
1727
|
+
// console.log(group.boxes);
|
|
1728
|
+
if ((!isBoxPlotDataSummary(group.boxes[0].values) &&
|
|
1729
|
+
group.boxes[0].values.length == 0) ||
|
|
1730
|
+
(!isBoxPlotDataSummary(group.boxes[1].values) &&
|
|
1731
|
+
group.boxes[1].values.length == 0)) {
|
|
1732
|
+
return; // Don't show an annotation if we don't have data for both boxes
|
|
1733
|
+
}
|
|
1734
|
+
const medianA = isBoxPlotDataSummary(group.boxes[0].values)
|
|
1735
|
+
? group.boxes[0].values.median
|
|
1736
|
+
: computeMedian(group.boxes[0].values);
|
|
1737
|
+
const medianB = isBoxPlotDataSummary(group.boxes[1].values)
|
|
1738
|
+
? group.boxes[1].values.median
|
|
1739
|
+
: computeMedian(group.boxes[1].values);
|
|
1740
|
+
const q1A = isBoxPlotDataSummary(group.boxes[0].values)
|
|
1741
|
+
? group.boxes[0].values.q1
|
|
1742
|
+
: computeQuartile(group.boxes[0].values, 1);
|
|
1743
|
+
const q3A = isBoxPlotDataSummary(group.boxes[0].values)
|
|
1744
|
+
? group.boxes[0].values.q3
|
|
1745
|
+
: computeQuartile(group.boxes[0].values, 3);
|
|
1746
|
+
const q1B = isBoxPlotDataSummary(group.boxes[1].values)
|
|
1747
|
+
? group.boxes[1].values.q1
|
|
1748
|
+
: computeQuartile(group.boxes[1].values, 1);
|
|
1749
|
+
const q3B = isBoxPlotDataSummary(group.boxes[1].values)
|
|
1750
|
+
? group.boxes[1].values.q3
|
|
1751
|
+
: computeQuartile(group.boxes[1].values, 3);
|
|
1752
|
+
const differenceBetweenMedians = medianB - medianA;
|
|
1753
|
+
// If we have the quartiles for the first box, we can determine if the second box's median is inside or outside the box
|
|
1754
|
+
const annotationColor = isBoxPlotDataSummary(group.boxes[0].values)
|
|
1755
|
+
? q3B < q1A || q1B > q3A
|
|
1756
|
+
? "red"
|
|
1757
|
+
: medianB < q1A || medianB > q3A
|
|
1758
|
+
? "orange"
|
|
1759
|
+
: "gray"
|
|
1760
|
+
: "gray";
|
|
1761
|
+
if (annotationColor === "gray") {
|
|
1762
|
+
return; // Don't show an annotation if the medians are close enough that they overlap in the boxes
|
|
1763
|
+
}
|
|
1764
|
+
differenceAnnotations.push({
|
|
1765
|
+
yref: "y",
|
|
1766
|
+
xref: "x",
|
|
1767
|
+
x: medianB > medianA ? medianB : medianA, // Position the annotation at the larger median
|
|
1768
|
+
y: groupIndex, // Align with the center of the group
|
|
1769
|
+
text: ` ${differenceBetweenMedians.toFixed(1)}`,
|
|
1770
|
+
showarrow: false,
|
|
1771
|
+
xanchor: "left",
|
|
1772
|
+
align: "left",
|
|
1773
|
+
font: {
|
|
1774
|
+
size: 12,
|
|
1775
|
+
color: annotationColor,
|
|
1776
|
+
style: "italic",
|
|
1777
|
+
},
|
|
1778
|
+
});
|
|
1779
|
+
differenceBetweenMediansLines.push({
|
|
1780
|
+
type: "line",
|
|
1781
|
+
x0: medianA,
|
|
1782
|
+
x1: medianB,
|
|
1783
|
+
xref: "x",
|
|
1784
|
+
y0: groupIndex,
|
|
1785
|
+
y1: groupIndex,
|
|
1786
|
+
yref: "y",
|
|
1787
|
+
line: {
|
|
1788
|
+
color: annotationColor,
|
|
1789
|
+
width: 2,
|
|
1790
|
+
},
|
|
1791
|
+
});
|
|
1792
|
+
differenceBetweenMediansLines.push({
|
|
1793
|
+
type: "line",
|
|
1794
|
+
x0: medianA,
|
|
1795
|
+
x1: medianA,
|
|
1796
|
+
xref: "x",
|
|
1797
|
+
y0: groupIndex - 0.05,
|
|
1798
|
+
y1: groupIndex,
|
|
1799
|
+
yref: "y",
|
|
1800
|
+
line: {
|
|
1801
|
+
color: annotationColor,
|
|
1802
|
+
width: 2,
|
|
1803
|
+
},
|
|
1804
|
+
});
|
|
1805
|
+
differenceBetweenMediansLines.push({
|
|
1806
|
+
type: "line",
|
|
1807
|
+
x0: medianB,
|
|
1808
|
+
x1: medianB,
|
|
1809
|
+
xref: "x",
|
|
1810
|
+
y0: groupIndex,
|
|
1811
|
+
y1: groupIndex + 0.05,
|
|
1812
|
+
yref: "y",
|
|
1813
|
+
line: {
|
|
1814
|
+
color: annotationColor,
|
|
1815
|
+
width: 2,
|
|
1816
|
+
},
|
|
1817
|
+
});
|
|
1818
|
+
});
|
|
1819
|
+
const extraLayoutConfig = {
|
|
1820
|
+
yaxis: {
|
|
1821
|
+
type: "linear", // Use linear axis for numeric positioning of boxes
|
|
1822
|
+
tickmode: "array",
|
|
1823
|
+
tickvals,
|
|
1824
|
+
ticktext,
|
|
1825
|
+
range: [-0.5, groups.length - 0.5], // Add some padding to the y-axis range to accommodate the boxes
|
|
1826
|
+
tickcolor: "#ffffff",
|
|
1827
|
+
showgrid: false,
|
|
1828
|
+
},
|
|
1829
|
+
margin: {
|
|
1830
|
+
t: 5,
|
|
1831
|
+
r: 50,
|
|
1832
|
+
},
|
|
1833
|
+
shapes: [
|
|
1834
|
+
...separatorShapes,
|
|
1835
|
+
...differenceBetweenMediansLines,
|
|
1836
|
+
...groupAnnotations,
|
|
1837
|
+
],
|
|
1838
|
+
annotations: differenceAnnotations,
|
|
1839
|
+
};
|
|
1840
|
+
const legendNode = (jsxs(Box$1, { display: "flex", justifyContent: "flex-end", mb: 2, children: [jsx(LegendBoxPlotItem, { label: pairLabels[0], color: "#626280", boxStyle: "shaded" }), jsx(LegendBoxPlotItem, { label: pairLabels[1], color: "#626280", boxStyle: "outlined" })] }));
|
|
1841
|
+
const containerStyles = Object.assign({ width: width, height: height, position: "relative", display: "flex", flexDirection: "column", gap: 0 }, containerStyleOverrides);
|
|
1842
|
+
return (jsxs("div", { style: Object.assign({}, containerStyles), children: [showLegend && legendNode, jsx(BoxPlot, { data: boxPlotData, width: width, height: height, title: title, xAxisTitle: xAxisTitle, yAxisTitle: yAxisTitle, extraLayoutConfig: extraLayoutConfig, containerStyleOverrides: containerStyleOverrides, plotId: `${plotId}-boxplot` })] }));
|
|
1843
|
+
};
|
|
1844
|
+
|
|
1845
|
+
const Plot$1 = lazy(() => import('react-plotly.js'));
|
|
1846
|
+
const SummaryComparisonPlot = (props) => {
|
|
1847
|
+
const { groups, height = 250, title = "", xAxisTitle, yAxisTitle, containerStyleOverrides, plotId = "summary-comparison-plot", tooltipPosition = "right", startXAxisAtZero = true, } = props;
|
|
1848
|
+
// Ref for plot container
|
|
1849
|
+
const containerRef = useRef(null);
|
|
1850
|
+
// State for custom tooltip
|
|
1851
|
+
const [tooltip, setTooltip] = useState({
|
|
1852
|
+
visible: false,
|
|
1853
|
+
x: 0,
|
|
1854
|
+
y: 0,
|
|
1855
|
+
content: "",
|
|
1856
|
+
color: "#1f77b4",
|
|
1857
|
+
});
|
|
1858
|
+
const handleHover = (event) => {
|
|
1859
|
+
var _a, _b;
|
|
1860
|
+
if (!event.points || event.points.length === 0)
|
|
1861
|
+
return;
|
|
1862
|
+
const point = event.points[0];
|
|
1863
|
+
const eventData = point.data;
|
|
1864
|
+
if (!eventData)
|
|
1865
|
+
return;
|
|
1866
|
+
// Use the name of the trace to identify the group, then find the corresponding data for that group to display in the tooltip
|
|
1867
|
+
// This way we don't have to worry if we hovered over the line or the marker, we can show the same tooltip content based on the group
|
|
1868
|
+
const groupName = eventData.name || "Unknown Group";
|
|
1869
|
+
const groupData = (_a = groups.find((g) => g.groupLabel === groupName)) === null || _a === void 0 ? void 0 : _a.data;
|
|
1870
|
+
if (!groupData)
|
|
1871
|
+
return;
|
|
1872
|
+
let content = `<div style="margin-bottom: 8px;">`;
|
|
1873
|
+
let contentColor = "orange";
|
|
1874
|
+
let tooltipX = event.event.clientX;
|
|
1875
|
+
let tooltipY = event.event.clientY;
|
|
1876
|
+
content += `<strong>${groupName}</strong><br/>`;
|
|
1877
|
+
const summarizedMinText = groupData.summarizedMin
|
|
1878
|
+
? groupData.summarizedMin.toFixed(2)
|
|
1879
|
+
: "NA";
|
|
1880
|
+
const summarizedMaxText = groupData.summarizedMax
|
|
1881
|
+
? groupData.summarizedMax.toFixed(2)
|
|
1882
|
+
: "NA";
|
|
1883
|
+
const comparedMedianText = groupData.comparedMedian !== undefined
|
|
1884
|
+
? groupData.comparedMedian.toFixed(2)
|
|
1885
|
+
: "NA";
|
|
1886
|
+
content += `
|
|
1887
|
+
<table style="width: 100%; margin-top: 4px;">
|
|
1888
|
+
<tr>
|
|
1889
|
+
<td style="text-align: left; padding: 2px 18px 2px 0;">Population avg-\u03C3:</td>
|
|
1890
|
+
<td style="text-align: right; padding: 2px 0;">${summarizedMinText}</td>
|
|
1891
|
+
</tr>
|
|
1892
|
+
<tr>
|
|
1893
|
+
<td style="text-align: left; padding: 2px 18px 2px 0;">Population avg+\u03C3:</td>
|
|
1894
|
+
<td style="text-align: right; padding: 2px 0;">${summarizedMaxText}</td>
|
|
1895
|
+
</tr>
|
|
1896
|
+
<tr>
|
|
1897
|
+
<td style="text-align: left; padding: 2px 18px 2px 0;">My Median:</td>
|
|
1898
|
+
<td style="text-align: right; padding: 2px 0;">${comparedMedianText}</td>
|
|
1899
|
+
</tr>
|
|
1900
|
+
</table>
|
|
1901
|
+
`;
|
|
1902
|
+
contentColor = eventData.line
|
|
1903
|
+
? eventData.line.color
|
|
1904
|
+
: eventData.marker.color || contentColor;
|
|
1905
|
+
// Position tooltip at the end of the line (75th percentile)
|
|
1906
|
+
// Use the xaxis d2p method to convert data coordinate to pixel coordinate
|
|
1907
|
+
if (point.xaxis && typeof point.xaxis.d2p === "function") {
|
|
1908
|
+
const pixelX = tooltipPosition === "right"
|
|
1909
|
+
? point.xaxis.d2p(groupData.summarizedMax)
|
|
1910
|
+
: point.xaxis.d2p(groupData.summarizedMin);
|
|
1911
|
+
const pixelY = point.yaxis.d2p(eventData.y[0]); // Use the y coordinate of the line for vertical positioning
|
|
1912
|
+
const containerRect = (_b = containerRef.current) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
|
|
1913
|
+
if (containerRect) {
|
|
1914
|
+
tooltipX = pixelX + point.xaxis._offset + containerRect.left; // Adjust for x-axis offset and container position
|
|
1915
|
+
tooltipY = pixelY + point.yaxis._offset + containerRect.top; // Adjust for y-axis offset and container position
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
setTooltip({
|
|
1919
|
+
visible: true,
|
|
1920
|
+
x: tooltipX,
|
|
1921
|
+
y: tooltipY,
|
|
1922
|
+
content,
|
|
1923
|
+
color: contentColor,
|
|
1924
|
+
});
|
|
1925
|
+
};
|
|
1926
|
+
const handleUnhover = () => {
|
|
1927
|
+
setTooltip((prev) => (Object.assign(Object.assign({}, prev), { visible: false })));
|
|
1928
|
+
};
|
|
1929
|
+
// Transform the data into a format suitable for a plotly scatterplot
|
|
1930
|
+
const plotlyData = groups.flatMap((group, groupIndex) => {
|
|
1931
|
+
const traces = [];
|
|
1932
|
+
if (group.data.summarizedMin !== undefined &&
|
|
1933
|
+
group.data.summarizedMax !== undefined) {
|
|
1934
|
+
traces.push({
|
|
1935
|
+
type: "scatter",
|
|
1936
|
+
mode: "lines",
|
|
1937
|
+
name: group.groupLabel,
|
|
1938
|
+
x: [group.data.summarizedMin, group.data.summarizedMax],
|
|
1939
|
+
y: [0 + groupIndex * 0.1, 0 + groupIndex * 0.1],
|
|
1940
|
+
line: {
|
|
1941
|
+
color: colorToRGBA(group.color || "orange", 1),
|
|
1942
|
+
width: 2,
|
|
1943
|
+
},
|
|
1944
|
+
showlegend: false,
|
|
1945
|
+
hoverinfo: "none",
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
if (group.data.comparedMedian !== undefined) {
|
|
1949
|
+
traces.push({
|
|
1950
|
+
type: "scatter",
|
|
1951
|
+
mode: "markers",
|
|
1952
|
+
name: group.groupLabel,
|
|
1953
|
+
x: [group.data.comparedMedian],
|
|
1954
|
+
y: [0 + groupIndex * 0.1],
|
|
1955
|
+
marker: {
|
|
1956
|
+
color: group.color || "orange",
|
|
1957
|
+
size: 10,
|
|
1958
|
+
symbol: "circle",
|
|
1959
|
+
line: {
|
|
1960
|
+
color: "white",
|
|
1961
|
+
width: 1,
|
|
1962
|
+
},
|
|
1963
|
+
},
|
|
1964
|
+
showlegend: false,
|
|
1965
|
+
hoverinfo: "none",
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
return traces;
|
|
1969
|
+
});
|
|
1970
|
+
const xRangeMin = groups.reduce((min, group) => {
|
|
1971
|
+
var _a, _b;
|
|
1972
|
+
return Math.min(min, (_a = group.data.summarizedMin) !== null && _a !== void 0 ? _a : Infinity, (_b = group.data.comparedMedian) !== null && _b !== void 0 ? _b : Infinity);
|
|
1973
|
+
}, 0);
|
|
1974
|
+
const xRangeMax = groups.reduce((max, group) => {
|
|
1975
|
+
var _a, _b;
|
|
1976
|
+
return Math.max(max, (_a = group.data.summarizedMax) !== null && _a !== void 0 ? _a : -Infinity, (_b = group.data.comparedMedian) !== null && _b !== void 0 ? _b : -Infinity);
|
|
1977
|
+
}, 0);
|
|
1978
|
+
const layout = {
|
|
1979
|
+
width: undefined,
|
|
1980
|
+
height: height,
|
|
1981
|
+
autosize: true,
|
|
1982
|
+
margin: {
|
|
1983
|
+
l: 130,
|
|
1984
|
+
r: 35, // Balance between ensuring the mean annotation doesn't get cut off and having too much margin.
|
|
1985
|
+
t: title ? 50 : 5,
|
|
1986
|
+
b: 50,
|
|
1987
|
+
pad: 4,
|
|
1988
|
+
// ...extraLayoutConfig.margin, // Merge in any extra margin config provided via props
|
|
1989
|
+
},
|
|
1990
|
+
title: {
|
|
1991
|
+
text: title,
|
|
1992
|
+
font: {
|
|
1993
|
+
size: 16,
|
|
1994
|
+
},
|
|
1995
|
+
xref: "paper",
|
|
1996
|
+
x: 0.5,
|
|
1997
|
+
xanchor: "center",
|
|
1998
|
+
},
|
|
1999
|
+
xaxis: {
|
|
2000
|
+
title: {
|
|
2001
|
+
text: xAxisTitle,
|
|
2002
|
+
font: {
|
|
2003
|
+
size: 14,
|
|
2004
|
+
},
|
|
2005
|
+
},
|
|
2006
|
+
showgrid: true,
|
|
2007
|
+
showline: true,
|
|
2008
|
+
fixedrange: true, // Disable zooming
|
|
2009
|
+
zeroline: false,
|
|
2010
|
+
range: [
|
|
2011
|
+
startXAxisAtZero ? 0 : xRangeMin - (xRangeMax - xRangeMin) * 0.3,
|
|
2012
|
+
xRangeMax + (xRangeMax - xRangeMin) * 0.3,
|
|
2013
|
+
], // Add padding to the x-axis range
|
|
2014
|
+
mirror: true,
|
|
2015
|
+
gridcolor: "#efefef",
|
|
2016
|
+
gridwidth: 0.2,
|
|
2017
|
+
zerolinecolor: "#969696",
|
|
2018
|
+
zerolinewidth: 1,
|
|
2019
|
+
linecolor: "#bababa",
|
|
2020
|
+
linewidth: 1,
|
|
2021
|
+
},
|
|
2022
|
+
yaxis: {
|
|
2023
|
+
mirror: "ticks",
|
|
2024
|
+
gridcolor: "#efefef",
|
|
2025
|
+
gridwidth: 0.2,
|
|
2026
|
+
zerolinecolor: "#969696",
|
|
2027
|
+
zerolinewidth: 1,
|
|
2028
|
+
linecolor: "#bababa",
|
|
2029
|
+
linewidth: 1,
|
|
2030
|
+
showticklabels: true,
|
|
2031
|
+
showgrid: false,
|
|
2032
|
+
showline: true,
|
|
2033
|
+
zeroline: false,
|
|
2034
|
+
fixedrange: true, // Disable zooming and pan interactions
|
|
2035
|
+
tickmode: "array",
|
|
2036
|
+
tickvals: groups.map((_, index) => 0 + index * 0.1),
|
|
2037
|
+
ticktext: groups.map((group) => group.groupLabel),
|
|
2038
|
+
ticks: "inside",
|
|
2039
|
+
range: [-0.08, 0.08 + (groups.length - 1) * 0.1], // Add padding around the groups
|
|
2040
|
+
automargin: true,
|
|
2041
|
+
tickcolor: "white", // Hide default ticks since we're using them for group labels in the paired comparisons plot
|
|
2042
|
+
},
|
|
2043
|
+
hovermode: "y",
|
|
2044
|
+
};
|
|
2045
|
+
const containerStyles = Object.assign({ width: "100%", height: height, position: "relative", display: "flex", flexDirection: "column", gap: 0 }, containerStyleOverrides);
|
|
2046
|
+
const config = {
|
|
2047
|
+
responsive: true, // Enable responsive mode for width
|
|
2048
|
+
displayModeBar: false, // Hide the mode bar
|
|
2049
|
+
displaylogo: false, // Hide the Plotly logo
|
|
2050
|
+
scrollZoom: false, // Disable zooming with scroll
|
|
2051
|
+
staticPlot: false, // Enable interactivity
|
|
2052
|
+
};
|
|
2053
|
+
return (jsx("div", { ref: containerRef,
|
|
2054
|
+
// className={`plot-container ${plotId}`}
|
|
2055
|
+
style: Object.assign({}, containerStyles), children: jsx(Suspense, { fallback: jsx(Loading, {}), children: jsxs("div", { style: {
|
|
2056
|
+
position: "relative",
|
|
2057
|
+
width: "100%",
|
|
2058
|
+
height: "100%",
|
|
2059
|
+
}, children: [jsx(Plot$1, { data: plotlyData, layout: layout, config: config, useResizeHandler: true, onHover: handleHover, onUnhover: handleUnhover, style: {
|
|
2060
|
+
width: "100%",
|
|
2061
|
+
height: `${height}px`,
|
|
2062
|
+
display: "block",
|
|
2063
|
+
transition: "opacity 0.15s ease-in-out",
|
|
2064
|
+
} }, `boxplot-${plotId || "default"}`), tooltip.visible && (jsx("div", { style: {
|
|
2065
|
+
position: "fixed",
|
|
2066
|
+
left: tooltipPosition === "right" ? tooltip.x + 10 : undefined,
|
|
2067
|
+
right: tooltipPosition === "left"
|
|
2068
|
+
? window.innerWidth - tooltip.x + 10 // Measured in pixels from the right edge of the screen
|
|
2069
|
+
: undefined,
|
|
2070
|
+
transform: tooltipPosition === "right" ? "translateX(10px)" : undefined,
|
|
2071
|
+
top: tooltip.y - 18,
|
|
2072
|
+
backgroundColor: "rgba(255, 255, 255, 0.8)",
|
|
2073
|
+
borderRadius: "4px",
|
|
2074
|
+
color: "#333",
|
|
2075
|
+
padding: "8px 12px",
|
|
2076
|
+
fontSize: "13px",
|
|
2077
|
+
lineHeight: "1.5",
|
|
2078
|
+
pointerEvents: "none",
|
|
2079
|
+
zIndex: 1000,
|
|
2080
|
+
whiteSpace: "nowrap",
|
|
2081
|
+
boxShadow: "0 2px 4px rgba(0,0,0,0.2)",
|
|
2082
|
+
borderLeft: `7px solid ${tooltip.color}`,
|
|
2083
|
+
fontFamily: '"Open Sans", verdana, arial, sans-serif',
|
|
2084
|
+
}, dangerouslySetInnerHTML: { __html: tooltip.content } }))] }) }) }));
|
|
2085
|
+
};
|
|
2086
|
+
const SummaryComparisonPlotLegend = ({ comparedDataLabel, summarizedDataLabel, color = "orange", }) => {
|
|
2087
|
+
return (jsxs("div", { style: {
|
|
2088
|
+
display: "flex",
|
|
2089
|
+
gap: "20px",
|
|
2090
|
+
alignItems: "center",
|
|
2091
|
+
flexDirection: "row",
|
|
2092
|
+
}, children: [jsxs("div", { style: { display: "flex", gap: "5px", alignItems: "center" }, children: [jsx(Box, { width: 13, height: 13, sx: {
|
|
2093
|
+
backgroundColor: color,
|
|
2094
|
+
borderRadius: "20px",
|
|
2095
|
+
} }), jsx("span", { children: comparedDataLabel })] }), jsxs("div", { style: { display: "flex", gap: "5px", alignItems: "center" }, children: [jsx(Box, { width: 30, height: 3, sx: { backgroundColor: color } }), jsx("span", { children: summarizedDataLabel })] })] }));
|
|
2096
|
+
};
|
|
2097
|
+
|
|
1448
2098
|
const Plot = lazy(() => import('react-plotly.js'));
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
2099
|
+
// This component plots a line chart in the foreground with a histogram in the backgroun. The histogram
|
|
2100
|
+
// is used only for context, while the line is intended to have the focus. To that end, the line plot has hover interactions
|
|
2101
|
+
// while the histogram does not.
|
|
2102
|
+
// Additionally, because of plotly restrictions in the web (overlaying y issues) we cannot draw both plots
|
|
2103
|
+
// as normal. Instead, we first render a histogram, extract the calculated bin positions and heights from the rendered plot, and
|
|
2104
|
+
// then render a second plot with just the line and use the extracted histogram bin information to draw the histogram as shapes in
|
|
2105
|
+
// the background of the line plot. This allows us to have a shared x-axis and aligned y-axes for both plots, while only having hover
|
|
2106
|
+
// interactions on the line plot.
|
|
2107
|
+
const LineWithHistogram = (props) => {
|
|
2108
|
+
const { lineData, histogramData, width = 600, height = 400, title, xAxisTitle, yAxisTitle, containerStyleOverrides, plotId = "lineplot", extraLayoutConfig = {}, y2AxisTitle, revision = 0, } = props;
|
|
2109
|
+
// Ref for plot container
|
|
2110
|
+
const containerRef = useRef(null);
|
|
2111
|
+
// State to store extracted histogram bin information
|
|
2112
|
+
const [histogramBins, setHistogramBins] = useState([]);
|
|
2113
|
+
// State to trigger re-render of hidden histogram plot when data changes
|
|
2114
|
+
const [hiddenPlotKey, setHiddenPlotKey] = useState(0);
|
|
2115
|
+
// When histogram data changes, reset bins and remount the hidden plot so onInitialized re-fires
|
|
2116
|
+
useEffect(() => {
|
|
2117
|
+
setHistogramBins([]);
|
|
2118
|
+
setHiddenPlotKey((k) => k + 1);
|
|
2119
|
+
}, [histogramData.x]);
|
|
2120
|
+
// Callback to extract histogram bin data from Plotly after it's rendered
|
|
2121
|
+
const handlePlotInitialized = (figure, graphDiv) => {
|
|
2122
|
+
try {
|
|
2123
|
+
// Only extract once - prevent infinite loop
|
|
2124
|
+
if (histogramBins.length > 0) {
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
// Access the calculated histogram data from Plotly's internal data
|
|
2128
|
+
const histogramTrace = graphDiv.data[0]; // First trace is the histogram
|
|
2129
|
+
if (histogramTrace && histogramTrace.type === "histogram") {
|
|
2130
|
+
// Plotly computes x (bin edges) and y (bin heights) after rendering
|
|
2131
|
+
// We need to access the computed bins from the graph div
|
|
2132
|
+
const fullData = graphDiv.calcdata;
|
|
2133
|
+
if (fullData && fullData[0]) {
|
|
2134
|
+
const histCalcData = fullData[0]; // Contains bin information
|
|
2135
|
+
const bins = [];
|
|
2136
|
+
// Extract bin information from Plotly's calculated data
|
|
2137
|
+
for (let i = 0; i < histCalcData.length; i++) {
|
|
2138
|
+
const point = histCalcData[i];
|
|
2139
|
+
if (point.p0 !== undefined &&
|
|
2140
|
+
point.p1 !== undefined &&
|
|
2141
|
+
point.s !== undefined) {
|
|
2142
|
+
bins.push({
|
|
2143
|
+
x0: point.p0,
|
|
2144
|
+
x1: point.p1,
|
|
2145
|
+
y: point.s, // bin height
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
if (bins.length > 0) {
|
|
2150
|
+
setHistogramBins(bins);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
catch (error) {
|
|
2156
|
+
console.error("Error extracting histogram bins:", error);
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
// Data for the hidden histogram plot used to extract bin information.
|
|
2160
|
+
// This plot is not visible and is only used for bin calculation.
|
|
2161
|
+
const histogramPlotlyData = [
|
|
1452
2162
|
{
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
marker: {
|
|
2163
|
+
type: "histogram",
|
|
2164
|
+
x: histogramData.x,
|
|
2165
|
+
name: histogramData.name || "histogram",
|
|
2166
|
+
marker: {
|
|
2167
|
+
color: histogramData.color || "rgb(211, 211, 212)",
|
|
2168
|
+
line: {
|
|
2169
|
+
color: "white",
|
|
2170
|
+
width: 0.5,
|
|
2171
|
+
},
|
|
2172
|
+
},
|
|
1457
2173
|
},
|
|
1458
2174
|
];
|
|
1459
|
-
const
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
2175
|
+
const plotlyData = [
|
|
2176
|
+
// Only the line trace - no histogram trace in main plot
|
|
2177
|
+
// Line trace on primary y-axis for hover interactions
|
|
2178
|
+
Object.assign({ type: "scatter", mode: "lines+markers", x: lineData.x, y: lineData.y, name: lineData.name, line: {
|
|
2179
|
+
color: lineData.color || "#1f77b4",
|
|
2180
|
+
shape: "hv", // Step-wise line.
|
|
2181
|
+
}, marker: {
|
|
2182
|
+
color: lineData.color || "#1f77b4",
|
|
2183
|
+
size: 6,
|
|
2184
|
+
}, hoverinfo: lineData.hovertemplate ? "text" : "x+y+name" }, (lineData.hovertemplate && { hovertemplate: lineData.hovertemplate })),
|
|
2185
|
+
// Dummy invisible point on yaxis2 to force the axis to render
|
|
2186
|
+
// Position it at the top of the histogram range to ensure the
|
|
2187
|
+
// y2 axis scales appropriately even if the line data doesn't reach that high.
|
|
2188
|
+
// This is a hack to work around Plotly's behavior of not rendering an axis if no data
|
|
2189
|
+
// is plotted on it.
|
|
2190
|
+
...(histogramBins.length > 0
|
|
2191
|
+
? [
|
|
2192
|
+
{
|
|
2193
|
+
type: "scatter",
|
|
2194
|
+
mode: "markers",
|
|
2195
|
+
yaxis: "y2",
|
|
2196
|
+
x: [lineData.x[0]], // First x point
|
|
2197
|
+
y: [Math.max(...histogramBins.map((bin) => bin.y))], // Top of histogram range
|
|
2198
|
+
marker: {
|
|
2199
|
+
size: 0.1,
|
|
2200
|
+
opacity: 0,
|
|
2201
|
+
color: "rgba(0,0,0,0)",
|
|
2202
|
+
},
|
|
2203
|
+
hoverinfo: "skip",
|
|
2204
|
+
showlegend: false,
|
|
2205
|
+
},
|
|
2206
|
+
]
|
|
2207
|
+
: []),
|
|
2208
|
+
];
|
|
2209
|
+
// Convert histogram bins to Plotly shapes (rectangles drawn in the background)
|
|
2210
|
+
// Use yaxis2 coordinates so bins align with the right y-axis
|
|
2211
|
+
const histogramShapes = histogramBins.length > 0
|
|
2212
|
+
? histogramBins.map((bin) => ({
|
|
2213
|
+
type: "rect",
|
|
2214
|
+
xref: "x",
|
|
2215
|
+
yref: "y2", // Use secondary y-axis to match the histogram scale
|
|
2216
|
+
x0: bin.x0,
|
|
2217
|
+
x1: bin.x1,
|
|
2218
|
+
y0: 0, // Start at y2 = 0
|
|
2219
|
+
y1: bin.y, // Height matches bin count on y2 axis
|
|
2220
|
+
fillcolor: histogramData.color || "rgb(211, 211, 212)",
|
|
2221
|
+
line: {
|
|
2222
|
+
color: histogramData.color || "rgb(211, 211, 212)", // Match fill color
|
|
2223
|
+
width: 0, // No border
|
|
2224
|
+
},
|
|
2225
|
+
layer: "below", // Ensure shapes are drawn below traces
|
|
2226
|
+
}))
|
|
2227
|
+
: [];
|
|
2228
|
+
const layout = Object.assign(Object.assign({}, extraLayoutConfig), { title: {
|
|
2229
|
+
text: title,
|
|
2230
|
+
}, showlegend: false, autosize: true, width: undefined, height: undefined, hovermode: "closest", margin: {
|
|
2231
|
+
l: 50,
|
|
2232
|
+
r: 80, // Balance between ensuring the mean annotation doesn't get cut off and having too much margin.
|
|
2233
|
+
t: title ? 50 : 20, // Adjust top margin based on whether a title is provided
|
|
2234
|
+
b: 50,
|
|
2235
|
+
pad: 4,
|
|
2236
|
+
},
|
|
2237
|
+
// Add histogram shapes as background rectangles
|
|
2238
|
+
shapes: histogramShapes, xaxis: {
|
|
2239
|
+
title: {
|
|
2240
|
+
text: xAxisTitle,
|
|
2241
|
+
},
|
|
2242
|
+
showgrid: true,
|
|
2243
|
+
zeroline: false,
|
|
2244
|
+
showline: true,
|
|
2245
|
+
mirror: "ticks",
|
|
2246
|
+
gridcolor: "#efefef",
|
|
2247
|
+
gridwidth: 0.2,
|
|
2248
|
+
zerolinecolor: "#969696",
|
|
2249
|
+
zerolinewidth: 1,
|
|
2250
|
+
linecolor: "#bababa",
|
|
2251
|
+
linewidth: 1,
|
|
2252
|
+
fixedrange: true, // Disable zooming
|
|
2253
|
+
ticklabelposition: "outside",
|
|
2254
|
+
}, yaxis: {
|
|
2255
|
+
title: {
|
|
2256
|
+
text: yAxisTitle || "", // Set default to empty string to prevent Plotly from adding its own default title
|
|
2257
|
+
standoff: 12, // Add space between title and axis
|
|
2258
|
+
font: {
|
|
2259
|
+
color: lineData.color || "#1f77b4", // Match y-axis title color to line color
|
|
2260
|
+
},
|
|
2261
|
+
},
|
|
2262
|
+
automargin: true, // Required for standoff to work properly
|
|
2263
|
+
showgrid: true,
|
|
2264
|
+
zeroline: false,
|
|
2265
|
+
showline: true,
|
|
2266
|
+
mirror: "ticks",
|
|
2267
|
+
gridcolor: "#efefef",
|
|
2268
|
+
gridwidth: 0.2,
|
|
2269
|
+
zerolinecolor: "#969696",
|
|
2270
|
+
zerolinewidth: 1,
|
|
2271
|
+
linecolor: "#bababa",
|
|
2272
|
+
linewidth: 1,
|
|
2273
|
+
fixedrange: true, // Disable zooming
|
|
2274
|
+
tickfont: {
|
|
2275
|
+
color: lineData.color || "#1f77b4", // Match y-axis tick color to line color
|
|
2276
|
+
},
|
|
2277
|
+
tickcolor: "white", // Hide default ticks since we're using them for group labels in the paired comparisons plot
|
|
2278
|
+
ticklen: 10, // Give ticks a length to push labels away from y axis.
|
|
2279
|
+
},
|
|
2280
|
+
// Second y-axis for histogram bin counts (right side)
|
|
2281
|
+
yaxis2: {
|
|
2282
|
+
side: "right",
|
|
2283
|
+
overlaying: "y",
|
|
2284
|
+
showgrid: false, // Hide grid for histogram axis
|
|
2285
|
+
zeroline: false, // Hide zero line
|
|
2286
|
+
showline: true,
|
|
2287
|
+
linecolor: "#bababa",
|
|
2288
|
+
linewidth: 1,
|
|
2289
|
+
tickfont: {
|
|
2290
|
+
color: histogramData.color || "rgb(150, 150, 150)",
|
|
2291
|
+
},
|
|
2292
|
+
title: {
|
|
2293
|
+
text: y2AxisTitle || "Count",
|
|
2294
|
+
font: {
|
|
2295
|
+
color: histogramData.color || "rgb(150, 150, 150)",
|
|
2296
|
+
},
|
|
2297
|
+
standoff: 22,
|
|
2298
|
+
},
|
|
2299
|
+
tickprefix: " ", // Add space between the ticks and axis
|
|
2300
|
+
fixedrange: true,
|
|
2301
|
+
} });
|
|
2302
|
+
const config = {
|
|
2303
|
+
responsive: true, // Make the plot responsive
|
|
2304
|
+
displayModeBar: false, // Hide the mode bar
|
|
2305
|
+
displaylogo: false, // Hide the Plotly logo
|
|
2306
|
+
scrollZoom: false, // Disable zooming with scroll
|
|
2307
|
+
staticPlot: false, // Enable interactivity
|
|
1463
2308
|
};
|
|
1464
|
-
|
|
2309
|
+
const containerStyles = Object.assign({ width: "100%", height: "100%", position: "relative" }, containerStyleOverrides);
|
|
2310
|
+
return (jsx("div", { ref: containerRef, style: Object.assign({}, containerStyles), children: jsxs(Suspense, { fallback: jsx(Loading, {}), children: [jsx("div", { style: {
|
|
2311
|
+
position: "relative",
|
|
2312
|
+
width: "100%",
|
|
2313
|
+
height: "100%",
|
|
2314
|
+
}, children: jsx(Plot, { data: plotlyData, layout: layout, config: config, useResizeHandler: true, style: {
|
|
2315
|
+
width: "100%",
|
|
2316
|
+
height: "100%",
|
|
2317
|
+
display: "block",
|
|
2318
|
+
transition: "opacity 0.15s ease-in-out",
|
|
2319
|
+
}, revision: revision }, `lineplot-${plotId || "default"}`) }), jsx("div", { style: { display: "none" }, children: jsx(Plot, { data: histogramPlotlyData, layout: { width: 400, height: 300 }, onInitialized: handlePlotInitialized }, `hidden-histogram-${plotId || "default"}-${hiddenPlotKey}`) })] }) }));
|
|
1465
2320
|
};
|
|
1466
2321
|
|
|
1467
|
-
export { HistogramPlot, RadialHistogramPlot, StatsDonut,
|
|
2322
|
+
export { BoxPlot, HistogramPlot, LineWithHistogram, PairedComparisonsBoxPlot, RadialHistogramPlot, StatsDonut, SummaryComparisonPlot, SummaryComparisonPlotLegend, isDateArray, isNumberArray };
|
|
1468
2323
|
//# sourceMappingURL=index.esm.js.map
|