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.
- {openseries-2.1.0 → openseries-2.1.2}/PKG-INFO +1 -1
- {openseries-2.1.0 → openseries-2.1.2}/openseries/report.py +657 -369
- {openseries-2.1.0 → openseries-2.1.2}/pyproject.toml +7 -8
- {openseries-2.1.0 → openseries-2.1.2}/LICENSE.md +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/README.md +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/openseries/__init__.py +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/openseries/_common_model.py +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/openseries/_risk.py +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/openseries/datefixer.py +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/openseries/frame.py +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/openseries/load_plotly.py +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/openseries/owntypes.py +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/openseries/plotly_captor_logo.json +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/openseries/plotly_layouts.json +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/openseries/portfoliotools.py +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/openseries/py.typed +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/openseries/series.py +0 -0
- {openseries-2.1.0 → openseries-2.1.2}/openseries/simulation.py +0 -0
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
|
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
|
|
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
|
|
566
|
-
|
|
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
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
-
"
|
|
802
|
-
"
|
|
803
|
-
"
|
|
804
|
-
"
|
|
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
|
-
"
|
|
807
|
-
"'important');\n"
|
|
808
|
-
"
|
|
809
|
-
"'important');\n"
|
|
810
|
-
"
|
|
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',
|
|
815
|
-
"
|
|
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',
|
|
1133
|
+
" div.style.setProperty('overflow', overflowValue, "
|
|
820
1134
|
"'important');\n"
|
|
821
|
-
" div.style.setProperty('overflow-y',
|
|
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',
|
|
827
|
-
"
|
|
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
|
-
"
|
|
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',
|
|
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
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
filename
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
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.
|
|
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.
|
|
67
|
-
pytest = ">=9.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.
|
|
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
|
-
|
|
78
|
-
sphinx = ">=8.2.3"
|
|
77
|
+
sphinx = ">=9.0.4"
|
|
79
78
|
sphinx-autobuild = ">=2025.8.25"
|
|
80
|
-
sphinx-autodoc-typehints = ">=3.
|
|
81
|
-
sphinx-rtd-theme = ">=3.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|