td-plots 1.7.2 → 1.8.2
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/LinePlot.d.ts +24 -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 -3
- package/dist/index.esm.js +762 -10
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +766 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ var Button = require('@mui/material/Button');
|
|
|
8
8
|
var Tooltip = require('@mui/material/Tooltip');
|
|
9
9
|
var utils = require('@mui/material/utils');
|
|
10
10
|
var IconButton = require('@mui/material/IconButton');
|
|
11
|
+
var material = require('@mui/material');
|
|
12
|
+
var Box = require('@mui/material/Box');
|
|
11
13
|
|
|
12
14
|
function styleInject(css, ref) {
|
|
13
15
|
if ( ref === void 0 ) ref = {};
|
|
@@ -36,7 +38,7 @@ function styleInject(css, ref) {
|
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
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;
|
|
41
|
+
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}.histogram .plot-container:hover .histogram-controls.show-on-hover{opacity:1;visibility:visible}";
|
|
40
42
|
styleInject(css_248z);
|
|
41
43
|
|
|
42
44
|
function formatDecimal(x) {
|
|
@@ -424,6 +426,47 @@ const plotlyMToMilliseconds = (mString) => {
|
|
|
424
426
|
}
|
|
425
427
|
return 0;
|
|
426
428
|
};
|
|
429
|
+
// Helper function to convert color to rgba with specified opacity
|
|
430
|
+
const colorToRGBA = (color, opacity) => {
|
|
431
|
+
// For named colors, create a temporary element to get computed rgb
|
|
432
|
+
// This is a fallback that works in browser environments
|
|
433
|
+
const temp = document.createElement("div");
|
|
434
|
+
temp.style.color = color;
|
|
435
|
+
document.body.appendChild(temp);
|
|
436
|
+
const computed = window.getComputedStyle(temp).color;
|
|
437
|
+
document.body.removeChild(temp);
|
|
438
|
+
const rgbMatch = computed.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
439
|
+
if (rgbMatch) {
|
|
440
|
+
return `rgba(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]}, ${opacity})`;
|
|
441
|
+
}
|
|
442
|
+
// Fallback: return original color
|
|
443
|
+
return color;
|
|
444
|
+
};
|
|
445
|
+
const computeMedian = (data) => {
|
|
446
|
+
if (data.length === 0) {
|
|
447
|
+
return 0;
|
|
448
|
+
}
|
|
449
|
+
const values = data.sort((a, b) => a - b);
|
|
450
|
+
const mid = Math.floor(values.length / 2);
|
|
451
|
+
return values.length % 2 !== 0
|
|
452
|
+
? values[mid]
|
|
453
|
+
: (values[mid - 1] + values[mid]) / 2;
|
|
454
|
+
};
|
|
455
|
+
const computeQuartile = (data, quartile) => {
|
|
456
|
+
if (data.length === 0) {
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
459
|
+
const values = data.sort((a, b) => a - b);
|
|
460
|
+
const pos = (values.length - 1) * (quartile / 4);
|
|
461
|
+
const base = Math.floor(pos);
|
|
462
|
+
const rest = pos - base;
|
|
463
|
+
if (values[base + 1] !== undefined) {
|
|
464
|
+
return values[base] + rest * (values[base + 1] - values[base]);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
return values[base];
|
|
468
|
+
}
|
|
469
|
+
};
|
|
427
470
|
|
|
428
471
|
// Loading component renders a circular spinner above an element (usually a plot)
|
|
429
472
|
const Loading = () => {
|
|
@@ -498,7 +541,7 @@ var SettingsIcon = utils.createSvgIcon(/*#__PURE__*/jsxRuntime.jsx("path", {
|
|
|
498
541
|
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"
|
|
499
542
|
}), 'Settings');
|
|
500
543
|
|
|
501
|
-
const Plot$
|
|
544
|
+
const Plot$6 = React.lazy(() => import('react-plotly.js'));
|
|
502
545
|
const HistogramPlot = (props) => {
|
|
503
546
|
var _a, _b, _c, _d;
|
|
504
547
|
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;
|
|
@@ -1133,7 +1176,9 @@ const HistogramPlot = (props) => {
|
|
|
1133
1176
|
.map((n) => n * ONEAVGMONTH)
|
|
1134
1177
|
.concat([defaultBinSize])
|
|
1135
1178
|
: unitOfTime === "day"
|
|
1136
|
-
? [1, 2, 3, 5, 7, 30]
|
|
1179
|
+
? [1, 2, 3, 5, 7, 30]
|
|
1180
|
+
.map((n) => n * ONEDAY)
|
|
1181
|
+
.concat([defaultBinSize])
|
|
1137
1182
|
: unitOfTime === "hr"
|
|
1138
1183
|
? [1, 2, 3, 6, 12, 24]
|
|
1139
1184
|
.map((n) => n * ONEHOUR)
|
|
@@ -1195,7 +1240,7 @@ const HistogramPlot = (props) => {
|
|
|
1195
1240
|
return valueLabel;
|
|
1196
1241
|
};
|
|
1197
1242
|
}
|
|
1198
|
-
return (jsxRuntime.jsx("div", { ref: containerRef, className: `plot-container ${plotId}`, style: Object.assign({ "--selection-color": selectorsColor }, containerStyles), children: jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx(Loading, {}), children: jsxRuntime.jsxs("div", { style: {
|
|
1243
|
+
return (jsxRuntime.jsx("div", { ref: containerRef, className: `plot-container histogram ${plotId}`, style: Object.assign({ "--selection-color": selectorsColor }, containerStyles), children: jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx(Loading, {}), children: jsxRuntime.jsxs("div", { style: {
|
|
1199
1244
|
position: "relative",
|
|
1200
1245
|
width: "100%",
|
|
1201
1246
|
height: "100%",
|
|
@@ -1248,7 +1293,7 @@ const HistogramPlot = (props) => {
|
|
|
1248
1293
|
const newBinSize = newValue;
|
|
1249
1294
|
setBinSize(newBinSize);
|
|
1250
1295
|
onBinSizeChange === null || onBinSizeChange === void 0 ? void 0 : onBinSizeChange(newBinSize);
|
|
1251
|
-
}, colorOverride: barColor, valueLabelFormat: valueLabelFormat, titleStylingOverrides: settingsTitleStylingOverrides, disabledTooltipText: "Requires 10 or more data points" }) }) })] })), jsxRuntime.jsx(Plot$
|
|
1296
|
+
}, colorOverride: barColor, valueLabelFormat: valueLabelFormat, titleStylingOverrides: settingsTitleStylingOverrides, disabledTooltipText: "Requires 10 or more data points" }) }) })] })), jsxRuntime.jsx(Plot$6, { data: plotlyData, layout: layout, config: config, onSelected: handleSelection, onClick: handleClick, onDeselect: () => {
|
|
1252
1297
|
onDeselect();
|
|
1253
1298
|
setSelectedRange(null); // Remove selected box
|
|
1254
1299
|
}, onUpdate: handlePlotUpdate, useResizeHandler: true, style: {
|
|
@@ -1259,7 +1304,7 @@ const HistogramPlot = (props) => {
|
|
|
1259
1304
|
} }, `histogram-${plotId || "default"}`)] }) }) }));
|
|
1260
1305
|
};
|
|
1261
1306
|
|
|
1262
|
-
const Plot$
|
|
1307
|
+
const Plot$5 = React.lazy(() => import('react-plotly.js'));
|
|
1263
1308
|
const RadialHistogramPlot = (props) => {
|
|
1264
1309
|
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
|
|
1265
1310
|
} = props;
|
|
@@ -1359,14 +1404,14 @@ const RadialHistogramPlot = (props) => {
|
|
|
1359
1404
|
staticPlot: false,
|
|
1360
1405
|
};
|
|
1361
1406
|
const containerStyles = Object.assign({ width: "100%", height: "100%", position: "relative" }, containerStyleOverrides);
|
|
1362
|
-
return (jsxRuntime.jsx("div", { ref: containerRef, className: "plot-container radial-histogram-container", style: Object.assign({ '--selection-color': selectorsColor }, containerStyles), children: jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx(Loading, {}), children: jsxRuntime.jsx(Plot$
|
|
1407
|
+
return (jsxRuntime.jsx("div", { ref: containerRef, className: "plot-container radial-histogram-container", style: Object.assign({ '--selection-color': selectorsColor }, containerStyles), children: jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx(Loading, {}), children: jsxRuntime.jsx(Plot$5, { data: plotlyData, layout: layout, config: config, onSelected: onSelected, onClick: onClick, useResizeHandler: true, style: {
|
|
1363
1408
|
width: "100%",
|
|
1364
1409
|
height: "100%",
|
|
1365
1410
|
display: "block"
|
|
1366
1411
|
} }) }) }));
|
|
1367
1412
|
};
|
|
1368
1413
|
|
|
1369
|
-
const Plot$
|
|
1414
|
+
const Plot$4 = React.lazy(() => import('react-plotly.js'));
|
|
1370
1415
|
const StatsDonut = (props) => {
|
|
1371
1416
|
const { withCenterLabel = false, centerLabel, centerValue, showLegend = true, legendPosition = "right", containerStyle = {}, } = props;
|
|
1372
1417
|
// Configure legend position based on prop
|
|
@@ -1414,7 +1459,7 @@ const StatsDonut = (props) => {
|
|
|
1414
1459
|
},
|
|
1415
1460
|
]
|
|
1416
1461
|
: [];
|
|
1417
|
-
return (jsxRuntime.jsx("div", { style: Object.assign({ width: "100%", height: "100%", minHeight: "300px", display: "flex", justifyContent: "center", alignItems: "center" }, containerStyle), children: jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx(Loading, {}), children: jsxRuntime.jsx(Plot$
|
|
1462
|
+
return (jsxRuntime.jsx("div", { style: Object.assign({ width: "100%", height: "100%", minHeight: "300px", display: "flex", justifyContent: "center", alignItems: "center" }, containerStyle), children: jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx(Loading, {}), children: jsxRuntime.jsx(Plot$4, { data: [
|
|
1418
1463
|
{
|
|
1419
1464
|
type: "pie",
|
|
1420
1465
|
values: props.values,
|
|
@@ -1447,6 +1492,713 @@ const StatsDonut = (props) => {
|
|
|
1447
1492
|
}, useResizeHandler: true }) }) }));
|
|
1448
1493
|
};
|
|
1449
1494
|
|
|
1495
|
+
const Plot$3 = React.lazy(() => import('react-plotly.js'));
|
|
1496
|
+
const BoxPlot = (props) => {
|
|
1497
|
+
const { data, width = 600, height = 400, title = "Box Plot", xAxisTitle, yAxisTitle, containerStyleOverrides, plotId = "boxplot", extraLayoutConfig = {}, } = props;
|
|
1498
|
+
// Ref for plot container
|
|
1499
|
+
const containerRef = React.useRef(null);
|
|
1500
|
+
// State for custom tooltip
|
|
1501
|
+
const [tooltip, setTooltip] = React.useState({ visible: false, x: 0, y: 0, content: "", color: "#1f77b4" });
|
|
1502
|
+
const plotlyData = data.map((trace) => {
|
|
1503
|
+
const boxDefinition = isBoxPlotDataSummary(trace.values)
|
|
1504
|
+
? {
|
|
1505
|
+
y0: trace.y !== undefined ? trace.y : [trace.label], // Position the box at this y-value
|
|
1506
|
+
lowerfence: [trace.values.lowerWhisker],
|
|
1507
|
+
q1: [trace.values.q1],
|
|
1508
|
+
median: [trace.values.median],
|
|
1509
|
+
q3: [trace.values.q3],
|
|
1510
|
+
upperfence: [trace.values.upperWhisker],
|
|
1511
|
+
mean: trace.values.mean ? [trace.values.mean] : undefined,
|
|
1512
|
+
}
|
|
1513
|
+
: {
|
|
1514
|
+
x: trace.values,
|
|
1515
|
+
y0: trace.y !== undefined ? trace.y : undefined,
|
|
1516
|
+
}; // Values map to x because the boxplot is horizontal
|
|
1517
|
+
return Object.assign({ type: "box", orientation: "h", name: trace.label, fillcolor: trace.fill === "none" ? "rgba(255, 255, 255, 0)" : undefined, line: {
|
|
1518
|
+
color: trace.color || "#1f77b4", // Default to Plotly's default blue if no color provided
|
|
1519
|
+
}, boxpoints: false, hoverinfo: "none" }, boxDefinition);
|
|
1520
|
+
});
|
|
1521
|
+
const layout = Object.assign(Object.assign({}, extraLayoutConfig), { title: {
|
|
1522
|
+
text: title,
|
|
1523
|
+
}, showlegend: false, autosize: true, width: undefined, height: undefined, margin: Object.assign({ l: 50, r: 35, t: 50, b: 50, pad: 4 }, extraLayoutConfig.margin), xaxis: {
|
|
1524
|
+
title: {
|
|
1525
|
+
text: xAxisTitle,
|
|
1526
|
+
},
|
|
1527
|
+
// range: displayXAxis, // Fixed range prevents axis shifting during interaction or data updates
|
|
1528
|
+
showgrid: true,
|
|
1529
|
+
zeroline: false,
|
|
1530
|
+
showline: true,
|
|
1531
|
+
mirror: "ticks",
|
|
1532
|
+
gridcolor: "#efefef",
|
|
1533
|
+
gridwidth: 0.2,
|
|
1534
|
+
zerolinecolor: "#969696",
|
|
1535
|
+
zerolinewidth: 1,
|
|
1536
|
+
linecolor: "#bababa",
|
|
1537
|
+
linewidth: 1,
|
|
1538
|
+
fixedrange: true, // Disable zooming
|
|
1539
|
+
ticklabelposition: "outside",
|
|
1540
|
+
// tickformat: isDateArray(data) ? dateTickFormat : d3FormatValueString, // Format ticks for dates
|
|
1541
|
+
// automargin: true, // Adjust margin if tick labels rotate
|
|
1542
|
+
// hoverformat: isNumberArray(allData) ? d3FormatValueString : undefined,
|
|
1543
|
+
}, yaxis: Object.assign({ title: {
|
|
1544
|
+
text: yAxisTitle || "", // Set default to empty string to prevent Plotly from adding its own default title
|
|
1545
|
+
standoff: 12, // Add space between title and axis
|
|
1546
|
+
}, 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) });
|
|
1547
|
+
const config = {
|
|
1548
|
+
responsive: true, // Make the plot responsive
|
|
1549
|
+
displayModeBar: false, // Hide the mode bar
|
|
1550
|
+
displaylogo: false, // Hide the Plotly logo
|
|
1551
|
+
scrollZoom: false, // Disable zooming with scroll
|
|
1552
|
+
staticPlot: false, // Enable interactivity
|
|
1553
|
+
};
|
|
1554
|
+
const containerStyles = Object.assign({ width: "100%", height: "100%", position: "relative" }, containerStyleOverrides);
|
|
1555
|
+
return (jsxRuntime.jsx("div", { ref: containerRef,
|
|
1556
|
+
// className={`plot-container ${plotId}`}
|
|
1557
|
+
style: Object.assign({}, containerStyles), children: jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx(Loading, {}), children: jsxRuntime.jsxs("div", { style: {
|
|
1558
|
+
position: "relative",
|
|
1559
|
+
width: "100%",
|
|
1560
|
+
height: "100%",
|
|
1561
|
+
}, children: [jsxRuntime.jsx(Plot$3, { data: plotlyData, layout: layout, config: config, useResizeHandler: true,
|
|
1562
|
+
// onHover={handleHover}
|
|
1563
|
+
// onUnhover={handleUnhover}
|
|
1564
|
+
style: {
|
|
1565
|
+
width: "100%",
|
|
1566
|
+
height: "100%",
|
|
1567
|
+
display: "block",
|
|
1568
|
+
transition: "opacity 0.15s ease-in-out",
|
|
1569
|
+
} }, `boxplot-${plotId || "default"}`), tooltip.visible && (jsxRuntime.jsx("div", { style: {
|
|
1570
|
+
position: "fixed",
|
|
1571
|
+
left: tooltip.x + 10,
|
|
1572
|
+
top: tooltip.y - 10,
|
|
1573
|
+
backgroundColor: "white",
|
|
1574
|
+
color: "#333",
|
|
1575
|
+
padding: "8px 12px",
|
|
1576
|
+
borderRadius: "4px",
|
|
1577
|
+
fontSize: "13px",
|
|
1578
|
+
lineHeight: "1.5",
|
|
1579
|
+
pointerEvents: "none",
|
|
1580
|
+
zIndex: 1000,
|
|
1581
|
+
whiteSpace: "nowrap",
|
|
1582
|
+
boxShadow: "0 2px 4px rgba(0,0,0,0.2)",
|
|
1583
|
+
border: `2px solid ${tooltip.color}`,
|
|
1584
|
+
fontFamily: '"Open Sans", verdana, arial, sans-serif',
|
|
1585
|
+
}, dangerouslySetInnerHTML: { __html: tooltip.content } }))] }) }) }));
|
|
1586
|
+
};
|
|
1587
|
+
function isBoxPlotDataSummary(value) {
|
|
1588
|
+
return (typeof value === "object" &&
|
|
1589
|
+
value !== null &&
|
|
1590
|
+
typeof value.lowerWhisker === "number" &&
|
|
1591
|
+
typeof value.q1 === "number" &&
|
|
1592
|
+
typeof value.median === "number" &&
|
|
1593
|
+
typeof value.q3 === "number" &&
|
|
1594
|
+
typeof value.upperWhisker === "number" &&
|
|
1595
|
+
(value.mean === undefined || typeof value.mean === "number"));
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const SVGBoxWithLine = (props) => {
|
|
1599
|
+
const boxWidth = 35;
|
|
1600
|
+
const boxHeight = 15;
|
|
1601
|
+
const lineWidth = boxWidth * 2; // 90% width
|
|
1602
|
+
const lineY = boxHeight / 2; // Vertical center
|
|
1603
|
+
return (jsxRuntime.jsxs("svg", { width: lineWidth, height: boxHeight, viewBox: `0 0 ${lineWidth + 3} ${boxHeight + 3}`, children: [jsxRuntime.jsx("line", { x1: 0, y1: lineY, x2: (lineWidth - boxWidth) / 2, y2: lineY, stroke: props.color, strokeWidth: "2" }), jsxRuntime.jsx("line", { x1: (lineWidth + boxWidth) / 2, y1: lineY, x2: lineWidth, y2: lineY, stroke: props.color, strokeWidth: "2" }), jsxRuntime.jsx("line", { x1: 0, y1: lineY - 5, x2: 0, y2: lineY + 5, stroke: props.color, strokeWidth: "2" }), jsxRuntime.jsx("line", { x1: lineWidth, y1: lineY - 5, x2: lineWidth, y2: lineY + 5, stroke: props.color, strokeWidth: "2" }), jsxRuntime.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 }), jsxRuntime.jsx("rect", { x: (lineWidth - boxWidth) / 2, y: "0", width: boxWidth, height: boxHeight, stroke: props.color, strokeWidth: "2", fill: "none" }), jsxRuntime.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" })] }));
|
|
1604
|
+
};
|
|
1605
|
+
// export const OutlinedBox = (props: BoxProps) => {
|
|
1606
|
+
// return (
|
|
1607
|
+
// <Box
|
|
1608
|
+
// border={`2px solid ${props.color}`}
|
|
1609
|
+
// width={BOX_SIZE}
|
|
1610
|
+
// height={BOX_SIZE}
|
|
1611
|
+
// mr={1}
|
|
1612
|
+
// />
|
|
1613
|
+
// );
|
|
1614
|
+
// };
|
|
1615
|
+
// export const FilledBox = (props: BoxProps) => {
|
|
1616
|
+
// return (
|
|
1617
|
+
// <Box
|
|
1618
|
+
// width={BOX_SIZE}
|
|
1619
|
+
// height={BOX_SIZE}
|
|
1620
|
+
// sx={{
|
|
1621
|
+
// backgroundColor: props.color,
|
|
1622
|
+
// border: `2px solid ${props.color}`,
|
|
1623
|
+
// }}
|
|
1624
|
+
// mr={1}
|
|
1625
|
+
// />
|
|
1626
|
+
// );
|
|
1627
|
+
// };
|
|
1628
|
+
// export const ShadedBox = (props: BoxProps) => {
|
|
1629
|
+
// return (
|
|
1630
|
+
// <Box
|
|
1631
|
+
// width={BOX_SIZE}
|
|
1632
|
+
// height={BOX_SIZE}
|
|
1633
|
+
// sx={{
|
|
1634
|
+
// border: `2px solid ${props.color}`,
|
|
1635
|
+
// position: "relative",
|
|
1636
|
+
// "&::before": {
|
|
1637
|
+
// content: '""',
|
|
1638
|
+
// position: "absolute",
|
|
1639
|
+
// top: 0,
|
|
1640
|
+
// left: 0,
|
|
1641
|
+
// right: 0,
|
|
1642
|
+
// bottom: 0,
|
|
1643
|
+
// backgroundColor: `${props.color}`,
|
|
1644
|
+
// opacity: 0.5,
|
|
1645
|
+
// },
|
|
1646
|
+
// }}
|
|
1647
|
+
// mr={1}
|
|
1648
|
+
// />
|
|
1649
|
+
// );
|
|
1650
|
+
// };
|
|
1651
|
+
// export const LegendItem = (props: LegendItemProps) => {
|
|
1652
|
+
// return (
|
|
1653
|
+
// <Box
|
|
1654
|
+
// key={props.label}
|
|
1655
|
+
// display="flex"
|
|
1656
|
+
// alignItems="center"
|
|
1657
|
+
// mr={1}
|
|
1658
|
+
// sx={{
|
|
1659
|
+
// fontFamily: "Open Sans, verdana, arial, sans-serif",
|
|
1660
|
+
// }}
|
|
1661
|
+
// >
|
|
1662
|
+
// {props.boxStyle === "shaded" ? (
|
|
1663
|
+
// <ShadedBox color={props.color} />
|
|
1664
|
+
// ) : props.boxStyle === "outlined" ? (
|
|
1665
|
+
// <OutlinedBox color={props.color} />
|
|
1666
|
+
// ) : (
|
|
1667
|
+
// <FilledBox color={props.color} />
|
|
1668
|
+
// )}
|
|
1669
|
+
// {props.label}
|
|
1670
|
+
// </Box>
|
|
1671
|
+
// );
|
|
1672
|
+
// };
|
|
1673
|
+
const LegendBoxPlotItem = (props) => {
|
|
1674
|
+
return (jsxRuntime.jsxs(Box, { display: "flex", alignItems: "center", mr: 4, sx: {
|
|
1675
|
+
fontFamily: "Open Sans, verdana, arial, sans-serif",
|
|
1676
|
+
}, children: [jsxRuntime.jsx(SVGBoxWithLine, { color: props.color, boxStyle: props.boxStyle }), props.label] }, props.label));
|
|
1677
|
+
};
|
|
1678
|
+
|
|
1679
|
+
// This component takes grouped data and transforms it for displaying with the BoxPlot component.
|
|
1680
|
+
// 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)
|
|
1681
|
+
// 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.
|
|
1682
|
+
const PairedComparisonsBoxPlot = (props) => {
|
|
1683
|
+
const { groups, pairLabels, width = 600, height = 400, title = "", xAxisTitle, yAxisTitle, containerStyleOverrides, showLegend = true, plotId = "paired-comparisons-boxplot", } = props;
|
|
1684
|
+
// Transform the grouped data into an array for BoxPlot
|
|
1685
|
+
const boxPlotData = groups.flatMap((group, groupIndex) => {
|
|
1686
|
+
const groupYPosition = groupIndex; // Position the group on the y-axis based on its index
|
|
1687
|
+
group.color || "orange";
|
|
1688
|
+
return group.boxes.map((box, boxIndex) => (Object.assign(Object.assign({}, box), {
|
|
1689
|
+
// color: box.color || groupColor, // Use box color if provided, otherwise use group color
|
|
1690
|
+
color: "#75757f", fill: boxIndex % 2 === 0 ? "none" : "auto", y: groupYPosition + 0.15 + (boxIndex - 1) * 0.3 })));
|
|
1691
|
+
});
|
|
1692
|
+
// We have to construct nice ticks. We can position them with the group indices.
|
|
1693
|
+
const tickvals = groups.map((_, index) => index);
|
|
1694
|
+
const ticktext = groups.map((group) => group.groupLabel);
|
|
1695
|
+
// Draw some gray rectangles to live behind the boxes to help separate the groups
|
|
1696
|
+
const separatorShapes = groups.map((group, groupIndex) => {
|
|
1697
|
+
return {
|
|
1698
|
+
type: "rect",
|
|
1699
|
+
x0: 0,
|
|
1700
|
+
x1: 1,
|
|
1701
|
+
xref: "paper",
|
|
1702
|
+
y0: groupIndex - 0.5,
|
|
1703
|
+
y1: groupIndex + 0.5,
|
|
1704
|
+
yref: "y",
|
|
1705
|
+
fillcolor: groupIndex % 2 === 0 ? "#ffffff" : "#f8f8f8",
|
|
1706
|
+
opacity: 0.05,
|
|
1707
|
+
layer: "below",
|
|
1708
|
+
line: {
|
|
1709
|
+
width: 0,
|
|
1710
|
+
},
|
|
1711
|
+
};
|
|
1712
|
+
});
|
|
1713
|
+
const groupAnnotations = groups.map((group, groupIndex) => ({
|
|
1714
|
+
type: "line",
|
|
1715
|
+
yref: "y",
|
|
1716
|
+
xref: "paper",
|
|
1717
|
+
x0: -0.01,
|
|
1718
|
+
x1: -0.01,
|
|
1719
|
+
y0: groupIndex - 0.4, // Align with the center of the group
|
|
1720
|
+
y1: groupIndex + 0.4,
|
|
1721
|
+
line: {
|
|
1722
|
+
color: group.color || "orange",
|
|
1723
|
+
width: 7,
|
|
1724
|
+
},
|
|
1725
|
+
}));
|
|
1726
|
+
const differenceAnnotations = [];
|
|
1727
|
+
const differenceBetweenMediansLines = [];
|
|
1728
|
+
groups.forEach((group, groupIndex) => {
|
|
1729
|
+
// console.log(group.boxes);
|
|
1730
|
+
if ((!isBoxPlotDataSummary(group.boxes[0].values) &&
|
|
1731
|
+
group.boxes[0].values.length == 0) ||
|
|
1732
|
+
(!isBoxPlotDataSummary(group.boxes[1].values) &&
|
|
1733
|
+
group.boxes[1].values.length == 0)) {
|
|
1734
|
+
return; // Don't show an annotation if we don't have data for both boxes
|
|
1735
|
+
}
|
|
1736
|
+
const medianA = isBoxPlotDataSummary(group.boxes[0].values)
|
|
1737
|
+
? group.boxes[0].values.median
|
|
1738
|
+
: computeMedian(group.boxes[0].values);
|
|
1739
|
+
const medianB = isBoxPlotDataSummary(group.boxes[1].values)
|
|
1740
|
+
? group.boxes[1].values.median
|
|
1741
|
+
: computeMedian(group.boxes[1].values);
|
|
1742
|
+
const q1A = isBoxPlotDataSummary(group.boxes[0].values)
|
|
1743
|
+
? group.boxes[0].values.q1
|
|
1744
|
+
: computeQuartile(group.boxes[0].values, 1);
|
|
1745
|
+
const q3A = isBoxPlotDataSummary(group.boxes[0].values)
|
|
1746
|
+
? group.boxes[0].values.q3
|
|
1747
|
+
: computeQuartile(group.boxes[0].values, 3);
|
|
1748
|
+
const q1B = isBoxPlotDataSummary(group.boxes[1].values)
|
|
1749
|
+
? group.boxes[1].values.q1
|
|
1750
|
+
: computeQuartile(group.boxes[1].values, 1);
|
|
1751
|
+
const q3B = isBoxPlotDataSummary(group.boxes[1].values)
|
|
1752
|
+
? group.boxes[1].values.q3
|
|
1753
|
+
: computeQuartile(group.boxes[1].values, 3);
|
|
1754
|
+
const differenceBetweenMedians = medianB - medianA;
|
|
1755
|
+
// If we have the quartiles for the first box, we can determine if the second box's median is inside or outside the box
|
|
1756
|
+
const annotationColor = isBoxPlotDataSummary(group.boxes[0].values)
|
|
1757
|
+
? q3B < q1A || q1B > q3A
|
|
1758
|
+
? "red"
|
|
1759
|
+
: medianB < q1A || medianB > q3A
|
|
1760
|
+
? "orange"
|
|
1761
|
+
: "gray"
|
|
1762
|
+
: "gray";
|
|
1763
|
+
if (annotationColor === "gray") {
|
|
1764
|
+
return; // Don't show an annotation if the medians are close enough that they overlap in the boxes
|
|
1765
|
+
}
|
|
1766
|
+
differenceAnnotations.push({
|
|
1767
|
+
yref: "y",
|
|
1768
|
+
xref: "x",
|
|
1769
|
+
x: medianB > medianA ? medianB : medianA, // Position the annotation at the larger median
|
|
1770
|
+
y: groupIndex, // Align with the center of the group
|
|
1771
|
+
text: ` ${differenceBetweenMedians.toFixed(1)}`,
|
|
1772
|
+
showarrow: false,
|
|
1773
|
+
xanchor: "left",
|
|
1774
|
+
align: "left",
|
|
1775
|
+
font: {
|
|
1776
|
+
size: 12,
|
|
1777
|
+
color: annotationColor,
|
|
1778
|
+
style: "italic",
|
|
1779
|
+
},
|
|
1780
|
+
});
|
|
1781
|
+
differenceBetweenMediansLines.push({
|
|
1782
|
+
type: "line",
|
|
1783
|
+
x0: medianA,
|
|
1784
|
+
x1: medianB,
|
|
1785
|
+
xref: "x",
|
|
1786
|
+
y0: groupIndex,
|
|
1787
|
+
y1: groupIndex,
|
|
1788
|
+
yref: "y",
|
|
1789
|
+
line: {
|
|
1790
|
+
color: annotationColor,
|
|
1791
|
+
width: 2,
|
|
1792
|
+
},
|
|
1793
|
+
});
|
|
1794
|
+
differenceBetweenMediansLines.push({
|
|
1795
|
+
type: "line",
|
|
1796
|
+
x0: medianA,
|
|
1797
|
+
x1: medianA,
|
|
1798
|
+
xref: "x",
|
|
1799
|
+
y0: groupIndex - 0.05,
|
|
1800
|
+
y1: groupIndex,
|
|
1801
|
+
yref: "y",
|
|
1802
|
+
line: {
|
|
1803
|
+
color: annotationColor,
|
|
1804
|
+
width: 2,
|
|
1805
|
+
},
|
|
1806
|
+
});
|
|
1807
|
+
differenceBetweenMediansLines.push({
|
|
1808
|
+
type: "line",
|
|
1809
|
+
x0: medianB,
|
|
1810
|
+
x1: medianB,
|
|
1811
|
+
xref: "x",
|
|
1812
|
+
y0: groupIndex,
|
|
1813
|
+
y1: groupIndex + 0.05,
|
|
1814
|
+
yref: "y",
|
|
1815
|
+
line: {
|
|
1816
|
+
color: annotationColor,
|
|
1817
|
+
width: 2,
|
|
1818
|
+
},
|
|
1819
|
+
});
|
|
1820
|
+
});
|
|
1821
|
+
const extraLayoutConfig = {
|
|
1822
|
+
yaxis: {
|
|
1823
|
+
type: "linear", // Use linear axis for numeric positioning of boxes
|
|
1824
|
+
tickmode: "array",
|
|
1825
|
+
tickvals,
|
|
1826
|
+
ticktext,
|
|
1827
|
+
range: [-0.5, groups.length - 0.5], // Add some padding to the y-axis range to accommodate the boxes
|
|
1828
|
+
tickcolor: "#ffffff",
|
|
1829
|
+
showgrid: false,
|
|
1830
|
+
},
|
|
1831
|
+
margin: {
|
|
1832
|
+
t: 5,
|
|
1833
|
+
r: 50,
|
|
1834
|
+
},
|
|
1835
|
+
shapes: [
|
|
1836
|
+
...separatorShapes,
|
|
1837
|
+
...differenceBetweenMediansLines,
|
|
1838
|
+
...groupAnnotations,
|
|
1839
|
+
],
|
|
1840
|
+
annotations: differenceAnnotations,
|
|
1841
|
+
};
|
|
1842
|
+
const legendNode = (jsxRuntime.jsxs(material.Box, { display: "flex", justifyContent: "flex-end", mb: 2, children: [jsxRuntime.jsx(LegendBoxPlotItem, { label: pairLabels[0], color: "#626280", boxStyle: "shaded" }), jsxRuntime.jsx(LegendBoxPlotItem, { label: pairLabels[1], color: "#626280", boxStyle: "outlined" })] }));
|
|
1843
|
+
const containerStyles = Object.assign({ width: width, height: height, position: "relative", display: "flex", flexDirection: "column", gap: 0 }, containerStyleOverrides);
|
|
1844
|
+
return (jsxRuntime.jsxs("div", { style: Object.assign({}, containerStyles), children: [showLegend && legendNode, jsxRuntime.jsx(BoxPlot, { data: boxPlotData, width: width, height: height, title: title, xAxisTitle: xAxisTitle, yAxisTitle: yAxisTitle, extraLayoutConfig: extraLayoutConfig, containerStyleOverrides: containerStyleOverrides, plotId: `${plotId}-boxplot` })] }));
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1847
|
+
const Plot$2 = React.lazy(() => import('react-plotly.js'));
|
|
1848
|
+
const SummaryComparisonPlot = (props) => {
|
|
1849
|
+
const { groups, height = 250, title = "", xAxisTitle, yAxisTitle, containerStyleOverrides, plotId = "summary-comparison-plot", tooltipPosition = "right", startXAxisAtZero = true, } = props;
|
|
1850
|
+
// Ref for plot container
|
|
1851
|
+
const containerRef = React.useRef(null);
|
|
1852
|
+
// State for custom tooltip
|
|
1853
|
+
const [tooltip, setTooltip] = React.useState({
|
|
1854
|
+
visible: false,
|
|
1855
|
+
x: 0,
|
|
1856
|
+
y: 0,
|
|
1857
|
+
content: "",
|
|
1858
|
+
color: "#1f77b4",
|
|
1859
|
+
});
|
|
1860
|
+
const handleHover = (event) => {
|
|
1861
|
+
var _a, _b;
|
|
1862
|
+
if (!event.points || event.points.length === 0)
|
|
1863
|
+
return;
|
|
1864
|
+
const point = event.points[0];
|
|
1865
|
+
const eventData = point.data;
|
|
1866
|
+
if (!eventData)
|
|
1867
|
+
return;
|
|
1868
|
+
// Use the name of the trace to identify the group, then find the corresponding data for that group to display in the tooltip
|
|
1869
|
+
// 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
|
|
1870
|
+
const groupName = eventData.name || "Unknown Group";
|
|
1871
|
+
const groupData = (_a = groups.find((g) => g.groupLabel === groupName)) === null || _a === void 0 ? void 0 : _a.data;
|
|
1872
|
+
if (!groupData)
|
|
1873
|
+
return;
|
|
1874
|
+
let content = `<div style="margin-bottom: 8px;">`;
|
|
1875
|
+
let contentColor = "orange";
|
|
1876
|
+
let tooltipX = event.event.clientX;
|
|
1877
|
+
let tooltipY = event.event.clientY;
|
|
1878
|
+
content += `<strong>${groupName}</strong><br/>`;
|
|
1879
|
+
const summarizedMinText = groupData.summarizedMin
|
|
1880
|
+
? groupData.summarizedMin.toFixed(2)
|
|
1881
|
+
: "NA";
|
|
1882
|
+
const summarizedMaxText = groupData.summarizedMax
|
|
1883
|
+
? groupData.summarizedMax.toFixed(2)
|
|
1884
|
+
: "NA";
|
|
1885
|
+
const comparedMedianText = groupData.comparedMedian !== undefined
|
|
1886
|
+
? groupData.comparedMedian.toFixed(2)
|
|
1887
|
+
: "NA";
|
|
1888
|
+
content += `
|
|
1889
|
+
<table style="width: 100%; margin-top: 4px;">
|
|
1890
|
+
<tr>
|
|
1891
|
+
<td style="text-align: left; padding: 2px 18px 2px 0;">Population P25:</td>
|
|
1892
|
+
<td style="text-align: right; padding: 2px 0;">${summarizedMinText}</td>
|
|
1893
|
+
</tr>
|
|
1894
|
+
<tr>
|
|
1895
|
+
<td style="text-align: left; padding: 2px 18px 2px 0;">Population P75:</td>
|
|
1896
|
+
<td style="text-align: right; padding: 2px 0;">${summarizedMaxText}</td>
|
|
1897
|
+
</tr>
|
|
1898
|
+
<tr>
|
|
1899
|
+
<td style="text-align: left; padding: 2px 18px 2px 0;">My Median:</td>
|
|
1900
|
+
<td style="text-align: right; padding: 2px 0;">${comparedMedianText}</td>
|
|
1901
|
+
</tr>
|
|
1902
|
+
</table>
|
|
1903
|
+
`;
|
|
1904
|
+
contentColor = eventData.line
|
|
1905
|
+
? eventData.line.color
|
|
1906
|
+
: eventData.marker.color || contentColor;
|
|
1907
|
+
// Position tooltip at the end of the line (75th percentile)
|
|
1908
|
+
// Use the xaxis d2p method to convert data coordinate to pixel coordinate
|
|
1909
|
+
if (point.xaxis && typeof point.xaxis.d2p === "function") {
|
|
1910
|
+
const pixelX = tooltipPosition === "right"
|
|
1911
|
+
? point.xaxis.d2p(groupData.summarizedMax)
|
|
1912
|
+
: point.xaxis.d2p(groupData.summarizedMin);
|
|
1913
|
+
const pixelY = point.yaxis.d2p(eventData.y[0]); // Use the y coordinate of the line for vertical positioning
|
|
1914
|
+
const containerRect = (_b = containerRef.current) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
|
|
1915
|
+
if (containerRect) {
|
|
1916
|
+
tooltipX = pixelX + point.xaxis._offset + containerRect.left; // Adjust for x-axis offset and container position
|
|
1917
|
+
tooltipY = pixelY + point.yaxis._offset + containerRect.top; // Adjust for y-axis offset and container position
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
setTooltip({
|
|
1921
|
+
visible: true,
|
|
1922
|
+
x: tooltipX,
|
|
1923
|
+
y: tooltipY,
|
|
1924
|
+
content,
|
|
1925
|
+
color: contentColor,
|
|
1926
|
+
});
|
|
1927
|
+
};
|
|
1928
|
+
const handleUnhover = () => {
|
|
1929
|
+
setTooltip((prev) => (Object.assign(Object.assign({}, prev), { visible: false })));
|
|
1930
|
+
};
|
|
1931
|
+
// Transform the data into a format suitable for a plotly scatterplot
|
|
1932
|
+
const plotlyData = groups.flatMap((group, groupIndex) => {
|
|
1933
|
+
const traces = [];
|
|
1934
|
+
if (group.data.summarizedMin !== undefined &&
|
|
1935
|
+
group.data.summarizedMax !== undefined) {
|
|
1936
|
+
traces.push({
|
|
1937
|
+
type: "scatter",
|
|
1938
|
+
mode: "lines",
|
|
1939
|
+
name: group.groupLabel,
|
|
1940
|
+
x: [group.data.summarizedMin, group.data.summarizedMax],
|
|
1941
|
+
y: [0 + groupIndex * 0.1, 0 + groupIndex * 0.1],
|
|
1942
|
+
line: {
|
|
1943
|
+
color: colorToRGBA(group.color || "orange", 1),
|
|
1944
|
+
width: 2,
|
|
1945
|
+
},
|
|
1946
|
+
showlegend: false,
|
|
1947
|
+
hoverinfo: "none",
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
if (group.data.comparedMedian !== undefined) {
|
|
1951
|
+
traces.push({
|
|
1952
|
+
type: "scatter",
|
|
1953
|
+
mode: "markers",
|
|
1954
|
+
name: group.groupLabel,
|
|
1955
|
+
x: [group.data.comparedMedian],
|
|
1956
|
+
y: [0 + groupIndex * 0.1],
|
|
1957
|
+
marker: {
|
|
1958
|
+
color: group.color || "orange",
|
|
1959
|
+
size: 10,
|
|
1960
|
+
symbol: "circle",
|
|
1961
|
+
line: {
|
|
1962
|
+
color: "white",
|
|
1963
|
+
width: 1,
|
|
1964
|
+
},
|
|
1965
|
+
},
|
|
1966
|
+
showlegend: false,
|
|
1967
|
+
hoverinfo: "none",
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
return traces;
|
|
1971
|
+
});
|
|
1972
|
+
const xRangeMin = groups.reduce((min, group) => {
|
|
1973
|
+
var _a, _b;
|
|
1974
|
+
return Math.min(min, (_a = group.data.summarizedMin) !== null && _a !== void 0 ? _a : Infinity, (_b = group.data.comparedMedian) !== null && _b !== void 0 ? _b : Infinity);
|
|
1975
|
+
}, 0);
|
|
1976
|
+
const xRangeMax = groups.reduce((max, group) => {
|
|
1977
|
+
var _a, _b;
|
|
1978
|
+
return Math.max(max, (_a = group.data.summarizedMax) !== null && _a !== void 0 ? _a : -Infinity, (_b = group.data.comparedMedian) !== null && _b !== void 0 ? _b : -Infinity);
|
|
1979
|
+
}, 0);
|
|
1980
|
+
const layout = {
|
|
1981
|
+
width: undefined,
|
|
1982
|
+
height: height,
|
|
1983
|
+
autosize: true,
|
|
1984
|
+
margin: {
|
|
1985
|
+
l: 130,
|
|
1986
|
+
r: 35, // Balance between ensuring the mean annotation doesn't get cut off and having too much margin.
|
|
1987
|
+
t: title ? 50 : 5,
|
|
1988
|
+
b: 50,
|
|
1989
|
+
pad: 4,
|
|
1990
|
+
// ...extraLayoutConfig.margin, // Merge in any extra margin config provided via props
|
|
1991
|
+
},
|
|
1992
|
+
title: {
|
|
1993
|
+
text: title,
|
|
1994
|
+
font: {
|
|
1995
|
+
size: 16,
|
|
1996
|
+
},
|
|
1997
|
+
xref: "paper",
|
|
1998
|
+
x: 0.5,
|
|
1999
|
+
xanchor: "center",
|
|
2000
|
+
},
|
|
2001
|
+
xaxis: {
|
|
2002
|
+
title: {
|
|
2003
|
+
text: xAxisTitle,
|
|
2004
|
+
font: {
|
|
2005
|
+
size: 14,
|
|
2006
|
+
},
|
|
2007
|
+
},
|
|
2008
|
+
showgrid: true,
|
|
2009
|
+
showline: true,
|
|
2010
|
+
fixedrange: true, // Disable zooming
|
|
2011
|
+
zeroline: false,
|
|
2012
|
+
range: [
|
|
2013
|
+
startXAxisAtZero ? 0 : xRangeMin - (xRangeMax - xRangeMin) * 0.3,
|
|
2014
|
+
xRangeMax + (xRangeMax - xRangeMin) * 0.3,
|
|
2015
|
+
], // Add padding to the x-axis range
|
|
2016
|
+
mirror: true,
|
|
2017
|
+
gridcolor: "#efefef",
|
|
2018
|
+
gridwidth: 0.2,
|
|
2019
|
+
zerolinecolor: "#969696",
|
|
2020
|
+
zerolinewidth: 1,
|
|
2021
|
+
linecolor: "#bababa",
|
|
2022
|
+
linewidth: 1,
|
|
2023
|
+
},
|
|
2024
|
+
yaxis: {
|
|
2025
|
+
mirror: "ticks",
|
|
2026
|
+
gridcolor: "#efefef",
|
|
2027
|
+
gridwidth: 0.2,
|
|
2028
|
+
zerolinecolor: "#969696",
|
|
2029
|
+
zerolinewidth: 1,
|
|
2030
|
+
linecolor: "#bababa",
|
|
2031
|
+
linewidth: 1,
|
|
2032
|
+
showticklabels: true,
|
|
2033
|
+
showgrid: false,
|
|
2034
|
+
showline: true,
|
|
2035
|
+
zeroline: false,
|
|
2036
|
+
fixedrange: true, // Disable zooming and pan interactions
|
|
2037
|
+
tickmode: "array",
|
|
2038
|
+
tickvals: groups.map((_, index) => 0 + index * 0.1),
|
|
2039
|
+
ticktext: groups.map((group) => group.groupLabel),
|
|
2040
|
+
ticks: "inside",
|
|
2041
|
+
range: [-0.08, 0.08 + (groups.length - 1) * 0.1], // Add padding around the groups
|
|
2042
|
+
automargin: true,
|
|
2043
|
+
tickcolor: "white", // Hide default ticks since we're using them for group labels in the paired comparisons plot
|
|
2044
|
+
},
|
|
2045
|
+
hovermode: "y",
|
|
2046
|
+
};
|
|
2047
|
+
const containerStyles = Object.assign({ width: "100%", height: height, position: "relative", display: "flex", flexDirection: "column", gap: 0 }, containerStyleOverrides);
|
|
2048
|
+
const config = {
|
|
2049
|
+
responsive: true, // Enable responsive mode for width
|
|
2050
|
+
displayModeBar: false, // Hide the mode bar
|
|
2051
|
+
displaylogo: false, // Hide the Plotly logo
|
|
2052
|
+
scrollZoom: false, // Disable zooming with scroll
|
|
2053
|
+
staticPlot: false, // Enable interactivity
|
|
2054
|
+
};
|
|
2055
|
+
return (jsxRuntime.jsx("div", { ref: containerRef,
|
|
2056
|
+
// className={`plot-container ${plotId}`}
|
|
2057
|
+
style: Object.assign({}, containerStyles), children: jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx(Loading, {}), children: jsxRuntime.jsxs("div", { style: {
|
|
2058
|
+
position: "relative",
|
|
2059
|
+
width: "100%",
|
|
2060
|
+
height: "100%",
|
|
2061
|
+
}, children: [jsxRuntime.jsx(Plot$2, { data: plotlyData, layout: layout, config: config, useResizeHandler: true, onHover: handleHover, onUnhover: handleUnhover, style: {
|
|
2062
|
+
width: "100%",
|
|
2063
|
+
height: `${height}px`,
|
|
2064
|
+
display: "block",
|
|
2065
|
+
transition: "opacity 0.15s ease-in-out",
|
|
2066
|
+
} }, `boxplot-${plotId || "default"}`), tooltip.visible && (jsxRuntime.jsx("div", { style: {
|
|
2067
|
+
position: "fixed",
|
|
2068
|
+
left: tooltipPosition === "right" ? tooltip.x + 10 : undefined,
|
|
2069
|
+
right: tooltipPosition === "left"
|
|
2070
|
+
? window.innerWidth - tooltip.x + 10 // Measured in pixels from the right edge of the screen
|
|
2071
|
+
: undefined,
|
|
2072
|
+
transform: tooltipPosition === "right" ? "translateX(10px)" : undefined,
|
|
2073
|
+
top: tooltip.y - 18,
|
|
2074
|
+
backgroundColor: "rgba(255, 255, 255, 0.8)",
|
|
2075
|
+
borderRadius: "4px",
|
|
2076
|
+
color: "#333",
|
|
2077
|
+
padding: "8px 12px",
|
|
2078
|
+
fontSize: "13px",
|
|
2079
|
+
lineHeight: "1.5",
|
|
2080
|
+
pointerEvents: "none",
|
|
2081
|
+
zIndex: 1000,
|
|
2082
|
+
whiteSpace: "nowrap",
|
|
2083
|
+
boxShadow: "0 2px 4px rgba(0,0,0,0.2)",
|
|
2084
|
+
borderLeft: `7px solid ${tooltip.color}`,
|
|
2085
|
+
fontFamily: '"Open Sans", verdana, arial, sans-serif',
|
|
2086
|
+
}, dangerouslySetInnerHTML: { __html: tooltip.content } }))] }) }) }));
|
|
2087
|
+
};
|
|
2088
|
+
const SummaryComparisonPlotLegend = ({ comparedDataLabel, summarizedDataLabel, color = "orange", }) => {
|
|
2089
|
+
return (jsxRuntime.jsxs("div", { style: {
|
|
2090
|
+
display: "flex",
|
|
2091
|
+
gap: "20px",
|
|
2092
|
+
alignItems: "center",
|
|
2093
|
+
flexDirection: "row",
|
|
2094
|
+
}, children: [jsxRuntime.jsxs("div", { style: { display: "flex", gap: "5px", alignItems: "center" }, children: [jsxRuntime.jsx(Box, { width: 13, height: 13, sx: {
|
|
2095
|
+
backgroundColor: color,
|
|
2096
|
+
borderRadius: "20px",
|
|
2097
|
+
} }), jsxRuntime.jsx("span", { children: comparedDataLabel })] }), jsxRuntime.jsxs("div", { style: { display: "flex", gap: "5px", alignItems: "center" }, children: [jsxRuntime.jsx(Box, { width: 30, height: 3, sx: { backgroundColor: color } }), jsxRuntime.jsx("span", { children: summarizedDataLabel })] })] }));
|
|
2098
|
+
};
|
|
2099
|
+
|
|
2100
|
+
const Plot$1 = React.lazy(() => import('react-plotly.js'));
|
|
2101
|
+
const LinePlot = (props) => {
|
|
2102
|
+
var _a, _b;
|
|
2103
|
+
const { data, width = 600, height = 400, title, xAxisTitle, yAxisTitle, containerStyleOverrides, plotId = "boxplot", extraLayoutConfig = {}, additionalYAxis, additionalContextTrace, } = props;
|
|
2104
|
+
// Ref for plot container
|
|
2105
|
+
const containerRef = React.useRef(null);
|
|
2106
|
+
const plotlyData = data.map((trace) => {
|
|
2107
|
+
return {
|
|
2108
|
+
type: "scatter",
|
|
2109
|
+
mode: "lines+markers",
|
|
2110
|
+
x: trace.x,
|
|
2111
|
+
y: trace.y,
|
|
2112
|
+
name: trace.label,
|
|
2113
|
+
line: {
|
|
2114
|
+
color: trace.color || "#1f77b4", // Default to Plotly's default blue if no color provided
|
|
2115
|
+
},
|
|
2116
|
+
marker: {
|
|
2117
|
+
color: trace.color || "#1f77b4",
|
|
2118
|
+
size: 6,
|
|
2119
|
+
},
|
|
2120
|
+
zorder: 1, // Ensure lines are above any additional context traces (like scatter points)
|
|
2121
|
+
};
|
|
2122
|
+
});
|
|
2123
|
+
const layout = Object.assign(Object.assign(Object.assign({}, extraLayoutConfig), { title: {
|
|
2124
|
+
text: title,
|
|
2125
|
+
}, showlegend: false, autosize: true, width: undefined, height: undefined, hovermode: "closest", margin: {
|
|
2126
|
+
l: 50,
|
|
2127
|
+
r: additionalContextTrace ? 80 : 35, // Balance between ensuring the mean annotation doesn't get cut off and having too much margin.
|
|
2128
|
+
t: title ? 50 : 20, // Adjust top margin based on whether a title is provided
|
|
2129
|
+
b: 50,
|
|
2130
|
+
pad: 4,
|
|
2131
|
+
}, xaxis: {
|
|
2132
|
+
title: {
|
|
2133
|
+
text: xAxisTitle,
|
|
2134
|
+
},
|
|
2135
|
+
// range: displayXAxis, // Fixed range prevents axis shifting during interaction or data updates
|
|
2136
|
+
showgrid: true,
|
|
2137
|
+
zeroline: false,
|
|
2138
|
+
showline: true,
|
|
2139
|
+
mirror: "ticks",
|
|
2140
|
+
gridcolor: "#efefef",
|
|
2141
|
+
gridwidth: 0.2,
|
|
2142
|
+
zerolinecolor: "#969696",
|
|
2143
|
+
zerolinewidth: 1,
|
|
2144
|
+
linecolor: "#bababa",
|
|
2145
|
+
linewidth: 1,
|
|
2146
|
+
fixedrange: true, // Disable zooming
|
|
2147
|
+
ticklabelposition: "outside",
|
|
2148
|
+
}, yaxis: {
|
|
2149
|
+
title: {
|
|
2150
|
+
text: yAxisTitle || "", // Set default to empty string to prevent Plotly from adding its own default title
|
|
2151
|
+
standoff: 12, // Add space between title and axis
|
|
2152
|
+
font: {
|
|
2153
|
+
color: ((_a = data[0]) === null || _a === void 0 ? void 0 : _a.color) || "#1f77b4", // Match y-axis title color to line color
|
|
2154
|
+
},
|
|
2155
|
+
},
|
|
2156
|
+
automargin: true, // Required for standoff to work properly
|
|
2157
|
+
showgrid: true,
|
|
2158
|
+
zeroline: false,
|
|
2159
|
+
showline: true,
|
|
2160
|
+
mirror: "ticks",
|
|
2161
|
+
gridcolor: "#efefef",
|
|
2162
|
+
gridwidth: 0.2,
|
|
2163
|
+
zerolinecolor: "#969696",
|
|
2164
|
+
zerolinewidth: 1,
|
|
2165
|
+
linecolor: "#bababa",
|
|
2166
|
+
linewidth: 1,
|
|
2167
|
+
fixedrange: true, // Disable zooming
|
|
2168
|
+
tickfont: {
|
|
2169
|
+
color: ((_b = data[0]) === null || _b === void 0 ? void 0 : _b.color) || "#1f77b4", // Match y-axis tick color to line color
|
|
2170
|
+
},
|
|
2171
|
+
tickcolor: "white", // Hide default ticks since we're using them for group labels in the paired comparisons plot
|
|
2172
|
+
ticklen: 10, // Give ticks a length to push labels away from y axis.
|
|
2173
|
+
// ticksuffix: " ", // Add space between y axis and ticks
|
|
2174
|
+
} }), (additionalYAxis
|
|
2175
|
+
? { yaxis2: Object.assign(Object.assign({}, additionalYAxis), { side: "right", overlaying: "y" }) }
|
|
2176
|
+
: {}));
|
|
2177
|
+
const config = {
|
|
2178
|
+
responsive: true, // Make the plot responsive
|
|
2179
|
+
displayModeBar: false, // Hide the mode bar
|
|
2180
|
+
displaylogo: false, // Hide the Plotly logo
|
|
2181
|
+
scrollZoom: false, // Disable zooming with scroll
|
|
2182
|
+
staticPlot: false, // Enable interactivity
|
|
2183
|
+
};
|
|
2184
|
+
const containerStyles = Object.assign({ width: "100%", height: "100%", position: "relative" }, containerStyleOverrides);
|
|
2185
|
+
return (jsxRuntime.jsx("div", { ref: containerRef, style: Object.assign({}, containerStyles), children: jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx(Loading, {}), children: jsxRuntime.jsx("div", { style: {
|
|
2186
|
+
position: "relative",
|
|
2187
|
+
width: "100%",
|
|
2188
|
+
height: "100%",
|
|
2189
|
+
}, children: jsxRuntime.jsx(Plot$1, { data: [
|
|
2190
|
+
...(additionalContextTrace ? [additionalContextTrace] : []),
|
|
2191
|
+
...plotlyData,
|
|
2192
|
+
], layout: layout, config: config, useResizeHandler: true, style: {
|
|
2193
|
+
width: "100%",
|
|
2194
|
+
height: "100%",
|
|
2195
|
+
display: "block",
|
|
2196
|
+
transition: "opacity 0.15s ease-in-out",
|
|
2197
|
+
}, onHover: () => {
|
|
2198
|
+
console.log("hovering");
|
|
2199
|
+
} }, `boxplot-${plotId || "default"}`) }) }) }));
|
|
2200
|
+
};
|
|
2201
|
+
|
|
1450
2202
|
const Plot = React.lazy(() => import('react-plotly.js'));
|
|
1451
2203
|
const TestPlot = (props) => {
|
|
1452
2204
|
var _a, _b;
|
|
@@ -1466,9 +2218,14 @@ const TestPlot = (props) => {
|
|
|
1466
2218
|
return jsxRuntime.jsx(Plot, { data: data, layout: layout });
|
|
1467
2219
|
};
|
|
1468
2220
|
|
|
2221
|
+
exports.BoxPlot = BoxPlot;
|
|
1469
2222
|
exports.HistogramPlot = HistogramPlot;
|
|
2223
|
+
exports.LinePlot = LinePlot;
|
|
2224
|
+
exports.PairedComparisonsBoxPlot = PairedComparisonsBoxPlot;
|
|
1470
2225
|
exports.RadialHistogramPlot = RadialHistogramPlot;
|
|
1471
2226
|
exports.StatsDonut = StatsDonut;
|
|
2227
|
+
exports.SummaryComparisonPlot = SummaryComparisonPlot;
|
|
2228
|
+
exports.SummaryComparisonPlotLegend = SummaryComparisonPlotLegend;
|
|
1472
2229
|
exports.TestPlot = TestPlot;
|
|
1473
2230
|
exports.isDateArray = isDateArray;
|
|
1474
2231
|
exports.isNumberArray = isNumberArray;
|