openseries 2.1.0__tar.gz → 2.1.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openseries
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: Tools for analyzing financial timeseries.
5
5
  License-Expression: BSD-3-Clause
6
6
  License-File: LICENSE.md
@@ -21,7 +21,6 @@ if TYPE_CHECKING: # pragma: no cover
21
21
 
22
22
  from pandas import DataFrame, Index, Series, Timestamp, concat
23
23
  from plotly.io import to_html # type: ignore[import-untyped]
24
- from plotly.offline import plot # type: ignore[import-untyped]
25
24
  from plotly.subplots import make_subplots # type: ignore[import-untyped]
26
25
 
27
26
  from .load_plotly import load_plotly_dict
@@ -418,7 +417,168 @@ def _prepare_table_data(
418
417
  return cleanedtablevalues, columns, aligning, color_lst
419
418
 
420
419
 
421
- def _build_mobile_layout_figure(
420
+ def _configure_figure_layout(
421
+ figure: Figure,
422
+ copied: OpenFrame,
423
+ *,
424
+ add_logo: bool,
425
+ vertical_legend: bool,
426
+ title: str | None,
427
+ ) -> None:
428
+ """Configure figure layout with logo, legend, and title.
429
+
430
+ Args:
431
+ figure: Plotly figure to configure.
432
+ copied: Copied OpenFrame data.
433
+ add_logo: Whether to add logo.
434
+ vertical_legend: Whether to use vertical legend.
435
+ title: Optional title for the figure.
436
+ """
437
+ fig, logo = load_plotly_dict()
438
+
439
+ if add_logo:
440
+ logo_copy = logo.copy()
441
+ figure.add_layout_image(logo_copy)
442
+
443
+ figure.update_layout(fig.get("layout"))
444
+ colorway: list[str] = cast("dict[str, list[str]]", fig["layout"]).get(
445
+ "colorway",
446
+ [],
447
+ )
448
+
449
+ if vertical_legend:
450
+ legend = {
451
+ "yanchor": "bottom",
452
+ "y": -0.04,
453
+ "xanchor": "right",
454
+ "x": 0.98,
455
+ "orientation": "v",
456
+ }
457
+ else:
458
+ legend = {
459
+ "yanchor": "bottom",
460
+ "y": -0.2,
461
+ "xanchor": "right",
462
+ "x": 0.98,
463
+ "orientation": "h",
464
+ }
465
+
466
+ layout_updates: dict[str, object] = {
467
+ "legend": legend,
468
+ "colorway": colorway[: copied.item_count],
469
+ "autosize": True,
470
+ "margin": {"l": 50, "r": 50, "t": 80, "b": 50, "pad": 10},
471
+ }
472
+
473
+ figure.update_layout(**layout_updates)
474
+
475
+ figure.update_xaxes(gridcolor="#EEEEEE", automargin=True, tickangle=-45)
476
+ figure.update_yaxes(tickformat=".2%", gridcolor="#EEEEEE", automargin=True)
477
+ title_size = 36
478
+
479
+ if title:
480
+ figure.update_layout(
481
+ {"title": {"text": f"<b>{title}</b><br>", "font": {"size": title_size}}},
482
+ )
483
+
484
+
485
+ def _get_bar_dataframe(
486
+ copied: OpenFrame,
487
+ bar_freq: LiteralBizDayFreq,
488
+ ) -> DataFrame:
489
+ """Get bar chart DataFrame based on year fraction.
490
+
491
+ Args:
492
+ copied: Copied OpenFrame data.
493
+ bar_freq: The date offset string for bar plot frequency.
494
+
495
+ Returns:
496
+ Bar chart DataFrame.
497
+ """
498
+ quarter_of_year = 0.25
499
+ if copied.yearfrac < quarter_of_year:
500
+ tmp = copied.from_deepcopy()
501
+ return tmp.value_to_ret().tsdf.iloc[1:]
502
+ return calendar_period_returns(data=copied, freq=bar_freq)
503
+
504
+
505
+ def _build_desktop_figure(
506
+ copied: OpenFrame,
507
+ bdf: DataFrame,
508
+ cleanedtablevalues: list[list[str]],
509
+ columns: list[str],
510
+ aligning: list[str],
511
+ color_lst: list[str],
512
+ ) -> Figure:
513
+ """Build the desktop figure with plots and table.
514
+
515
+ Args:
516
+ copied: Copied OpenFrame data.
517
+ bdf: Bar chart DataFrame.
518
+ cleanedtablevalues: Table cell values.
519
+ columns: Table column headers.
520
+ aligning: Table cell alignment.
521
+ color_lst: Table cell colors.
522
+
523
+ Returns:
524
+ Desktop figure with plots and table.
525
+ """
526
+ figure = make_subplots(
527
+ rows=2,
528
+ cols=2,
529
+ specs=[
530
+ [{"type": "xy"}, {"rowspan": 2, "type": "table"}],
531
+ [{"type": "xy"}, None],
532
+ ],
533
+ )
534
+
535
+ for item, lbl in enumerate(copied.columns_lvl_zero):
536
+ figure.add_scatter(
537
+ x=copied.tsdf.index,
538
+ y=copied.tsdf.iloc[:, item],
539
+ hovertemplate="%{y:.2%}<br>%{x|%Y-%m-%d}",
540
+ line={"width": 2.5, "dash": "solid"},
541
+ mode="lines",
542
+ name=lbl,
543
+ showlegend=True,
544
+ row=1,
545
+ col=1,
546
+ )
547
+
548
+ for item in range(copied.item_count):
549
+ col_name = cast("tuple[str, ValueType]", bdf.iloc[:, item].name)
550
+ figure.add_bar(
551
+ x=bdf.index,
552
+ y=bdf.iloc[:, item],
553
+ hovertemplate="%{y:.2%}<br>%{x}",
554
+ name=col_name[0],
555
+ showlegend=False,
556
+ row=2,
557
+ col=1,
558
+ )
559
+
560
+ figure.add_table(
561
+ header={
562
+ "values": columns,
563
+ "align": "center",
564
+ "fill_color": "grey",
565
+ "font": {"color": "white"},
566
+ },
567
+ cells={
568
+ "values": cleanedtablevalues,
569
+ "align": aligning,
570
+ "height": 25,
571
+ "fill_color": color_lst,
572
+ "font": {"color": ["white"] + ["black"] * len(columns)},
573
+ },
574
+ row=1,
575
+ col=2,
576
+ )
577
+
578
+ return figure
579
+
580
+
581
+ def _build_mobile_figure(
422
582
  copied: OpenFrame,
423
583
  bdf: DataFrame,
424
584
  *,
@@ -426,7 +586,7 @@ def _build_mobile_layout_figure(
426
586
  vertical_legend: bool,
427
587
  title: str | None,
428
588
  ) -> Figure:
429
- """Build a mobile-optimized vertical layout figure with only charts.
589
+ """Build the mobile figure with charts only.
430
590
 
431
591
  Args:
432
592
  copied: Copied OpenFrame data.
@@ -436,7 +596,7 @@ def _build_mobile_layout_figure(
436
596
  title: Optional title for the figure.
437
597
 
438
598
  Returns:
439
- Mobile layout figure (without table).
599
+ Mobile layout figure.
440
600
  """
441
601
  plot_height = 400
442
602
  bar_height = 350
@@ -482,26 +642,105 @@ def _build_mobile_layout_figure(
482
642
  col=1,
483
643
  )
484
644
 
485
- _configure_figure_layout(
486
- figure_mobile,
487
- copied,
488
- add_logo=add_logo,
489
- vertical_legend=vertical_legend,
490
- title=title,
491
- mobile=True,
492
- total_min_height=total_min_height,
645
+ fig, logo = load_plotly_dict()
646
+
647
+ if add_logo:
648
+ logo_copy = logo.copy()
649
+ logo_copy["x"] = 0.99
650
+ logo_copy["xanchor"] = "right"
651
+ figure_mobile.add_layout_image(logo_copy)
652
+
653
+ figure_mobile.update_layout(fig.get("layout"))
654
+ colorway: list[str] = cast("dict[str, list[str]]", fig["layout"]).get(
655
+ "colorway",
656
+ [],
493
657
  )
494
658
 
659
+ if vertical_legend:
660
+ legend = {
661
+ "yanchor": "bottom",
662
+ "y": -0.04,
663
+ "xanchor": "right",
664
+ "x": 0.98,
665
+ "orientation": "v",
666
+ }
667
+ else:
668
+ legend = {
669
+ "yanchor": "bottom",
670
+ "y": -0.2,
671
+ "xanchor": "right",
672
+ "x": 0.98,
673
+ "orientation": "h",
674
+ }
675
+
676
+ layout_updates: dict[str, object] = {
677
+ "legend": legend,
678
+ "colorway": colorway[: copied.item_count],
679
+ "autosize": False,
680
+ "height": total_min_height,
681
+ "margin": {"l": 50, "r": 50, "t": 80, "b": 50, "pad": 10},
682
+ }
683
+
684
+ figure_mobile.update_layout(**layout_updates)
685
+
686
+ figure_mobile.update_xaxes(
687
+ gridcolor="#EEEEEE",
688
+ automargin=True,
689
+ tickangle=-45,
690
+ row=1,
691
+ col=1,
692
+ )
693
+ figure_mobile.update_xaxes(
694
+ gridcolor="#EEEEEE",
695
+ automargin=True,
696
+ tickangle=-45,
697
+ row=2,
698
+ col=1,
699
+ )
700
+ figure_mobile.update_yaxes(
701
+ tickformat=".2%",
702
+ gridcolor="#EEEEEE",
703
+ automargin=True,
704
+ row=1,
705
+ col=1,
706
+ )
707
+ figure_mobile.update_yaxes(
708
+ tickformat=".2%",
709
+ gridcolor="#EEEEEE",
710
+ automargin=True,
711
+ row=2,
712
+ col=1,
713
+ )
714
+
715
+ plot_height = 400
716
+ bar_height = 350
717
+ spacing = 0.08
718
+ plot_domain_top = 1.0
719
+ plot_domain_bottom = 1.0 - (plot_height / total_min_height)
720
+ bar_domain_top = plot_domain_bottom - spacing
721
+ bar_domain_bottom = bar_domain_top - (bar_height / total_min_height)
722
+
723
+ figure_mobile.update_layout(
724
+ yaxis_domain=[plot_domain_bottom, plot_domain_top],
725
+ yaxis2_domain=[bar_domain_bottom, bar_domain_top],
726
+ )
727
+
728
+ title_size = 24
729
+ if title:
730
+ figure_mobile.update_layout(
731
+ {"title": {"text": f"<b>{title}</b><br>", "font": {"size": title_size}}},
732
+ )
733
+
495
734
  return figure_mobile
496
735
 
497
736
 
498
- def _generate_html_table(
737
+ def _generate_html_table_string(
499
738
  cleanedtablevalues: list[list[str]],
500
739
  columns: list[str],
501
740
  aligning: list[str],
502
741
  color_lst: list[str],
503
742
  ) -> str:
504
- """Generate an HTML table from table data.
743
+ """Generate HTML table string for mobile layout.
505
744
 
506
745
  Args:
507
746
  cleanedtablevalues: Table cell values.
@@ -562,148 +801,28 @@ def _generate_html_table(
562
801
  return table_html
563
802
 
564
803
 
565
- def _configure_figure_layout(
566
- figure: Figure,
567
- copied: OpenFrame,
568
- *,
569
- add_logo: bool,
570
- vertical_legend: bool,
571
- title: str | None,
572
- mobile: bool = False,
573
- total_min_height: int | None = None,
574
- table_min_height: int | None = None,
575
- ) -> None:
576
- """Configure figure layout with logo, legend, and title.
804
+ def _get_output_directory(directory: Path | None) -> Path:
805
+ """Get the output directory path.
577
806
 
578
807
  Args:
579
- figure: Plotly figure to configure.
580
- copied: Copied OpenFrame data.
581
- add_logo: Whether to add logo.
582
- vertical_legend: Whether to use vertical legend.
583
- title: Optional title for the figure.
584
- mobile: Whether this is a mobile layout. Defaults to False.
585
- total_min_height: Minimum total height for mobile layout in pixels.
586
- table_min_height: Minimum height for table subplot in pixels.
587
- """
588
- fig, logo = load_plotly_dict()
589
-
590
- if add_logo:
591
- figure.add_layout_image(logo)
592
-
593
- figure.update_layout(fig.get("layout"))
594
- colorway: list[str] = cast("dict[str, list[str]]", fig["layout"]).get(
595
- "colorway",
596
- [],
597
- )
598
-
599
- if vertical_legend:
600
- legend = {
601
- "yanchor": "bottom",
602
- "y": -0.04,
603
- "xanchor": "right",
604
- "x": 0.98,
605
- "orientation": "v",
606
- }
607
- else:
608
- legend = {
609
- "yanchor": "bottom",
610
- "y": -0.2,
611
- "xanchor": "right",
612
- "x": 0.98,
613
- "orientation": "h",
614
- }
808
+ directory: Optional directory path.
615
809
 
616
- if mobile:
617
- if vertical_legend:
618
- legend = {
619
- "yanchor": "top",
620
- "y": 1.02,
621
- "xanchor": "left",
622
- "x": 0,
623
- "orientation": "h",
624
- }
625
- else:
626
- legend = {
627
- "yanchor": "top",
628
- "y": 1.02,
629
- "xanchor": "left",
630
- "x": 0,
631
- "orientation": "h",
632
- }
633
-
634
- layout_updates: dict[str, object] = {
635
- "legend": legend,
636
- "colorway": colorway[: copied.item_count],
637
- "autosize": True,
638
- "margin": {"l": 50, "r": 50, "t": 80, "b": 50, "pad": 10},
639
- }
640
-
641
- if mobile and total_min_height is not None:
642
- layout_updates["height"] = total_min_height
643
- layout_updates["autosize"] = False
644
-
645
- figure.update_layout(**layout_updates)
646
-
647
- if mobile:
648
- figure.update_xaxes(
649
- gridcolor="#EEEEEE",
650
- automargin=True,
651
- tickangle=-45,
652
- row=1,
653
- col=1,
654
- )
655
- figure.update_xaxes(
656
- gridcolor="#EEEEEE",
657
- automargin=True,
658
- tickangle=-45,
659
- row=2,
660
- col=1,
661
- )
662
- figure.update_yaxes(
663
- tickformat=".2%",
664
- gridcolor="#EEEEEE",
665
- automargin=True,
666
- row=1,
667
- col=1,
668
- )
669
- figure.update_yaxes(
670
- tickformat=".2%",
671
- gridcolor="#EEEEEE",
672
- automargin=True,
673
- row=2,
674
- col=1,
675
- )
676
- if table_min_height is not None and total_min_height is not None:
677
- plot_height = 400
678
- bar_height = 350
679
- spacing = 0.08
680
- plot_domain_top = 1.0
681
- plot_domain_bottom = 1.0 - (plot_height / total_min_height)
682
- bar_domain_top = plot_domain_bottom - spacing
683
- bar_domain_bottom = bar_domain_top - (bar_height / total_min_height)
684
-
685
- figure.update_layout(
686
- yaxis_domain=[plot_domain_bottom, plot_domain_top],
687
- yaxis2_domain=[bar_domain_bottom, bar_domain_top],
688
- )
689
- title_size = 24
690
- else:
691
- figure.update_xaxes(gridcolor="#EEEEEE", automargin=True, tickangle=-45)
692
- figure.update_yaxes(tickformat=".2%", gridcolor="#EEEEEE", automargin=True)
693
- title_size = 36
694
-
695
- if title:
696
- figure.update_layout(
697
- {"title": {"text": f"<b>{title}</b><br>", "font": {"size": title_size}}},
698
- )
810
+ Returns:
811
+ Resolved directory path.
812
+ """
813
+ if directory:
814
+ return Path(directory).resolve()
815
+ if Path.home().joinpath("Documents").exists():
816
+ return Path.home() / "Documents"
817
+ return Path(stack()[2].filename).parent
699
818
 
700
819
 
701
- def _generate_responsive_html(
820
+ def _generate_responsive_html_string(
702
821
  html_desktop: str,
703
822
  html_mobile: str,
704
823
  div_id_desktop: str,
705
824
  div_id_mobile: str,
706
- table_html: str | None = None,
825
+ table_html: str,
707
826
  ) -> str:
708
827
  """Generate responsive HTML wrapper with CSS and JavaScript.
709
828
 
@@ -712,20 +831,21 @@ def _generate_responsive_html(
712
831
  html_mobile: Mobile layout HTML.
713
832
  div_id_desktop: Desktop container div ID.
714
833
  div_id_mobile: Mobile container div ID.
715
- table_html: Optional HTML table for mobile layout.
834
+ table_html: HTML table string for mobile layout.
716
835
 
717
836
  Returns:
718
837
  Responsive HTML string.
719
838
  """
720
839
  desktop_style = "width:100%;"
721
840
  mobile_style = "width:100%; display:none;"
722
- match_media = 'window.matchMedia("(max-width: 960px)").matches'
841
+ match_media = (
842
+ '(window.matchMedia("(max-width: 960px)").matches || '
843
+ '"ontouchstart" in window || navigator.maxTouchPoints > 0)'
844
+ )
723
845
  desktop_get = f'document.getElementById("{div_id_desktop}_container")'
724
846
  mobile_get = f'document.getElementById("{div_id_mobile}_container")'
725
847
 
726
- mobile_content = html_mobile
727
- if table_html:
728
- mobile_content += f"\n{table_html}"
848
+ mobile_content = html_mobile + f"\n{table_html}"
729
849
 
730
850
  return (
731
851
  f'<div id="{div_id_desktop}_container" '
@@ -738,7 +858,7 @@ def _generate_responsive_html(
738
858
  f"</div>\n"
739
859
  "<style>\n"
740
860
  "body { overflow-y: auto; }\n"
741
- "@media (max-width: 960px) {\n"
861
+ "@media (max-width: 960px), (pointer: coarse), (hover: none) {\n"
742
862
  " .plotly-desktop { display: none !important; }\n"
743
863
  " .plotly-mobile { display: block !important; "
744
864
  "overflow: visible !important; }\n"
@@ -762,13 +882,55 @@ def _generate_responsive_html(
762
882
  " .plotly-mobile [style*='overflow'] { "
763
883
  "overflow: visible !important; overflow-y: visible !important; }\n"
764
884
  "}\n"
765
- "@media (min-width: 961px) {\n"
885
+ "@media (min-width: 961px) and (pointer: fine) and (hover: hover) {\n"
766
886
  " .plotly-desktop { display: block !important; }\n"
767
887
  " .plotly-mobile { display: none !important; }\n"
888
+ " .plotly-desktop .js-plotly-plot { "
889
+ "overflow: hidden !important; overflow-y: hidden !important; "
890
+ "overflow-x: hidden !important; }\n"
891
+ " .plotly-desktop .js-plotly-plot > div { "
892
+ "overflow: hidden !important; overflow-y: hidden !important; "
893
+ "overflow-x: hidden !important; max-height: 100% !important; }\n"
894
+ " .plotly-desktop .js-plotly-plot svg { "
895
+ "overflow: hidden !important; }\n"
896
+ " .plotly-desktop * { "
897
+ "overflow-y: hidden !important; }\n"
898
+ ' .plotly-desktop [style*="overflow"] { '
899
+ "overflow: hidden !important; overflow-y: hidden !important; }\n"
900
+ " .plotly-desktop .scrollbar-kit { "
901
+ "display: none !important; visibility: hidden !important; "
902
+ "opacity: 0 !important; }\n"
903
+ " .plotly-desktop .scrollbar-slider { "
904
+ "display: none !important; visibility: hidden !important; "
905
+ "opacity: 0 !important; }\n"
906
+ " .plotly-desktop .scrollbar-glyph { "
907
+ "display: none !important; visibility: hidden !important; "
908
+ "opacity: 0 !important; }\n"
768
909
  "}\n"
769
910
  "</style>\n"
770
911
  "<script>\n"
771
912
  "(function() {\n"
913
+ " function adjustLogoPosition(container) {\n"
914
+ " if (!container) return;\n"
915
+ " var plots = container.querySelectorAll('.js-plotly-plot');\n"
916
+ " plots.forEach(function(plot) {\n"
917
+ " var svg = plot.querySelector('svg');\n"
918
+ " if (!svg) return;\n"
919
+ " var images = svg.querySelectorAll('image[xref=\"paper\"]');\n"
920
+ " images.forEach(function(img) {\n"
921
+ " var isNarrow = window.matchMedia("
922
+ "'(max-width: 960px)').matches;\n"
923
+ " if (isNarrow) {\n"
924
+ " img.setAttribute('x', '0.99');\n"
925
+ " img.setAttribute('xanchor', 'right');\n"
926
+ " } else {\n"
927
+ " img.setAttribute('x', '0.01');\n"
928
+ " img.removeAttribute('xanchor');\n"
929
+ " }\n"
930
+ " });\n"
931
+ " });\n"
932
+ " }\n"
933
+ "\n"
772
934
  " function updateLayout() {\n"
773
935
  f" var isMobile = {match_media};\n"
774
936
  f" var desktopContainer = {desktop_get};\n"
@@ -789,50 +951,272 @@ def _generate_responsive_html(
789
951
  'desktopContainer.style.display = "block";\n'
790
952
  " if (mobileContainer) "
791
953
  'mobileContainer.style.display = "none";\n'
954
+ " disableTableScrolling(desktopContainer);\n"
955
+ " setTimeout(function() { "
956
+ "adjustTableSize(desktopContainer); "
957
+ "adjustLogoPosition(desktopContainer); }, 100);\n"
792
958
  " }\n"
959
+ " adjustLogoPosition(desktopContainer);\n"
960
+ " }\n"
961
+ "\n"
962
+ " function adjustTableSize(container) {\n"
963
+ " if (!container) return;\n"
964
+ " \n"
965
+ " function forceNoScroll() {\n"
966
+ " var scrollbarKits = container.querySelectorAll("
967
+ "'.scrollbar-kit');\n"
968
+ " scrollbarKits.forEach(function(el) {\n"
969
+ " el.style.setProperty('display', 'none', "
970
+ "'important');\n"
971
+ " el.style.setProperty('visibility', 'hidden', "
972
+ "'important');\n"
973
+ " el.style.setProperty('opacity', '0', 'important');\n"
974
+ " });\n"
975
+ " \n"
976
+ " var scrollbarSliders = container.querySelectorAll("
977
+ "'.scrollbar-slider');\n"
978
+ " scrollbarSliders.forEach(function(el) {\n"
979
+ " el.style.setProperty('display', 'none', "
980
+ "'important');\n"
981
+ " el.style.setProperty('visibility', 'hidden', "
982
+ "'important');\n"
983
+ " el.style.setProperty('opacity', '0', 'important');\n"
984
+ " });\n"
985
+ " \n"
986
+ " var scrollbarGlyphs = container.querySelectorAll("
987
+ "'.scrollbar-glyph');\n"
988
+ " scrollbarGlyphs.forEach(function(el) {\n"
989
+ " el.style.setProperty('display', 'none', "
990
+ "'important');\n"
991
+ " el.style.setProperty('visibility', 'hidden', "
992
+ "'important');\n"
993
+ " el.style.setProperty('opacity', '0', 'important');\n"
994
+ " });\n"
995
+ " \n"
996
+ " var allElements = container.querySelectorAll('*');\n"
997
+ " allElements.forEach(function(el) {\n"
998
+ " var computed = window.getComputedStyle(el);\n"
999
+ " if (computed.overflowY === 'auto' || "
1000
+ "computed.overflowY === 'scroll' || computed.overflow === 'auto' || "
1001
+ "computed.overflow === 'scroll') {\n"
1002
+ " el.style.setProperty('overflow', 'hidden', "
1003
+ "'important');\n"
1004
+ " el.style.setProperty('overflow-y', 'hidden', "
1005
+ "'important');\n"
1006
+ " el.style.setProperty('overflow-x', 'hidden', "
1007
+ "'important');\n"
1008
+ " }\n"
1009
+ " if (el.scrollHeight > el.clientHeight && "
1010
+ "el.clientHeight > 0) {\n"
1011
+ " el.style.setProperty('max-height', "
1012
+ "el.clientHeight + 'px', 'important');\n"
1013
+ " el.style.setProperty('overflow', 'hidden', "
1014
+ "'important');\n"
1015
+ " el.style.setProperty('overflow-y', 'hidden', "
1016
+ "'important');\n"
1017
+ " }\n"
1018
+ " });\n"
1019
+ " }\n"
1020
+ " \n"
1021
+ " var plots = container.querySelectorAll('.js-plotly-plot');\n"
1022
+ " plots.forEach(function(plot) {\n"
1023
+ " var svg = plot.querySelector('svg');\n"
1024
+ " if (!svg) return;\n"
1025
+ " var tableGroup = svg.querySelector('g[class*=\"table\"]');\n"
1026
+ " if (!tableGroup) return;\n"
1027
+ " var plotRect = plot.getBoundingClientRect();\n"
1028
+ " if (plotRect.height === 0) return;\n"
1029
+ " \n"
1030
+ " function adjustTable() {\n"
1031
+ " try {\n"
1032
+ " forceNoScroll();\n"
1033
+ " \n"
1034
+ " var availableHeight = plotRect.height - 100;\n"
1035
+ " if (availableHeight <= 0) return;\n"
1036
+ " \n"
1037
+ " var rows = tableGroup.querySelectorAll("
1038
+ "'g[class*=\"row\"]');\n"
1039
+ " if (rows.length === 0) return;\n"
1040
+ " \n"
1041
+ " var rowCount = rows.length;\n"
1042
+ " var calculatedRowHeight = availableHeight / rowCount;\n"
1043
+ " var minRowHeight = 18;\n"
1044
+ " var maxRowHeight = 35;\n"
1045
+ " var rowHeight = Math.max(minRowHeight, "
1046
+ "Math.min(maxRowHeight, calculatedRowHeight));\n"
1047
+ " \n"
1048
+ " var baseFontSize = 12;\n"
1049
+ " var fontScale = rowHeight / 25;\n"
1050
+ " var fontSize = baseFontSize * fontScale;\n"
1051
+ " var minFontSize = 9;\n"
1052
+ " var maxFontSize = 14;\n"
1053
+ " fontSize = Math.max(minFontSize, "
1054
+ "Math.min(maxFontSize, fontSize));\n"
1055
+ " \n"
1056
+ " rows.forEach(function(row, rowIndex) {\n"
1057
+ " var yPos = rowIndex * rowHeight;\n"
1058
+ " var rects = row.querySelectorAll('rect');\n"
1059
+ " var texts = row.querySelectorAll('text');\n"
1060
+ " \n"
1061
+ " rects.forEach(function(rect) {\n"
1062
+ " var currentHeight = "
1063
+ "parseFloat(rect.getAttribute('height') || '25');\n"
1064
+ " if (currentHeight > 0) {\n"
1065
+ " rect.setAttribute('height', "
1066
+ "rowHeight.toString());\n"
1067
+ " var currentY = "
1068
+ "parseFloat(rect.getAttribute('y') || '0');\n"
1069
+ " var rowBaseY = Math.floor(currentY / "
1070
+ "currentHeight) * rowHeight;\n"
1071
+ " rect.setAttribute('y', "
1072
+ "rowBaseY.toString());\n"
1073
+ " }\n"
1074
+ " });\n"
1075
+ " \n"
1076
+ " texts.forEach(function(text) {\n"
1077
+ " text.setAttribute('font-size', "
1078
+ "fontSize.toString());\n"
1079
+ " });\n"
1080
+ " });\n"
1081
+ " \n"
1082
+ " var existingTransform = "
1083
+ "tableGroup.getAttribute('transform') || '';\n"
1084
+ " var translateMatch = "
1085
+ "existingTransform.match(/translate\\(([^)]+)\\)/);\n"
1086
+ " var translate = translateMatch ? "
1087
+ "translateMatch[0] : 'translate(0,0)';\n"
1088
+ " tableGroup.setAttribute('transform', translate);\n"
1089
+ " \n"
1090
+ " forceNoScroll();\n"
1091
+ " } catch (e) {\n"
1092
+ " console.log('Table adjustment error:', e);\n"
1093
+ " }\n"
1094
+ " }\n"
1095
+ " \n"
1096
+ " setTimeout(adjustTable, 200);\n"
1097
+ " setTimeout(adjustTable, 600);\n"
1098
+ " setTimeout(adjustTable, 1200);\n"
1099
+ " setTimeout(adjustTable, 2000);\n"
1100
+ " });\n"
793
1101
  " }\n"
794
1102
  "\n"
795
1103
  " function disableTableScrolling(container) {\n"
796
1104
  " if (!container) return;\n"
1105
+ " var isDesktop = container.classList && "
1106
+ "container.classList.contains('plotly-desktop');\n"
1107
+ " var overflowValue = isDesktop ? 'hidden' : 'visible';\n"
797
1108
  " var plots = container.querySelectorAll('.js-plotly-plot');\n"
798
1109
  " plots.forEach(function(plot) {\n"
799
1110
  " var allElements = plot.querySelectorAll('*');\n"
800
1111
  " allElements.forEach(function(el) {\n"
801
- " var style = window.getComputedStyle(el);\n"
802
- " var overflow = style.overflow;\n"
803
- " var overflowY = style.overflowY;\n"
804
- " if (overflow === 'auto' || overflow === 'scroll' || "
1112
+ " if (el.tagName === 'DIV' || el.tagName === 'SVG') {\n"
1113
+ " var style = window.getComputedStyle(el);\n"
1114
+ " var overflow = style.overflow;\n"
1115
+ " var overflowY = style.overflowY;\n"
1116
+ " if (overflow === 'auto' || overflow === 'scroll' || "
805
1117
  "overflowY === 'auto' || overflowY === 'scroll') {\n"
806
- " el.style.setProperty('overflow', 'visible', "
807
- "'important');\n"
808
- " el.style.setProperty('overflow-y', 'visible', "
809
- "'important');\n"
810
- " el.style.setProperty('overflow-x', 'visible', "
811
- "'important');\n"
1118
+ " el.style.setProperty('overflow', "
1119
+ "overflowValue, 'important');\n"
1120
+ " el.style.setProperty('overflow-y', "
1121
+ "overflowValue, 'important');\n"
1122
+ " el.style.setProperty('overflow-x', "
1123
+ "overflowValue, 'important');\n"
1124
+ " }\n"
812
1125
  " }\n"
813
1126
  " });\n"
814
- " plot.style.setProperty('overflow', 'visible', 'important');\n"
815
- " plot.style.setProperty('overflow-y', 'visible', "
1127
+ " plot.style.setProperty('overflow', overflowValue, "
1128
+ "'important');\n"
1129
+ " plot.style.setProperty('overflow-y', overflowValue, "
816
1130
  "'important');\n"
817
1131
  " var plotDivs = plot.querySelectorAll('div');\n"
818
1132
  " plotDivs.forEach(function(div) {\n"
819
- " div.style.setProperty('overflow', 'visible', "
1133
+ " div.style.setProperty('overflow', overflowValue, "
820
1134
  "'important');\n"
821
- " div.style.setProperty('overflow-y', 'visible', "
1135
+ " div.style.setProperty('overflow-y', overflowValue, "
822
1136
  "'important');\n"
823
1137
  " });\n"
824
1138
  " var svgs = plot.querySelectorAll('svg');\n"
825
1139
  " svgs.forEach(function(svg) {\n"
826
- " svg.style.setProperty('overflow', 'visible', 'important');\n"
827
- " svg.setAttribute('overflow', 'visible');\n"
1140
+ " svg.style.setProperty('overflow', overflowValue, "
1141
+ "'important');\n"
1142
+ " svg.setAttribute('overflow', overflowValue);\n"
828
1143
  " });\n"
829
1144
  " });\n"
830
1145
  " }\n"
831
1146
  "\n"
832
1147
  " function setupScrollObserver(container) {\n"
833
1148
  " if (!container) return;\n"
1149
+ " var isDesktop = container.classList && "
1150
+ "container.classList.contains('plotly-desktop');\n"
1151
+ " \n"
1152
+ " function preventScrollbars() {\n"
1153
+ " if (isDesktop) {\n"
1154
+ " var scrollbarKits = container.querySelectorAll("
1155
+ "'.scrollbar-kit');\n"
1156
+ " scrollbarKits.forEach(function(el) {\n"
1157
+ " el.style.setProperty('display', 'none', "
1158
+ "'important');\n"
1159
+ " el.style.setProperty('visibility', 'hidden', "
1160
+ "'important');\n"
1161
+ " el.style.setProperty('opacity', '0', 'important');\n"
1162
+ " });\n"
1163
+ " \n"
1164
+ " var scrollbarSliders = container.querySelectorAll("
1165
+ "'.scrollbar-slider');\n"
1166
+ " scrollbarSliders.forEach(function(el) {\n"
1167
+ " el.style.setProperty('display', 'none', "
1168
+ "'important');\n"
1169
+ " el.style.setProperty('visibility', 'hidden', "
1170
+ "'important');\n"
1171
+ " el.style.setProperty('opacity', '0', 'important');\n"
1172
+ " });\n"
1173
+ " \n"
1174
+ " var scrollbarGlyphs = container.querySelectorAll("
1175
+ "'.scrollbar-glyph');\n"
1176
+ " scrollbarGlyphs.forEach(function(el) {\n"
1177
+ " el.style.setProperty('display', 'none', "
1178
+ "'important');\n"
1179
+ " el.style.setProperty('visibility', 'hidden', "
1180
+ "'important');\n"
1181
+ " el.style.setProperty('opacity', '0', 'important');\n"
1182
+ " });\n"
1183
+ " \n"
1184
+ " var plots = container.querySelectorAll('.js-plotly-plot');\n"
1185
+ " plots.forEach(function(plot) {\n"
1186
+ " var allElements = plot.querySelectorAll('*');\n"
1187
+ " allElements.forEach(function(el) {\n"
1188
+ " if (el.tagName === 'DIV' || "
1189
+ "el.tagName === 'SVG') {\n"
1190
+ " var style = window.getComputedStyle(el);\n"
1191
+ " if (style.overflow === 'auto' || "
1192
+ "style.overflow === 'scroll' || style.overflowY === 'auto' || "
1193
+ "style.overflowY === 'scroll') {\n"
1194
+ " el.style.setProperty('overflow', "
1195
+ "'hidden', 'important');\n"
1196
+ " el.style.setProperty('overflow-y', "
1197
+ "'hidden', 'important');\n"
1198
+ " el.style.setProperty('overflow-x', "
1199
+ "'hidden', 'important');\n"
1200
+ " }\n"
1201
+ " }\n"
1202
+ " });\n"
1203
+ " plot.style.setProperty('overflow', 'hidden', "
1204
+ "'important');\n"
1205
+ " var svg = plot.querySelector('svg');\n"
1206
+ " if (svg) {\n"
1207
+ " svg.style.setProperty('overflow', 'hidden', "
1208
+ "'important');\n"
1209
+ " svg.setAttribute('overflow', 'hidden');\n"
1210
+ " }\n"
1211
+ " });\n"
1212
+ " adjustTableSize(container);\n"
1213
+ " } else {\n"
1214
+ " disableTableScrolling(container);\n"
1215
+ " }\n"
1216
+ " }\n"
1217
+ " \n"
834
1218
  " var observer = new MutationObserver(function(mutations) {\n"
835
- " disableTableScrolling(container);\n"
1219
+ " preventScrollbars();\n"
836
1220
  " });\n"
837
1221
  " observer.observe(container, {\n"
838
1222
  " childList: true,\n"
@@ -840,13 +1224,25 @@ def _generate_responsive_html(
840
1224
  " attributes: true,\n"
841
1225
  " attributeFilter: ['style', 'class']\n"
842
1226
  " });\n"
1227
+ " preventScrollbars();\n"
843
1228
  " return observer;\n"
844
1229
  " }\n"
845
1230
  "\n"
846
- " window.addEventListener('resize', updateLayout);\n"
1231
+ " window.addEventListener('resize', function() {\n"
1232
+ " updateLayout();\n"
1233
+ " setTimeout(function() {\n"
1234
+ f" var desktopContainer = {desktop_get};\n"
1235
+ " if (desktopContainer && "
1236
+ 'desktopContainer.style.display !== "none") {\n'
1237
+ " adjustTableSize(desktopContainer);\n"
1238
+ " adjustLogoPosition(desktopContainer);\n"
1239
+ " }\n"
1240
+ " }, 100);\n"
1241
+ " });\n"
847
1242
  " updateLayout();\n"
848
1243
  " var checkInterval = setInterval(function() {\n"
849
1244
  f" var mobileContainer = {mobile_get};\n"
1245
+ f" var desktopContainer = {desktop_get};\n"
850
1246
  " if (mobileContainer && "
851
1247
  'mobileContainer.style.display !== "none") {\n'
852
1248
  " disableTableScrolling(mobileContainer);\n"
@@ -855,6 +1251,15 @@ def _generate_responsive_html(
855
1251
  "setupScrollObserver(mobileContainer);\n"
856
1252
  " }\n"
857
1253
  " }\n"
1254
+ " if (desktopContainer && "
1255
+ 'desktopContainer.style.display !== "none") {\n'
1256
+ " adjustTableSize(desktopContainer);\n"
1257
+ " adjustLogoPosition(desktopContainer);\n"
1258
+ " if (!desktopContainer._scrollObserver) {\n"
1259
+ " desktopContainer._scrollObserver = "
1260
+ "setupScrollObserver(desktopContainer);\n"
1261
+ " }\n"
1262
+ " }\n"
858
1263
  " }, 50);\n"
859
1264
  " setTimeout(function() { clearInterval(checkInterval); }, 2000);\n"
860
1265
  "})();\n"
@@ -862,124 +1267,6 @@ def _generate_responsive_html(
862
1267
  )
863
1268
 
864
1269
 
865
- def _generate_output(
866
- figure: Figure,
867
- figure_mobile: Figure | None,
868
- filename: str,
869
- output_type: LiteralPlotlyOutput,
870
- *,
871
- auto_open: bool,
872
- include_plotlyjs: LiteralPlotlyJSlib,
873
- plotfile: Path,
874
- table_html: str | None = None,
875
- ) -> str:
876
- """Generate output string based on output type.
877
-
878
- Args:
879
- figure: Plotly figure (desktop layout).
880
- figure_mobile: Optional Plotly figure (mobile layout).
881
- filename: Output filename.
882
- output_type: Type of output to generate.
883
- auto_open: Whether to auto-open file.
884
- include_plotlyjs: How to include plotly.js.
885
- plotfile: Path to plot file.
886
- table_html: Optional HTML table string for mobile layout.
887
-
888
- Returns:
889
- Output string (filename or HTML div).
890
- """
891
- fig, _ = load_plotly_dict()
892
-
893
- if output_type == "file":
894
- if figure_mobile is not None:
895
- div_id_desktop = filename.split(sep=".")[0] + "_desktop"
896
- div_id_mobile = filename.split(sep=".")[0] + "_mobile"
897
-
898
- html_desktop = to_html(
899
- fig=figure,
900
- div_id=div_id_desktop,
901
- auto_play=False,
902
- full_html=False,
903
- include_plotlyjs=include_plotlyjs,
904
- config=fig["config"],
905
- )
906
-
907
- html_mobile = to_html(
908
- fig=figure_mobile,
909
- div_id=div_id_mobile,
910
- auto_play=False,
911
- full_html=False,
912
- include_plotlyjs=False,
913
- config=fig["config"],
914
- )
915
-
916
- responsive_html = _generate_responsive_html(
917
- html_desktop=html_desktop,
918
- html_mobile=html_mobile,
919
- div_id_desktop=div_id_desktop,
920
- div_id_mobile=div_id_mobile,
921
- table_html=table_html,
922
- )
923
-
924
- with plotfile.open(mode="w", encoding="utf-8") as f:
925
- f.write(responsive_html)
926
-
927
- if auto_open:
928
- webbrowser.open(f"file://{plotfile.resolve()}")
929
-
930
- return str(plotfile)
931
- plot(
932
- figure_or_data=figure,
933
- filename=str(plotfile),
934
- auto_open=auto_open,
935
- auto_play=False,
936
- link_text="",
937
- include_plotlyjs=include_plotlyjs,
938
- output_type=output_type,
939
- config=fig["config"],
940
- )
941
- return str(plotfile)
942
-
943
- div_id = filename.split(sep=".")[0]
944
- if figure_mobile is not None:
945
- div_id_mobile = div_id + "_mobile"
946
- html_desktop = to_html(
947
- fig=figure,
948
- div_id=div_id,
949
- auto_play=False,
950
- full_html=False,
951
- include_plotlyjs=include_plotlyjs,
952
- config=fig["config"],
953
- )
954
- html_mobile = to_html(
955
- fig=figure_mobile,
956
- div_id=div_id_mobile,
957
- auto_play=False,
958
- full_html=False,
959
- include_plotlyjs=False,
960
- config=fig["config"],
961
- )
962
- return _generate_responsive_html(
963
- html_desktop=html_desktop,
964
- html_mobile=html_mobile,
965
- div_id_desktop=div_id,
966
- div_id_mobile=div_id_mobile,
967
- table_html=table_html,
968
- )
969
-
970
- return cast(
971
- "str",
972
- to_html(
973
- fig=figure,
974
- div_id=div_id,
975
- auto_play=False,
976
- full_html=False,
977
- include_plotlyjs=include_plotlyjs,
978
- config=fig["config"],
979
- ),
980
- )
981
-
982
-
983
1270
  def report_html(
984
1271
  data: OpenFrame,
985
1272
  bar_freq: LiteralBizDayFreq = "BYE",
@@ -1009,7 +1296,7 @@ def report_html(
1009
1296
  Defaults to False.
1010
1297
  add_logo: If True a Captor logo is added to the plot. Defaults to True.
1011
1298
  vertical_legend: Determines whether to vertically align the legend's
1012
- labels. Defaults to True.
1299
+ labels. Defaults to False.
1013
1300
 
1014
1301
  Returns:
1015
1302
  Plotly Figure and a div section or a HTML filename with location.
@@ -1022,47 +1309,6 @@ def report_html(
1022
1309
  copied.yearfrac
1023
1310
  )
1024
1311
 
1025
- figure = make_subplots(
1026
- rows=2,
1027
- cols=2,
1028
- specs=[
1029
- [{"type": "xy"}, {"rowspan": 2, "type": "table"}],
1030
- [{"type": "xy"}, None],
1031
- ],
1032
- )
1033
-
1034
- for item, lbl in enumerate(copied.columns_lvl_zero):
1035
- figure.add_scatter(
1036
- x=copied.tsdf.index,
1037
- y=copied.tsdf.iloc[:, item],
1038
- hovertemplate="%{y:.2%}<br>%{x|%Y-%m-%d}",
1039
- line={"width": 2.5, "dash": "solid"},
1040
- mode="lines",
1041
- name=lbl,
1042
- showlegend=True,
1043
- row=1,
1044
- col=1,
1045
- )
1046
-
1047
- quarter_of_year = 0.25
1048
- if copied.yearfrac < quarter_of_year:
1049
- tmp = copied.from_deepcopy()
1050
- bdf = tmp.value_to_ret().tsdf.iloc[1:]
1051
- else:
1052
- bdf = calendar_period_returns(data=copied, freq=bar_freq)
1053
-
1054
- for item in range(copied.item_count):
1055
- col_name = cast("tuple[str, ValueType]", bdf.iloc[:, item].name)
1056
- figure.add_bar(
1057
- x=bdf.index,
1058
- y=bdf.iloc[:, item],
1059
- hovertemplate="%{y:.2%}<br>%{x}",
1060
- name=col_name[0],
1061
- showlegend=False,
1062
- row=2,
1063
- col=1,
1064
- )
1065
-
1066
1312
  formats = [
1067
1313
  "{:.2%}",
1068
1314
  "{:.2%}",
@@ -1097,30 +1343,17 @@ def report_html(
1097
1343
  copied=copied,
1098
1344
  )
1099
1345
 
1100
- figure.add_table(
1101
- header={
1102
- "values": columns,
1103
- "align": "center",
1104
- "fill_color": "grey",
1105
- "font": {"color": "white"},
1106
- },
1107
- cells={
1108
- "values": cleanedtablevalues,
1109
- "align": aligning,
1110
- "height": 25,
1111
- "fill_color": color_lst,
1112
- "font": {"color": ["white"] + ["black"] * len(columns)},
1113
- },
1114
- row=1,
1115
- col=2,
1346
+ bdf = _get_bar_dataframe(copied, bar_freq)
1347
+ figure = _build_desktop_figure(
1348
+ copied=copied,
1349
+ bdf=bdf,
1350
+ cleanedtablevalues=cleanedtablevalues,
1351
+ columns=columns,
1352
+ aligning=aligning,
1353
+ color_lst=color_lst,
1116
1354
  )
1117
1355
 
1118
- if directory:
1119
- dirpath = Path(directory).resolve()
1120
- elif Path.home().joinpath("Documents").exists():
1121
- dirpath = Path.home() / "Documents"
1122
- else:
1123
- dirpath = Path(stack()[1].filename).parent
1356
+ dirpath = _get_output_directory(directory)
1124
1357
 
1125
1358
  if not filename:
1126
1359
  filename = "".join(choice(ascii_letters) for _ in range(6)) + ".html"
@@ -1128,15 +1361,14 @@ def report_html(
1128
1361
  plotfile = dirpath / filename
1129
1362
 
1130
1363
  _configure_figure_layout(
1131
- figure,
1132
- copied,
1364
+ figure=figure,
1365
+ copied=copied,
1133
1366
  add_logo=add_logo,
1134
1367
  vertical_legend=vertical_legend,
1135
1368
  title=title,
1136
- mobile=False,
1137
1369
  )
1138
1370
 
1139
- figure_mobile = _build_mobile_layout_figure(
1371
+ figure_mobile = _build_mobile_figure(
1140
1372
  copied=copied,
1141
1373
  bdf=bdf,
1142
1374
  add_logo=add_logo,
@@ -1144,22 +1376,78 @@ def report_html(
1144
1376
  title=title,
1145
1377
  )
1146
1378
 
1147
- table_html = _generate_html_table(
1379
+ table_html = _generate_html_table_string(
1148
1380
  cleanedtablevalues=cleanedtablevalues,
1149
1381
  columns=columns,
1150
1382
  aligning=aligning,
1151
1383
  color_lst=color_lst,
1152
1384
  )
1153
1385
 
1154
- string_output = _generate_output(
1155
- figure,
1156
- figure_mobile,
1157
- filename,
1158
- output_type,
1159
- auto_open=auto_open,
1160
- include_plotlyjs=include_plotlyjs,
1161
- plotfile=plotfile,
1162
- table_html=table_html,
1163
- )
1386
+ fig, _ = load_plotly_dict()
1387
+
1388
+ if output_type == "file":
1389
+ div_id_desktop = filename.split(sep=".")[0] + "_desktop"
1390
+ div_id_mobile = filename.split(sep=".")[0] + "_mobile"
1391
+
1392
+ html_desktop = to_html(
1393
+ fig=figure,
1394
+ div_id=div_id_desktop,
1395
+ auto_play=False,
1396
+ full_html=False,
1397
+ include_plotlyjs=include_plotlyjs,
1398
+ config=fig["config"],
1399
+ )
1400
+
1401
+ html_mobile = to_html(
1402
+ fig=figure_mobile,
1403
+ div_id=div_id_mobile,
1404
+ auto_play=False,
1405
+ full_html=False,
1406
+ include_plotlyjs=False,
1407
+ config=fig["config"],
1408
+ )
1409
+
1410
+ responsive_html = _generate_responsive_html_string(
1411
+ html_desktop=html_desktop,
1412
+ html_mobile=html_mobile,
1413
+ div_id_desktop=div_id_desktop,
1414
+ div_id_mobile=div_id_mobile,
1415
+ table_html=table_html,
1416
+ )
1417
+
1418
+ with plotfile.open(mode="w", encoding="utf-8") as f:
1419
+ f.write(responsive_html)
1420
+
1421
+ if auto_open:
1422
+ webbrowser.open(f"file://{plotfile.resolve()}")
1423
+
1424
+ string_output = str(plotfile)
1425
+ else:
1426
+ div_id = filename.split(sep=".")[0]
1427
+ div_id_mobile = div_id + "_mobile"
1428
+ html_desktop = to_html(
1429
+ fig=figure,
1430
+ div_id=div_id,
1431
+ auto_play=False,
1432
+ full_html=False,
1433
+ include_plotlyjs=include_plotlyjs,
1434
+ config=fig["config"],
1435
+ )
1436
+ html_mobile = to_html(
1437
+ fig=figure_mobile,
1438
+ div_id=div_id_mobile,
1439
+ auto_play=False,
1440
+ full_html=False,
1441
+ include_plotlyjs=False,
1442
+ config=fig["config"],
1443
+ )
1444
+
1445
+ string_output = _generate_responsive_html_string(
1446
+ html_desktop=html_desktop,
1447
+ html_mobile=html_mobile,
1448
+ div_id_desktop=div_id,
1449
+ div_id_mobile=div_id_mobile,
1450
+ table_html=table_html,
1451
+ )
1164
1452
 
1165
1453
  return figure, string_output
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openseries"
3
- version = "2.1.0"
3
+ version = "2.1.2"
4
4
  description = "Tools for analyzing financial timeseries."
5
5
  authors = [
6
6
  { name = "Martin Karrin", email = "martin.karrin@captor.se" },
@@ -63,22 +63,21 @@ dependencies = [
63
63
  [tool.poetry.group.dev.dependencies]
64
64
  mypy = "1.19.0"
65
65
  pandas-stubs = ">=2.1.2"
66
- pre-commit = ">=4.4.0"
67
- pytest = ">=9.0.0"
66
+ pre-commit = ">=4.5.0"
67
+ pytest = ">=9.0.2"
68
68
  pytest-cov = ">=7.0.0"
69
69
  pytest-xdist = ">=3.8.0"
70
- ruff = "0.14.8"
70
+ ruff = "0.14.9"
71
71
  types-openpyxl = ">=3.1.2"
72
72
  scipy-stubs = ">=1.14.1.0"
73
73
  types-python-dateutil = ">=2.8.2"
74
74
  types-requests = ">=2.20.0"
75
75
 
76
76
  [tool.poetry.group.docs.dependencies]
77
- myst-parser = ">=4.0.1"
78
- sphinx = ">=8.2.3"
77
+ sphinx = ">=9.0.4"
79
78
  sphinx-autobuild = ">=2025.8.25"
80
- sphinx-autodoc-typehints = ">=3.5.2"
81
- sphinx-rtd-theme = ">=3.0.2"
79
+ sphinx-autodoc-typehints = ">=3.6.0"
80
+ sphinx-rtd-theme = ">=3.1.0rc1"
82
81
 
83
82
  [build-system]
84
83
  requires = ["poetry-core>=2.2.1"]
File without changes
File without changes