MarcSkovMadsen commited on
Commit
e2acbfd
·
verified ·
1 Parent(s): 3e0644f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +309 -125
app.py CHANGED
@@ -1,134 +1,318 @@
1
- import time
2
- from concurrent.futures import ThreadPoolExecutor
3
- from contextlib import contextmanager
 
4
 
5
- import numpy as np
 
 
 
 
 
 
6
  import panel as pn
7
  import param
8
- from asyncio import wrap_future
9
-
10
- class ProgressExtMod(pn.viewable.Viewer):
11
- """A custom component for easy progress reporting"""
12
-
13
- completed = param.Integer(default=0)
14
- bar_color = param.String(default="info")
15
- num_tasks = param.Integer(default=100, bounds=(1, None))
16
-
17
- # @param.depends('completed', 'num_tasks')
18
- @property
19
- def value(self) -> int:
20
- """Returns the progress value
21
-
22
- Returns:
23
- int: The progress value
24
- """
25
- return int(100 * (self.completed / self.num_tasks))
26
-
27
- def reset(self):
28
- """Resets the value and message"""
29
- # Please note the order matters as the Widgets updates two times. One for each change
30
- self.completed = 0
31
-
32
- def __panel__(self):
33
- return self.view
34
-
35
- @param.depends("completed", "bar_color")
36
- def view(self):
37
- """View the widget
38
- Returns:
39
- pn.viewable.Viewable: Add this to your app to see the progress reported
40
- """
41
- if self.value:
42
- return pn.widgets.Progress(
43
- active=True, value=self.value, align="center", sizing_mode="stretch_width"
44
- )
45
- return None
46
-
47
- @contextmanager
48
- def increment(self):
49
- """Increments the value
50
-
51
- Can be used as context manager or decorator
52
-
53
- Yields:
54
- None: Nothing is yielded
55
- """
56
- self.completed += 1
57
- yield
58
- if self.completed == self.num_tasks:
59
- self.reset()
60
-
61
- executor = ThreadPoolExecutor(max_workers=2) # pylint: disable=consider-using-with
62
- progress = ProgressExtMod()
63
-
64
-
65
- class AsyncComponent(pn.viewable.Viewer):
66
- """A component that demonstrates how to run a Blocking Background task asynchronously
67
- in Panel"""
68
-
69
- select = param.Selector(objects=range(10))
70
- slider = param.Number(2, bounds=(0, 10))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- run_blocking_task = param.Event(label="RUN")
73
- result = param.Number(0)
74
- view = param.Parameter()
75
-
76
- def __init__(self, **params):
77
- super().__init__(**params)
78
-
79
- self._layout = pn.Column(
80
- pn.pane.Markdown("## Blocking Task Running in Background"),
81
- pn.Param(
82
- self,
83
- parameters=["run_blocking_task", "result"],
84
- widgets={"result": {"disabled": True}, "run_blocking_task": {"button_type": "primary"}},
85
- show_name=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  ),
87
- progress,
88
- pn.pane.Markdown("## Other, Non-Blocked Tasks"),
89
- pn.Param(
90
- self,
91
- parameters=["select", "slider"],
92
- widgets={"text": {"disabled": True}},
93
- show_name=False,
94
  ),
95
- self.text
96
  )
 
 
 
 
97
 
98
- def __panel__(self):
99
- return self._layout
100
-
101
- @param.depends("slider", "select")
102
- def text(self):
103
- if self.select:
104
- select = self.select
105
- else:
106
- select = 0
107
- return f"{select} + {self.slider} = {select + self.slider}"
108
-
109
- @pn.depends("run_blocking_task", watch=True)
110
- async def _run_blocking_tasks(self, num_tasks=10):
111
- """Runs background tasks num_tasks times"""
112
- num_tasks = 20
113
- progress.num_tasks = num_tasks
114
- for _ in range(num_tasks):
115
- future = executor.submit(self._run_blocking_task)
116
- result = await wrap_future(future)
117
- self._update(result)
118
-
119
- @progress.increment()
120
- def _update(self, number):
121
- self.result += number
122
-
123
- @staticmethod
124
- def _run_blocking_task():
125
- time.sleep(np.random.randint(1, 2))
126
- return 5
127
-
128
- if pn.state.served:
129
- pn.extension()
 
 
 
 
 
 
130
 
