td-plots 1.11.1 → 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.
@@ -18,7 +18,6 @@ export interface SplitBoxPlotProps {
18
18
  text?: string;
19
19
  color?: string;
20
20
  }[];
21
- yTitleLocationOverride?: number;
22
21
  }
23
22
  declare const SplitBoxPlot: (props: SplitBoxPlotProps) => import("react/jsx-runtime").JSX.Element;
24
23
  export default SplitBoxPlot;
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: {
@@ -1819,7 +1818,7 @@ const PairedComparisonsBoxPlot = (props) => {
1819
1818
  // This component shows a set of main boxplots as one collection, then allowes for additional
1820
1819
  // boxes to be shown in a subplot above the main boxplots for comparison.
1821
1820
  const SplitBoxPlot = (props) => {
1822
- const { groups, additionalGroups = [], width = 600, height = 400, title = "", xAxisTitle, yAxisTitle, secondXAxisTitle, secondYAxisTitle, containerStyleOverrides, plotId = "paired-comparisons-boxplot", xAnnotations = [], yTitleLocationOverride, } = props;
1821
+ const { groups, additionalGroups = [], width = 600, height = 400, title = "", xAxisTitle, yAxisTitle, secondXAxisTitle, secondYAxisTitle, containerStyleOverrides, plotId = "paired-comparisons-boxplot", xAnnotations = [], } = props;
1823
1822
  // Transform the grouped data into an array for BoxPlot
1824
1823
  const boxPlotData = [
1825
1824
  ...groups.flatMap((group, groupIndex) => {
@@ -1923,8 +1922,9 @@ const SplitBoxPlot = (props) => {
1923
1922
  });
1924
1923
  const totalGroups = groups.length + additionalGroups.length;
1925
1924
  const yAxisSplitProportion = additionalGroups.length > 0 ? groups.length / totalGroups : 1;
1926
- // Render y-axis titles as annotations so both always land at the same paper x,
1927
- // Using this annotations approach because we found the standoff prop to be unreliable across subplots with different label widths.
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
1928
  const charWidth = 8; // ~px per character for Open Sans 14px (Plotly title font)
1929
1929
  const primaryMaxChars = Math.max(0, ...groups.map((g) => g.label.length));
1930
1930
  const secondaryMaxChars = additionalGroups.length > 0
@@ -1932,16 +1932,13 @@ const SplitBoxPlot = (props) => {
1932
1932
  : primaryMaxChars;
1933
1933
  const maxLabelChars = Math.max(primaryMaxChars, secondaryMaxChars);
1934
1934
  const approxLabelPx = maxLabelChars * charWidth;
1935
- const leftMargin = approxLabelPx + 75; // tick labels + title + gaps
1936
- const plotContentWidth = Math.max(1, width - leftMargin - 50);
1937
- // 0.83 corrects for charWidth=8 overestimating rendered tick-label width (Open Sans 12px ≈ 6.6px/char)
1938
- const titleXPaper = -(approxLabelPx * 0.83) / plotContentWidth;
1939
1935
  const yAxisTitleAnnotations = [];
1940
1936
  if (yAxisTitle) {
1941
1937
  const domainTop = additionalGroups.length > 0 ? yAxisSplitProportion - 0.01 : 1;
1942
1938
  yAxisTitleAnnotations.push({
1943
1939
  text: yAxisTitle,
1944
- x: yTitleLocationOverride || titleXPaper,
1940
+ x: 0,
1941
+ xshift: -120,
1945
1942
  y: domainTop / 2,
1946
1943
  xref: "paper",
1947
1944
  yref: "paper",
@@ -1959,7 +1956,8 @@ const SplitBoxPlot = (props) => {
1959
1956
  if (secondYAxisTitle && additionalGroups.length > 0) {
1960
1957
  yAxisTitleAnnotations.push({
1961
1958
  text: secondYAxisTitle,
1962
- x: yTitleLocationOverride || titleXPaper,
1959
+ x: 0,
1960
+ xshift: -120,
1963
1961
  y: (yAxisSplitProportion + 1) / 2,
1964
1962
  xref: "paper",
1965
1963
  yref: "paper",
@@ -1979,7 +1977,7 @@ const SplitBoxPlot = (props) => {
1979
1977
  tickmode: "array",
1980
1978
  tickvals,
1981
1979
  ticktext,
1982
- automargin: true,
1980
+ // automargin: true,
1983
1981
  range: [-0.5, groups.length - 0.5], // Add some padding to the y-axis range to accommodate the boxes
1984
1982
  domain: additionalGroups.length > 0 ? [0, yAxisSplitProportion - 0.01] : [0, 1], // Ensure primary y-axis takes full height
1985
1983
  tickcolor: "#ffffff",
@@ -1994,8 +1992,8 @@ const SplitBoxPlot = (props) => {
1994
1992
  anchor: "y", // Anchor to primary y-axis
1995
1993
  }, margin: {
1996
1994
  t: title ? 50 : 70,
1997
- r: 50,
1998
- l: leftMargin,
1995
+ r: 20,
1996
+ l: Math.max(130, approxLabelPx + 40),
1999
1997
  }, shapes: [
2000
1998
  ...separatorShapes,
2001
1999
  // ...differenceBetweenMediansLines,
@@ -2018,12 +2016,13 @@ const SplitBoxPlot = (props) => {
2018
2016
  // ...differenceAnnotations,
2019
2017
  ...xAnnotations.map((annotation, annotationIndex) => ({
2020
2018
  x: annotation.x,
2021
- y: 1.05 + annotationIndex * 0.05, // Stack annotations above the plot with some spacing
2019
+ y: 1,
2022
2020
  xref: "x",
2023
2021
  yref: "paper",
2024
2022
  text: annotation.text || "",
2025
2023
  showarrow: false,
2026
2024
  yanchor: "bottom",
2025
+ yshift: 18 + annotationIndex * 20, // stack in pixels — stable across screen sizes
2027
2026
  font: {
2028
2027
  color: annotation.color || "rgba(255, 0, 0, 0.7)",
2029
2028
  size: 12,
@@ -2243,9 +2242,10 @@ const SummaryComparisonPlot = (props) => {
2243
2242
  // Add tick label for the compared annotation value
2244
2243
  if (comparedAnnotations.length > 0) {
2245
2244
  comparedAnnotations.push({
2246
- x: -0.03, // Position to the left of the y-axis
2245
+ x: 0, // Position to the left of the y-axis
2247
2246
  y: 1.1, // Should match the y position of the compared median annotation
2248
2247
  xref: "paper",
2248
+ xshift: -10,
2249
2249
  yref: "paper",
2250
2250
  text: "My AVG",
2251
2251
  showarrow: false,
@@ -2651,21 +2651,37 @@ const BarPlot = (props) => {
2651
2651
  // Ref for plot container
2652
2652
  const containerRef = useRef(null);
2653
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);
2654
2663
  const [barGroupTooltip, setBarGroupTooltip] = useState(null);
2655
2664
  const y2AxisPosition = 0.72; // Position of the secondary y-axis (histogram) as a fraction of the plot height
2656
2665
  const capturePlotMeta = (_figure, graphDiv) => {
2657
- var _a, _b, _c;
2666
+ var _a, _b, _c, _d, _e, _f;
2658
2667
  try {
2659
2668
  const m = (_a = graphDiv._fullLayout) === null || _a === void 0 ? void 0 : _a.margin;
2660
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;
2661
2671
  if (m && r) {
2662
2672
  plotMetaRef.current = {
2663
2673
  xRange: r,
2664
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,
2665
2676
  };
2677
+ if (h) {
2678
+ const newPlotH = h - m.t - m.b;
2679
+ if (newPlotH > 0)
2680
+ setPlotAreaHeight(newPlotH);
2681
+ }
2666
2682
  }
2667
2683
  }
2668
- catch (_d) { }
2684
+ catch (_g) { }
2669
2685
  };
2670
2686
  const handleBarGroupMouseMove = (e) => {
2671
2687
  var _a;
@@ -2675,18 +2691,18 @@ const BarPlot = (props) => {
2675
2691
  setBarGroupTooltip(null);
2676
2692
  return;
2677
2693
  }
2678
- const { xRange, margin } = plotMetaRef.current;
2694
+ const { xRange, margin, height: plotlyHeight } = plotMetaRef.current;
2679
2695
  const rect = containerRef.current.getBoundingClientRect();
2680
2696
  const mouseX = e.clientX - rect.left;
2681
2697
  const mouseY = e.clientY - rect.top;
2682
- const containerHeight = containerRef.current.clientHeight;
2683
2698
  const containerWidth = containerRef.current.clientWidth;
2684
2699
  const plotW = containerWidth - margin.l - margin.r;
2685
- containerHeight - margin.t - margin.b;
2686
- const plotBottom = containerHeight - margin.b;
2687
- // barGroupShapes sit at y0=-0.02, y1=-0.04 in paper coords (just below the x-axis line)
2688
- const stripTop = plotBottom - 10; // Plot bottom extends further than the x axis. Value here adjusted manually.
2689
- 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.
2690
2706
  if (mouseY < stripTop || mouseY > stripBottom) {
2691
2707
  setBarGroupTooltip(null);
2692
2708
  return;
@@ -2837,8 +2853,12 @@ const BarPlot = (props) => {
2837
2853
  yref: "paper",
2838
2854
  x0: group.min,
2839
2855
  x1: group.max > xMax ? xMax + barWidth : group.max,
2840
- y0: -0.02,
2841
- 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,
2842
2862
  fillcolor: group.color,
2843
2863
  line: {
2844
2864
  width: 1,
@@ -2851,7 +2871,10 @@ const BarPlot = (props) => {
2851
2871
  xref: "x",
2852
2872
  yref: "paper",
2853
2873
  x: barGroups[0].min + barDataWidth / 2,
2854
- 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,
2855
2878
  text: barGroupTooltipTitle,
2856
2879
  showarrow: false,
2857
2880
  yanchor: "middle",
@@ -2890,7 +2913,9 @@ const BarPlot = (props) => {
2890
2913
  l: 50,
2891
2914
  r: 35, // Balance between ensuring the mean annotation doesn't get cut off and having too much margin.
2892
2915
  t: 40 + (title ? 50 : 0), // Add extra top margin if there is a title
2893
- b: 50,
2916
+ b: barGroups.length > 0
2917
+ ? BAR_GROUP_SHAPE_TOP_PX + BAR_GROUP_SHAPE_HEIGHT_PX + 15
2918
+ : 50,
2894
2919
  pad: 4,
2895
2920
  }, xaxis: {
2896
2921
  title: {
@@ -2909,12 +2934,15 @@ const BarPlot = (props) => {
2909
2934
  linewidth: 1,
2910
2935
  fixedrange: true, // Disable zooming
2911
2936
  automargin: true, // Adjust margin if tick labels rotate
2912
- ticklen: 21,
2937
+ ticklen: barGroups.length > 0
2938
+ ? BAR_GROUP_SHAPE_TOP_PX + BAR_GROUP_SHAPE_HEIGHT_PX
2939
+ : 8, // tick length in px.
2913
2940
  tickcolor: "white",
2914
2941
  } }, (data2 && {
2915
2942
  xaxis2: {
2916
2943
  title: {
2917
2944
  text: xAxis2Title,
2945
+ standoff: 22,
2918
2946
  },
2919
2947
  side: "top",
2920
2948
  anchor: "y2", // Anchor to secondary y-axis
@@ -2935,7 +2963,7 @@ const BarPlot = (props) => {
2935
2963
  })), { yaxis: {
2936
2964
  title: {
2937
2965
  text: yAxisTitle !== null && yAxisTitle !== void 0 ? yAxisTitle : "Count",
2938
- standoff: 12, // Add space between title and axis
2966
+ standoff: 15, // Add space between title and axis
2939
2967
  },
2940
2968
  domain: data2 ? [0, y2AxisPosition - 0.01] : [0, 1], // Ensure primary y-axis takes full height
2941
2969
  automargin: true, // Required for standoff to work properly
@@ -2982,7 +3010,7 @@ const BarPlot = (props) => {
2982
3010
  x0: annotation.x,
2983
3011
  x1: annotation.x,
2984
3012
  y0: 0,
2985
- y1: 1.05,
3013
+ y1: 1 + (X_ANNOTATION_TOP_PX - 4) / plotAreaHeight,
2986
3014
  xref: "x",
2987
3015
  yref: "paper",
2988
3016
  line: {
@@ -2995,7 +3023,7 @@ const BarPlot = (props) => {
2995
3023
  ...(barGroupShapeAnnotation ? [barGroupShapeAnnotation] : []),
2996
3024
  ...xAnnotations.map((annotation) => ({
2997
3025
  x: annotation.x,
2998
- y: 1.06,
3026
+ y: 1 + X_ANNOTATION_TOP_PX / plotAreaHeight,
2999
3027
  xref: "x",
3000
3028
  yref: "paper",
3001
3029
  text: annotation.text || "",