td-plots 1.11.0 → 1.11.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.
@@ -14,4 +14,7 @@ export type LegendBoxPlotItemProps = LegendItemProps & {
14
14
  };
15
15
  export declare const LegendBoxPlotItem: (props: LegendBoxPlotItemProps) => import("react/jsx-runtime").JSX.Element;
16
16
  export declare const LegendColorItem: (props: LegendItemProps) => import("react/jsx-runtime").JSX.Element;
17
- export declare const LegendLineItem: (props: LegendItemProps) => import("react/jsx-runtime").JSX.Element;
17
+ export type LegendLineItemProps = LegendItemProps & {
18
+ strokeDasharray?: string;
19
+ };
20
+ export declare const LegendLineItem: (props: LegendLineItemProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,16 +1,16 @@
1
- type SummaryComparisonData = {
2
- comparedMedian?: number;
3
- summarizedMin?: number;
4
- summarizedMax?: number;
5
- summarizedMean?: number;
1
+ type BracketData = {
2
+ userValue?: number;
3
+ populationMin: number;
4
+ populationMax: number;
6
5
  };
7
- type SummaryComparisonGroup = {
6
+ type BracketDataWithMeta = {
8
7
  groupLabel: string;
9
- data: SummaryComparisonData;
8
+ data: BracketData;
10
9
  color?: string;
11
10
  };
12
11
  export type SummaryComparisonPlotProps = {
13
- groups: SummaryComparisonGroup[];
12
+ groups: BracketDataWithMeta[];
13
+ userColor?: string;
14
14
  width?: number;
15
15
  height?: number;
16
16
  title?: string;
@@ -21,6 +21,11 @@ export type SummaryComparisonPlotProps = {
21
21
  tooltipPosition?: "left" | "right";
22
22
  startXAxisAtZero?: boolean;
23
23
  unit?: string;
24
+ tooltipLabels?: {
25
+ populationMinLabel?: string;
26
+ populationMaxLabel?: string;
27
+ userValueLabel?: string;
28
+ };
24
29
  };
25
30
  declare const SummaryComparisonPlot: (props: SummaryComparisonPlotProps) => import("react/jsx-runtime").JSX.Element;
26
31
  export default SummaryComparisonPlot;
package/dist/index.d.ts CHANGED
@@ -19,6 +19,6 @@ export type { LineWithHistogramProps } from "./components/LineWithHistogram";
19
19
  export { default as BarPlot } from "./components/BarPlot";
20
20
  export type { BarPlotProps } from "./components/BarPlot";
21
21
  export { LegendColorItem, LegendLineItem } from "./components/LegendUtils";
22
- export type { LegendItemProps } from "./components/LegendUtils";
22
+ export type { LegendItemProps, LegendLineItemProps, } from "./components/LegendUtils";
23
23
  export { isDateArray, isNumberArray } from "./components/Utils";
24
24
  export type { PlotParams } from "react-plotly.js";
package/dist/index.esm.js CHANGED
@@ -1538,7 +1538,6 @@ const BoxPlot = (props) => {
1538
1538
  staticPlot: false, // Enable interactivity
1539
1539
  };
1540
1540
  const containerStyles = Object.assign({ width: "100%", height: "100%", position: "relative" }, containerStyleOverrides);
1541
- console.log("layout:", layout); // Debugging log to inspect the final layout object
1542
1541
  return (jsx("div", { ref: containerRef,
1543
1542
  // className={`plot-container ${plotId}`}
1544
1543
  style: Object.assign({}, containerStyles), children: jsx(Suspense, { fallback: jsx(Loading, {}), children: jsxs("div", { style: {
@@ -1619,6 +1618,7 @@ const LegendColorItem = (props) => {
1619
1618
  const LegendLineItem = (props) => {
1620
1619
  const lineWidth = props.width || 40;
1621
1620
  const lineHeight = props.height || 4;
1621
+ const dashArray = props.strokeDasharray || "";
1622
1622
  const labelKey = props.label == null
1623
1623
  ? ""
1624
1624
  : typeof props.label === "string"
@@ -1629,7 +1629,7 @@ const LegendLineItem = (props) => {
1629
1629
  alignItems: "center",
1630
1630
  mr: 4,
1631
1631
  fontFamily: "Open Sans, verdana, arial, sans-serif",
1632
- }, children: [jsx("svg", { width: lineWidth, height: lineHeight, style: { marginRight: 8 }, children: jsx("line", { x1: 0, y1: lineHeight / 2, x2: lineWidth, y2: lineHeight / 2, stroke: props.color, strokeWidth: lineHeight }) }), props.label] }, labelKey));
1632
+ }, children: [jsx("svg", { width: lineWidth, height: lineHeight, style: { marginRight: 8 }, children: jsx("line", { x1: 0, y1: lineHeight / 2, x2: lineWidth, y2: lineHeight / 2, stroke: props.color, strokeWidth: lineHeight, strokeDasharray: dashArray }) }), props.label] }, labelKey));
1633
1633
  };
1634
1634
 
1635
1635
  // This component takes grouped data and transforms it for displaying with the BoxPlot component.
@@ -1922,65 +1922,117 @@ const SplitBoxPlot = (props) => {
1922
1922
  });
1923
1923
  const totalGroups = groups.length + additionalGroups.length;
1924
1924
  const yAxisSplitProportion = additionalGroups.length > 0 ? groups.length / totalGroups : 1;
1925
+ // Render y-axis titles as annotations so we can specifically set xshift in px from the axis.
1926
+ // Calculating approximate label width in pixels to ensure the title is far enough away from the longest label,
1927
+ // with a minimum distance to look good even with short labels.
1928
+ const charWidth = 8; // ~px per character for Open Sans 14px (Plotly title font)
1929
+ const primaryMaxChars = Math.max(0, ...groups.map((g) => g.label.length));
1930
+ const secondaryMaxChars = additionalGroups.length > 0
1931
+ ? Math.max(0, ...additionalGroups.map((g) => g.label.length))
1932
+ : primaryMaxChars;
1933
+ const maxLabelChars = Math.max(primaryMaxChars, secondaryMaxChars);
1934
+ const approxLabelPx = maxLabelChars * charWidth;
1935
+ const yAxisTitleAnnotations = [];
1936
+ if (yAxisTitle) {
1937
+ const domainTop = additionalGroups.length > 0 ? yAxisSplitProportion - 0.01 : 1;
1938
+ yAxisTitleAnnotations.push({
1939
+ text: yAxisTitle,
1940
+ x: 0,
1941
+ xshift: -120,
1942
+ y: domainTop / 2,
1943
+ xref: "paper",
1944
+ yref: "paper",
1945
+ showarrow: false,
1946
+ textangle: "-90",
1947
+ xanchor: "center",
1948
+ yanchor: "middle",
1949
+ font: {
1950
+ family: '"Open Sans", verdana, arial, sans-serif',
1951
+ size: 14,
1952
+ color: "rgb(68, 68, 68)",
1953
+ },
1954
+ });
1955
+ }
1956
+ if (secondYAxisTitle && additionalGroups.length > 0) {
1957
+ yAxisTitleAnnotations.push({
1958
+ text: secondYAxisTitle,
1959
+ x: 0,
1960
+ xshift: -120,
1961
+ y: (yAxisSplitProportion + 1) / 2,
1962
+ xref: "paper",
1963
+ yref: "paper",
1964
+ showarrow: false,
1965
+ textangle: "-90",
1966
+ xanchor: "center",
1967
+ yanchor: "middle",
1968
+ font: {
1969
+ family: '"Open Sans", verdana, arial, sans-serif',
1970
+ size: 14,
1971
+ color: "rgb(68, 68, 68)",
1972
+ }, // styled to look like plotly's default title styling
1973
+ });
1974
+ }
1925
1975
  const extraLayoutConfig = Object.assign(Object.assign({ yaxis: {
1926
1976
  type: "linear", // Use linear axis for numeric positioning of boxes
1927
1977
  tickmode: "array",
1928
1978
  tickvals,
1929
1979
  ticktext,
1930
- automargin: true,
1980
+ // automargin: true,
1931
1981
  range: [-0.5, groups.length - 0.5], // Add some padding to the y-axis range to accommodate the boxes
1932
- domain: additionalGroups ? [0, yAxisSplitProportion - 0.01] : [0, 1], // Ensure primary y-axis takes full height
1982
+ domain: additionalGroups.length > 0 ? [0, yAxisSplitProportion - 0.01] : [0, 1], // Ensure primary y-axis takes full height
1933
1983
  tickcolor: "#ffffff",
1934
1984
  ticklen: 0,
1985
+ ticksuffix: "", // clear the BoxPlot default suffix so label widths match yaxis2
1935
1986
  showgrid: false,
1936
1987
  //@ts-ignore
1937
1988
  ticklabelstandoff: 7,
1938
1989
  ticklabelposition: "outside left",
1939
- title: {
1940
- text: yAxisTitle || "", // Set default to empty string to prevent Plotly from adding its own default title
1941
- standoff: 45,
1942
- },
1990
+ title: { text: "" }, // rendered via annotation instead
1943
1991
  }, xaxis: {
1944
1992
  anchor: "y", // Anchor to primary y-axis
1945
1993
  }, margin: {
1946
- t: title ? 50 : 40,
1947
- r: 50,
1994
+ t: title ? 50 : 70,
1995
+ r: 20,
1996
+ l: Math.max(130, approxLabelPx + 40),
1948
1997
  }, shapes: [
1949
1998
  ...separatorShapes,
1950
1999
  // ...differenceBetweenMediansLines,
1951
- ...xAnnotations.map((annotation) => ({
2000
+ ...xAnnotations.map((annotation, annotationIndex) => ({
1952
2001
  type: "line",
1953
2002
  x0: annotation.x,
1954
2003
  x1: annotation.x,
1955
2004
  y0: 0,
1956
- y1: 1.05,
2005
+ y1: 1.05 + annotationIndex * 0.05,
1957
2006
  xref: "x",
1958
2007
  yref: "paper",
1959
2008
  line: {
1960
2009
  color: annotation.color || "rgba(255, 0, 0, 0.7)",
1961
2010
  width: 2,
1962
- dash: "dash",
2011
+ dash: "dot",
1963
2012
  },
1964
2013
  })),
1965
2014
  ], annotations: [
2015
+ ...yAxisTitleAnnotations,
1966
2016
  // ...differenceAnnotations,
1967
- ...xAnnotations.map((annotation) => ({
2017
+ ...xAnnotations.map((annotation, annotationIndex) => ({
1968
2018
  x: annotation.x,
1969
- y: 1.06,
2019
+ y: 1,
1970
2020
  xref: "x",
1971
2021
  yref: "paper",
1972
2022
  text: annotation.text || "",
1973
2023
  showarrow: false,
1974
2024
  yanchor: "bottom",
2025
+ yshift: 18 + annotationIndex * 20, // stack in pixels — stable across screen sizes
1975
2026
  font: {
1976
2027
  color: annotation.color || "rgba(255, 0, 0, 0.7)",
1977
2028
  size: 12,
1978
2029
  },
1979
2030
  })),
1980
- ] }, (additionalGroups && {
2031
+ ] }, (additionalGroups.length > 0 && {
1981
2032
  xaxis2: {
1982
2033
  title: {
1983
2034
  text: secondXAxisTitle || "", // Set default to empty string to prevent Plotly from adding its own default title
2035
+ standoff: 10 + xAnnotations.length * 12, // Add extra space to accommodate annotations.
1984
2036
  },
1985
2037
  side: "top",
1986
2038
  anchor: "y2", // Anchor to secondary y-axis
@@ -1998,17 +2050,16 @@ const SplitBoxPlot = (props) => {
1998
2050
  automargin: true,
1999
2051
  matches: "x", // Match the range of xaxis to keep them synchronized
2000
2052
  },
2001
- })), (additionalGroups && {
2053
+ })), (additionalGroups.length > 0 && {
2002
2054
  yaxis2: {
2003
2055
  domain: [yAxisSplitProportion, 1], // Adjust domain for secondary y-axis (histogram)
2004
- title: {
2005
- text: secondYAxisTitle || "", // Set default to empty string to prevent Plotly from adding its own default title
2006
- standoff: 45, // Add space between title and axis
2007
- },
2056
+ title: { text: "" }, // rendered via annotation instead
2008
2057
  range: [-0.5, additionalGroups.length - 0.5], // Add some padding to the y-axis range to accommodate the boxes
2009
2058
  automargin: true, // Required for standoff to work properly
2010
2059
  showgrid: true,
2011
2060
  zeroline: false,
2061
+ ticklen: 0,
2062
+ ticksuffix: "",
2012
2063
  ticklabelposition: "outside left",
2013
2064
  showline: true,
2014
2065
  mirror: "ticks",
@@ -2025,15 +2076,15 @@ const SplitBoxPlot = (props) => {
2025
2076
  ticklabelstandoff: 7,
2026
2077
  },
2027
2078
  }));
2028
- console.log("extraLayoutConfig:", extraLayoutConfig);
2029
2079
  const containerStyles = Object.assign({ width: width, height: height, position: "relative", display: "flex", flexDirection: "column", gap: 0 }, containerStyleOverrides);
2030
- console.log(boxPlotData);
2031
2080
  return (jsx("div", { style: Object.assign({}, containerStyles), children: jsx(BoxPlot, { data: boxPlotData, width: width, height: height, title: title, xAxisTitle: xAxisTitle, yAxisTitle: yAxisTitle, extraLayoutConfig: extraLayoutConfig, containerStyleOverrides: containerStyleOverrides, plotId: `${plotId}-boxplot` }) }));
2032
2081
  };
2033
2082
 
2034
2083
  const Plot$2 = lazy(() => Promise.resolve().then(function () { return reactPlotlyWrapper$1; }));
2035
2084
  const SummaryComparisonPlot = (props) => {
2036
- const { groups, height = 250, title = "", xAxisTitle, yAxisTitle, containerStyleOverrides, plotId = "summary-comparison-plot", tooltipPosition = "right", startXAxisAtZero = true, unit = "", } = props;
2085
+ const { groups, userColor = "orange", height = 250, title = "", xAxisTitle, yAxisTitle, containerStyleOverrides, plotId = "summary-comparison-plot", tooltipPosition = "right", startXAxisAtZero = true, unit = "",
2086
+ // xAnnotations = [], TO DO
2087
+ tooltipLabels = {}, } = props;
2037
2088
  // Ref for plot container
2038
2089
  const containerRef = useRef(null);
2039
2090
  // State for custom tooltip
@@ -2063,40 +2114,42 @@ const SummaryComparisonPlot = (props) => {
2063
2114
  let tooltipX = event.event.clientX;
2064
2115
  let tooltipY = event.event.clientY;
2065
2116
  content += `<strong>${groupName}</strong><br/>`;
2066
- const summarizedMinText = groupData.summarizedMin
2067
- ? groupData.summarizedMin.toFixed(2)
2117
+ const populationMinText = groupData.populationMin !== undefined
2118
+ ? groupData.populationMin.toFixed(2)
2068
2119
  : "NA";
2069
- const summarizedMaxText = groupData.summarizedMax
2070
- ? groupData.summarizedMax.toFixed(2)
2071
- : "NA";
2072
- const comparedMedianText = groupData.comparedMedian !== undefined
2073
- ? groupData.comparedMedian.toFixed(2)
2120
+ const populationMaxText = groupData.populationMax !== undefined
2121
+ ? groupData.populationMax.toFixed(2)
2074
2122
  : "NA";
2075
2123
  content += `
2076
2124
  <table style="width: 100%; margin-top: 4px;">
2077
2125
  <tr>
2078
- <td style="text-align: left; padding: 2px 18px 2px 0;">Population avg-\u03C3:</td>
2079
- <td style="text-align: right; padding: 2px 0;">${summarizedMinText}</td>
2126
+ <td style="text-align: left; padding: 2px 18px 2px 0;">${tooltipLabels.populationMinLabel || "Population avg-\u03C3"}:</td>
2127
+ <td style="text-align: right; padding: 2px 0;">${populationMinText}</td>
2080
2128
  </tr>
2081
2129
  <tr>
2082
- <td style="text-align: left; padding: 2px 18px 2px 0;">Population avg+\u03C3:</td>
2083
- <td style="text-align: right; padding: 2px 0;">${summarizedMaxText}</td>
2130
+ <td style="text-align: left; padding: 2px 18px 2px 0;">${tooltipLabels.populationMaxLabel || "Population avg+\u03C3"}:</td>
2131
+ <td style="text-align: right; padding: 2px 0;">${populationMaxText}</td>
2084
2132
  </tr>
2133
+ `;
2134
+ if (groupData.userValue !== undefined) {
2135
+ const userValueText = groupData.userValue.toFixed(2);
2136
+ content += `
2085
2137
  <tr>
2086
- <td style="text-align: left; padding: 2px 18px 2px 0;">My Median:</td>
2087
- <td style="text-align: right; padding: 2px 0;">${comparedMedianText}</td>
2138
+ <td style="text-align: left; padding: 2px 18px 2px 0;">${tooltipLabels.userValueLabel || "My value"}:</td>
2139
+ <td style="text-align: right; padding: 2px 0;">${userValueText}</td>
2088
2140
  </tr>
2089
- </table>
2090
- `;
2141
+ `;
2142
+ }
2143
+ content += `</table></div>`;
2091
2144
  contentColor = eventData.line
2092
2145
  ? eventData.line.color
2093
2146
  : eventData.marker.color || contentColor;
2094
- // Position tooltip at the end of the line (75th percentile)
2147
+ // Position tooltip at the end of the line (populationMax for right tooltip, populationMin for left tooltip)
2095
2148
  // Use the xaxis d2p method to convert data coordinate to pixel coordinate
2096
2149
  if (point.xaxis && typeof point.xaxis.d2p === "function") {
2097
2150
  const pixelX = tooltipPosition === "right"
2098
- ? point.xaxis.d2p(groupData.summarizedMax)
2099
- : point.xaxis.d2p(groupData.summarizedMin);
2151
+ ? point.xaxis.d2p(groupData.populationMax)
2152
+ : point.xaxis.d2p(groupData.populationMin);
2100
2153
  const pixelY = point.yaxis.d2p(eventData.y[0]); // Use the y coordinate of the line for vertical positioning
2101
2154
  const containerRect = (_b = containerRef.current) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
2102
2155
  if (containerRect) {
@@ -2120,63 +2173,68 @@ const SummaryComparisonPlot = (props) => {
2120
2173
  // Transform the data into a format suitable for a plotly scatterplot
2121
2174
  const plotlyData = groups.flatMap((group, groupIndex) => {
2122
2175
  const traces = [];
2123
- if (group.data.summarizedMin !== undefined &&
2124
- group.data.summarizedMax !== undefined) {
2125
- traces.push({
2126
- type: "scatter",
2127
- mode: "lines",
2128
- name: group.groupLabel,
2129
- x: [group.data.summarizedMin, group.data.summarizedMax],
2130
- y: [groupIndex * 0.1, groupIndex * 0.1],
2131
- line: {
2132
- color: colorToRGBA(group.color || "orange", 1),
2133
- width: 6,
2134
- },
2135
- showlegend: false,
2136
- hoverinfo: "none",
2137
- });
2138
- }
2139
- if (group.data.comparedMedian !== undefined) {
2140
- traces.push({
2141
- type: "scatter",
2142
- mode: "markers",
2143
- name: group.groupLabel,
2144
- x: [group.data.comparedMedian],
2145
- y: [groupIndex * 0.1],
2146
- marker: {
2147
- color: colorToRGBA(group.color || "orange", 1),
2148
- size: 10,
2149
- symbol: "circle",
2150
- line: {
2151
- color: "white",
2152
- width: 1,
2153
- },
2154
- },
2155
- showlegend: false,
2156
- hoverinfo: "none",
2157
- });
2176
+ traces.push({
2177
+ type: "scatter",
2178
+ mode: "lines",
2179
+ name: group.groupLabel,
2180
+ x: [group.data.populationMin, group.data.populationMax],
2181
+ y: [groupIndex * 0.1, groupIndex * 0.1],
2182
+ line: {
2183
+ color: colorToRGBA(group.color || "orange", 1),
2184
+ width: 6,
2185
+ },
2186
+ showlegend: false,
2187
+ hoverinfo: "none",
2188
+ });
2189
+ if (group.data.userValue !== undefined) {
2190
+ const CIRCLE_RADIUS_PX = 6;
2158
2191
  comparedLines.push({
2159
2192
  type: "line",
2160
2193
  name: group.groupLabel,
2161
- x0: group.data.comparedMedian,
2162
- x1: group.data.comparedMedian,
2194
+ x0: group.data.userValue,
2195
+ x1: group.data.userValue,
2163
2196
  y0: 0,
2164
2197
  y1: 1.1,
2165
2198
  yref: "paper",
2166
2199
  line: {
2167
- color: group.color || "orange",
2200
+ color: colorToRGBA(userColor, 1),
2168
2201
  width: 2,
2169
2202
  },
2170
2203
  });
2204
+ comparedLines.push({
2205
+ type: "circle",
2206
+ xref: "x",
2207
+ yref: "y",
2208
+ // Pixel-mode: xanchor/yanchor are data coords for the center;
2209
+ // x0/x1/y0/y1 are pixel offsets, so the circle stays a fixed
2210
+ // visual size regardless of the axis scale.
2211
+ xsizemode: "pixel",
2212
+ ysizemode: "pixel",
2213
+ xanchor: group.data.userValue,
2214
+ yanchor: groupIndex * 0.1,
2215
+ x0: -CIRCLE_RADIUS_PX,
2216
+ x1: CIRCLE_RADIUS_PX,
2217
+ y0: -CIRCLE_RADIUS_PX,
2218
+ y1: CIRCLE_RADIUS_PX,
2219
+ fillcolor: colorToRGBA(userColor, 1),
2220
+ line: {
2221
+ color: "white",
2222
+ width: 1,
2223
+ },
2224
+ });
2171
2225
  comparedAnnotations.push({
2172
- x: group.data.comparedMedian,
2173
- y: 1.08,
2226
+ x: group.data.userValue,
2227
+ y: 1.1,
2174
2228
  xref: "x",
2175
2229
  yref: "paper",
2176
- text: `${group.data.comparedMedian.toFixed(2)} ${unit}`,
2230
+ text: `${group.data.userValue.toFixed(2)} ${unit}`,
2177
2231
  showarrow: false,
2178
2232
  xanchor: "center",
2179
2233
  yanchor: "bottom",
2234
+ font: {
2235
+ color: colorToRGBA(userColor, 1),
2236
+ size: 12,
2237
+ },
2180
2238
  });
2181
2239
  }
2182
2240
  return traces;
@@ -2184,9 +2242,10 @@ const SummaryComparisonPlot = (props) => {
2184
2242
  // Add tick label for the compared annotation value
2185
2243
  if (comparedAnnotations.length > 0) {
2186
2244
  comparedAnnotations.push({
2187
- x: -0.03, // Position to the left of the y-axis
2188
- y: 1.03,
2245
+ x: 0, // Position to the left of the y-axis
2246
+ y: 1.1, // Should match the y position of the compared median annotation
2189
2247
  xref: "paper",
2248
+ xshift: -10,
2190
2249
  yref: "paper",
2191
2250
  text: "My AVG",
2192
2251
  showarrow: false,
@@ -2194,14 +2253,17 @@ const SummaryComparisonPlot = (props) => {
2194
2253
  yanchor: "bottom",
2195
2254
  });
2196
2255
  }
2197
- const xRangeMin = groups.reduce((min, group) => {
2198
- var _a, _b;
2199
- return Math.min(min, (_a = group.data.summarizedMin) !== null && _a !== void 0 ? _a : Infinity, (_b = group.data.comparedMedian) !== null && _b !== void 0 ? _b : Infinity);
2200
- }, 0);
2256
+ // xRangeMin a little funky because we want to ensure a non-zero value is calculated
2257
+ // in case startXAxisAtZero is false. populationMin is our anchor and
2258
+ // if that is not defined then we can fall back to a min of 0. If userValue is not defined
2259
+ // we want to listen to populationMin for computing xRangeMin.
2260
+ const xRangeMin = groups.reduce((min, group) => { var _a; return Math.min(min, group.data.populationMin, (_a = group.data.userValue) !== null && _a !== void 0 ? _a : Infinity); }, Infinity);
2261
+ // Similar logic for xRangeMax. If nothing is defined, set the max to 0.
2262
+ // Otherwise, listen to populationMax and userValue if given.
2201
2263
  const xRangeMax = groups.reduce((max, group) => {
2202
- var _a, _b;
2203
- return Math.max(max, (_a = group.data.summarizedMax) !== null && _a !== void 0 ? _a : -Infinity, (_b = group.data.comparedMedian) !== null && _b !== void 0 ? _b : -Infinity);
2204
- }, 0);
2264
+ var _a;
2265
+ return Math.max(max, group.data.populationMax, (_a = group.data.userValue) !== null && _a !== void 0 ? _a : -Infinity);
2266
+ }, -Infinity);
2205
2267
  const layout = {
2206
2268
  width: undefined,
2207
2269
  height: height,
@@ -2250,6 +2312,12 @@ const SummaryComparisonPlot = (props) => {
2250
2312
  linewidth: 1,
2251
2313
  },
2252
2314
  yaxis: {
2315
+ title: {
2316
+ text: yAxisTitle,
2317
+ font: {
2318
+ size: 14,
2319
+ },
2320
+ },
2253
2321
  mirror: "ticks",
2254
2322
  gridcolor: "#efefef",
2255
2323
  gridwidth: 0.2,
@@ -2323,12 +2391,26 @@ const SummaryComparisonPlotLegend = ({ comparedDataLabel, summarizedDataLabel, c
2323
2391
  gap: "20px",
2324
2392
  alignItems: "center",
2325
2393
  flexDirection: "row",
2326
- }, children: [jsxs("div", { style: { display: "flex", gap: "5px", alignItems: "center" }, children: [jsx(Box, { sx: {
2394
+ }, children: [jsxs("div", { style: { display: "flex", gap: "5px", alignItems: "center" }, children: [jsxs("div", { style: {
2395
+ position: "relative",
2327
2396
  width: 13,
2328
- height: 13,
2329
- backgroundColor: color,
2330
- borderRadius: "20px",
2331
- } }), jsx("span", { children: comparedDataLabel })] }), jsxs("div", { style: { display: "flex", gap: "5px", alignItems: "center" }, children: [jsx(Box, { sx: { width: 30, height: 3, backgroundColor: color } }), jsx("span", { children: summarizedDataLabel })] })] }));
2397
+ height: 20,
2398
+ display: "flex",
2399
+ alignItems: "center",
2400
+ justifyContent: "center",
2401
+ }, children: [jsx(Box, { sx: {
2402
+ position: "absolute",
2403
+ width: 3,
2404
+ height: 20,
2405
+ backgroundColor: color,
2406
+ } }), jsx(Box, { sx: {
2407
+ position: "relative",
2408
+ width: 10,
2409
+ height: 10,
2410
+ backgroundColor: color,
2411
+ borderRadius: "20px",
2412
+ border: `1px solid white`,
2413
+ } })] }), jsx("span", { children: comparedDataLabel })] }), jsxs("div", { style: { display: "flex", gap: "5px", alignItems: "center" }, children: [jsx(Box, { sx: { width: 30, height: 5, backgroundColor: color } }), jsx("span", { children: summarizedDataLabel })] })] }));
2332
2414
  };
2333
2415
 
2334
2416
  const Plot$1 = lazy(() => Promise.resolve().then(function () { return reactPlotlyWrapper$1; }));
@@ -2569,21 +2651,37 @@ const BarPlot = (props) => {
2569
2651
  // Ref for plot container
2570
2652
  const containerRef = useRef(null);
2571
2653
  const plotMetaRef = useRef(null);
2654
+ // Pixel offsets from the x-axis line for the bar group shapes.
2655
+ // 45px clears tick marks + labels + axis title; the 15px band sits below all of them.
2656
+ const BAR_GROUP_SHAPE_TOP_PX = 8;
2657
+ const BAR_GROUP_SHAPE_HEIGHT_PX = 20;
2658
+ // px above the top of the plot area — lands between x2 tick labels (~15px) and x2 title (~35px)
2659
+ const X_ANNOTATION_TOP_PX = 22;
2660
+ // Tracks the rendered plot area height so we can convert the shape's fixed pixel
2661
+ // center back into a paper y coordinate for the annotation.
2662
+ const [plotAreaHeight, setPlotAreaHeight] = useState(300);
2572
2663
  const [barGroupTooltip, setBarGroupTooltip] = useState(null);
2573
2664
  const y2AxisPosition = 0.72; // Position of the secondary y-axis (histogram) as a fraction of the plot height
2574
2665
  const capturePlotMeta = (_figure, graphDiv) => {
2575
- var _a, _b, _c;
2666
+ var _a, _b, _c, _d, _e, _f;
2576
2667
  try {
2577
2668
  const m = (_a = graphDiv._fullLayout) === null || _a === void 0 ? void 0 : _a.margin;
2578
2669
  const r = (_c = (_b = graphDiv._fullLayout) === null || _b === void 0 ? void 0 : _b.xaxis) === null || _c === void 0 ? void 0 : _c.range;
2670
+ const h = (_d = graphDiv._fullLayout) === null || _d === void 0 ? void 0 : _d.height;
2579
2671
  if (m && r) {
2580
2672
  plotMetaRef.current = {
2581
2673
  xRange: r,
2582
2674
  margin: { l: m.l, r: m.r, t: m.t, b: m.b },
2675
+ height: (_f = h !== null && h !== void 0 ? h : (_e = containerRef.current) === null || _e === void 0 ? void 0 : _e.clientHeight) !== null && _f !== void 0 ? _f : 400,
2583
2676
  };
2677
+ if (h) {
2678
+ const newPlotH = h - m.t - m.b;
2679
+ if (newPlotH > 0)
2680
+ setPlotAreaHeight(newPlotH);
2681
+ }
2584
2682
  }
2585
2683
  }
2586
- catch (_d) { }
2684
+ catch (_g) { }
2587
2685
  };
2588
2686
  const handleBarGroupMouseMove = (e) => {
2589
2687
  var _a;
@@ -2593,18 +2691,18 @@ const BarPlot = (props) => {
2593
2691
  setBarGroupTooltip(null);
2594
2692
  return;
2595
2693
  }
2596
- const { xRange, margin } = plotMetaRef.current;
2694
+ const { xRange, margin, height: plotlyHeight } = plotMetaRef.current;
2597
2695
  const rect = containerRef.current.getBoundingClientRect();
2598
2696
  const mouseX = e.clientX - rect.left;
2599
2697
  const mouseY = e.clientY - rect.top;
2600
- const containerHeight = containerRef.current.clientHeight;
2601
2698
  const containerWidth = containerRef.current.clientWidth;
2602
2699
  const plotW = containerWidth - margin.l - margin.r;
2603
- containerHeight - margin.t - margin.b;
2604
- const plotBottom = containerHeight - margin.b;
2605
- // barGroupShapes sit at y0=-0.02, y1=-0.04 in paper coords (just below the x-axis line)
2606
- const stripTop = plotBottom - 10; // Plot bottom extends further than the x axis. Value here adjusted manually.
2607
- const stripBottom = plotBottom + 20; // approximate. Doesn't have to be perfect.
2700
+ // Use Plotly's own reported height so plotBottom is consistent with the margin values
2701
+ const plotBottom = plotlyHeight - margin.b - 35; // Subtracting and extra 35px to finally get it in the right spot.
2702
+ // Shapes use yanchor=0 (x-axis line) with negative pixel y values, which maps to
2703
+ // positive screen-y (downward). Strip bounds mirror those pixel offsets exactly.
2704
+ const stripTop = plotBottom;
2705
+ const stripBottom = plotBottom + BAR_GROUP_SHAPE_TOP_PX + BAR_GROUP_SHAPE_HEIGHT_PX + 5; // Give a little extra room on the bottom.
2608
2706
  if (mouseY < stripTop || mouseY > stripBottom) {
2609
2707
  setBarGroupTooltip(null);
2610
2708
  return;
@@ -2755,8 +2853,12 @@ const BarPlot = (props) => {
2755
2853
  yref: "paper",
2756
2854
  x0: group.min,
2757
2855
  x1: group.max > xMax ? xMax + barWidth : group.max,
2758
- y0: -0.02,
2759
- y1: -0.07,
2856
+ // Anchor at the x-axis line (paper y=0) so the pixel offset is consistent
2857
+ // regardless of plot height — avoids overlap with tick labels on small screens.
2858
+ yanchor: 0,
2859
+ ysizemode: "pixel",
2860
+ y0: -BAR_GROUP_SHAPE_TOP_PX,
2861
+ y1: -28,
2760
2862
  fillcolor: group.color,
2761
2863
  line: {
2762
2864
  width: 1,
@@ -2769,7 +2871,10 @@ const BarPlot = (props) => {
2769
2871
  xref: "x",
2770
2872
  yref: "paper",
2771
2873
  x: barGroups[0].min + barDataWidth / 2,
2772
- y: -0.043,
2874
+ // Convert the pixel center of the shape band to a paper y coordinate using
2875
+ // the actual rendered plot area height so it stays inside the shape on any screen size.
2876
+ y: -18 /
2877
+ plotAreaHeight,
2773
2878
  text: barGroupTooltipTitle,
2774
2879
  showarrow: false,
2775
2880
  yanchor: "middle",
@@ -2808,7 +2913,9 @@ const BarPlot = (props) => {
2808
2913
  l: 50,
2809
2914
  r: 35, // Balance between ensuring the mean annotation doesn't get cut off and having too much margin.
2810
2915
  t: 40 + (title ? 50 : 0), // Add extra top margin if there is a title
2811
- b: 50,
2916
+ b: barGroups.length > 0
2917
+ ? BAR_GROUP_SHAPE_TOP_PX + BAR_GROUP_SHAPE_HEIGHT_PX + 15
2918
+ : 50,
2812
2919
  pad: 4,
2813
2920
  }, xaxis: {
2814
2921
  title: {
@@ -2827,12 +2934,15 @@ const BarPlot = (props) => {
2827
2934
  linewidth: 1,
2828
2935
  fixedrange: true, // Disable zooming
2829
2936
  automargin: true, // Adjust margin if tick labels rotate
2830
- ticklen: 21,
2937
+ ticklen: barGroups.length > 0
2938
+ ? BAR_GROUP_SHAPE_TOP_PX + BAR_GROUP_SHAPE_HEIGHT_PX
2939
+ : 8, // tick length in px.
2831
2940
  tickcolor: "white",
2832
2941
  } }, (data2 && {
2833
2942
  xaxis2: {
2834
2943
  title: {
2835
2944
  text: xAxis2Title,
2945
+ standoff: 22,
2836
2946
  },
2837
2947
  side: "top",
2838
2948
  anchor: "y2", // Anchor to secondary y-axis
@@ -2853,7 +2963,7 @@ const BarPlot = (props) => {
2853
2963
  })), { yaxis: {
2854
2964
  title: {
2855
2965
  text: yAxisTitle !== null && yAxisTitle !== void 0 ? yAxisTitle : "Count",
2856
- standoff: 12, // Add space between title and axis
2966
+ standoff: 15, // Add space between title and axis
2857
2967
  },
2858
2968
  domain: data2 ? [0, y2AxisPosition - 0.01] : [0, 1], // Ensure primary y-axis takes full height
2859
2969
  automargin: true, // Required for standoff to work properly
@@ -2900,7 +3010,7 @@ const BarPlot = (props) => {
2900
3010
  x0: annotation.x,
2901
3011
  x1: annotation.x,
2902
3012
  y0: 0,
2903
- y1: 1.05,
3013
+ y1: 1 + (X_ANNOTATION_TOP_PX - 4) / plotAreaHeight,
2904
3014
  xref: "x",
2905
3015
  yref: "paper",
2906
3016
  line: {
@@ -2913,7 +3023,7 @@ const BarPlot = (props) => {
2913
3023
  ...(barGroupShapeAnnotation ? [barGroupShapeAnnotation] : []),
2914
3024
  ...xAnnotations.map((annotation) => ({
2915
3025
  x: annotation.x,
2916
- y: 1.06,
3026
+ y: 1 + X_ANNOTATION_TOP_PX / plotAreaHeight,
2917
3027
  xref: "x",
2918
3028
  yref: "paper",
2919
3029
  text: annotation.text || "",