131
- component = AsyncComponent()
132
- pn.template.FastListTemplate(
133
- site="Awesome Panel", site_url="https://awesome-panel.org", title="Async Tasks", main=[component], main_layout=None, main_max_width="400px"
134
- ).servable()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Interactive Reciprocal Tariffs Calculator
3
+ Original Source: Yi-Cheng Wu (https://github.com/jrycw/tech/blob/master/posts/clone-reciprocal-tariffs-table/20250403.qmd)
4
+ """
5
 
6
+ # =============================================================================
7
+ # Imports
8
+ # =============================================================================
9
+ import random
10
+
11
+ import polars as pl
12
+ from great_tables import GT, google_font, html, loc, style, vals
13
  import panel as pn
14
  import param
15
+ from panel.custom import JSComponent
16
+
17
+ # Initialize Panel extension with bootstrap design
18
+ pn.extension(design="bootstrap")
19
+
20
+
21
+ # =============================================================================
22
+ # Constants & Global Variables
23
+ # =============================================================================
24
+ # Color palette
25
+ DARK_NAVY_BLUE = "#0B162A" # background
26
+ LIGHT_BLUE = "#B5D3E7" # even row background
27
+ WHITE = "#FFFFFF" # odd row background
28
+ YELLOW = "#F6D588" # reciprocal tariffs background
29
+ GOLD = "#FFF8DE" # logo background
30
+
31
+ # Logo image (first element of the returned tuple)
32
+ logo = vals.fmt_image("logo.png", height=150)[0]
33
+
34
+ # Base tariffs imposed data
35
+ TARIFS_IMPOSED = {
36
+ "country": [
37
+ "China", "European Union", "Vietnam", "Taiwan", "Japan", "India",
38
+ "South Korea", "Thailand", "Switzerland", "Indonesia", "Malaysia",
39
+ "Cambodia", "United Kingdom", "South Africa", "Brazil", "Bangladesh",
40
+ "Singapore", "Israel", "Philippines", "Chile", "Australia", "Pakistan",
41
+ "Turkey", "Sri Lanka", "Colombia",
42
+ ],
43
+ "tariffs_charged": [
44
+ "67%", "39%", "90%", "64%", "46%", "52%", "50%", "72%", "61%", "64%",
45
+ "47%", "97%", "10%", "60%", "10%", "74%", "10%", "33%", "34%", "10%",
46
+ "10%", "58%", "10%", "88%", "10%",
47
+ ],
48
+ "reciprocal_tariffs": [
49
+ "34%", "20%", "46%", "32%", "24%", "26%", "25%", "36%", "31%", "32%",
50
+ "24%", "49%", "10%", "30%", "10%", "37%", "10%", "17%", "17%", "10%",
51
+ "10%", "29%", "10%", "44%", "10%",
52
+ ],
53
+ }
54
+
55
+
56
+ # =============================================================================
57
+ # Helper Functions for Tariffs Calculations
58
+ # =============================================================================
59
+ def scale_tariffs(tarifs=TARIFS_IMPOSED, factor: float = 0.5) -> dict:
60
+ """
61
+ Create a new tariffs dictionary where the 'reciprocal_tariffs' are
62
+ scaled based on the factor applied to the 'tariffs_charged'.
63
+
64
+ The 'tariffs_charged' column remains unchanged.
65
+ """
66
+ new_reciprocal = []
67
+ for tariff in TARIFS_IMPOSED["tariffs_charged"]:
68
+ # Convert percentage string to integer, scale it, and round to nearest integer.
69
+ num = int(tariff.strip('%'))
70
+ scaled_value = round(num * factor)
71
+ new_reciprocal.append(f"{scaled_value}%")
72
+
73
+ return {
74
+ "country": TARIFS_IMPOSED["country"],
75
+ "tariffs_charged": TARIFS_IMPOSED["tariffs_charged"],
76
+ "reciprocal_tariffs": new_reciprocal,
77
+ }
78
+
79
+
80
+ def randomize_tariffs(scale: float = 0.5) -> dict:
81
+ """
82
+ Create a new tariffs dictionary with randomized 'tariffs_charged'
83
+ values (between 10% and 100%) and corresponding 'reciprocal_tariffs'
84
+ values computed as the scaled (rounded) half.
85
+ """
86
+ new_charged = []
87
+ new_reciprocal = []
88
+
89
+ for _ in TARIFS_IMPOSED["country"]:
90
+ rand_value = random.randint(10, 100)
91
+ new_charged.append(f"{rand_value}%")
92
+ reciprocal_value = round(rand_value * scale)
93
+ new_reciprocal.append(f"{reciprocal_value}%")
94
 
95
+ return {
96
+ "country": TARIFS_IMPOSED["country"],
97
+ "tariffs_charged": new_charged,
98
+ "reciprocal_tariffs": new_reciprocal,
99
+ }
100
+
101
+
102
+ # =============================================================================
103
+ # HTML & Style Helpers
104
+ # =============================================================================
105
+ def change_border_radius(content: str, border_radius: int, background_color1: str, background_color2: str) -> str:
106
+ """
107
+ Wrap the content with nested divs that have the specified border radius and background colors.
108
+ """
109
+ return f"""
110
+ <div style="background: {background_color1}; border-radius: {border_radius}px; padding: 2px">
111
+ <div style="background: {background_color2}; border-radius: {border_radius}px; padding: 2px">
112
+ {content}
113
+ </div>
114
+ </div>
115
+ """
116
+
117
+
118
+ def change_border_radius_expr(
119
+ cols: pl.Expr,
120
+ return_dtype: pl.DataType,
121
+ border_radius: int,
122
+ background_color1: str,
123
+ background_color2: str,
124
+ ) -> pl.Expr:
125
+ """
126
+ Apply the change_border_radius function to each element in a Polars column.
127
+ """
128
+ return cols.map_elements(
129
+ lambda x: change_border_radius(x, border_radius, background_color1, background_color2),
130
+ return_dtype=return_dtype,
131
+ )
132
+
133
+
134
+ # =============================================================================
135
+ # Table Creation Function
136
+ # =============================================================================
137
+ def create_tarifs_table(tarifs: dict = TARIFS_IMPOSED) -> GT:
138
+ """
139
+ Create and style the tariffs table using Polars and Great Tables (GT).
140
+ The table is styled with alternating row colors and custom fonts.
141
+ """
142
+ # Build the Polars DataFrame with an index (used to alternate row styles)
143
+ df = (
144
+ pl.DataFrame(tarifs)
145
+ .with_row_index("mod")
146
+ .with_columns(pl.col("mod").mod(2))
147
+ .with_columns(*[pl.lit("").alias(str(i)) for i in range(4)])
148
+ .with_columns(
149
+ # Apply border radius styling for "country" and "tariffs_charged" based on the mod value
150
+ pl.when(pl.col("mod").eq(0))
151
+ .then(
152
+ change_border_radius_expr(
153
+ pl.col("country", "tariffs_charged"),
154
+ pl.String,
155
+ 5,
156
+ DARK_NAVY_BLUE,
157
+ LIGHT_BLUE,
158
+ )
159
+ )
160
+ .otherwise(
161
+ change_border_radius_expr(
162
+ pl.col("country", "tariffs_charged"),
163
+ pl.String,
164
+ 5,
165
+ DARK_NAVY_BLUE,
166
+ WHITE,
167
+ )
168
  ),
169
+ # Style the "reciprocal_tariffs" column
170
+ change_border_radius_expr(
171
+ pl.col("reciprocal_tariffs"), pl.String, 5, DARK_NAVY_BLUE, YELLOW
 
 
 
 
172
  ),
 
173
  )
174
+ .select(["0", "country", "1", "tariffs_charged", "2", "reciprocal_tariffs", "3"])
175
+ # Add an extra empty row at the end of the table
176
+ .pipe(lambda df_: pl.concat([df_, pl.DataFrame({col: "" for col in df_.columns})], how="vertical"))
177
+ )
178
 
179
+ # Create a GT table and apply styling options
180
+ table = (
181
+ GT(df)
182
+ .cols_align("center", columns=["tariffs_charged", "reciprocal_tariffs"])
183
+ .cols_label({
184
+ "country": html(
185
+ f"""<div>
186
+ {logo}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Reciprocal Tariffs&nbsp;&nbsp;&nbsp;<br>
187
+ Country
188
+ </div>"""
189
+ ),
190
+ "tariffs_charged": html(
191
+ """Tariffs Charged<br>to the U.S.A.<br>Including Currency Manipulation and Trade Barriers"""
192
+ ),
193
+ "reciprocal_tariffs": html("U.S.A. Discounted<br>Reciprocal Tariffs"),
194
+ "0": "", "1": "", "2": "", "3": "",
195
+ })
196
+ .cols_width({
197
+ "country": "50%",
198
+ "0": "3%", "1": "7%", "2": "7%", "3": "3%",
199
+ "tariffs_charged": "18%",
200
+ "reciprocal_tariffs": "18%",
201
+ })
202
+ .tab_style(style=style.fill(color=DARK_NAVY_BLUE), locations=[loc.column_labels(), loc.body()])
203
+ .tab_style(style=style.borders(sides="all", color=DARK_NAVY_BLUE), locations=loc.body())
204
+ .tab_style(
205
+ style=style.text(font=google_font(name="Trajan Pro"), weight="bold", size="xx-large"),
206
+ locations=loc.body(),
207
+ )
208
+ .tab_style(
209
+ style=style.text(font=google_font(name="Georgia"), weight="bold", size="large"),
210
+ locations=loc.column_labels(),
211
+ )
212
+ .tab_style(style=style.text(color=WHITE), locations=loc.column_labels())
213
+ .tab_style(style=style.css("text-align: center;"), locations=loc.column_labels())
214
+ .tab_options(column_labels_border_bottom_style="hidden")
215
+ .tab_style(style=style.fill(color=DARK_NAVY_BLUE), locations=loc.body(rows=[-1]))
216
+ )
217
 
218
+ return table
219
+
220
+
221
+ # =============================================================================
222
+ # Panel Widgets and Callbacks
223
+ # =============================================================================
224
+ # Reactive variable for tariffs data
225
+ tarifs_rx = pn.rx(TARIFS_IMPOSED, sizing_mode="stretch_width")
226
+
227
+ # Scale slider to control tariff scaling factor
228
+ scale_slider = pn.widgets.FloatSlider(name="DISCOUNT FACTOR", value=0.5, step=0.1, start=0.1, end=2.0, sizing_mode="stretch_width")
229
+
230
+
231
+ @pn.depends(scale_slider, watch=True)
232
+ def update_scaled_tariffs(scale_value: float):
233
+ """
234
+ Update the tariffs data based on the scaling factor from the slider.
235
+ """
236
+ tarifs_rx.rx.value = scale_tariffs(tarifs_rx.rx.value, scale_value)
237
+
238
+
239
+ # Button to randomize tariff values
240
+ randomize_button = pn.widgets.Button(name="RANDOM TARIFS", width=200, button_type="primary")
241
+
242
+
243
+ @pn.depends(randomize_button, watch=True)
244
+ def update_random_tariffs(_):
245
+ """
246
+ Update the tariffs data with randomized values when the randomize button is clicked.
247
+ """
248
+ tarifs_rx.rx.value = randomize_tariffs(scale_slider.value)
249
+
250
+
251
+ # Button to reset tariffs to the original imposed values
252
+ reset_button = pn.widgets.Button(name="PSEUDO TARIFS", width=200, button_type="primary")
253
+
254
+
255
+ @pn.depends(reset_button, watch=True)
256
+ def reset_tariffs(_):
257
+ """
258
+ Reset the tariffs data to the original imposed tariffs.
259
+ """
260
+ tarifs_rx.rx.value = TARIFS_IMPOSED
261
+
262
+
263
+ # =============================================================================
264
+ # Custom JavaScript Component: Confetti Button
265
+ # =============================================================================
266
+ class ConfettiButton(JSComponent):
267
+ """
268
+ A custom Panel component that renders a button.
269
+ When clicked, it triggers a confetti animation using an external JS module.
270
+ """
271
+ scale = param.Number(0.5, allow_refs=True)
272
+
273
+ _stylesheets = ["""
274
+ button {
275
+ width: 100%;
276
+ background: #F6D588;
277
+ color: #0B162A;
278
+ border: none;
279
+ padding: 10px;
280
+ border-radius: 4px;
281
+ }
282
+ button:hover {
283
+ background: #F6D599;
284
+ }
285
+ """]
286
+ _esm = """
287
+ import confetti from "https://esm.sh/canvas-confetti@1.6.0";
288
+
289
+ export function render({ model }) {
290
+ let btn = document.createElement("button");
291
+ btn.innerHTML = "IMPOSE NOW. ❤️ TARIFS.";
292
+ btn.addEventListener("click", () => {
293
+ const options = { scalar: 1.0 + 2.0 * model.scale, particleCount: 200 * model.scale, spread: 360 };
294
+ confetti(options);
295
+ });
296
+ return btn;
297
+ }
298
+ """
299
+
300
+
301
+ # Create an instance of the confetti button
302
+ confetti_button = ConfettiButton(scale=scale_slider, sizing_mode="stretch_width", margin=10)
303
+
304
+
305
+ # =============================================================================
306
+ # Layout and Serve
307
+ # =============================================================================
308
+ layout = pn.Column(
309
+ "# Reciprocal Tarifs Calculator",
310
+ pn.Row(reset_button, randomize_button, scale_slider),
311
+ confetti_button,
312
+ pn.pane.HTML(tarifs_rx.rx.pipe(create_tarifs_table)),
313
+ width=1000,
314
+ styles={"margin-right": "auto", "margin-left": "auto"},
315
+ stylesheets=[ "div: {background-color: #F6D588}" ],
316
+ )
317
+
318
+ layout.servable()