streamlit-react-components 0.1.0__py3-none-any.whl → 1.0.4__py3-none-any.whl

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.
@@ -8,6 +8,7 @@ from .data_table import data_table
8
8
  from .step_indicator import step_indicator
9
9
  from .button_group import button_group
10
10
  from .chart_legend import chart_legend
11
+ from .plotly_chart import plotly_chart
11
12
 
12
13
  __all__ = [
13
14
  "panel",
@@ -18,4 +19,5 @@ __all__ = [
18
19
  "step_indicator",
19
20
  "button_group",
20
21
  "chart_legend",
22
+ "plotly_chart",
21
23
  ]
@@ -26,8 +26,11 @@ def button_group(
26
26
  - id: Unique identifier
27
27
  - label: Button text (optional)
28
28
  - icon: Button icon/emoji (optional)
29
- - color: Button color: "blue", "green", "red", "yellow" (optional)
29
+ - color: Preset name ("blue", "green", "red", "yellow", "purple",
30
+ "slate") or hex value like "#94a3b8" (optional)
30
31
  - disabled: Whether button is disabled (optional)
32
+ - style: Inline CSS styles dict for this button (optional)
33
+ - className: Tailwind CSS classes for this button (optional)
31
34
  style: Inline CSS styles as a dictionary
32
35
  class_name: Tailwind CSS classes
33
36
  key: Unique key for the component
@@ -36,6 +39,7 @@ def button_group(
36
39
  The ID of the clicked button, or None if no click
37
40
 
38
41
  Example:
42
+ # Using preset colors
39
43
  clicked = button_group(
40
44
  buttons=[
41
45
  {"id": "view", "icon": "👁️"},
@@ -44,6 +48,14 @@ def button_group(
44
48
  {"id": "reject", "icon": "✕", "color": "red"}
45
49
  ]
46
50
  )
51
+
52
+ # Using hex colors and custom styling
53
+ clicked = button_group(
54
+ buttons=[
55
+ {"id": "custom", "icon": "🎨", "color": "#ff5733"},
56
+ {"id": "styled", "label": "Styled", "style": {"padding": "12px"}}
57
+ ]
58
+ )
47
59
  if clicked == "approve":
48
60
  approve_item()
49
61
  """
@@ -0,0 +1,284 @@
1
+ """PlotlyChart component - Render Plotly charts with full interactivity."""
2
+
3
+ import streamlit as st
4
+ import streamlit.components.v1 as components
5
+ from pathlib import Path
6
+ from typing import Dict, Any, Optional, List, Union
7
+
8
+ _FRONTEND_DIR = Path(__file__).parent.parent / "_frontend"
9
+
10
+ _component = components.declare_component(
11
+ "streamlit_react_components",
12
+ path=str(_FRONTEND_DIR),
13
+ )
14
+
15
+
16
+ # Global key for expanded chart dialog - only ONE dialog can be open at a time
17
+ _EXPANDED_DIALOG_KEY = "_plotly_expanded_chart_dialog"
18
+ _PROCESSED_EXPAND_KEY = "_plotly_processed_expand_ts"
19
+
20
+
21
+ def _maybe_render_expanded_dialog() -> None:
22
+ """Check if an expanded chart dialog should be rendered. Call at start of plotly_chart()."""
23
+ import plotly.graph_objects as go
24
+
25
+ if _EXPANDED_DIALOG_KEY not in st.session_state:
26
+ return
27
+
28
+ dialog_data = st.session_state[_EXPANDED_DIALOG_KEY]
29
+ if not dialog_data.get("open"):
30
+ return
31
+
32
+ # Clear the flag IMMEDIATELY to prevent re-rendering on subsequent plotly_chart calls
33
+ st.session_state[_EXPANDED_DIALOG_KEY] = {"open": False}
34
+
35
+ figure_dict = dialog_data["figure"]
36
+ title = dialog_data.get("title", "")
37
+
38
+ @st.dialog(title or "Chart View", width="large")
39
+ def _expanded_dialog():
40
+ fig = go.Figure(figure_dict)
41
+ st.plotly_chart(fig, width="stretch", key="_expanded_plotly_chart")
42
+
43
+ _expanded_dialog()
44
+
45
+
46
+ def _dataframe_to_figure(
47
+ data: Any,
48
+ x: Optional[str],
49
+ y: Optional[Union[str, List[str]]],
50
+ color: Optional[str],
51
+ chart_type: str,
52
+ title: Optional[str],
53
+ ) -> Dict[str, Any]:
54
+ """Convert a DataFrame to a Plotly figure dict."""
55
+ traces = []
56
+ layout: Dict[str, Any] = {}
57
+
58
+ if title:
59
+ layout["title"] = title
60
+
61
+ # Determine y columns
62
+ y_cols = [y] if isinstance(y, str) else (y or [])
63
+
64
+ # Color grouping
65
+ if color and color in data.columns:
66
+ groups = data[color].unique()
67
+ for group in groups:
68
+ group_data = data[data[color] == group]
69
+ for y_col in y_cols:
70
+ trace = _create_trace(
71
+ chart_type,
72
+ group_data[x].tolist() if x else list(range(len(group_data))),
73
+ group_data[y_col].tolist(),
74
+ f"{group}" if len(y_cols) == 1 else f"{group} - {y_col}",
75
+ )
76
+ traces.append(trace)
77
+ else:
78
+ # No color grouping
79
+ x_data = data[x].tolist() if x else list(range(len(data)))
80
+ for y_col in y_cols:
81
+ trace = _create_trace(
82
+ chart_type,
83
+ x_data,
84
+ data[y_col].tolist(),
85
+ y_col if len(y_cols) > 1 else None,
86
+ )
87
+ traces.append(trace)
88
+
89
+ return {"data": traces, "layout": layout}
90
+
91
+
92
+ def _create_trace(
93
+ chart_type: str,
94
+ x_data: List[Any],
95
+ y_data: List[Any],
96
+ name: Optional[str],
97
+ ) -> Dict[str, Any]:
98
+ """Create a single Plotly trace based on chart type."""
99
+ base: Dict[str, Any] = {"x": x_data, "y": y_data}
100
+ if name:
101
+ base["name"] = name
102
+
103
+ if chart_type == "line":
104
+ return {"type": "scatter", "mode": "lines", **base}
105
+ elif chart_type == "scatter":
106
+ return {"type": "scatter", "mode": "markers", **base}
107
+ elif chart_type == "bar":
108
+ return {"type": "bar", **base}
109
+ elif chart_type == "area":
110
+ return {"type": "scatter", "mode": "lines", "fill": "tozeroy", **base}
111
+ elif chart_type == "histogram":
112
+ return {"type": "histogram", "x": x_data}
113
+ elif chart_type == "pie":
114
+ return {"type": "pie", "labels": x_data, "values": y_data}
115
+ else:
116
+ # Default to scatter with lines
117
+ return {"type": "scatter", "mode": "lines", **base}
118
+
119
+
120
+ def plotly_chart(
121
+ figure: Optional[Any] = None,
122
+ data: Optional[Any] = None,
123
+ x: Optional[str] = None,
124
+ y: Optional[Union[str, List[str]]] = None,
125
+ color: Optional[str] = None,
126
+ chart_type: str = "line",
127
+ title: Optional[str] = None,
128
+ config: Optional[Dict[str, Any]] = None,
129
+ on_click: bool = False,
130
+ on_select: bool = False,
131
+ on_hover: bool = False,
132
+ on_relayout: bool = False,
133
+ expandable: bool = False,
134
+ modal_title: str = "",
135
+ style: Optional[Dict[str, Any]] = None,
136
+ class_name: str = "",
137
+ key: Optional[str] = None,
138
+ ) -> Optional[Dict[str, Any]]:
139
+ """
140
+ Render a Plotly chart with full interactivity and custom styling.
141
+
142
+ Supports two modes:
143
+ 1. Pass a Plotly figure object directly
144
+ 2. Pass a pandas DataFrame with chart configuration
145
+
146
+ Args:
147
+ figure: Plotly figure object (go.Figure) or dict with 'data' and 'layout'.
148
+ Takes precedence over `data` parameter.
149
+ data: pandas DataFrame for quick chart creation
150
+ x: Column name for x-axis (when using DataFrame)
151
+ y: Column name(s) for y-axis - string or list of strings (when using DataFrame)
152
+ color: Column name for color grouping (when using DataFrame)
153
+ chart_type: Chart type when using DataFrame - "line", "bar", "scatter",
154
+ "area", "pie", or "histogram" (default "line")
155
+ title: Chart title (when using DataFrame)
156
+ config: Plotly config options (displayModeBar, scrollZoom, etc.)
157
+ on_click: Enable click events (returns clicked points)
158
+ on_select: Enable selection events (box/lasso selection)
159
+ on_hover: Enable hover events (returns hovered points)
160
+ on_relayout: Enable relayout events (zoom/pan state)
161
+ expandable: Show expand button to open chart in full-page dialog
162
+ modal_title: Title displayed in dialog header when expanded
163
+ style: Inline CSS styles as a dictionary
164
+ class_name: Tailwind CSS classes
165
+ key: Unique key for the component
166
+
167
+ Returns:
168
+ Event dict with 'type' and event data, or None if no event.
169
+ Event types: 'click', 'select', 'hover', 'relayout'
170
+
171
+ Example using Plotly figure:
172
+ import plotly.graph_objects as go
173
+
174
+ fig = go.Figure(
175
+ data=[go.Scatter(x=[1,2,3], y=[4,5,6], mode='lines+markers')],
176
+ layout=go.Layout(title='My Chart')
177
+ )
178
+
179
+ event = plotly_chart(
180
+ figure=fig,
181
+ on_click=True,
182
+ on_select=True,
183
+ style={"height": "400px"}
184
+ )
185
+
186
+ if event and event['type'] == 'click':
187
+ st.write(f"Clicked: {event['points']}")
188
+
189
+ Example using DataFrame:
190
+ import pandas as pd
191
+
192
+ df = pd.DataFrame({
193
+ 'month': ['Jan', 'Feb', 'Mar', 'Apr'],
194
+ 'sales': [100, 150, 120, 180],
195
+ 'orders': [50, 75, 60, 90]
196
+ })
197
+
198
+ # Simple line chart
199
+ event = plotly_chart(
200
+ data=df,
201
+ x='month',
202
+ y='sales',
203
+ chart_type='line',
204
+ title='Monthly Sales',
205
+ on_click=True
206
+ )
207
+
208
+ # Multiple y columns as bar chart
209
+ event = plotly_chart(
210
+ data=df,
211
+ x='month',
212
+ y=['sales', 'orders'],
213
+ chart_type='bar',
214
+ title='Sales vs Orders'
215
+ )
216
+
217
+ # Scatter with color grouping
218
+ event = plotly_chart(
219
+ data=df,
220
+ x='sales',
221
+ y='orders',
222
+ color='month',
223
+ chart_type='scatter'
224
+ )
225
+
226
+ Example with expandable modal:
227
+ event = plotly_chart(
228
+ figure=fig,
229
+ expandable=True,
230
+ modal_title="Sales Dashboard",
231
+ style={"height": "300px"}
232
+ )
233
+ # Click the expand button to open chart in full-page dialog
234
+ """
235
+ # Check if we should render an expanded dialog (from previous expand click)
236
+ # This MUST be called first, before any other plotly_chart processing
237
+ _maybe_render_expanded_dialog()
238
+
239
+ # Determine figure dict
240
+ if figure is not None:
241
+ # Convert Plotly figure to dict if needed
242
+ if hasattr(figure, "to_dict"):
243
+ figure_dict = figure.to_dict()
244
+ else:
245
+ figure_dict = figure
246
+ elif data is not None:
247
+ # Create figure from DataFrame
248
+ figure_dict = _dataframe_to_figure(data, x, y, color, chart_type, title)
249
+ else:
250
+ raise ValueError("Either 'figure' or 'data' parameter is required")
251
+
252
+ # Render the component
253
+ result = _component(
254
+ component="plotly_chart",
255
+ figure=figure_dict,
256
+ config=config,
257
+ onClickEnabled=on_click,
258
+ onSelectEnabled=on_select,
259
+ onHoverEnabled=on_hover,
260
+ onRelayoutEnabled=on_relayout,
261
+ expandable=expandable,
262
+ modalTitle=modal_title,
263
+ style=style,
264
+ className=class_name,
265
+ key=key,
266
+ default=None,
267
+ )
268
+
269
+ # Handle expand event - store in session state and trigger rerun
270
+ if result and isinstance(result, dict) and result.get("type") == "expand":
271
+ event_ts = result.get("timestamp", 0)
272
+ last_processed = st.session_state.get(_PROCESSED_EXPAND_KEY, 0)
273
+
274
+ # Only process if this is a NEW expand click (different timestamp)
275
+ if event_ts > last_processed:
276
+ st.session_state[_PROCESSED_EXPAND_KEY] = event_ts
277
+ st.session_state[_EXPANDED_DIALOG_KEY] = {
278
+ "open": True,
279
+ "figure": result.get("figure", figure_dict),
280
+ "title": result.get("modalTitle", modal_title)
281
+ }
282
+ st.rerun()
283
+
284
+ return result
@@ -30,7 +30,13 @@ def section_header(
30
30
  - id: Unique identifier for the action
31
31
  - label: Button text (optional)
32
32
  - icon: Button icon (optional)
33
- - color: Button color: "blue", "green", "red", "yellow" (optional)
33
+ - color: Preset name ("blue", "green", "red", "yellow", "purple",
34
+ "slate") or hex value like "#94a3b8" (optional)
35
+ - style: Inline CSS styles dict for this button (optional)
36
+ - className: Tailwind CSS classes for this button (optional)
37
+ - href: URL for link actions. External URLs (http/https) open
38
+ in a new tab. Internal paths return the ID for use with
39
+ st.switch_page() (optional)
34
40
  style: Inline CSS styles as a dictionary
35
41
  class_name: Tailwind CSS classes
36
42
  key: Unique key for the component
@@ -39,13 +45,37 @@ def section_header(
39
45
  The ID of the clicked action button, or None if no click
40
46
 
41
47
  Example:
48
+ # Using preset colors
42
49
  clicked = section_header(
43
50
  title="Dashboard",
44
51
  icon="📊",
45
52
  actions=[{"id": "refresh", "label": "Refresh", "color": "blue"}]
46
53
  )
47
- if clicked == "refresh":
48
- st.rerun()
54
+
55
+ # Using hex colors and custom styling
56
+ clicked = section_header(
57
+ title="Dashboard",
58
+ actions=[
59
+ {"id": "custom", "label": "Custom", "color": "#94a3b8"},
60
+ {"id": "styled", "label": "Styled", "style": {"padding": "12px"}}
61
+ ]
62
+ )
63
+
64
+ # External link (opens in new tab)
65
+ clicked = section_header(
66
+ title="Resources",
67
+ actions=[
68
+ {"id": "docs", "label": "Documentation", "href": "https://docs.example.com", "icon": "📚"}
69
+ ]
70
+ )
71
+
72
+ # Internal navigation
73
+ clicked = section_header(
74
+ title="Settings",
75
+ actions=[{"id": "home", "label": "Home", "icon": "🏠"}]
76
+ )
77
+ if clicked == "home":
78
+ st.switch_page("pages/home.py")
49
79
  """
50
80
  return _component(
51
81
  component="section_header",
@@ -17,36 +17,117 @@ def stat_card(
17
17
  value: Union[str, int, float],
18
18
  color: str = "blue",
19
19
  icon: str = "",
20
+ planned: Optional[Union[str, int, float]] = None,
21
+ delta: Optional[Union[str, int, float]] = None,
22
+ delta_style: str = "auto",
23
+ delta_thresholds: Optional[Dict[str, float]] = None,
24
+ unit: str = "",
25
+ action: Optional[Dict[str, str]] = None,
20
26
  style: Optional[Dict[str, Any]] = None,
21
27
  class_name: str = "",
22
28
  key: Optional[str] = None,
23
- ) -> None:
29
+ ) -> Optional[str]:
24
30
  """
25
31
  Display a styled statistics card with a label and value.
26
32
 
27
33
  Args:
28
34
  label: The description label (e.g., "Total Users")
29
35
  value: The statistic value to display
30
- color: Accent color: "blue", "green", "red", "yellow", "purple"
36
+ color: Accent color - preset name ("blue", "green", "red", "yellow",
37
+ "purple", "slate") or hex value (e.g., "#94a3b8")
31
38
  icon: Optional emoji or icon to display with the label
39
+ planned: Optional planned/target value to display
40
+ delta: Optional delta/difference value to display
41
+ delta_style: How to style the delta - "auto" (green/red based on sign),
42
+ "neutral" (no color), "percentage" (show as %),
43
+ "inverse" (red for positive, green for negative)
44
+ delta_thresholds: Optional thresholds for delta color based on magnitude
45
+ e.g., {"warning": 10, "danger": 20}
46
+ unit: Unit of measurement (e.g., "kg", "%", "$")
47
+ action: Optional action config with keys:
48
+ - id: Unique identifier (returned on click)
49
+ - label: Display text
50
+ - icon: Optional emoji/icon (e.g., "📊")
51
+ - style: "button" (default) or "link"
52
+ - className: Optional Tailwind classes for custom styling
32
53
  style: Inline CSS styles as a dictionary
33
54
  class_name: Tailwind CSS classes
34
55
  key: Unique key for the component
35
56
 
57
+ Returns:
58
+ The action button id if clicked, None otherwise
59
+
36
60
  Example:
61
+ # Basic usage (backwards compatible)
37
62
  stat_card(
38
63
  label="Within Threshold",
39
64
  value="4",
40
65
  color="green",
41
66
  style={"minWidth": "150px"}
42
67
  )
68
+
69
+ # With all new features
70
+ clicked = stat_card(
71
+ label="Production Output",
72
+ value=1234,
73
+ planned=1200,
74
+ delta=34,
75
+ delta_style="auto",
76
+ unit="kg",
77
+ color="green",
78
+ icon="📊",
79
+ action={"id": "details", "label": "View Details"}
80
+ )
81
+ if clicked == "details":
82
+ st.write("Details clicked!")
83
+
84
+ # With thresholds
85
+ stat_card(
86
+ label="Defect Rate",
87
+ value=15,
88
+ delta=5,
89
+ delta_style="inverse",
90
+ delta_thresholds={"warning": 3, "danger": 10},
91
+ unit="%"
92
+ )
93
+
94
+ # With styled action button
95
+ stat_card(
96
+ label="Revenue",
97
+ value=1234,
98
+ action={
99
+ "id": "view",
100
+ "label": "View Details",
101
+ "icon": "👁️",
102
+ "style": "button",
103
+ "className": "bg-cyan-600 hover:bg-cyan-500"
104
+ }
105
+ )
106
+
107
+ # With link-style action
108
+ stat_card(
109
+ label="Orders",
110
+ value=567,
111
+ action={
112
+ "id": "expand",
113
+ "label": "See more",
114
+ "icon": "→",
115
+ "style": "link"
116
+ }
117
+ )
43
118
  """
44
- _component(
119
+ return _component(
45
120
  component="stat_card",
46
121
  label=label,
47
122
  value=str(value),
48
123
  color=color,
49
124
  icon=icon,
125
+ planned=str(planned) if planned is not None else None,
126
+ delta=float(delta) if delta is not None else None,
127
+ deltaStyle=delta_style,
128
+ deltaThresholds=delta_thresholds,
129
+ unit=unit,
130
+ action=action,
50
131
  style=style,
51
132
  className=class_name,
52
133
  key=key,
@@ -15,6 +15,7 @@ _component = components.declare_component(
15
15
  def checkbox_group(
16
16
  items: List[Dict[str, Any]],
17
17
  label: str = "",
18
+ layout: str = "vertical",
18
19
  style: Optional[Dict[str, Any]] = None,
19
20
  class_name: str = "",
20
21
  key: Optional[str] = None,
@@ -28,6 +29,7 @@ def checkbox_group(
28
29
  - label: Display label
29
30
  - checked: Initial checked state (optional, default False)
30
31
  label: Optional group label
32
+ layout: Layout direction - "vertical" (default) or "horizontal"
31
33
  style: Inline CSS styles as a dictionary
32
34
  class_name: Tailwind CSS classes
33
35
  key: Unique key for the component
@@ -36,6 +38,7 @@ def checkbox_group(
36
38
  List of checked item IDs
37
39
 
38
40
  Example:
41
+ # Vertical layout (default)
39
42
  selected = checkbox_group(
40
43
  label="Parameters",
41
44
  items=[
@@ -45,6 +48,13 @@ def checkbox_group(
45
48
  ]
46
49
  )
47
50
  # Returns: ["vphp", "lot_co"] if those are checked
51
+
52
+ # Horizontal layout
53
+ selected = checkbox_group(
54
+ label="Options",
55
+ items=[...],
56
+ layout="horizontal"
57
+ )
48
58
  """
49
59
  # Get default checked items
50
60
  default_checked = [item["id"] for item in items if item.get("checked", False)]
@@ -53,6 +63,7 @@ def checkbox_group(
53
63
  component="checkbox_group",
54
64
  label=label,
55
65
  items=items,
66
+ layout=layout,
56
67
  style=style,
57
68
  className=class_name,
58
69
  key=key,
@@ -34,7 +34,8 @@ def form_slider(
34
34
  max_val: Maximum value
35
35
  step: Step increment (default 1)
36
36
  unit: Unit suffix to display (e.g., "%", "hrs")
37
- color: Accent color: "blue", "green", "red", "yellow", "purple"
37
+ color: Accent color - preset name ("blue", "green", "red", "yellow",
38
+ "purple", "slate") or hex value (e.g., "#94a3b8")
38
39
  style: Inline CSS styles as a dictionary
39
40
  class_name: Tailwind CSS classes
40
41
  key: Unique key for the component
@@ -43,6 +44,7 @@ def form_slider(
43
44
  The current slider value
44
45
 
45
46
  Example:
47
+ # Using preset color
46
48
  threshold = form_slider(
47
49
  label="Upper Threshold",
48
50
  value=90,
@@ -51,6 +53,15 @@ def form_slider(
51
53
  unit="%",
52
54
  color="red"
53
55
  )
56
+
57
+ # Using hex color
58
+ threshold = form_slider(
59
+ label="Custom Slider",
60
+ value=50,
61
+ min_val=0,
62
+ max_val=100,
63
+ color="#ff5733"
64
+ )
54
65
  """
55
66
  result = _component(
56
67
  component="form_slider",