4141 _UNSET ,
4242 _Unset ,
4343)
44+ from .tvchart .mixin import TVChartStateMixin
4445from .toolbar import Toolbar , get_toolbar_script , wrap_content_with_toolbars
4546from .widget_protocol import BaseWidget # noqa: TC001
4647
@@ -1667,7 +1668,7 @@ async def get_widget_html_async(widget_id: str) -> str | None:
16671668 return await _state .get_widget_html_async (widget_id )
16681669
16691670
1670- class InlineWidget (GridStateMixin , PlotlyStateMixin , ToolbarStateMixin ):
1671+ class InlineWidget (GridStateMixin , PlotlyStateMixin , TVChartStateMixin , ToolbarStateMixin ):
16711672 """Base inline widget that renders via FastAPI server and IFrame.
16721673
16731674 Implements BaseWidget protocol for unified API across rendering backends.
@@ -3773,6 +3774,186 @@ def _preload_chart_data(user_id: str = "default") -> dict[str, str]:
37733774 return preload
37743775
37753776
3777+ def generate_tvchart_html (
3778+ chart_html : str ,
3779+ config_payload : str ,
3780+ chart_id : str ,
3781+ widget_id : str ,
3782+ title : str = "Chart" ,
3783+ theme : ThemeLiteral | None = None ,
3784+ toolbars : list [dict [str , Any ] | Toolbar ] | None = None ,
3785+ modals : list [dict [str , Any ] | Modal ] | None = None ,
3786+ inline_css : str = "" ,
3787+ full_document : bool = True ,
3788+ token : str | None = None ,
3789+ ) -> str :
3790+ """Generate HTML for a TradingView Lightweight Chart.
3791+
3792+ Parameters
3793+ ----------
3794+ chart_html : str
3795+ The chart container ``<div>`` (and any toolbar/modal markup).
3796+ config_payload : str
3797+ JSON string with ``chartOptions``, ``series``, ``storage``, etc.
3798+ chart_id : str
3799+ DOM id of the chart container element.
3800+ widget_id : str
3801+ Unique widget identifier (used by the pywry bridge).
3802+ title : str
3803+ Page title.
3804+ theme : 'dark' or 'light', optional
3805+ Color theme.
3806+ toolbars : list, optional
3807+ Toolbar configurations.
3808+ modals : list, optional
3809+ Modal configurations.
3810+ inline_css : str
3811+ Extra CSS to inject.
3812+ full_document : bool
3813+ If True, return complete HTML document; if False, content fragment only.
3814+ token : str or None
3815+ Widget auth token for the pywry bridge.
3816+
3817+ Returns
3818+ -------
3819+ str
3820+ """
3821+ from .assets import (
3822+ get_pywry_css ,
3823+ get_scrollbar_js ,
3824+ get_toast_css ,
3825+ get_tvchart_defaults_js ,
3826+ get_tvchart_js ,
3827+ )
3828+ from .modal import wrap_content_with_modals
3829+ from .notebook import _wrap_content_with_toolbars
3830+
3831+ if theme is None :
3832+ theme = _get_default_theme ()
3833+
3834+ tvchart_js = get_tvchart_js ()
3835+ tvchart_script = f"<script>{ tvchart_js } </script>" if tvchart_js else ""
3836+ tvchart_defaults = get_tvchart_defaults_js ()
3837+ tvchart_defaults_script = f"<script>{ tvchart_defaults } </script>" if tvchart_defaults else ""
3838+
3839+ # Chart init script — waits for LightweightCharts then renders
3840+ chart_init_script = f"""<script>
3841+ (function() {{
3842+ function initChart() {{
3843+ if (typeof LightweightCharts === 'undefined') {{
3844+ setTimeout(initChart, 50);
3845+ return;
3846+ }}
3847+ var payload = { config_payload } ;
3848+ var container = document.getElementById('{ chart_id } ');
3849+ if (!container) {{
3850+ setTimeout(initChart, 50);
3851+ return;
3852+ }}
3853+ if (window.PYWRY_TVCHART_RENDER) {{
3854+ window.PYWRY_TVCHART_RENDER('{ chart_id } ', container, payload);
3855+ }} else if (window.PYWRY_TVCHART_CREATE) {{
3856+ window.PYWRY_TVCHART_CREATE('{ chart_id } ', container, payload);
3857+ }}
3858+ }}
3859+ initChart();
3860+ }})();
3861+ </script>"""
3862+
3863+ if not full_document :
3864+ # Content fragment for anywidget — caller handles wrapping
3865+ wrapped = _wrap_content_with_toolbars (chart_html , toolbars )
3866+ if modals :
3867+ modal_html , modal_scripts = wrap_content_with_modals ("" , modals )
3868+ wrapped = f"{ wrapped } { modal_html } { modal_scripts } "
3869+ return f"{ wrapped } \n { chart_init_script } "
3870+
3871+ # Full document for IFrame / browser mode
3872+ pywry_css = get_pywry_css ()
3873+ pywry_style = f"<style>{ pywry_css } </style>" if pywry_css else ""
3874+ toast_css = get_toast_css ()
3875+ toast_style = f"<style>{ toast_css } </style>" if toast_css else ""
3876+ scrollbar_js = get_scrollbar_js ()
3877+ scrollbar_script = f"<script>{ scrollbar_js } </script>" if scrollbar_js else ""
3878+ inline_style = f"<style>{ inline_css } </style>" if inline_css else ""
3879+
3880+ if theme == "dark" :
3881+ widget_theme_class = "pywry-theme-dark"
3882+ elif theme == "system" :
3883+ widget_theme_class = "pywry-theme-system"
3884+ else :
3885+ widget_theme_class = "pywry-theme-light"
3886+
3887+ # Build widget content with toolbars
3888+ widget_content = wrap_content_with_toolbars (chart_html , toolbars )
3889+
3890+ # Inject modals
3891+ modal_block = ""
3892+ if modals :
3893+ modal_html , modal_scripts = wrap_content_with_modals ("" , modals )
3894+ modal_block = f"{ modal_html } { modal_scripts } "
3895+
3896+ return f"""<!DOCTYPE html>
3897+ <html class="{ theme } ">
3898+ <head>
3899+ <meta charset="utf-8">
3900+ <title>{ title } </title>
3901+ { tvchart_script }
3902+ { tvchart_defaults_script }
3903+ { pywry_style }
3904+ { toast_style }
3905+ { inline_style }
3906+ { scrollbar_script }
3907+ <style>
3908+ html, body {{
3909+ margin: 0;
3910+ padding: 0;
3911+ width: 100%;
3912+ height: 100%;
3913+ overflow: hidden;
3914+ background: var(--pywry-bg-primary);
3915+ }}
3916+ .pywry-widget {{
3917+ --pywry-widget-width: 100%;
3918+ --pywry-widget-height: 100%;
3919+ width: 100%;
3920+ height: 100%;
3921+ display: flex;
3922+ flex-direction: column;
3923+ border: none;
3924+ border-radius: 0;
3925+ box-sizing: border-box;
3926+ background-color: var(--pywry-bg-primary);
3927+ }}
3928+ .pywry-toolbar {{
3929+ border: none;
3930+ }}
3931+ .pywry-content {{
3932+ flex: 1;
3933+ min-height: 0;
3934+ box-sizing: border-box;
3935+ overflow: hidden;
3936+ }}
3937+ .pywry-tvchart-container {{
3938+ flex: 1;
3939+ min-height: 0;
3940+ width: 100%;
3941+ height: 100%;
3942+ box-sizing: border-box;
3943+ }}
3944+ </style>
3945+ </head>
3946+ <body>
3947+ <div class="pywry-widget pywry-custom-scrollbar { widget_theme_class } ">
3948+ { widget_content }
3949+ </div>
3950+ { modal_block }
3951+ { _get_pywry_bridge_js (widget_id , token )}
3952+ { chart_init_script }
3953+ </body>
3954+ </html>"""
3955+
3956+
37763957def show_tvchart (
37773958 data : Any = None ,
37783959 callbacks : dict [str , Callable [..., Any ]] | None = None ,
@@ -3845,10 +4026,8 @@ def show_tvchart(
38454026 import json as _json
38464027 import uuid as _uuid
38474028
3848- from .modal import wrap_content_with_modals
3849- from .notebook import _wrap_content_with_toolbars
4029+ from .notebook import create_tvchart_widget
38504030 from .runtime import is_headless
3851- from .widget import HAS_ANYWIDGET , PyWryTVChartWidget
38524031
38534032 if theme is None :
38544033 theme = _get_default_theme ()
@@ -3930,25 +4109,21 @@ def show_tvchart(
39304109
39314110 chart_html = f'<div id="{ chart_id } " class="pywry-tvchart-container"></div>'
39324111
3933- # Inject toolbars
3934- chart_html = _wrap_content_with_toolbars (chart_html , toolbars )
3935-
3936- # Inject modals
3937- if modals :
3938- modal_html , modal_scripts = wrap_content_with_modals ("" , modals )
3939- chart_html = f"{ chart_html } { modal_html } { modal_scripts } "
3940-
3941- if HAS_ANYWIDGET and not open_browser and not is_headless ():
3942- widget = PyWryTVChartWidget (
3943- content = chart_html ,
3944- chart_config = config_payload ,
3945- theme = theme ,
3946- width = width ,
3947- height = f"{ height } px" ,
3948- chart_id = chart_id ,
3949- )
3950- else :
3951- widget = PyWryTVChartWidget (content = chart_html )
4112+ # Create widget using auto-backend selection
4113+ # Force InlineWidget (IFrame) for BROWSER mode since it has open_in_browser()
4114+ widget = create_tvchart_widget (
4115+ chart_html = chart_html ,
4116+ config_payload = config_payload ,
4117+ chart_id = chart_id ,
4118+ widget_id = widget_id ,
4119+ title = title ,
4120+ theme = theme ,
4121+ width = width ,
4122+ height = height ,
4123+ toolbars = toolbars ,
4124+ modals = modals ,
4125+ force_iframe = open_browser ,
4126+ )
39524127
39534128 if callbacks :
39544129 for event_type , callback in callbacks .items ():
@@ -3960,14 +4135,17 @@ def show_tvchart(
39604135 wire_storage (user_id = "default" )
39614136
39624137 if provider is not None :
3963- widget ._wire_datafeed_provider (provider )
4138+ wire_datafeed = getattr (widget , "_wire_datafeed_provider" , None )
4139+ if callable (wire_datafeed ):
4140+ wire_datafeed (provider )
39644141
4142+ # Display
39654143 if is_headless ():
39664144 pass
4145+ elif open_browser :
4146+ open_fn = getattr (widget , "open_in_browser" , None )
4147+ if callable (open_fn ):
4148+ open_fn ()
39674149 else :
3968- open_in_browser = getattr (widget , "open_in_browser" , None )
3969- if open_browser and callable (open_in_browser ):
3970- open_in_browser ()
3971- else :
3972- widget .display ()
4150+ widget .display ()
39734151 return widget
0 commit comments