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/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;height:100%!important;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:hover .histogram-controls.show-on-hover{opacity:1;visibility:visible}";
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$3 = lazy(() => import('react-plotly.js'));
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].map((n) => n * ONEDAY).concat([defaultBinSize])
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$3, { data: plotlyData, layout: layout, config: config, onSelected: handleSelection, onClick: handleClick, onDeselect: () => {
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$2 = lazy(() => import('react-plotly.js'));
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$2, { data: plotlyData, layout: layout, config: config, onSelected: onSelected, onClick: onClick, useResizeHandler: true, style: {
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$1 = lazy(() => import('react-plotly.js'));
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$1, { data: [
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
- const TestPlot = (props) => {
1450
- var _a, _b;
1451
- const data = [
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
- x: [1, 2, 3, 4],
1454
- y: [10, 15, 13, 17],
1455
- type: 'bar',
1456
- marker: { color: 'blue' },
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 layout = {
1460
- title: { text: 'Test Bar Plot' },
1461
- xaxis: { title: { text: (_a = props.xaxisTitle) !== null && _a !== void 0 ? _a : 'X Axis' } },
1462
- yaxis: { title: { text: (_b = props.yaxisTitle) !== null && _b !== void 0 ? _b : 'Y Axis' } },
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
- return jsx(Plot, { data: data, layout: layout });
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, TestPlot, isDateArray, isNumberArray };
2322
+ export { BoxPlot, HistogramPlot, LineWithHistogram, PairedComparisonsBoxPlot, RadialHistogramPlot, StatsDonut, SummaryComparisonPlot, SummaryComparisonPlotLegend, isDateArray, isNumberArray };
1468
2323
  //# sourceMappingURL=index.esm.js.map