├── .gitignore
├── LICENSE
├── PART_1_UI
├── README.md
├── exercise1
│ ├── README.md
│ ├── exercise1_app.py
│ └── exercise1_screenshot.png
├── exercise2
│ ├── README.md
│ ├── exercise2_app.py
│ └── exercise2_screenshot.png
├── exercise3
│ ├── README.md
│ ├── exercise3_app.py
│ ├── exercise3_screenshot.png
│ └── www
│ │ └── image.jpg
└── part1_app.py
├── PART_2_reactivity
├── README.md
├── exercise1
│ ├── README.md
│ ├── exercise1_app.py
│ └── exercise1_screenshot.png
├── exercise2
│ ├── README.md
│ ├── exercise2_app.py
│ └── exercise2_screenshot.png
├── exercise3
│ ├── README.md
│ ├── exercise3_app.py
│ ├── exercise3_screenshot.png
│ └── foods.csv
└── part2_app.py
├── PART_3_express
├── README.md
├── exercise1
│ ├── README.md
│ ├── exercise1_app.py
│ ├── exercise1_screenshot.png
│ └── extra-vehicular_activity.csv
├── exercise2
│ ├── README.md
│ ├── exercise2_app.py
│ ├── exercise2_screenshot.png
│ └── www
│ │ ├── adult.jpg
│ │ ├── old.jpg
│ │ └── young.jpg
└── part3_app.py
├── PART_4_Plotly-DataTable
├── README.md
├── exercise1
│ ├── README.md
│ ├── exercise1_app.py
│ └── exercise1_screenshot.png
├── exercise2
│ ├── README.md
│ ├── agenda.json
│ ├── exercise2_app.py
│ └── exercise2_screenshot.png
├── part4_dt_app.py
└── part4_plotly_app.py
├── README.md
├── SETUP
├── README.MD
└── test_app.py
├── docs
├── README.md
├── assets
│ └── shiny-for-python.svg
└── shinylive
│ └── index.html
├── pyproject.toml
├── requirements.txt
├── solutions
├── part1_ex1_solution_app.py
├── part1_ex2_solution_app.py
├── part1_ex3_solution_app.py
├── part2_ex1_solution_app.py
├── part2_ex2_solution_app.py
├── part2_ex3_solution_app.py
├── part3_ex1_solution_app.py
├── part3_ex2_solution_app.py
├── part4_ex1_solution_app.py
└── part4_ex2_solution_app.py
└── uv.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | _*
2 | untracked/
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/PART_1_UI/README.md:
--------------------------------------------------------------------------------
1 | # PART 1 - Creating the app UI
2 |
3 | ## App skeleton
4 |
5 | The Python Shiny app UI design process is highly similar to R. The basic app
6 | skeleton looks like this:
7 |
8 | ```python
9 | from shiny import App, ui, reactive, render
10 |
11 | app_ui = ui.page_fluid(
12 | )
13 |
14 | def server(input, output, session):
15 | pass
16 |
17 | app = App(app_ui, server)
18 | ```
19 |
20 | _Note that the UI variable is called `app_ui` and not `ui` as this is an
21 | imported Shiny object we need to access UI objects_
22 |
23 | ## What you can directly transfer from R Shiny
24 |
25 | ### Main layout elements
26 |
27 | The following UI elements have a direct Python Shiny equivalent
28 |
29 | | UI element | R code | Python Code |
30 | | ---------------- | ----------------- | ----------------------- |
31 | | Fluid Page | `fluidPage()` | `ui.page_fluid()` |
32 | | Row | `fluidRow()` | `ui.row()` |
33 | | Column | `column()` | `ui.column()` |
34 | | Tabset | `tabsetPanel()` | `ui.navset_tab()` |
35 | | - tab panel | `tabPanel()` | `ui.nav_panel()` |
36 | | Side bar | `sidebarLayout()` | `ui.layout_sidebar()`\* |
37 | | - side bar panel | `sidebarPanel()` | `ui.sidebar()` |
38 | | - main panel | `mainPanel()` | _not needed_ |
39 |
40 | \* note that you can also use `ui.page_sidebar` if this is the main layout
41 |
42 | _There are many more UI elements available, all under the `ui` object_
43 |
44 | ### Inputs
45 |
46 | All default Shiny inputs in Python are organised under `ui.input_`. The
47 | names are identical to those used in R, only with the _input_ part first and
48 | separated by underscores instead of using camel case.
49 |
50 | | Input | R code | Python code |
51 | | -------- | ----------------- | -------------------------- |
52 | | Slider | `sliderInput()` | `ui.input_slider()` |
53 | | Button | `actionButton()` | `ui.input_action_button()` |
54 | | Number | `numericInput()` | `ui.input_numeric()` |
55 | | Text | `textInput()` | `ui.input_text()` |
56 | | Checkbox | `checkboxInput()` | `ui.input_checkbox()` |
57 | | ... | ... | ... |
58 |
59 | The **arguments** inside the input functions are **identical to R** (e.g.
60 | inputId, label, etc)
61 |
62 | _NOTE: similar to R, every `ui.input_` has a corresponding
63 | `ui.update_` function to dynamically update the input*
64 |
65 | ### Outputs
66 |
67 | All outputs are organised under `ui.output_`
68 |
69 | | Output | R code | Python Code |
70 | | ------ | --------------- | ------------------- |
71 | | Text | `textOutput()` | `ui.output_text()` |
72 | | Table | `tableOutput()` | `ui.output_table()` |
73 | | Plot | `plotOutput` | `ui.output_plot()` |
74 | | UI | `uiOutput()` | `ui.output_ui()` |
75 |
76 | ### HTML Tags
77 |
78 | HTML tags allow you to insert any type of static UI available in HTML, examples
79 | are headers, images, links, or custom divs
80 |
81 | - In R shiny you can access HTML tags via `tags$` e.g. `tags$h1()`
82 | - In Python they are all under `ui.` e.g. `ui.h1()`
83 |
84 | ### Sourcing local images
85 |
86 | - In R shiny, all images need to reside in the `www` sub-folder of your app.
87 | - In python, you need to define the subfolder which contains the static assets.
88 | Assuming you would also create a `www` folder you would designate it using
89 |
90 | ```python
91 | from shiny import ui, App
92 | from pathlib import Path
93 |
94 | app_ui = ui.page_fluid(
95 | ui.tags.img(src = "image.png", alt = "An image")
96 | )
97 |
98 | def server(input, output, session):
99 | return
100 |
101 | app = App(app_ui, server, static_assets=Path(__file__).parent / "www")
102 | ```
103 |
104 | _Note that similarly to R, you do not put the static assets folder name in the
105 | path name when sourcing in data_
106 |
107 | ## References
108 |
109 | - [Layouts](https://shiny.posit.co/py/layouts/)
110 | - [Inputs, outputs and other components](https://shiny.posit.co/py/components/)
111 |
--------------------------------------------------------------------------------
/PART_1_UI/exercise1/README.md:
--------------------------------------------------------------------------------
1 | # PART 1 - Exercise 1 - Instructions
2 |
3 | ## Intro
4 |
5 | You are creating a simple Q&A form to gather questions on the topic of Python
6 | Shiny. You will do this by adding various Shiny inputs to a UI.
7 |
8 | _Note: If this is too basic for you, skip to exercise 2_
9 |
10 | ## Tasks
11 |
12 | - Recreate the layout as shown in the image below, you will need to add:
13 | - Text input to collect someone's name
14 | - Select input with options: General, Development, Deployment
15 | - Text _area_ where user can add their question
16 | - Action button to send the question
17 | - You can ignore the sever function for this exercise. This form will not be
18 | reactive yet as we only focus on building the UI
19 |
20 | ## Expected output
21 |
22 | 
23 |
24 | ## Shinylive Link
25 |
26 | https://pieterjanvc.github.io/RShiny2Python/shinylive/?part1_ex1
27 |
28 | ## References
29 |
30 | - [components](https://shiny.posit.co/py/components/)
31 |
--------------------------------------------------------------------------------
/PART_1_UI/exercise1/exercise1_app.py:
--------------------------------------------------------------------------------
1 | # PART 1 - Exercise 1
2 | # ////////////////////
3 |
4 | from shiny import App, ui
5 |
6 | app_ui = ui.page_fluid()
7 |
8 |
9 | # You can ignore the sever function for this exercise
10 | def server(input, output, session):
11 | pass
12 |
13 |
14 | app = App(app_ui, server)
15 |
--------------------------------------------------------------------------------
/PART_1_UI/exercise1/exercise1_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_1_UI/exercise1/exercise1_screenshot.png
--------------------------------------------------------------------------------
/PART_1_UI/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # PART 1 - Exercise 2 - Instructions
2 |
3 | ## Intro
4 |
5 | You will create a small survey UI that will collect info about people's
6 | experience with Shiny for Python.
7 |
8 | ## Tasks
9 |
10 | - Recreate the layout as shown in the image below. The type of input / output
11 | elements you need to add is provided as orange annotation:
12 | - All inputs are in the first column (width 6)
13 | - A plot output placeholder is in the second column (width 6)
14 | - The button is not part of any column, but sits below
15 | - You can ignore the sever function for this exercise (no reactivity yet)
16 |
17 | ## Expected output
18 |
19 | 
20 |
21 | ## Shinylive Link
22 |
23 | https://pieterjanvc.github.io/RShiny2Python/shinylive/?part1_ex2
24 |
25 | ## References
26 |
27 | - [layouts](https://shiny.posit.co/py/layouts/)
28 |
--------------------------------------------------------------------------------
/PART_1_UI/exercise2/exercise2_app.py:
--------------------------------------------------------------------------------
1 | # PART 1 - Exercise 2
2 | # ////////////////////
3 |
4 | from shiny import App, ui
5 |
6 | app_ui = ui.page_fluid()
7 |
8 |
9 | # You can ignore the sever function for this exercise
10 | def server(input, output, session):
11 | pass
12 |
13 |
14 | app = App(app_ui, server)
15 |
--------------------------------------------------------------------------------
/PART_1_UI/exercise2/exercise2_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_1_UI/exercise2/exercise2_screenshot.png
--------------------------------------------------------------------------------
/PART_1_UI/exercise3/README.md:
--------------------------------------------------------------------------------
1 | # PART 1 - Exercise 3 - Instructions
2 |
3 | ## Tasks
4 |
5 | Create an app with two tabs
6 |
7 | - TAB 1:
8 | - Sidebar layout with title: "Settings"
9 | - In the side panel: Group of checkboxes called "Features" with options A, B,
10 | C
11 | - In the main panel: Card with header "Info" and content paragraph "... some
12 | info ..."
13 | - TAB 2:
14 | - Shows the [image](www/image.jpg) located in the `www` folder
15 |
16 | ## Expected output
17 |
18 | 
19 |
20 | ## Shinylive Link
21 | https://pieterjanvc.github.io/RShiny2Python/shinylive/?part1_ex3
22 |
23 | ## References
24 |
25 | - [layouts](https://shiny.posit.co/py/layouts/)
26 |
--------------------------------------------------------------------------------
/PART_1_UI/exercise3/exercise3_app.py:
--------------------------------------------------------------------------------
1 | # PART 1 - Exercise 3
2 | # ///////////////////
3 |
4 | from shiny import App, ui
5 |
6 | #UI
7 | app_ui = ui.page_fluid()
8 |
9 | # Ignore for now
10 | def server(input, output, session):
11 | pass
12 |
13 |
14 | app = App(app_ui, server)
15 |
--------------------------------------------------------------------------------
/PART_1_UI/exercise3/exercise3_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_1_UI/exercise3/exercise3_screenshot.png
--------------------------------------------------------------------------------
/PART_1_UI/exercise3/www/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_1_UI/exercise3/www/image.jpg
--------------------------------------------------------------------------------
/PART_1_UI/part1_app.py:
--------------------------------------------------------------------------------
1 | # PART 1 - Live Demo
2 | # //////////////////
3 |
4 | from shiny import ui, render, App
5 | from pathlib import Path
6 |
7 | app_ui = ui.page_fluid(
8 | ui.layout_sidebar(
9 | ui.sidebar(ui.input_action_button("btn", "Click")),
10 | ui.input_text("txt", "Write some text"),
11 | ui.input_select("sel", "Choose:", choices=["Option 1", "Option 2"]),
12 | ui.output_text("outText"),
13 | ui.img(src = "image.jpg")
14 | )
15 | )
16 |
17 | def server(input, output, session):
18 | pass
19 |
20 | app = App(app_ui, server, static_assets=Path(__file__).parent / "exercise3" / "www")
21 |
--------------------------------------------------------------------------------
/PART_2_reactivity/README.md:
--------------------------------------------------------------------------------
1 | # PART 2 - Reactivity
2 |
3 | The logic behind **reactivity in R Shiny is identical in Python**, so again
4 | there are one-to-one translations between the various reactive components,
5 | although there are **some differences in syntax** from time to time.
6 |
7 | ## Accessing inputs
8 |
9 | - In R, inputs are accessed using `input$` e.g. `input$btn`
10 | - In Python, inputs are accessed using `input.()` e.g. `input.btn()`
11 |
12 | Note that **Python always uses parentheses `()` to access reactive objects**.
13 | This is more consistent than in R.
14 |
15 | ## Intro to decorators
16 |
17 | The major syntactic difference between R and Python Shiny is that **Python uses
18 | decorators to turn ordinary functions into different reactive environments**,
19 | whereas R Shiny has a dedicate function for each type fo reactive environment.
20 |
21 | A decorator is a function annotation what will add additional functionality to
22 | an existing function whenever it is evaluated. There is no need to know how
23 | these decorators work, just how you assign them.
24 |
25 | Decorators start with an `@` symbol followed by the decorator function name
26 |
27 | ```python
28 | @customDecorator
29 | def myFunction:
30 | return True
31 | ```
32 |
33 | All Shiny decorators can be found in the `reactive` or `render` objects imported
34 | via `from shiny import reactive, render`
35 |
36 | ## Assigning outputs
37 |
38 | Every R Shiny render function has an equivalent decorator in Python. Different
39 | from R is that there is no need to use the `output` object.
40 |
41 | | Output | R render function | Python function decorator |
42 | | ------ | ----------------- | ------------------------- |
43 | | Text | `renderText()` | `@render.text` |
44 | | Table | `renderTable()` | `@render.table` |
45 | | Plot | `renderPlot()` | `@render.plot` |
46 | | UI | `renderUI()` | `@render.ui` |
47 | | ... | ... | ... |
48 |
49 | ```python
50 | @render.text
51 | def txt():
52 | return "Hello" + str(input.name())
53 | ```
54 |
55 | - The **output function name** is the name of the **UI outputId**. In R shiny
56 | the example would have used `output$txt`
57 | - The **decorator** defines what type of output is being created (see table)
58 | - Similar to R, the value returned by the function must be compatible with the
59 | render type (e.g. in the example, this is text)
60 | - Make sure to use the `return` keywords or the output won't be rendered
61 |
62 | ## Other Reactive Environments
63 |
64 | Shiny has 4 main types of reactive environments that differ in how they react to
65 | trigger and whether they return a reactive variable. In python, decorators are
66 | again used to convert regular functions in different reactive environments. The
67 | `reactive` object contains all relevant decorators.
68 |
69 | | Environment behaviour | R function | Python decorators |
70 | | ---------------------------------- | ----------------- | -------------------------------------- |
71 | | Always trigger / Return nothing | `observe()` | `@reactive.effect` |
72 | | Always trigger / Return variable | `reactive()` | `@reactive.calc` |
73 | | Specific trigger / Return nothing | `observeEvent()` | `@reactive.effect` & `@reactive.event` |
74 | | Specific trigger / Return variable | `eventReactive()` | `@reactive.calc` & `@reactive.event` |
75 |
76 | Examples
77 |
78 | ```python
79 | @reactive.effect
80 | def _():
81 | print(input.a() + input.b())
82 | ```
83 |
84 | - This environment is identical to `observe()` in R
85 | - Given this environment **does not return anything**, convention says to use
86 | the empty assignment operator `_` for the function name
87 |
88 | ```python
89 | @reactive.calc
90 | @reactive.event(input.a)
91 | def sum():
92 | return input.a() + input.b()
93 | ```
94 |
95 | - This environment is identical to `eventReactive()` in R
96 | - This environment **returns a reactive variable** `sum()`
97 | - The environment will **only trigger when input.a() changes**. Note that in the
98 | reactive.event decorator the parentheses after input.a are omitted (will cause
99 | error if used)
100 |
101 | ## Reactive variables
102 |
103 | `reactive.value()` is the Python equivalent to `reactiveVal()` in R
104 |
105 | - To **assign** a reactive variable use the
106 | `var = reactive.value()` function.
107 | - To **access** a reactive variable, use `var()` or `var.get()`
108 | - To **update** a reactive varaible, use `var.set()`
109 |
110 | _Note: there is no equivalent to R's `reactiveValues()`, as this can all be
111 | achieved with the same `reactive.value()` function using a list or dictionary_
112 |
113 | ### Caution when updating reactive variables
114 |
115 | Whenever you want to assign the content of a reactive variable to a local
116 | variable, you must copy it to avoid unexpected behaviour.
117 |
118 | Example
119 | ```python
120 | x = myval().copy()
121 | _ = x.pop()
122 | myval.set(x)
123 | ```
124 |
--------------------------------------------------------------------------------
/PART_2_reactivity/exercise1/README.md:
--------------------------------------------------------------------------------
1 | # PART 2 - Exercise 1 - Instructions
2 |
3 | ## Intro
4 |
5 | This app explores famous movies throughout cinematic history that feature cats.
6 | The data is provided and UI has already been created.
7 |
8 | _You should be able to complete this exercise by just using render functions_
9 |
10 | ## Tasks
11 |
12 | 1. Populate the `img` output with an image HTML tag that contains the movie
13 | poster selected in the `movie` dropdown. The URL for each movie can be found
14 | in the `url_poster` column of the data frame (so no local images needed)
15 |
16 | 2. Render the data frame in the `tbl` output, only showing the "year", "title",
17 | "produced_by", "directed_by" columns
18 |
19 | 3. Filter the data frame based on the year selection set by the `era` slider
20 |
21 | _Some example python code has been provided demonstrating how to manipulate
22 | pandas data frames for this app as this is not the focus of this workshop_
23 |
24 | ## Expected output
25 |
26 | 
27 |
28 | ## Shinylive Link
29 |
30 | https://pieterjanvc.github.io/RShiny2Python/shinylive/?part2_ex1
31 |
32 | ## References
33 |
34 | - [render outputs](https://shiny.posit.co/py/components/#outputs)
35 |
--------------------------------------------------------------------------------
/PART_2_reactivity/exercise1/exercise1_app.py:
--------------------------------------------------------------------------------
1 | # PART 2 - Exercise 1
2 | # ///////////////////
3 |
4 | import requests
5 | import pandas as pd
6 | from io import StringIO
7 | from shiny import App, ui, render
8 |
9 | # See README.md for instructions
10 |
11 | # Get the data and process it
12 | url = "https://data.opendatasoft.com/api/explore/v2.1/catalog/datasets/cats-in-movies@public/exports/csv"
13 | resp = requests.get(url)
14 | data = pd.read_csv(StringIO(resp.content.decode("UTF-8")), sep=";").sort_values(
15 | by=["title"]
16 | )
17 |
18 | # UI
19 | app_ui = ui.page_fluid(
20 | ui.panel_title("Movies with cats"),
21 | ui.row(
22 | ui.column(
23 | 4,
24 | ui.input_select("movie", "Movie", choices=data["title"].tolist()),
25 | ui.output_ui("img"),
26 | ),
27 | ui.column(
28 | 8,
29 | ui.input_slider(
30 | "era",
31 | "Era",
32 | min=min(data["year"]),
33 | max=max(data["year"]),
34 | value=[min(data["year"]), max(data["year"])],
35 | ),
36 | ui.output_data_frame("tbl"),
37 | ),
38 | ),
39 | )
40 |
41 |
42 | # SERVER
43 | def server(input, output, session):
44 | # Get the image URL from the data frame
45 | data[data["title"] == "Alien"]["url_poster"].values[0]
46 |
47 | # Filter the data frame by era (year range)
48 | data[(data["year"] >= 1950) & (data["year"] <= 1960)]
49 |
50 | # Select specific columns
51 | data[["year", "title", "produced_by", "directed_by"]]
52 |
53 |
54 | app = App(app_ui, server)
55 |
--------------------------------------------------------------------------------
/PART_2_reactivity/exercise1/exercise1_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_2_reactivity/exercise1/exercise1_screenshot.png
--------------------------------------------------------------------------------
/PART_2_reactivity/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # PART 2 - Exercise 2 - Instructions
2 |
3 | ## Intro
4 |
5 | In this app you will implement a very simple version of the
6 | [hangman]() game. The data, UI and
7 | basic game code are provided.
8 |
9 | _You can test the game code by manually running it before making it reactive_
10 |
11 | ## Tasks
12 |
13 | - When the user clicks the `guess` button, the game code should run using the
14 | letter selected in the `letter` input and the `progress` output should update
15 | with the result
16 |
17 | - You will need to keep track of all guessed letters, and remove them from the
18 | `letter` input after each guess
19 |
20 | - When the user refreshes the page, the game will reset with a new random word
21 |
22 | - To keep things simple, the result should only start appearing on the page once
23 | the `guess` button has been clicked. If you want an extra challenge, try to
24 | make a blank result (i.e. all `-`) appear when the game starts.
25 |
26 | ## Expected output
27 |
28 | _This image shows the output somewhere in the middle of the game_
29 |
30 | 
31 |
32 | ## Shinylive Link
33 | https://pieterjanvc.github.io/RShiny2Python/shinylive/?part2_ex2
34 |
35 | ## References
36 |
37 | - [reactivity](https://shiny.posit.co/py/docs/reactive-foundations.html)
38 |
--------------------------------------------------------------------------------
/PART_2_reactivity/exercise2/exercise2_app.py:
--------------------------------------------------------------------------------
1 | # PART 2 - Exercise 2
2 | # ///////////////////
3 |
4 | import requests
5 | import string
6 | import random
7 | from shiny import App, ui, render, reactive
8 |
9 | # Get the data and process it
10 | url = "https://raw.githubusercontent.com/pkLazer/password_rank/refs/heads/master/4000-most-common-english-words-csv.csv"
11 | words = requests.get(url).text.splitlines()
12 | words = [word for word in words if len(word) == 6]
13 |
14 | # UI
15 | app_ui = ui.page_fluid(
16 | ui.panel_title("Hangman"),
17 | ui.output_ui("progress"),
18 | ui.input_select("letter", "Pick a letter", choices=list(string.ascii_lowercase)),
19 | ui.input_action_button("guess", "Guess"),
20 | )
21 |
22 |
23 | # SERVER
24 | def server(input, output, session):
25 | # None-reactive Hangman game code
26 | word = random.choice(words)
27 | guesses = ["m"]
28 | guess = "e"
29 | guesses.append(guess)
30 | remaining = [l for l in list(string.ascii_lowercase) if l not in guesses]
31 | result = " ".join([letter if letter in guesses else " - " for letter in list(word)])
32 | ui.h1(result, style="font-family: monospace; color: #BF408B;")
33 |
34 |
35 | app = App(app_ui, server)
36 |
--------------------------------------------------------------------------------
/PART_2_reactivity/exercise2/exercise2_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_2_reactivity/exercise2/exercise2_screenshot.png
--------------------------------------------------------------------------------
/PART_2_reactivity/exercise3/README.md:
--------------------------------------------------------------------------------
1 | # PART 2 - Exercise 3 - Instructions
2 |
3 | ## Intro
4 |
5 | Let explore a typical question you ask each other at social occasions:
6 |
7 | _If you could only eat one food for the rest of your live, what would it be?_
8 |
9 | Now let's see how healthy you would be and how this would affect your
10 | recommended daily intake of macro nutrients.
11 |
12 | You have been provided with an app that already has all required inputs, and
13 | some basic, non-interactive python / seaborn code to generate a plot showing the
14 | distribution of macro nutrients if you would only eat almonds and would target
15 | consuming 250g of carbs in a day.
16 |
17 | ## Tasks
18 |
19 | - Add a card to the UI with a the name "Nutritional values" containing a plot
20 | output to the UI underneath the provided input cards
21 | - Create a plot function on the server and move all the code inside of it
22 | - Link up all relevant inputs to the plot output will update as expected
23 | - Tip: If you want to access an input based on its name you can also use
24 | `input["name"]()`. This allows to dynamically select an input using a variable
25 |
26 | ### Extra challenge (optional)
27 |
28 | The dataset also contains a column with the number of grams for a set of given
29 | nutrients. Add a title to the plot that would mention how many grams (or pounds)
30 | you would have to eat to take in the current number of nutrients shown in the
31 | plot.
32 |
33 | ## Expected output
34 |
35 | 
36 |
37 | ## Shinylive Link
38 |
39 | https://pieterjanvc.github.io/RShiny2Python/shinylive/?part2_ex3
40 |
41 | ## References
42 |
43 | - [reactivity](https://shiny.posit.co/py/docs/reactive-foundations.html)
44 |
--------------------------------------------------------------------------------
/PART_2_reactivity/exercise3/exercise3_app.py:
--------------------------------------------------------------------------------
1 | # PART 2 - Exercise 3
2 |
3 | from shiny import App, ui, render, reactive
4 | import seaborn as sns
5 | import pandas as pd
6 | from pathlib import Path
7 |
8 | # data = pd.read_csv("PART_2_reactivity/exercise3/foods.csv")
9 | data = pd.read_csv(Path(__file__).parent / "foods.csv")
10 | data = data.sort_values('Food')
11 |
12 | # UI
13 | app_ui = ui.page_fluid(
14 | ui.panel_title("If I could only eat one thing ..."),
15 | ui.row(
16 | ui.column(
17 | 4,
18 | ui.card(
19 | ui.card_header("Selection"),
20 | ui.input_select("food", "Pick a Food", choices=list(data["Food"])),
21 | ui.input_select(
22 | "comp",
23 | "Daily intake component to match",
24 | choices=["Carbs", "Protein", "Fat", "Calories"],
25 | ),
26 | ),
27 | ),
28 | ui.column(
29 | 8,
30 | ui.card(
31 | ui.card_header("Target Daily intake"),
32 | ui.row(
33 | ui.column(
34 | 6,
35 | ui.input_slider(
36 | "Carbs", "Carbs (g)", min=10, max=500, value=250
37 | ),
38 | ui.input_slider(
39 | "Protein", "Protein (g)", min=10, max=200, value=50
40 | ),
41 | ),
42 | ui.column(
43 | 6,
44 | ui.input_slider("Fat", "Fat (g)", min=10, max=200, value=60),
45 | ui.input_slider(
46 | "Calories", "kCals", min=1000, max=4000, value=2000
47 | ),
48 | ),
49 | ),
50 | ),
51 | ),
52 | ),
53 | )
54 |
55 |
56 | # SERVER
57 | def server(input, output, session):
58 |
59 | # Select food to focus e.g. Almonds
60 | food = data[data["Food"] == "Almonds"][
61 | ["Grams", "Calories", "Protein", "Fat", "Carbs"]
62 | ]
63 |
64 | # Get in long format
65 | food = pd.melt(food, var_name="name")
66 |
67 | # Adjust based on component to match and set daily target intake e.g. 250g of carbs
68 | food["value"] = (
69 | food["value"]
70 | / food.loc[food["name"] == "Carbs", "value"].values[0]
71 | * 250
72 | )
73 |
74 | # Get the target daily intake values
75 | target = pd.DataFrame(
76 | {
77 | "name": ["Protein", "Fat", "Carbs"],
78 | "value": [50, 60, 250],
79 | }
80 | )
81 |
82 | # Create the bar plot showing consumed nutrients for chosen food
83 | plot = sns.barplot(
84 | x="name",
85 | y="value",
86 | data=food.iloc[2:5],
87 | color="#ff843d",
88 | label="Total Nutrients Consumed",
89 | )
90 |
91 | # Overlay barplot with target daily intake
92 | sns.barplot(
93 | x="name",
94 | y="value",
95 | data=target,
96 | color="gray",
97 | edgecolor="#007bc2",
98 | linewidth=2,
99 | facecolor="none",
100 | label="Recommended intake",
101 | )
102 |
103 | # Edit titla and labels
104 | plot.set_title("Nutritional values")
105 | plot.set_ylabel("Grams")
106 | plot.set_xlabel("Nutrient")
107 |
108 | app = App(app_ui, server)
109 |
--------------------------------------------------------------------------------
/PART_2_reactivity/exercise3/exercise3_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_2_reactivity/exercise3/exercise3_screenshot.png
--------------------------------------------------------------------------------
/PART_2_reactivity/exercise3/foods.csv:
--------------------------------------------------------------------------------
1 | Food,Grams,Calories,Protein,Fat,Sat.Fat,Fiber,Carbs,Category
2 | Cows' milk,976,660,32,40,36,0,48,Dairy products
3 | Buttermilk,246,127,9,5,4,0,13,Dairy products
4 | Fortified milk,1419,1373,89,42,23,1.4,119,Dairy products
5 | Powdered milk,103,515,27,28,24,0,39,Dairy products
6 | Goats' milk,244,165,8,10,8,0,11,Dairy products
7 | Cocoa,252,235,8,11,10,0,26,Dairy products
8 | Custard,248,285,13,14,11,0,28,Dairy products
9 | Ice cream,188,300,6,18,16,0,29,Dairy products
10 | Ice milk,190,275,9,10,9,0,32,Dairy products
11 | Cream or half-and-half,120,170,4,15,13,0,5,Dairy products
12 | Cheese,225,240,30,11,10,0,6,Dairy products
13 | Cream cheese,28,105,2,11,10,0,1,Dairy products
14 | Eggs Scrambled or fried,128,220,13,16,14,0,1,Dairy products
15 | Butter,112,113,114,115,116,117,118,"Fats, Oils, Shortenings"
16 | Butter,112,113,114,115,116,117,118,"Fats, Oils, Shortenings"
17 | Bacon,16,95,4,8,7,0,1,"Meat, Poultry"
18 | Clams,85,87,12,1,0,0,2,"Fish, Seafood"
19 | Crab meat,85,90,14,2,0,0,1,"Fish, Seafood"
20 | Fish sticks fried,112,200,19,10,5,0,8,"Fish, Seafood"
21 | Haddock,85,135,16,5,4,0,6,"Fish, Seafood"
22 | Oysters,230,231,232,233,234,235,236,"Fish, Seafood"
23 | Scallops,100,104,18,8,0,0,10,"Fish, Seafood"
24 | Red kidney,260,230,15,1,0,2.5,42,Vegetables A-E
25 | Dandelion greens,180,80,5,1,0,3.2,16,Vegetables A-E
26 | Kale,110,45,4,1,0,0.9,8,Vegetables F-P
27 | Parsnips,155,95,2,1,0,3,22,Vegetables F-P
28 | Peppers with beef and crumbs,150,255,19,9,8,1,24,Vegetables R-Z
29 | Potatoes Mashed with milk and butter,200,230,4,12,11,0.7,28,Vegetables R-Z
30 | Soybeans,200,260,22,11,0,3.2,20,Vegetables R-Z
31 | Sweet potatoes,110,155,2,1,0,1,36,Vegetables R-Z
32 | Turnip greens,145,45,4,1,0,1.8,8,Vegetables R-Z
33 | Avocado,108,185,2,18,12,1.8,6,Fruits A-F
34 | Blackberries,144,85,2,1,0,6.6,19,Fruits A-F
35 | Cherries,257,100,2,1,0,2,26,Fruits A-F
36 | Olives large,65,72,1,10,9,0.8,3,Fruits G-P
37 | OlivesRipe,65,105,1,13,12,1,1,Fruits G-P
38 | Prunes,270,300,3,1,0,0.8,81,Fruits G-P
39 | Watermelon,925,120,2,1,0,3.6,29,Fruits R-Z
40 | Rye,23,55,2,1,1,0.1,12,"Breads, cereals, fastfood,grains"
41 | Whole-wheat,454,1100,48,14,10,67.5,216,"Breads, cereals, fastfood,grains"
42 | Whole-wheat,23,55,2,1,0,0.31,11,"Breads, cereals, fastfood,grains"
43 | Corn bread ground meal,50,100,3,4,2,0.3,15,"Breads, cereals, fastfood,grains"
44 | Corn meal,118,360,9,4,2,1.6,74,"Breads, cereals, fastfood,grains"
45 | Flour,110,460,39,22,0,2.9,33,"Breads, cereals, fastfood,grains"
46 | Wheat (all purpose),110,400,12,1,0,0.3,84,"Breads, cereals, fastfood,grains"
47 | Wheat (whole),120,390,13,2,0,2.8,79,"Breads, cereals, fastfood,grains"
48 | Macaroni,140,155,5,1,0,0.1,32,"Breads, cereals, fastfood,grains"
49 | Noodles,160,200,7,2,2,0.1,37,"Breads, cereals, fastfood,grains"
50 | Oatmeal,236,150,5,3,2,4.6,26,"Breads, cereals, fastfood,grains"
51 | Popcorn salted,28,152,3,7,2,0.5,20,"Breads, cereals, fastfood,grains"
52 | Rice,208,748,15,3,0,1.2,154,"Breads, cereals, fastfood,grains"
53 | Rolls,50,411,3,12,11,0.1,23,"Breads, cereals, fastfood,grains"
54 | Spaghetti with meat sauce,250,285,13,10,6,0.5,35,"Breads, cereals, fastfood,grains"
55 | Shredded wheat biscuit,28,100,3,1,0,0.7,23,"Breads, cereals, fastfood,grains"
56 | Waffles,75,240,8,9,1,0.1,30,"Breads, cereals, fastfood,grains"
57 | Wheat germ,68,245,17,7,3,2.5,34,"Breads, cereals, fastfood,grains"
58 | Bean soup,250,190,8,5,4,0.6,30,Soups
59 | Beef soup,250,100,6,4,4,0.5,11,Soups
60 | Chicken soup,250,75,4,2,2,0,10,Soups
61 | Clam chowder,255,85,5,2,8,0.5,12,Soups
62 | Cream soups,255,200,7,12,11,1.2,18,Soups
63 | Split-pea soup,250,147,8,3,3,0.5,25,Soups
64 | Tomato soup,245,175,6,7,6,0.5,22,Soups
65 | Bread pudding,200,374,11,12,11,0.2,56,"Desserts, sweets"
66 | Chocolate fudge,120,420,5,14,12,0.3,70,"Desserts, sweets"
67 | Fruit cake,30,105,2,4,3,0.2,17,"Desserts, sweets"
68 | Sponge cake,40,115,3,2,2,0,22,"Desserts, sweets"
69 | Milk chocolate,56,290,2,6,6,0.2,44,"Desserts, sweets"
70 | Cherry Pie,135,340,3,13,11,0.1,55,"Desserts, sweets"
71 | Custard,130,265,7,11,10,0,34,"Desserts, sweets"
72 | Lemon meringue,120,300,4,12,10,0.1,45,"Desserts, sweets"
73 | Mince,135,340,3,9,8,0.7,62,"Desserts, sweets"
74 | Pumpkin Pie,130,265,5,12,11,8,34,"Desserts, sweets"
75 | Tapioca cream pudding,250,335,10,10,9,0,42,"Desserts, sweets"
76 | Almonds,70,425,13,38,28,1.8,13,Seeds and Nuts
77 | Brazil nuts,70,457,10,47,31,2,7,Seeds and Nuts
78 | Cashews,70,392,12,32,28,0.9,20,Seeds and Nuts
79 | coconut sweetened,50,274,1,20,19,2,26,Seeds and Nuts
80 | Peanut butter,50,300,12,25,17,0.9,9,Seeds and Nuts
81 | Peanuts,50,290,13,25,16,1.2,9,Seeds and Nuts
82 | Pecans,52,343,5,35,25,1.1,7,Seeds and Nuts
83 | Sesame seeds,50,280,9,24,13,3.1,10,Seeds and Nuts
84 | Sunflower seeds,50,280,12,26,7,1.9,10,Seeds and Nuts
85 | Walnuts,50,325,7,32,7,1,8,Seeds and Nuts
86 |
--------------------------------------------------------------------------------
/PART_2_reactivity/part2_app.py:
--------------------------------------------------------------------------------
1 | # PART 2 - Live Demo
2 | # //////////////////
3 |
4 | from shiny import App, reactive, render, ui
5 |
6 | app_ui = ui.page_fluid(
7 | ui.input_numeric("num", "Number", 2),
8 | ui.input_action_button("btn", "Click"),
9 | ui.output_ui("out"),
10 | )
11 |
12 | def server(input, output, session):
13 |
14 | @render.ui
15 | def out():
16 | return ui.HTML("The square of the number is: " + str(square()))
17 |
18 | @reactive.calc
19 | @reactive.event(input.btn)
20 | def square():
21 | return input.num() ** 2
22 |
23 | @reactive.effect
24 | @reactive.event(input.btn)
25 | def _():
26 | print(input.num())
27 |
28 | app = App(app_ui, server)
29 |
--------------------------------------------------------------------------------
/PART_3_express/README.md:
--------------------------------------------------------------------------------
1 | # PART 3 - Shiny Express
2 |
3 | ## Intro
4 |
5 | Unlike in R, Shiny for Python has two implementations of the framework
6 |
7 | - Core: Most stable version with all features and structured implementation
8 | - Express: Quick to write, flexible with less boiler plate code
9 |
10 | So far **you have been using the Core framework**, and this is what the majority
11 | of this workshop outside of this module focusses on.
12 |
13 | - Use Express syntax for quick prototyping or simple apps
14 | - Choose the core framework for larger, more complex apps
15 | - Express syntax does not have all features, like complex layout, modules,
16 | dynamic UI etc (see references for details)
17 |
18 | ## Express Syntax differences with Core
19 |
20 | ### Setup
21 |
22 | Unlike in Core, Shiny Express does not have a dedicated organisation of UI and
23 | server in separate functions, but rather makes use of decorators and context
24 | managers. This means that regular Python code and Shiny specific syntax can be
25 | mixed allowing a more fluid organisation of your code.
26 |
27 | Note that you **import objects from `shiny.express` instead of `shiny`**
28 |
29 | ```python
30 | from shiny.express import input, render, ui, app_opts
31 | ```
32 |
33 | - There is no need to use the `App` function at the end of the page as you would
34 | with the Core framework
35 | - If you want to set app options like static assets, you can use `app_opts`
36 |
37 | ```python
38 | app_opts(static_assets=Path(__file__).parent / "myFolder")
39 | ```
40 | ### Layout + inputs
41 |
42 | Given there is **no dedicated UI** function. Layout is organised using **context
43 | managers** using the `with` statement.
44 |
45 | ```python
46 | with ui.sidebar():
47 | ui.input_slider("slider", "Pick a value", 0, 5, 0)
48 | ```
49 |
50 | _This generates a sidebar layout with an input slider in it_
51 |
52 | If you now want to add something to the "main" panel, just put it outside of the
53 | context manager
54 |
55 | ```python
56 | with ui.sidebar():
57 | ui.input_slider("slider", "Pick a value", 0, 5, 0)
58 |
59 | ui.input_select("sel", "Choose", choices = ["A", "B", "C"])
60 | ```
61 |
62 | - The slider will be in the side bar
63 | - The select input will be in the main panel
64 | - You can nest context managers to create more elaborate layouts
65 |
66 | ### Outputs
67 |
68 | As again there is no dedicated server function, but any function can be made
69 | reactive using the appropriate decorators.
70 |
71 | - There are no dedicated UI placeholders for outputs. This means that functions
72 | that return an output (e.g. table, plot) it will insert it wherever it's been
73 | declared.
74 | - Outputs can be declared inside of a context manager and will then appear in
75 | that part of the UI
76 |
77 | ```python
78 | with ui.sidebar():
79 | ui.input_slider("slider", "Pick a value", 0, 5, 0)
80 |
81 | @render.text
82 | def sliderInfo():
83 | return f"You chose: {input.slider()}"
84 |
85 | @render.ui
86 | def picture():
87 | return ui.img(src = f"https://picsum.photos/id/{input.slider() + 10}/200/300",
88 | height = "300px", width = "200px")
89 |
90 | ```
91 |
92 | - The text output from `sliderInfo` will appear inside the sidebar underneath
93 | the slider itself
94 | - The `picture` output will appear in the main panel
95 |
96 | ### Other reactive functions
97 |
98 | Functions decorated with `@reactive.effect`, `@reactive.calc` and
99 | `@reactive.event` can be placed anywhere in the script. In case they produce an
100 | output, this can again be used in any other reactive environment (i.e. decorated
101 | function)
102 |
103 | Note: all code defined outside of a reactive function will only be run once at
104 | startup. Given there is no server, all code will be run every time a new
105 | instance starts.
106 |
107 | ## References
108 |
109 | - [Core vs Express](https://shiny.posit.co/py/docs/express-vs-core.html)
110 | - [Layouts](https://shiny.posit.co/py/layouts/)
111 | - [Express full documentation](https://shiny.posit.co/py/api/express/)
112 |
--------------------------------------------------------------------------------
/PART_3_express/exercise1/README.md:
--------------------------------------------------------------------------------
1 | # PART 3 - Exercise 1 - Instructions
2 |
3 | ## Intro
4 |
5 | You are exploring a dataset recording activities done by an astronaut or
6 | cosmonaut outside a spacecraft beyond the Earth's appreciable atmosphere. You
7 | already have written some basic python code to help you filter this dataset and
8 | better understand the content, however, it is rather tedious to have to
9 | constantly update specific filtering parameters and run the analysis again. You
10 | decide to use Shiny Express to help you quickly build an interactive exploration
11 | of the data.
12 |
13 | ## Tasks
14 |
15 | Starting with the provided python script, add **Shiny Express syntax** to make
16 | this script interactive.
17 |
18 | ### PART 1 - Start simple
19 |
20 | This task should be relatively simple and result in a functional app already
21 |
22 | - Insert a dropdown (select) element that will allow filtering on vehicle type
23 | - Add a slider that will allow additional filtering of the dataset by minimum
24 | duration. Set the min and max value to the min and max found in the whole
25 | dataset. _(Tip, you can use the min() and max() values over a pandas dataframe
26 | column)_
27 | - Convert the `subset` data frame into a reactive data frame (data_frame) and
28 | make sure the data is filtered based on the selected vehicle type
29 |
30 | ### PART 2 - Dynamically update the slider
31 |
32 | This task requires additional reactive environments and logic
33 |
34 | - Whenever the vehicle type changes, update the duration filter to only include
35 | the range for that type of vehicle instead of the values across the whole
36 | dataset
37 | - Tip: similar to R, every `ui.input_` has a corresponding
38 | `ui.update_` function to dynamically update the inputs
39 |
40 | ## Expected output
41 |
42 | _The output shown is for part 1. In part 2, the slider should have different min
43 | and max values_
44 |
45 | 
46 |
47 | ## Shinylive Link
48 | https://pieterjanvc.github.io/RShiny2Python/shinylive/?part3_ex1
49 |
50 | ## References
51 |
52 | - [dataset](https://catalog.data.gov/dataset/extra-vehicular-activity-eva-us-and-russia)
53 | - [Shiny Express](https://shiny.posit.co/py/api/express/)
54 |
--------------------------------------------------------------------------------
/PART_3_express/exercise1/exercise1_app.py:
--------------------------------------------------------------------------------
1 | # PART 3 - Exercise 1
2 | # ///////////////////
3 | from pathlib import Path
4 | import pandas as pd
5 | from datetime import datetime
6 |
7 | from shiny import reactive
8 | from shiny.express import input, render, ui, app_opts
9 |
10 | data = pd.read_csv(Path(__file__).parent / "extra-vehicular_activity.csv")
11 |
12 | # Data cleaning
13 | data.columns = data.columns.str.replace(" ", "")
14 | data["Date"] = pd.to_datetime(data["Date"])
15 | data["Duration"] = pd.to_datetime(data["Duration"], format="%H:%M")
16 | data["Duration"] = data["Duration"].dt.hour * 60 + data["Duration"].dt.minute
17 | data = data.drop(["EVA#", "Country"], axis=1)
18 | data = data.dropna()
19 |
20 |
21 | # Get a simplified list of vehicle types
22 | vehicleTypes = list(data["Vehicle"].str.extract(r"([^\s-]+)")[0].unique())
23 | vehicleTypes.sort()
24 |
25 | # Minimum duration in minutes
26 | minDuration = 60
27 |
28 | # Filter based on vehicleType and minimum duration
29 | subset = data[
30 | (data["Duration"] >= minDuration) & data["Vehicle"].str.contains(vehicleTypes[0])
31 | ]
32 |
33 | # Check the subset
34 | subset
35 |
--------------------------------------------------------------------------------
/PART_3_express/exercise1/exercise1_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_3_express/exercise1/exercise1_screenshot.png
--------------------------------------------------------------------------------
/PART_3_express/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # PART 3 - Exercise 2 - Instructions
2 |
3 | ## Intro
4 |
5 | You are creating a simple biography website template people can use to highlight
6 | specific events in their lifetime that have shaped them.
7 |
8 | ## Tasks
9 |
10 | ### PART 1 - Simple express layout
11 |
12 | Use Shiny express to create this site with 2 tabs (navset_card_tab)
13 |
14 | - Each tab represents a stage in the life of the person
15 | - Each tab has two columns
16 | - (width 3) A card with an image of the person
17 | - (width 9) A card with a paragraph of text
18 | - The images are located in the `www` folder and some text has been provided
19 |
20 | ### PART 2 - Generating express UI with a function
21 |
22 | - Create a function that will generate the repeating tab layout and has the
23 | following arguments:
24 | - tab: The name of the tab
25 | - image: Link to the image being displayed
26 | - text: paragraph of text
27 | - Add a 3rd tab using the function
28 | - Replace the first 2 tabs using the function as well to avoid repetition
29 |
30 | _Tip: You will need the `@expressify` decorator to make your function work_
31 |
32 | ## Expected output
33 |
34 | _Note that the 3rd tab should only be there for PART 2_
35 |
36 | 
37 |
38 | ## Shinylive Link
39 | https://pieterjanvc.github.io/RShiny2Python/shinylive/?part3_ex2
40 |
41 | ## References
42 |
43 | - [layouts](https://shiny.posit.co/py/layouts/)
44 | - [expressify](https://shiny.posit.co/py/api/express/express.expressify.html)
45 |
--------------------------------------------------------------------------------
/PART_3_express/exercise2/exercise2_app.py:
--------------------------------------------------------------------------------
1 | # PART 3 - Exercise 2
2 | # ///////////////////
3 |
4 | from pathlib import Path
5 | from shiny.express import input, render, ui, app_opts, expressify
6 |
7 | # Tab 1 - YOUNG
8 | # Image: "young.jpg"
9 | # Content: "How it all began ..."
10 |
11 | # Tab 2 - ADULT
12 | # Image: "adult.jpg"
13 | # Content: " ... what I aspired to ..."
14 |
15 | # --- ONLY NEEDED FOR PART 2 ---
16 | # Tab 3 - OLD
17 | # Image: "old.jpg"
18 | # Content: "... what I have become"
19 |
--------------------------------------------------------------------------------
/PART_3_express/exercise2/exercise2_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_3_express/exercise2/exercise2_screenshot.png
--------------------------------------------------------------------------------
/PART_3_express/exercise2/www/adult.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_3_express/exercise2/www/adult.jpg
--------------------------------------------------------------------------------
/PART_3_express/exercise2/www/old.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_3_express/exercise2/www/old.jpg
--------------------------------------------------------------------------------
/PART_3_express/exercise2/www/young.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_3_express/exercise2/www/young.jpg
--------------------------------------------------------------------------------
/PART_3_express/part3_app.py:
--------------------------------------------------------------------------------
1 | # PART 3 - Live Demo
2 | # //////////////////
3 |
4 | from shiny.express import input, ui, render
5 | from shiny import reactive
6 |
7 | with ui.sidebar():
8 | ui.input_checkbox("chk", "Check")
9 |
10 | @render.text
11 | def out():
12 | if input.chk():
13 | return "Checked!"
14 | else:
15 | return "Not Checked"
16 |
17 | ui.input_action_button("btn", "Click")
18 |
19 | @reactive.effect
20 | def _():
21 | print(input.btn())
22 |
--------------------------------------------------------------------------------
/PART_4_Plotly-DataTable/README.md:
--------------------------------------------------------------------------------
1 | # PART 4 - Interactive Plots and Tables
2 |
3 | ## Intro
4 |
5 | This workshop focuses on _Plotly_ for plots and _DataTables_ for tables, as both
6 | are well integrated into the Shiny ecosystem and are available in both R and
7 | Python. Shiny apps can monitor and capture clicks or other user events in these
8 | interactive plots plots and tables, allowing additional server-side triggers.
9 |
10 | _Note: This workshop does not focus on how to create plots or style tables, so
11 | please refer to the respective libraries documentation for details_
12 |
13 | ## Interactive Data Tables
14 |
15 | ### Basic table
16 |
17 | Data tables are part of the Shiny library in Python, so there is no need for any
18 | additional installations
19 |
20 | ```python
21 | app_ui = ui.page_fluid(
22 | ui.output_data_frame("tbl")
23 | )
24 |
25 | def server(input, output, session):
26 |
27 | @render.data_frame
28 | def tbl():
29 | return render.DataTable(df, selection_mode = "row")
30 |
31 | ```
32 |
33 | _Note: code to generate `df` not shown_
34 |
35 | - R uses `datatable` in function names (e.g. `datatableOutput()`) whereas Python
36 | mostly uses `data_frame`
37 | - To set data table options, wrap the data frame in `render.DataTable`, similar
38 | to `datatable()` in R
39 | - Set `selection_mode` to `none`, `row` or `rows` for row selection options
40 |
41 | ### Row selection event
42 |
43 | With `selection_mode` set to 'row' or 'rows', you can observe the following:
44 |
45 | ```python
46 | @reactive.effect
47 | @reactive.event(tbl.cell_selection)
48 | def _():
49 | print(tbl.cell_selection()["rows"])
50 | ```
51 |
52 | - The returned **result is a tuple** of selected row indices
53 | - Remember that **Python is 0-index based**, so the first row has index 0
54 | - Note that in R, this process is different as the table is not accessible as an
55 | object and you use a modified input instead, e.g. `input$tbl_rows_selected`
56 |
57 | ## Interactive Plotly Plots
58 |
59 | Unlike DataTables, Plotly is a separate library which has to be installed using
60 | `pip install plotly`. Plotly comes with different APIs, with **Plotly Express**
61 | being the most popular one. This is **not to be confused with Shiny Express** as
62 | plotly express is used in both the Core and Express version of Shiny.
63 |
64 | Plotly plots are widgets and Python Shiny comes with an additional library
65 | called `shinywidgets` to interact with them.
66 |
67 | _Note: This workshop is not focussing on creating Plotly plots, so all relevant
68 | code will be provided_
69 |
70 | ## Basic plotly plot
71 |
72 | ```python
73 | import plotly.express as px
74 | from shinywidgets import output_widget, render_widget
75 |
76 | app_ui = ui.page_fluid(
77 | output_widget("plt")
78 | )
79 |
80 | def server(input, output, session):
81 |
82 | @render_widget
83 | def plt():
84 | return px.scatter(df, x = "age", y = "height")
85 |
86 | ```
87 |
88 | _Note: code to generate `df` not shown_
89 |
90 | - The generic `output_widget` is used as a UI placeholder for the plot, as this
91 | is not part of the standard UI library found in the `ui` object.
92 | - Similarly, the decorator for the plot output is a custom `render_widget` one,
93 | though otherwise the function syntax is identical
94 |
95 | ### Data selection event
96 |
97 | Observing specific events like clicking a point in a Plotly plot is currently
98 | very different in Python Shiny than R and requires the use of another, lower
99 | level plotly API called 'graph_objects'. Let's look at a full example below
100 |
101 | ```python
102 | from shiny import App, reactive, render, ui
103 | from shinywidgets import output_widget, render_widget
104 | import plotly.express as px
105 | import plotly.graph_objects as go
106 |
107 | app_ui = ui.page_fluid(
108 | output_widget("plt"),
109 | ui.output_text("clicked")
110 | )
111 |
112 | def server(input, output, session):
113 | point_clicked = reactive.value([])
114 |
115 | def click_data(trace, points, selector):
116 | point_clicked.set(points.point_inds)
117 |
118 | @render_widget
119 | def plt():
120 | df = px.data.iris()
121 | fig = px.scatter(df, x="sepal_width", y="sepal_length")
122 | widget = go.FigureWidget(fig.data, fig.layout)
123 | widget.data[0].on_click(click_data)
124 | return widget
125 |
126 | @render.text
127 | def clicked():
128 | return f"Point Clicked: {point_clicked.get()}"
129 |
130 |
131 | app = App(app_ui, server)
132 |
133 | ```
134 |
135 | - Plotly plots are custom widgets that have a wrapper library shinywidgets
136 | making them useable in Shiny
137 | - Use the custom output_widget in the UI and set the ID for your plotly plot. So
138 | don't use ui.output_plot in this case!
139 | - Use the @render_widget decorator to create the reactive environment that
140 | creates the plot
141 | - Create a normal plotly plot using the express syntax (e.g. px.scatter)
142 | - Build the plotly widget by wrapping the plot into the FigureWidget function.
143 | Note that this function takes data and layout arguments, which you can get
144 | from the express plot
145 | - To capture a trigger (e.g. click) add a custom function (e.g. click_data) to
146 | the widget using widget.data[0].on_click. This function contains info about
147 | the trace, points and selector. You can then update a reactive variable (e.g.
148 | point_clicked) using the info returned after a user click
149 |
150 | ## Modifying existing data tables and Plotly plots
151 |
152 | **In R Shiny proxy objects can be created** to update existing data tables or
153 | Plotly plots without the need to fully regenerate them, which results in a a
154 | smoother user experience.
155 |
156 | Similar functionality is possible in **Python Shiny**, though this is not
157 | achieved through separate proxy object but **built-in to the table/plot
158 | object**. This is beyond the scope of this workshop, but know it is possible.
159 |
160 | ## References
161 |
162 | - [DataTable](https://shiny.posit.co/py/components/outputs/data-table/)
163 | - [Plotly](https://shiny.posit.co/py/components/outputs/plot-plotly/)
164 |
--------------------------------------------------------------------------------
/PART_4_Plotly-DataTable/exercise1/README.md:
--------------------------------------------------------------------------------
1 | # PART 4 - Exercise 1 - Instructions
2 |
3 | ## Intro
4 |
5 | You are building a simple To-Do tracker that allows you to create new tasks and
6 | mark existing ones as completed. You are given an app that already takes care of
7 | some server functionality (adding new tasks to an existing data frame)
8 |
9 | ## Tasks
10 |
11 | - Add a data table output to the app, right above the `completed` button and
12 | show the current task list as a data frame
13 | - When a row is selected and the `completed` button is clicked, that task should
14 | get the current time as a timestamp in the last column. Only one row can be
15 | selected at once
16 | - When the button is clicked without a row being selected, nothing should happen
17 |
18 | ## Expected output
19 |
20 | _The first two tasks have been marked as completed_
21 |
22 | 
23 |
24 | ## Shinylive Link
25 |
26 | https://pieterjanvc.github.io/RShiny2Python/shinylive/?part4_ex1
27 |
28 | ## References
29 |
30 | - [DataTable](https://shiny.posit.co/py/components/outputs/data-table/)
31 |
--------------------------------------------------------------------------------
/PART_4_Plotly-DataTable/exercise1/exercise1_app.py:
--------------------------------------------------------------------------------
1 | # PART 4 - Exercise 1
2 | # ///////////////////
3 |
4 | from shiny import App, ui, reactive, render, req
5 | import pandas as pd
6 | from datetime import datetime
7 |
8 | app_ui = ui.page_fluid(
9 | ui.card(
10 | ui.card_header("Create Task"),
11 | ui.input_text("task", "Description", width="auto"),
12 | ui.input_action_button("add", "Add task", width="150px"),
13 | ),
14 | ui.card(
15 | ui.card_header("ToDo list"),
16 | ui.input_action_button(
17 | "completed", "Mark selected task as complete", width="300px"
18 | ),
19 | ),
20 | )
21 |
22 |
23 | def server(input, output, session):
24 | # Start with empty data frame
25 | todos = reactive.value(pd.DataFrame())
26 |
27 | # Add a new todo
28 | @reactive.effect
29 | @reactive.event(input.add)
30 | def _():
31 | req(input.task().strip())
32 | newTask = pd.DataFrame(
33 | {
34 | "created": [datetime.now().strftime("%Y-%m-%d %H:%M:%S")],
35 | "task": [input.task()],
36 | "completed": [None],
37 | }
38 | )
39 | todos.set(pd.concat([todos(), newTask], ignore_index=True))
40 | ui.update_text("task", value="")
41 |
42 |
43 | app = App(app_ui, server)
44 |
--------------------------------------------------------------------------------
/PART_4_Plotly-DataTable/exercise1/exercise1_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_4_Plotly-DataTable/exercise1/exercise1_screenshot.png
--------------------------------------------------------------------------------
/PART_4_Plotly-DataTable/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # PART 4 - Exercise 2 - Instructions
2 |
3 | ## Intro
4 |
5 | Let's use Shiny to create a simple, interactive agenda for this year's Shiny
6 | conference. By displaying the agenda visually, users should be able to quickly
7 | see all the different events ordered by track. By hovering over an event some,
8 | basic information is shown, but clicking it will show all session details in a
9 | separate output.
10 |
11 | You have been given the UI and some basic code that will get the data into the
12 | shape needed to plot the basic timeline using the plotly library. You also have
13 | a function to generate the metadata for each event as formatted HTML.
14 |
15 | ## Tasks
16 |
17 | - Add the plotly plot server function (i.e. _plt_)
18 | - Add the session details / metadata server function (i.e. _details_)
19 | - Add an _on click_ event listener that will capture which session has been
20 | clicked by the user and use that information to generate the session details
21 | to be displayed below the plot
22 |
23 | _Note: If you are are using **Positron**, trying to render the **plotly plot**
24 | outside of Shiny (i.e. just in your IDE) **might not work** and result in the
25 | editor freezing. You should however be able to see the plot appear in the Shiny
26 | app. If please **use the shinylive link in case of issues**_
27 |
28 | ## Expected output
29 |
30 | 
31 |
32 | ## Shinylive Link
33 |
34 | https://pieterjanvc.github.io/RShiny2Python/shinylive/?part4_ex2
35 |
36 | ## References
37 |
38 | - [Plotly in Shiny](https://shiny.posit.co/py/components/outputs/plot-plotly/)
39 | - [Plotly Gantt Charts in Python](https://plotly.com/python/gantt/)
40 |
--------------------------------------------------------------------------------
/PART_4_Plotly-DataTable/exercise2/agenda.json:
--------------------------------------------------------------------------------
1 | {
2 | "session_0": {
3 | "track": "Track 1",
4 | "start_time": "2025-04-09T12:00:00",
5 | "end_time": "2025-04-09T15:00:00",
6 | "title": "Workshop: Optimizing Performance in Shiny: Tips and Best Practices",
7 | "speakers": [
8 | "Samuel Calderon"
9 | ],
10 | "tags": [
11 | "workshop",
12 | "intermediate",
13 | "Best Practices"
14 | ],
15 | "colour": "#8eb9ff"
16 | },
17 | "session_1": {
18 | "track": "Track 2",
19 | "start_time": "2025-04-09T12:00:00",
20 | "end_time": "2025-04-09T14:00:00",
21 | "title": "Workshop: DIY: Unclog Your Scripts with Plumber in R",
22 | "speakers": [
23 | "Deepansh Khurana"
24 | ],
25 | "tags": [
26 | "workshop",
27 | "intermediate",
28 | "Best Practices",
29 | "Technical Deep Dives"
30 | ],
31 | "colour": "#8eb9ff"
32 | },
33 | "session_2": {
34 | "track": "Track 2",
35 | "start_time": "2025-04-09T15:00:00",
36 | "end_time": "2025-04-09T16:30:00",
37 | "title": "Workshop: Designing Inclusive Shiny Dashboards: Accessibility Best Practices and Innovations",
38 | "speakers": [
39 | "Abigail Stamm",
40 | "Eric Kvale"
41 | ],
42 | "tags": [
43 | "workshop",
44 | "intermediate",
45 | "Best Practices",
46 | "UI/UX"
47 | ],
48 | "colour": "#8eb9ff"
49 | },
50 | "session_3": {
51 | "track": "Track 1",
52 | "start_time": "2025-04-09T16:00:00",
53 | "end_time": "2025-04-09T19:00:00",
54 | "title": "Workshop: Transferring your R Shiny skills to Python",
55 | "speakers": [
56 | "PJ Van Camp"
57 | ],
58 | "tags": [
59 | "workshop",
60 | "intermediate",
61 | "advanced",
62 | "Shiny for Python"
63 | ],
64 | "colour": "#8eb9ff"
65 | },
66 | "session_4": {
67 | "track": "Track 2",
68 | "start_time": "2025-04-09T17:00:00",
69 | "end_time": "2025-04-09T19:00:00",
70 | "title": "Workshop: Why Is It Triggering Twice? Understanding Shiny Reactivity",
71 | "speakers": [
72 | "Douglas Mesquita"
73 | ],
74 | "tags": [
75 | "workshop",
76 | "Best Practices"
77 | ],
78 | "colour": "#8eb9ff"
79 | },
80 | "session_5": {
81 | "track": "Track 1",
82 | "start_time": "2025-04-10T12:00:00",
83 | "end_time": "2025-04-10T12:10:00",
84 | "title": "Opening remarks",
85 | "speakers": [],
86 | "tags": [],
87 | "colour": "#e97b79"
88 | },
89 | "session_6": {
90 | "track": "Track 1",
91 | "start_time": "2025-04-10T12:10:00",
92 | "end_time": "2025-04-10T12:30:00",
93 | "title": "Beyond {shiny}: The Future of Mobile Apps with R",
94 | "speakers": [
95 | "Colin Fay"
96 | ],
97 | "tags": [
98 | "20 min talk",
99 | "intermediate",
100 | "Shiny Innovation"
101 | ],
102 | "colour": "#61b9b0"
103 | },
104 | "session_7": {
105 | "track": "Track 2",
106 | "start_time": "2025-04-10T12:10:00",
107 | "end_time": "2025-04-10T12:30:00",
108 | "title": "Choosing the right tool for interactive dashboards: flexdashboard, Quarto, or Shiny",
109 | "speakers": [
110 | "Isabella Vel\u00e1squez"
111 | ],
112 | "tags": [
113 | "20 min talk",
114 | "intermediate",
115 | "Best Practices"
116 | ],
117 | "colour": "#61b9b0"
118 | },
119 | "session_8": {
120 | "track": "Track 1",
121 | "start_time": "2025-04-10T12:30:00",
122 | "end_time": "2025-04-10T12:50:00",
123 | "title": "Interactive Shiny Applications in R Documentation with {roxy.shinylive}",
124 | "speakers": [
125 | "Pawel Rucki"
126 | ],
127 | "tags": [
128 | "20 min talk",
129 | "intermediate",
130 | "advanced",
131 | "Technical Deep Dives",
132 | "Shiny Innovation"
133 | ],
134 | "colour": "#61b9b0"
135 | },
136 | "session_9": {
137 | "track": "Track 2",
138 | "start_time": "2025-04-10T12:30:00",
139 | "end_time": "2025-04-10T12:50:00",
140 | "title": "Refactor or Preserve? Challenging the 'If It Ain\u2019t Broken, Don\u2019t Fix It' Mindset in Shiny App Lifecy",
141 | "speakers": [
142 | "Dror Berel"
143 | ],
144 | "tags": [
145 | "20 min talk",
146 | "intermediate",
147 | "Best Practices"
148 | ],
149 | "colour": "#61b9b0"
150 | },
151 | "session_10": {
152 | "track": "Track 1",
153 | "start_time": "2025-04-10T12:50:00",
154 | "end_time": "2025-04-10T13:00:00",
155 | "title": "Quick break",
156 | "speakers": [],
157 | "tags": [],
158 | "colour": "#e97b79"
159 | },
160 | "session_11": {
161 | "track": "Track 1",
162 | "start_time": "2025-04-10T13:00:00",
163 | "end_time": "2025-04-10T14:00:00",
164 | "title": "Building LLM-Powered Shiny apps via ellmer and chatlas",
165 | "speakers": [
166 | "Carson Sievert"
167 | ],
168 | "tags": [
169 | "tutorial (60 min)",
170 | "LLM/AI and Shiny",
171 | "Shiny Innovation"
172 | ],
173 | "colour": "#e5a657"
174 | },
175 | "session_12": {
176 | "track": "Track 1",
177 | "start_time": "2025-04-10T14:00:00",
178 | "end_time": "2025-04-10T14:20:00",
179 | "title": "Building AI Bots With R-Shiny",
180 | "speakers": [
181 | "Albert Rapp"
182 | ],
183 | "tags": [
184 | "20 min talk",
185 | "intermediate",
186 | "advanced",
187 | "LLM/AI and Shiny"
188 | ],
189 | "colour": "#61b9b0"
190 | },
191 | "session_13": {
192 | "track": "Track 2",
193 | "start_time": "2025-04-10T14:00:00",
194 | "end_time": "2025-04-10T14:20:00",
195 | "title": "Death By Dropdown? A Developer\u2019s Guide To Building Dashboards That Won\u2019t Fry Your Client\u2019s Brain",
196 | "speakers": [
197 | "Milena Eickhoff",
198 | "Jeremy Winget, PhD"
199 | ],
200 | "tags": [
201 | "20 min talk",
202 | "intermediate",
203 | "UI/UX"
204 | ],
205 | "colour": "#61b9b0"
206 | },
207 | "session_14": {
208 | "track": "Track 1",
209 | "start_time": "2025-04-10T14:20:00",
210 | "end_time": "2025-04-10T14:40:00",
211 | "title": "Building state of the art RAG-LLM applications with R Shiny.",
212 | "speakers": [
213 | "Mohamed El Fodil Ihaddaden"
214 | ],
215 | "tags": [
216 | "20 min talk",
217 | "advanced",
218 | "LLM/AI and Shiny"
219 | ],
220 | "colour": "#61b9b0"
221 | },
222 | "session_15": {
223 | "track": "Track 2",
224 | "start_time": "2025-04-10T14:20:00",
225 | "end_time": "2025-04-10T14:40:00",
226 | "title": "Building Your Pit of Success: Practical Strategies for Shiny App Development and Deployment",
227 | "speakers": [
228 | "Umair Durrani"
229 | ],
230 | "tags": [
231 | "20 min talk",
232 | "beginner",
233 | "Best Practices"
234 | ],
235 | "colour": "#61b9b0"
236 | },
237 | "session_16": {
238 | "track": "Track 1",
239 | "start_time": "2025-04-10T14:40:00",
240 | "end_time": "2025-04-10T15:00:00",
241 | "title": "Coffee break",
242 | "speakers": [],
243 | "tags": [],
244 | "colour": "#e97b79"
245 | },
246 | "session_17": {
247 | "track": "Track 1",
248 | "start_time": "2025-04-10T15:00:00",
249 | "end_time": "2025-04-10T15:20:00",
250 | "title": "AI Kung-Fu: Training your model in Two Function Calls",
251 | "speakers": [
252 | "Barret Schloerke"
253 | ],
254 | "tags": [
255 | "20 min talk",
256 | "intermediate",
257 | "LLM/AI and Shiny"
258 | ],
259 | "colour": "#61b9b0"
260 | },
261 | "session_18": {
262 | "track": "Track 2",
263 | "start_time": "2025-04-10T15:00:00",
264 | "end_time": "2025-04-10T15:20:00",
265 | "title": "Creating and Sharing Scalable Applications with Shiny",
266 | "speakers": [
267 | "Daniel Chen"
268 | ],
269 | "tags": [
270 | "20 min talk",
271 | "beginner",
272 | "Best Practices"
273 | ],
274 | "colour": "#61b9b0"
275 | },
276 | "session_19": {
277 | "track": "Track 1",
278 | "start_time": "2025-04-10T15:20:00",
279 | "end_time": "2025-04-10T15:30:00",
280 | "title": "Harnessing Agentic AI with Shiny",
281 | "speakers": [
282 | "Yigit Aydede"
283 | ],
284 | "tags": [
285 | "shiny app showcase (5 min)",
286 | "intermediate",
287 | "LLM/AI and Shiny"
288 | ],
289 | "colour": "#c2858c"
290 | },
291 | "session_20": {
292 | "track": "Track 2",
293 | "start_time": "2025-04-10T15:20:00",
294 | "end_time": "2025-04-10T15:30:00",
295 | "title": "Making Research Interactive: Developing a Shiny App for Open Science",
296 | "speakers": [
297 | "Riva Quiroga",
298 | "Joshua Kunst"
299 | ],
300 | "tags": [
301 | "shiny app showcase (5 min)",
302 | "intermediate",
303 | "beginner",
304 | "Shiny for Good"
305 | ],
306 | "colour": "#c2858c"
307 | },
308 | "session_21": {
309 | "track": "Track 1",
310 | "start_time": "2025-04-10T15:30:00",
311 | "end_time": "2025-04-10T15:50:00",
312 | "title": "Practical LLMs in Shiny: Breaking Down Problems, Building Up Solutions",
313 | "speakers": [
314 | "Piotr Pasza Storo\u017cenko"
315 | ],
316 | "tags": [
317 | "20 min talk",
318 | "intermediate",
319 | "LLM/AI and Shiny",
320 | "Shiny Innovation"
321 | ],
322 | "colour": "#61b9b0"
323 | },
324 | "session_22": {
325 | "track": "Track 2",
326 | "start_time": "2025-04-10T15:30:00",
327 | "end_time": "2025-04-10T15:50:00",
328 | "title": "Designing User-Centric Shiny Apps through Client Conversations: UI/UX strategies and Gherkin",
329 | "speakers": [
330 | "Jasmine Daly"
331 | ],
332 | "tags": [
333 | "20 min talk",
334 | "intermediate",
335 | "Best Practices",
336 | "UI/UX"
337 | ],
338 | "colour": "#61b9b0"
339 | },
340 | "session_23": {
341 | "track": "Track 1",
342 | "start_time": "2025-04-10T15:50:00",
343 | "end_time": "2025-04-10T16:00:00",
344 | "title": "Turning My Goodreads Data into a Shiny App No One Asked For",
345 | "speakers": [
346 | "Gigi Kenneth"
347 | ],
348 | "tags": [
349 | "shiny app showcase (5 min)",
350 | "beginner",
351 | "LLM/AI and Shiny",
352 | "Shiny for Fun"
353 | ],
354 | "colour": "#c2858c"
355 | },
356 | "session_24": {
357 | "track": "Track 1",
358 | "start_time": "2025-04-10T16:00:00",
359 | "end_time": "2025-04-10T16:20:00",
360 | "title": "Curbcut: A case study in building the \u201cworld\u2019s best\u201d large, public-facing Shiny app",
361 | "speakers": [
362 | "Maxime B\u00e9langer De Blois",
363 | "David Wachsmuth"
364 | ],
365 | "tags": [
366 | "20 min talk",
367 | "intermediate",
368 | "advanced",
369 | "Shiny for Good",
370 | "Real World Use Cases",
371 | "Shiny Innovation"
372 | ],
373 | "colour": "#61b9b0"
374 | },
375 | "session_25": {
376 | "track": "Track 2",
377 | "start_time": "2025-04-10T16:00:00",
378 | "end_time": "2025-04-10T16:20:00",
379 | "title": "Beyond the Rainbow: Why Color Is Key to Effective & Inclusive App UI Design",
380 | "speakers": [
381 | "Hubert Ha\u0142un"
382 | ],
383 | "tags": [
384 | "20 min talk",
385 | "advanced",
386 | "UI/UX"
387 | ],
388 | "colour": "#61b9b0"
389 | },
390 | "session_26": {
391 | "track": "Track 1",
392 | "start_time": "2025-04-10T16:30:00",
393 | "end_time": "2025-04-10T17:00:00",
394 | "title": "Networking / Longer Break",
395 | "speakers": [],
396 | "tags": [],
397 | "colour": "#e97b79"
398 | },
399 | "session_27": {
400 | "track": "Track 1",
401 | "start_time": "2025-04-10T17:00:00",
402 | "end_time": "2025-04-10T17:20:00",
403 | "title": "No-code data analysis and dashboards with the blockr ecosystem",
404 | "speakers": [
405 | "Nicolas Bennett"
406 | ],
407 | "tags": [
408 | "20 min talk",
409 | "intermediate",
410 | "Technical Deep Dives",
411 | "UI/UX",
412 | "Shiny Innovation"
413 | ],
414 | "colour": "#61b9b0"
415 | },
416 | "session_28": {
417 | "track": "Track 2",
418 | "start_time": "2025-04-10T17:00:00",
419 | "end_time": "2025-04-10T17:10:00",
420 | "title": "Shiny Application for Real-Time Air Quality Monitoring",
421 | "speakers": [
422 | "Edgar Luis C\u00e1ceres Angulo",
423 | "Andr\u00e9s Daniel Brios Abanto"
424 | ],
425 | "tags": [
426 | "shiny app showcase (5 min)",
427 | "advanced",
428 | "Real World Use Cases"
429 | ],
430 | "colour": "#c2858c"
431 | },
432 | "session_29": {
433 | "track": "Track 2",
434 | "start_time": "2025-04-10T17:10:00",
435 | "end_time": "2025-04-10T17:20:00",
436 | "title": "Interactive Visualizations of Global Debt Networks with Shiny",
437 | "speakers": [
438 | "Christoph Scheuch"
439 | ],
440 | "tags": [
441 | "shiny app showcase (5 min)",
442 | "intermediate",
443 | "beginner",
444 | "Real World Use Cases"
445 | ],
446 | "colour": "#c2858c"
447 | },
448 | "session_30": {
449 | "track": "Track 1",
450 | "start_time": "2025-04-10T17:20:00",
451 | "end_time": "2025-04-10T17:40:00",
452 | "title": "Adoption tracking for Connect Shiny applications",
453 | "speakers": [
454 | "Marcin Dubel"
455 | ],
456 | "tags": [
457 | "20 min talk",
458 | "beginner",
459 | "Enterprise Deployment",
460 | "Shiny Innovation"
461 | ],
462 | "colour": "#61b9b0"
463 | },
464 | "session_31": {
465 | "track": "Track 2",
466 | "start_time": "2025-04-10T17:20:00",
467 | "end_time": "2025-04-10T17:40:00",
468 | "title": "Theming Made Easy: Introducing brand.yml for Shiny",
469 | "speakers": [
470 | "Garrick Aden-Buie"
471 | ],
472 | "tags": [
473 | "20 min talk",
474 | "beginner",
475 | "UI/UX",
476 | "Shiny Innovation"
477 | ],
478 | "colour": "#61b9b0"
479 | },
480 | "session_32": {
481 | "track": "Track 1",
482 | "start_time": "2025-04-10T17:40:00",
483 | "end_time": "2025-04-10T18:00:00",
484 | "title": "Can an AI pass the Shiny developer interview?",
485 | "speakers": [
486 | "Pavel Demin"
487 | ],
488 | "tags": [
489 | "20 min talk",
490 | "intermediate",
491 | "advanced",
492 | "beginner"
493 | ],
494 | "colour": "#61b9b0"
495 | },
496 | "session_33": {
497 | "track": "Track 1",
498 | "start_time": "2025-04-10T18:00:00",
499 | "end_time": "2025-04-10T18:10:00",
500 | "title": "Speaking to the Data: Democratizing AI-Driven Exploration with Shiny for Python and CIP Dataverse",
501 | "speakers": [
502 | "Piero Palacios",
503 | "Henry Juarez"
504 | ],
505 | "tags": [
506 | "shiny app showcase (5 min)",
507 | "intermediate",
508 | "LLM/AI and Shiny",
509 | "Shiny for Python"
510 | ],
511 | "colour": "#c2858c"
512 | },
513 | "session_34": {
514 | "track": "Track 2",
515 | "start_time": "2025-04-10T18:00:00",
516 | "end_time": "2025-04-10T18:10:00",
517 | "title": "Modern Shiny Dashboard with bslib",
518 | "speakers": [
519 | "Philippe Peret"
520 | ],
521 | "tags": [
522 | "shiny app showcase (5 min)",
523 | "advanced",
524 | "UI/UX",
525 | "Shiny Innovation"
526 | ],
527 | "colour": "#c2858c"
528 | },
529 | "session_35": {
530 | "track": "Track 1",
531 | "start_time": "2025-04-10T18:10:00",
532 | "end_time": "2025-04-10T18:30:00",
533 | "title": "Coffee break",
534 | "speakers": [],
535 | "tags": [],
536 | "colour": "#e97b79"
537 | },
538 | "session_36": {
539 | "track": "Track 1",
540 | "start_time": "2025-04-10T18:30:00",
541 | "end_time": "2025-04-10T19:20:00",
542 | "title": "Keynote: AI and Shiny",
543 | "speakers": [
544 | "Winston Chang"
545 | ],
546 | "tags": [
547 | "keynote",
548 | "LLM/AI and Shiny",
549 | "Shiny Innovation"
550 | ],
551 | "colour": "#b53324"
552 | },
553 | "session_37": {
554 | "track": "Track 1",
555 | "start_time": "2025-04-10T19:20:00",
556 | "end_time": "2025-04-10T19:30:00",
557 | "title": "Summary of the day",
558 | "speakers": [],
559 | "tags": [],
560 | "colour": "#e97b79"
561 | },
562 | "session_38": {
563 | "track": "Track 1",
564 | "start_time": "2025-04-11T12:00:00",
565 | "end_time": "2025-04-11T12:20:00",
566 | "title": "{gsm.app}: Extensible Clinical Trial Monitoring Apps",
567 | "speakers": [
568 | "Jon Harmon",
569 | "Jeremy Wildfire"
570 | ],
571 | "tags": [
572 | "20 min talk",
573 | "intermediate",
574 | "advanced",
575 | "Life Sciences/Pharma"
576 | ],
577 | "colour": "#61b9b0"
578 | },
579 | "session_39": {
580 | "track": "Track 2",
581 | "start_time": "2025-04-11T12:00:00",
582 | "end_time": "2025-04-11T12:20:00",
583 | "title": "Building Elkem's R Universe, One Package at a Time",
584 | "speakers": [
585 | "Recle Vibal",
586 | "Kjell H\u00e5kon Berget"
587 | ],
588 | "tags": [
589 | "20 min talk",
590 | "intermediate",
591 | "Enterprise Deployment",
592 | "Real World Use Cases"
593 | ],
594 | "colour": "#61b9b0"
595 | },
596 | "session_40": {
597 | "track": "Track 1",
598 | "start_time": "2025-04-11T12:20:00",
599 | "end_time": "2025-04-11T12:40:00",
600 | "title": "Bridging Open-Source and Enterprise: Integrating {teal} into Shiny Frameworks",
601 | "speakers": [
602 | "Alexandros Kouretsis",
603 | "Paulo Bargo",
604 | "Ardalan Mirshani"
605 | ],
606 | "tags": [
607 | "20 min talk",
608 | "intermediate",
609 | "advanced",
610 | "beginner",
611 | "Enterprise Deployment",
612 | "Life Sciences/Pharma"
613 | ],
614 | "colour": "#61b9b0"
615 | },
616 | "session_41": {
617 | "track": "Track 2",
618 | "start_time": "2025-04-11T12:20:00",
619 | "end_time": "2025-04-11T12:40:00",
620 | "title": "Enterprise Deployment: Strategies for Monitoring Shiny Applications in Production",
621 | "speakers": [
622 | "Xavier Escrib\u00e0 Montagut"
623 | ],
624 | "tags": [
625 | "20 min talk",
626 | "advanced",
627 | "Enterprise Deployment"
628 | ],
629 | "colour": "#61b9b0"
630 | },
631 | "session_42": {
632 | "track": "Track 1",
633 | "start_time": "2025-04-11T12:40:00",
634 | "end_time": "2025-04-11T13:00:00",
635 | "title": "Interactive Visualizations for Medical Data Review: A teal Module Showcase",
636 | "speakers": [
637 | "Nina Qi",
638 | "Dony Unardi"
639 | ],
640 | "tags": [
641 | "shiny app showcase (5 min)",
642 | "intermediate",
643 | "Life Sciences/Pharma"
644 | ],
645 | "colour": "#c2858c"
646 | },
647 | "session_43": {
648 | "track": "Track 2",
649 | "start_time": "2025-04-11T12:40:00",
650 | "end_time": "2025-04-11T13:00:00",
651 | "title": "From Data to Narrative: Interactive Storytelling with Shiny",
652 | "speakers": [
653 | "Francisco Alfaro"
654 | ],
655 | "tags": [
656 | "20 min talk",
657 | "beginner",
658 | "Technical Deep Dives",
659 | "UI/UX"
660 | ],
661 | "colour": "#61b9b0"
662 | },
663 | "session_44": {
664 | "track": "Track 1",
665 | "start_time": "2025-04-11T13:00:00",
666 | "end_time": "2025-04-11T13:10:00",
667 | "title": "Interactive Reporting with Shiny for Clinical Trial Management",
668 | "speakers": [
669 | "Kristen Steenbergen"
670 | ],
671 | "tags": [
672 | "shiny app showcase (5 min)",
673 | "Life Sciences/Pharma"
674 | ],
675 | "colour": "#c2858c"
676 | },
677 | "session_45": {
678 | "track": "Track 2",
679 | "start_time": "2025-04-11T13:00:00",
680 | "end_time": "2025-04-11T13:20:00",
681 | "title": "Enhancing Shiny Projects with pre-commit Hooks, {renv} Profiles, and Release Management",
682 | "speakers": [
683 | "Tymoteusz Makowski"
684 | ],
685 | "tags": [
686 | "20 min talk",
687 | "Best Practices",
688 | "Technical Deep Dives"
689 | ],
690 | "colour": "#61b9b0"
691 | },
692 | "session_46": {
693 | "track": "Track 1",
694 | "start_time": "2025-04-11T13:10:00",
695 | "end_time": "2025-04-11T13:20:00",
696 | "title": "ChestVolume: Shiny App for 3D Chest Expansion Analysis",
697 | "speakers": [
698 | "Patrick Kwong"
699 | ],
700 | "tags": [
701 | "shiny app showcase (5 min)",
702 | "beginner",
703 | "Life Sciences/Pharma"
704 | ],
705 | "colour": "#c2858c"
706 | },
707 | "session_47": {
708 | "track": "Track 1",
709 | "start_time": "2025-04-11T13:20:00",
710 | "end_time": "2025-04-11T13:40:00",
711 | "title": "Empowering Multiomic Data Exploration with Scalable Shiny Applications",
712 | "speakers": [
713 | "Matias Romero Victorica",
714 | "Camila Cirignoli",
715 | "Mikhail Osipovitch"
716 | ],
717 | "tags": [
718 | "20 min talk",
719 | "intermediate",
720 | "Enterprise Deployment",
721 | "Life Sciences/Pharma"
722 | ],
723 | "colour": "#61b9b0"
724 | },
725 | "session_48": {
726 | "track": "Track 2",
727 | "start_time": "2025-04-11T13:20:00",
728 | "end_time": "2025-04-11T13:40:00",
729 | "title": "Shiny as a Service - leveraging Auth0 and Stripe",
730 | "speakers": [
731 | "Simon Bjerrum Eilersen"
732 | ],
733 | "tags": [
734 | "20 min talk",
735 | "advanced",
736 | "Enterprise Deployment"
737 | ],
738 | "colour": "#61b9b0"
739 | },
740 | "session_49": {
741 | "track": "Track 1",
742 | "start_time": "2025-04-11T13:40:00",
743 | "end_time": "2025-04-11T13:50:00",
744 | "title": "Diagnosing Autism with R Shiny",
745 | "speakers": [
746 | "Taylor Rodgers",
747 | "Lee Mason"
748 | ],
749 | "tags": [
750 | "shiny app showcase (5 min)",
751 | "beginner",
752 | "Shiny for Good"
753 | ],
754 | "colour": "#c2858c"
755 | },
756 | "session_50": {
757 | "track": "Track 2",
758 | "start_time": "2025-04-11T13:40:00",
759 | "end_time": "2025-04-11T14:00:00",
760 | "title": "Deploy with Confidence: Strategies to Minimize Failure and Maximize Success (on Posit Connect)",
761 | "speakers": [
762 | "Ryszard Szyma\u0144ski"
763 | ],
764 | "tags": [
765 | "20 min talk",
766 | "intermediate",
767 | "Best Practices",
768 | "Enterprise Deployment"
769 | ],
770 | "colour": "#61b9b0"
771 | },
772 | "session_51": {
773 | "track": "Track 1",
774 | "start_time": "2025-04-11T13:50:00",
775 | "end_time": "2025-04-11T14:00:00",
776 | "title": "Bridging the Gap: Making Cancer Data Accessible for Policy Makers",
777 | "speakers": [
778 | "Matt Grant"
779 | ],
780 | "tags": [
781 | "shiny app showcase (5 min)",
782 | "intermediate",
783 | "beginner",
784 | "Shiny for Good",
785 | "Life Sciences/Pharma"
786 | ],
787 | "colour": "#c2858c"
788 | },
789 | "session_52": {
790 | "track": "Track 1",
791 | "start_time": "2025-04-11T14:00:00",
792 | "end_time": "2025-04-11T14:20:00",
793 | "title": "Open Source in Pharma: Transforming Medical Monitoring for Clinical Trials with {ClinSight}",
794 | "speakers": [
795 | "Leonard Daniel Samson",
796 | "Aaron Clark",
797 | "Jeff Thompson"
798 | ],
799 | "tags": [
800 | "20 min talk",
801 | "intermediate",
802 | "advanced",
803 | "Life Sciences/Pharma",
804 | "Open Source"
805 | ],
806 | "colour": "#61b9b0"
807 | },
808 | "session_53": {
809 | "track": "Track 2",
810 | "start_time": "2025-04-11T14:00:00",
811 | "end_time": "2025-04-11T14:20:00",
812 | "title": "Shiny Solutions to High-Cardinality Geospatial Challenges",
813 | "speakers": [
814 | "Jeffrey Fowler"
815 | ],
816 | "tags": [
817 | "20 min talk",
818 | "intermediate",
819 | "Best Practices",
820 | "Technical Deep Dives"
821 | ],
822 | "colour": "#61b9b0"
823 | },
824 | "session_54": {
825 | "track": "Track 1",
826 | "start_time": "2025-04-11T14:20:00",
827 | "end_time": "2025-04-11T14:30:00",
828 | "title": "Signal Detection Tool: An R shiny app supporting automated detection of infectious disease outbreaks",
829 | "speakers": [
830 | "Ann Christin Vietor"
831 | ],
832 | "tags": [
833 | "shiny app showcase (5 min)",
834 | "beginner",
835 | "Real World Use Cases",
836 | "Life Sciences/Pharma"
837 | ],
838 | "colour": "#c2858c"
839 | },
840 | "session_55": {
841 | "track": "Track 2",
842 | "start_time": "2025-04-11T14:20:00",
843 | "end_time": "2025-04-11T14:30:00",
844 | "title": "R-ception: Train R-novices through a Shiny Training App",
845 | "speakers": [
846 | "Meike Go"
847 | ],
848 | "tags": [
849 | "shiny app showcase (5 min)",
850 | "intermediate",
851 | "Real World Use Cases"
852 | ],
853 | "colour": "#c2858c"
854 | },
855 | "session_56": {
856 | "track": "Track 1",
857 | "start_time": "2025-04-11T14:30:00",
858 | "end_time": "2025-04-11T14:40:00",
859 | "title": "Streamlining Portfolio Management with Shiny",
860 | "speakers": [
861 | "Ashley Dennie"
862 | ],
863 | "tags": [
864 | "shiny app showcase (5 min)",
865 | "beginner",
866 | "Real World Use Cases"
867 | ],
868 | "colour": "#c2858c"
869 | },
870 | "session_57": {
871 | "track": "Track 2",
872 | "start_time": "2025-04-11T14:30:00",
873 | "end_time": "2025-04-11T14:40:00",
874 | "title": "ShinyQDA: R Package and Shiny Application for the Analysis of Qualitative Data",
875 | "speakers": [
876 | "Jason Bryer"
877 | ],
878 | "tags": [
879 | "shiny app showcase (5 min)",
880 | "beginner",
881 | "Real World Use Cases"
882 | ],
883 | "colour": "#c2858c"
884 | },
885 | "session_58": {
886 | "track": "Track 1",
887 | "start_time": "2025-04-11T14:40:00",
888 | "end_time": "2025-04-11T15:00:00",
889 | "title": "Coffee break",
890 | "speakers": [],
891 | "tags": [],
892 | "colour": "#e97b79"
893 | },
894 | "session_59": {
895 | "track": "Track 1",
896 | "start_time": "2025-04-11T15:00:00",
897 | "end_time": "2025-04-11T15:50:00",
898 | "title": "Transforming Clinical Trials: How R & Shiny are speeding up and enhancing decision-making",
899 | "speakers": [
900 | "Aga Rasinska"
901 | ],
902 | "tags": [
903 | "keynote"
904 | ],
905 | "colour": "#b53324"
906 | },
907 | "session_60": {
908 | "track": "Track 1",
909 | "start_time": "2025-04-11T15:50:00",
910 | "end_time": "2025-04-11T16:10:00",
911 | "title": "In the Nix of Time: A new approach to Shiny development & deployment with Nix and {rix}",
912 | "speakers": [
913 | "Eric Nantz"
914 | ],
915 | "tags": [
916 | "20 min talk",
917 | "intermediate",
918 | "advanced",
919 | "Technical Deep Dives",
920 | "Enterprise Deployment"
921 | ],
922 | "colour": "#61b9b0"
923 | },
924 | "session_61": {
925 | "track": "Track 2",
926 | "start_time": "2025-04-11T15:50:00",
927 | "end_time": "2025-04-11T16:10:00",
928 | "title": "Yet Another Year in the Rhinoverse",
929 | "speakers": [
930 | "Jakub Nowicki"
931 | ],
932 | "tags": [
933 | "20 min talk",
934 | "intermediate",
935 | "beginner",
936 | "Open Source"
937 | ],
938 | "colour": "#61b9b0"
939 | },
940 | "session_62": {
941 | "track": "Track 1",
942 | "start_time": "2025-04-11T16:10:00",
943 | "end_time": "2025-04-11T16:30:00",
944 | "title": "The 'ggiraph' Cookbook: Recipes for Interactive and Performant Shiny Visualizations",
945 | "speakers": [
946 | "David Gohel"
947 | ],
948 | "tags": [
949 | "20 min talk",
950 | "intermediate",
951 | "Technical Deep Dives"
952 | ],
953 | "colour": "#61b9b0"
954 | },
955 | "session_63": {
956 | "track": "Track 2",
957 | "start_time": "2025-04-11T16:10:00",
958 | "end_time": "2025-04-11T16:30:00",
959 | "title": "Streamlined Shiny Development with CI/CD",
960 | "speakers": [
961 | "Peter Belai"
962 | ],
963 | "tags": [
964 | "20 min talk",
965 | "intermediate",
966 | "Best Practices",
967 | "Enterprise Deployment"
968 | ],
969 | "colour": "#61b9b0"
970 | },
971 | "session_64": {
972 | "track": "Track 1",
973 | "start_time": "2025-04-11T16:30:00",
974 | "end_time": "2025-04-11T17:00:00",
975 | "title": "Networking / Longer Break",
976 | "speakers": [],
977 | "tags": [],
978 | "colour": "#e97b79"
979 | },
980 | "session_65": {
981 | "track": "Track 1",
982 | "start_time": "2025-04-11T17:00:00",
983 | "end_time": "2025-04-11T17:10:00",
984 | "title": "Vivid Volcano: Empowering Wet-Lab Biologists with an Easy-to-Use Tool for Omics Data Analysis",
985 | "speakers": [
986 | "Tomasz St\u0119pkowski"
987 | ],
988 | "tags": [
989 | "shiny app showcase (5 min)",
990 | "beginner",
991 | "Life Sciences/Pharma"
992 | ],
993 | "colour": "#c2858c"
994 | },
995 | "session_66": {
996 | "track": "Track 2",
997 | "start_time": "2025-04-11T17:00:00",
998 | "end_time": "2025-04-11T17:10:00",
999 | "title": "Joel's Air Quality Application Session",
1000 | "speakers": [
1001 | "Joel Duah"
1002 | ],
1003 | "tags": [
1004 | "shiny app showcase (5 min)",
1005 | "advanced",
1006 | "Shiny for Good",
1007 | "Real World Use Cases"
1008 | ],
1009 | "colour": "#c2858c"
1010 | },
1011 | "session_67": {
1012 | "track": "Track 1",
1013 | "start_time": "2025-04-11T17:10:00",
1014 | "end_time": "2025-04-11T17:20:00",
1015 | "title": "Reviewing Clinical Data Efficiently with Shiny",
1016 | "speakers": [
1017 | "Winkle Lu"
1018 | ],
1019 | "tags": [
1020 | "shiny app showcase (5 min)",
1021 | "intermediate",
1022 | "Life Sciences/Pharma"
1023 | ],
1024 | "colour": "#c2858c"
1025 | },
1026 | "session_68": {
1027 | "track": "Track 2",
1028 | "start_time": "2025-04-11T17:10:00",
1029 | "end_time": "2025-04-11T17:20:00",
1030 | "title": "Generating personalized student synthetic datasets for large-scale educational assessment",
1031 | "speakers": [
1032 | "Daniel Morillo Cuadrado"
1033 | ],
1034 | "tags": [
1035 | "shiny app showcase (5 min)",
1036 | "intermediate",
1037 | "beginner",
1038 | "Shiny for Good",
1039 | "Real World Use Cases"
1040 | ],
1041 | "colour": "#c2858c"
1042 | },
1043 | "session_69": {
1044 | "track": "Track 1",
1045 | "start_time": "2025-04-11T17:20:00",
1046 | "end_time": "2025-04-11T17:40:00",
1047 | "title": "Scaling Shiny: From Hobbyist Projects to Enterprise Deployments with Posit Connect",
1048 | "speakers": [
1049 | "Alex Chisholm",
1050 | "Kelly O\u2019Briant"
1051 | ],
1052 | "tags": [
1053 | "20 min talk"
1054 | ],
1055 | "colour": "#61b9b0"
1056 | },
1057 | "session_70": {
1058 | "track": "Track 1",
1059 | "start_time": "2025-04-11T17:40:00",
1060 | "end_time": "2025-04-11T18:00:00",
1061 | "title": "Scaling Shiny: Seamless and Effortless Distributed Computing with mirai",
1062 | "speakers": [
1063 | "Charlie Gao"
1064 | ],
1065 | "tags": [
1066 | "20 min talk",
1067 | "advanced",
1068 | "Technical Deep Dives",
1069 | "Shiny Innovation"
1070 | ],
1071 | "colour": "#61b9b0"
1072 | },
1073 | "session_71": {
1074 | "track": "Track 2",
1075 | "start_time": "2025-04-11T17:40:00",
1076 | "end_time": "2025-04-11T18:00:00",
1077 | "title": "Unified Innovation: How We Started Our Shiny DevOps Journey",
1078 | "speakers": [
1079 | "Liz Whelan-Jackson"
1080 | ],
1081 | "tags": [
1082 | "20 min talk",
1083 | "intermediate",
1084 | "advanced",
1085 | "Best Practices",
1086 | "Real World Use Cases"
1087 | ],
1088 | "colour": "#61b9b0"
1089 | },
1090 | "session_72": {
1091 | "track": "Track 1",
1092 | "start_time": "2025-04-11T18:00:00",
1093 | "end_time": "2025-04-11T18:10:00",
1094 | "title": "Using Shiny to Safeguard Australia\u2019s Flora and Fauna",
1095 | "speakers": [
1096 | "Ryan Newis"
1097 | ],
1098 | "tags": [
1099 | "shiny app showcase (5 min)",
1100 | "intermediate",
1101 | "advanced",
1102 | "Shiny for Good"
1103 | ],
1104 | "colour": "#c2858c"
1105 | },
1106 | "session_73": {
1107 | "track": "Track 2",
1108 | "start_time": "2025-04-11T18:00:00",
1109 | "end_time": "2025-04-11T18:10:00",
1110 | "title": "Guitar Study Tracker Dashboard",
1111 | "speakers": [
1112 | "Dave Guenther"
1113 | ],
1114 | "tags": [
1115 | "shiny app showcase (5 min)",
1116 | "intermediate",
1117 | "advanced",
1118 | "beginner",
1119 | "Shiny for Fun"
1120 | ],
1121 | "colour": "#c2858c"
1122 | },
1123 | "session_74": {
1124 | "track": "Track 1",
1125 | "start_time": "2025-04-11T18:10:00",
1126 | "end_time": "2025-04-11T18:20:00",
1127 | "title": "Coffee break",
1128 | "speakers": [],
1129 | "tags": [],
1130 | "colour": "#e97b79"
1131 | },
1132 | "session_75": {
1133 | "track": "Track 1",
1134 | "start_time": "2025-04-11T18:20:00",
1135 | "end_time": "2025-04-11T19:20:00",
1136 | "title": "Tiny Shiny Hackathon Winners Announcement & Panel Discussion",
1137 | "speakers": [],
1138 | "tags": [],
1139 | "colour": "#e97b79"
1140 | },
1141 | "session_76": {
1142 | "track": "Track 1",
1143 | "start_time": "2025-04-11T19:20:00",
1144 | "end_time": "2025-04-11T19:35:00",
1145 | "title": "Conference closing remarks",
1146 | "speakers": [],
1147 | "tags": [],
1148 | "colour": "#e97b79"
1149 | }
1150 | }
--------------------------------------------------------------------------------
/PART_4_Plotly-DataTable/exercise2/exercise2_app.py:
--------------------------------------------------------------------------------
1 | # PART 4 - Exercise 2
2 | # ///////////////////
3 |
4 | from shiny import App, ui, reactive, render, req
5 | from shiny import App, reactive, render, ui
6 | from shinywidgets import output_widget, render_widget
7 | import json
8 | import pandas as pd
9 | from pathlib import Path
10 | import plotly.express as px
11 | import plotly.graph_objects as go
12 |
13 | # --- PROCESSING DATA
14 |
15 | # Get the conference agenda
16 | # file = "PART_4_Plotly-DataTable\\exercise2\\agenda.json"
17 | file = Path(__file__).parent / "agenda.json"
18 | with open(file, "r") as file:
19 | data = list(json.load(file).values())
20 |
21 | # Function to convert JSON to text
22 | def format_dict_with_bullets(d, indent=0):
23 | result = []
24 | for key, value in d.items():
25 | # Create an indent for nested dictionaries
26 | space = " " * indent
27 | if isinstance(value, dict):
28 | # If the value is a dictionary, call the function recursively
29 | result.append(f"{space}- {key}:")
30 | result.append(format_dict_with_bullets(value, indent + 4))
31 | else:
32 | # Otherwise, format the key-value pair as a bullet point
33 | result.append(f"{space}- {key}: {value} ")
34 | return "\n".join(result)
35 |
36 | # Generate a data frame from the JSON file
37 | df = pd.DataFrame(
38 | [
39 | {
40 | "track": item["track"],
41 | "start": item["start_time"],
42 | "end": item["end_time"],
43 | "title": item["title"],
44 | "color": item["colour"],
45 | }
46 | for item in data
47 | ]
48 | )
49 |
50 | # Create dates and tracks
51 | df["start"] = pd.to_datetime(df["start"])
52 | df["track"] = df["start"].dt.day_name() + " - " + df["track"]
53 | df["end"] = pd.to_datetime(df["end"])
54 | df["end"] = df["end"].apply(lambda x: x.replace(day=1))
55 | df["start"] = df["start"].apply(lambda x: x.replace(day=1))
56 | df["start"] = df["start"].dt.tz_localize("UTC").dt.tz_convert("US/Eastern")
57 | df["end"] = df["end"].dt.tz_localize("UTC").dt.tz_convert("US/Eastern")
58 |
59 | # --- APP
60 |
61 | app_ui = ui.page_fluid(
62 | ui.panel_title("Shiny 2025 Conference Agenda"),
63 | output_widget("plt"),
64 | ui.output_ui("details")
65 | )
66 |
67 | def server(input, output, session):
68 |
69 | # Code to generate a plotly timeline
70 | fig = px.timeline(
71 | df, x_start="start", x_end="end", y="track", hover_name="title"
72 | )
73 | # Format layout
74 | fig.update_xaxes(tickformat="%H:%M", type="date")
75 | fig.update_traces(marker=dict(color=df["color"]))
76 | fig.update_layout(plot_bgcolor="white", paper_bgcolor="white")
77 |
78 | # Code to generate the metadata as text (e.g. for first event)
79 | ui.h3(ui.HTML(format_dict_with_bullets(data[0])))
80 |
81 | app = App(app_ui, server)
82 |
--------------------------------------------------------------------------------
/PART_4_Plotly-DataTable/exercise2/exercise2_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pieterjanvc/RShiny2Python/ca0ffaa4ba8f8986e23f89fc4db1e12ee575160e/PART_4_Plotly-DataTable/exercise2/exercise2_screenshot.png
--------------------------------------------------------------------------------
/PART_4_Plotly-DataTable/part4_dt_app.py:
--------------------------------------------------------------------------------
1 |
2 | # PART 4 - Live Demo - Data Table
3 | # ///////////////////////////////
4 |
5 | from shiny import App, reactive, render, req, ui
6 | import pandas as pd
7 |
8 | info = pd.DataFrame({"x": [1,2,3], "y": ["A", "B", "C"]})
9 |
10 | app_ui = ui.page_fluid(
11 | ui.output_data_frame("tbl"),
12 | ui.output_text("out"),
13 | )
14 |
15 |
16 | def server(input, output, session):
17 | @render.data_frame
18 | def tbl():
19 | return render.DataTable(info, selection_mode="row")
20 |
21 | @render.text
22 | def out():
23 | req(tbl.cell_selection()["rows"])
24 | return info.iloc[tbl.cell_selection()["rows"][0]]["y"]
25 |
26 | app = App(app_ui, server)
27 |
--------------------------------------------------------------------------------
/PART_4_Plotly-DataTable/part4_plotly_app.py:
--------------------------------------------------------------------------------
1 |
2 | # PART 4 - Live Demo - Plotly
3 | # ///////////////////////////
4 |
5 | from shiny import App, reactive, render, req, ui
6 | from shinywidgets import output_widget, render_widget
7 | import pandas as pd
8 | import plotly.express as px
9 | import plotly.graph_objects as go
10 |
11 | app_ui = ui.page_fluid(
12 | output_widget('plotly'),
13 | ui.output_text("out"),
14 | )
15 |
16 | info = pd.DataFrame({"x": [1,2,3], "y": ["A", "B", "C"]})
17 |
18 | def server(input, output, session):
19 |
20 | bar = reactive.value()
21 |
22 | @render.text
23 | def out():
24 | return "You clicked bar " + str(bar() + 1)
25 |
26 | def plotly_click(trace, points, selector):
27 | bar.set(points.point_inds[0])
28 |
29 | @render_widget
30 | def plotly():
31 | x = px.bar(info, x = "x", y = "y")
32 | plt = go.FigureWidget(x.data, x.layout)
33 | plt.data[0].on_click(plotly_click)
34 | return plt
35 |
36 | app = App(app_ui, server)
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Transferring your R Shiny skills to Python
2 |
3 | _PJ Van Camp - [LinkedIn](https://www.linkedin.com/in/pjvancamp/)_
4 |
5 | ## Intro and Objectives
6 |
7 | This repository contains materials used in the 'Transferring your R Shiny skills
8 | to Python' workshop. The workshop aims to teach you how you can quickly start
9 | building Shiny apps in Python if you already know R Shiny and have basic Python
10 | coding skills.
11 |
12 | This workshops has the following objectives:
13 |
14 | - Describe the large conceptual overlap between R and Python Shiny
15 | - Explain the differences between the Core and Express Python Shiny framework
16 | - Demonstrate the power of Positron IDE for easily switching between R and
17 | Python Shiny
18 | - Define decorators in Python and their use in the Shiny framework
19 | - Convert core R Shiny functions (inputs, outputs, reactive environments) to
20 | Python syntax
21 | - Build a few simple apps to showcase how easy the transition is
22 | - Outline common R Shiny functionality that is not yet (fully) implemented in
23 | Python
24 |
25 | ## Repository Organisation
26 |
27 | ### Setup
28 |
29 | **Quick start**: Use the shinylive links provided in the README for each
30 | exercise as this requires no setup.
31 |
32 | If you want to learn more about shinylive or use a local development environment
33 | to do the exercises in your IDE of preference, please read this
34 | [setup Instructions README](SETUP/README.MD).
35 |
36 | ### Topics
37 |
38 | - PART 1 - [Creating Shiny UI](PART_1_UI/)
39 | - PART 2 - [Python Shiny reactivity syntax](PART_2_reactivity/)
40 | - PART 3 - [Shiny Express](PART_3_express/)
41 | - PART 4 - [Interactive tables and plots](PART_4_Plotly-DataTable/)
42 |
43 | Each part comes with a dedicated folder in this repository (see above):
44 |
45 | - A README file will provide background documentation and code examples
46 | - One or more exercise folders will contain all code, data and instructions
47 | (again as a README) needed to complete the exercise
48 | - Solutions to all exercises are available in the [solutions](solutions/)
49 | folder. Try not to look at them when you get stuck, but look though the
50 | documentation instead as this will increase your learning
51 |
52 | ## Workshop Format
53 |
54 | The workshop is broken down into 4 parts
55 |
56 | For each part there will be
57 |
58 | - 10-15 minutes introduction with a **tutorial**
59 | - 20 minutes **hands-on exercises**
60 | - Instructions for each exercise are in a dedicated README
61 | - You can mute the sound as there will be a timer on the screen indication
62 | when the next part starts
63 | - The main room will be used to answer questions or help with debugging
64 | - Solutions to all exercises are available in the solutions folder, but try to
65 | solve the problem without looking at them
66 | - 5 minutes **wrap-up** before moving on to the next part
67 |
68 | ## About this Repository
69 |
70 | All materials were originally created for a workshop hosted at the
71 | [2025 Shiny Conference](https://www.shinyconf.com/) and is shared under the
72 | [GPL-3 license](LICENSE). If you have any questions, you can reach out to PJ Van
73 | Camp pjvancamp@hms.harvard.edu or find me on
74 | [LinkedIn](https://www.linkedin.com/in/pjvancamp/)
75 |
--------------------------------------------------------------------------------
/SETUP/README.MD:
--------------------------------------------------------------------------------
1 | # Setting up for the Workshop
2 |
3 | As this workshop is about Shiny for Python, you need to have a python
4 | environment set up so you can start creating your own apps.
5 |
6 | - **Online with Shinylive**: This is recommended if you like to get started
7 | quickly and do not have a local Python Shiny development environment setup
8 | - **Local**: This setup is recommended if you have experience setting up new
9 | workspaces quicky and installing the various requirements, it will also
10 | provide the most comprehensive experience and you get to keep all of your work
11 |
12 | ## ONLINE USING SHINYLIVE
13 |
14 | Shinylive allows you to build and run (simple) Shiny apps all from within the
15 | browser without the need to install anything (e.g. no need to setup Python,
16 | Shiny or even an IDE).
17 |
18 | To learn more about the framework and its advantages and disadvantages, visit
19 | the [shinylive documentation](https://shiny.posit.co/py/docs/shinylive.html)
20 | website
21 |
22 | ### Access
23 |
24 | You can access Shinylive for Python via https://shinylive.io/py/editor/
25 |
26 | Note that for this workshop, **we will provide you with dedicated links for each
27 | exercise** that already has start code and relevant files uploaded. These links
28 | can be found in each exercise's README. You can at any point also copy-paste
29 | code from this repo into the editor if needed or even upload files
30 |
31 | ### Uploading local data
32 |
33 | NOTE: If you are using the dedicated links provided in each exercise's README in
34 | this repository, you should not have to upload any data unless specifically
35 | noted in the instructions.
36 |
37 | - You can upload data from your computer into a shinylive app by clicking the
38 | add `+` icon next to the `app.py` file.
39 | - To upload data to a subfolder, e.g. `www` add the relative path as part of the
40 | filename e.g. `www/image.png`
41 | - Alternatively, you can upload the data elsewhere, make it publicly available
42 | and use links instead to read the data in dynamically. We will provide this
43 | option where needed
44 |
45 | ### Using external libraries
46 |
47 | If your app requires additional libraries outside of the core python modules or
48 | the shiny library, you can **add a `requirements.txt` file** (create from
49 | scratch) and add the libraries there so they will be installed when you run the
50 | app.
51 |
52 | Note that not all Python libraries are available in Shinylive. For a full list
53 | refer to the
54 | [Pyodide website](https://pyodide.org/en/0.27.3/usage/packages-in-pyodide.html)
55 |
56 | ## LOCAL SETUP
57 |
58 | ### Step 1 - Clone this repository
59 |
60 | - Git Clone: Navigate to the folder where you want to store this project and
61 | then run `git clone https://github.com/pieterjanvc/RShiny2Python.git`
62 | - Manual Download:
63 | [Download](https://github.com/pieterjanvc/RShiny2Python/archive/refs/heads/main.zip)
64 | and Unzip the repo anywhere on your computer
65 |
66 | ### Step 2 - Make sure you have Python installed
67 |
68 | To check your current, default Python version, run the following command on the
69 | Terminal
70 |
71 | ```
72 | python --version
73 | ```
74 |
75 | _Python 3.13 was used whilst creating the exercises, but older versions should
76 | work as well_
77 |
78 | ### Step 3 - Pick an IDE
79 |
80 | It is highly recommended to choose an IDE that has extended Python support and
81 | integrates well with Shiny. Below is a list of the IDE's in order of
82 | recommendation.
83 |
84 | #### Positron
85 |
86 | Best if you code in both R and Python. This IDE is a fork of VS code, so if you
87 | are familiar with the latter it will be very easy to adopt working in it.
88 |
89 | 1. Install [Positron](https://positron.posit.co/download.html)
90 | 2. Install the Shiny extension
91 | - Open the extension tab in Positron (ctrl/cmd + shift + x)
92 | - Search for `Posit.shiny` and install the extension
93 | - Reload if needed
94 |
95 | #### Visual Studio (VS) Code
96 |
97 | _Very similar to Positron, but recommended in case you use other programming
98 | languages as well and prefer not to install another IDE_
99 |
100 | 1. Install [VS Code](https://code.visualstudio.com/)
101 | 2. Install the Shiny extension
102 | - Open the extension tab in Positron (ctrl/cmd + shift + x)
103 | - Search for `Posit.shiny` and install the extension
104 | - Reload if needed
105 |
106 | #### R Studio Desktop
107 |
108 | There is only limited support for using Python Shiny in RStudio, but if you
109 | really prefer to work in it and don't want to use _shinylive_, you can make
110 | this work.
111 |
112 | 1. Open RStudio
113 | 2. Install the reticulate package `install.packages("reticulate")`
114 | 3. Set the Python interpreter: Tools --> Global Options --> Python
115 |
116 | ### Step 4 - Setup Environment and Install Packages
117 |
118 | It is recommended to setup a virtual python environment to ensure you have all
119 | the correct package versions, but this is optional.
120 |
121 | - If you are using the built-in python environment manager use
122 | `requirements.txt` to install all dependencies
123 | - If you are using [uv](https://docs.astral.sh/uv/), you can use the `uv.lock`
124 | file
125 |
126 | Alternatively, install any required packages depending on the exercise you are
127 | at using the basic pip command in the Terminal. For example:
128 |
129 | ```sh
130 | pip install shiny
131 | ```
132 |
133 | _This workshop has been tested with Shiny 1.3+. If you have an older version
134 | already installed, add the `--upgrade` flag to this command to get the latest
135 | version_
136 |
137 | You can now try and run the [test_app.py](./test_app.py) file.
138 |
139 | - If you are using **Positron or VS Code** can click the **Run button** located
140 | on the top-right of the file if you have the extension installed
141 | - If you are using **RStudio** or would like to **Start the app from the
142 | Terminal** you run the following command
143 |
144 | ```sh
145 | shiny run --reload --launch-browser test_app.py
146 | ```
147 |
148 | _To stop the Shiny app, press ctrl/cmd + C in the terminal_
149 |
--------------------------------------------------------------------------------
/SETUP/test_app.py:
--------------------------------------------------------------------------------
1 | # Dummy Shiny App to test installation
2 | from shiny import ui, App
3 |
4 | app_ui = ui.page_fluid(ui.h1("Python Shiny is working!"))
5 |
6 |
7 | def server(Inputs, Outputs, Session):
8 | pass
9 |
10 |
11 | app = App(app_ui, server)
12 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 | ## About
3 |
4 | This workshop is aimed for people with R Shiny expertise who wish to learn more
5 | about transferring their Shiny skills to Python and start developing apps
6 | there as well.
7 |
8 | ## Workshop Repo
9 |
10 | All materials are available on GitHub at
11 | [https://github.com/pieterjanvc/RShiny2Python](https://github.com/pieterjanvc/RShiny2Python)
12 |
13 | ## Framework reference
14 | [](https://shiny.posit.co/py/)
15 |
--------------------------------------------------------------------------------
/docs/assets/shiny-for-python.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
81 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "rshiny2python"
3 | version = "0.1.0"
4 | description = "Workshop Repository"
5 | readme = "README.md"
6 | requires-python = ">=3.13"
7 | dependencies = [
8 | "ipykernel>=6.29.5",
9 | "pandas>=2.2.3",
10 | "plotly==5.24.1",
11 | "requests>=2.32.3",
12 | "seaborn>=0.13.2",
13 | "shiny>=1.3.0",
14 | "shinywidgets>=0.5.1",
15 | ]
16 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # This file was autogenerated by uv via the following command:
2 | # uv export --format requirements-txt
3 | anyio==4.9.0 \
4 | --hash=sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028 \
5 | --hash=sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c
6 | anywidget==0.9.18 \
7 | --hash=sha256:262cf459b517a7d044d6fbc84b953e9c83f026790b2dd3ce90f21a7f8eded00f \
8 | --hash=sha256:944b82ef1dd17b8ff0fb6d1f199f613caf9111338e6e2857da478f6e73770cb8
9 | appdirs==1.4.4 \
10 | --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \
11 | --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128
12 | appnope==0.1.4 ; sys_platform == 'darwin' \
13 | --hash=sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee \
14 | --hash=sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c
15 | asgiref==3.8.1 \
16 | --hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \
17 | --hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590
18 | asttokens==3.0.0 \
19 | --hash=sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7 \
20 | --hash=sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2
21 | certifi==2025.1.31 \
22 | --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \
23 | --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe
24 | cffi==1.17.1 ; implementation_name == 'pypy' \
25 | --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
26 | --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
27 | --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
28 | --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
29 | --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
30 | --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
31 | --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
32 | --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
33 | --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
34 | --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
35 | --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
36 | --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a
37 | charset-normalizer==3.4.1 \
38 | --hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \
39 | --hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \
40 | --hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \
41 | --hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \
42 | --hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \
43 | --hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \
44 | --hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \
45 | --hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \
46 | --hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \
47 | --hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \
48 | --hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \
49 | --hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \
50 | --hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \
51 | --hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \
52 | --hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd
53 | click==8.1.8 ; sys_platform != 'emscripten' \
54 | --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \
55 | --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a
56 | colorama==0.4.6 ; sys_platform == 'win32' \
57 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
58 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
59 | comm==0.2.2 \
60 | --hash=sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e \
61 | --hash=sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3
62 | contourpy==1.3.1 \
63 | --hash=sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1 \
64 | --hash=sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda \
65 | --hash=sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f \
66 | --hash=sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751 \
67 | --hash=sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546 \
68 | --hash=sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82 \
69 | --hash=sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c \
70 | --hash=sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c \
71 | --hash=sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242 \
72 | --hash=sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5 \
73 | --hash=sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2 \
74 | --hash=sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3 \
75 | --hash=sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342 \
76 | --hash=sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1 \
77 | --hash=sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1 \
78 | --hash=sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30 \
79 | --hash=sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2 \
80 | --hash=sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd \
81 | --hash=sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7 \
82 | --hash=sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699 \
83 | --hash=sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81
84 | cycler==0.12.1 \
85 | --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \
86 | --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c
87 | debugpy==1.8.13 \
88 | --hash=sha256:31abc9618be4edad0b3e3a85277bc9ab51a2d9f708ead0d99ffb5bb750e18503 \
89 | --hash=sha256:5268ae7fdca75f526d04465931cb0bd24577477ff50e8bb03dab90983f4ebd02 \
90 | --hash=sha256:79ce4ed40966c4c1631d0131606b055a5a2f8e430e3f7bf8fd3744b09943e8e8 \
91 | --hash=sha256:837e7bef95bdefba426ae38b9a94821ebdc5bea55627879cd48165c90b9e50ce \
92 | --hash=sha256:a0bd87557f97bced5513a74088af0b84982b6ccb2e254b9312e29e8a5c4270eb \
93 | --hash=sha256:d4ba115cdd0e3a70942bd562adba9ec8c651fe69ddde2298a1be296fc331906f
94 | decorator==5.2.1 \
95 | --hash=sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360 \
96 | --hash=sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a
97 | executing==2.2.0 \
98 | --hash=sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa \
99 | --hash=sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755
100 | fonttools==4.57.0 \
101 | --hash=sha256:3122c604a675513c68bd24c6a8f9091f1c2376d18e8f5fe5a101746c81b3e98f \
102 | --hash=sha256:408ce299696012d503b714778d89aa476f032414ae57e57b42e4b92363e0b8ef \
103 | --hash=sha256:4dea5893b58d4637ffa925536462ba626f8a1b9ffbe2f5c272cdf2c6ebadb817 \
104 | --hash=sha256:727ece10e065be2f9dd239d15dd5d60a66e17eac11aea47d447f9f03fdbc42de \
105 | --hash=sha256:767604f244dc17c68d3e2dbf98e038d11a18abc078f2d0f84b6c24571d9c0b13 \
106 | --hash=sha256:8e2e12d0d862f43d51e5afb8b9751c77e6bec7d2dc00aad80641364e9df5b199 \
107 | --hash=sha256:bbceffc80aa02d9e8b99f2a7491ed8c4a783b2fc4020119dc405ca14fb5c758c \
108 | --hash=sha256:dff02c5c8423a657c550b48231d0a48d7e2b2e131088e55983cfe74ccc2c7cc9 \
109 | --hash=sha256:f022601f3ee9e1f6658ed6d184ce27fa5216cee5b82d279e0f0bde5deebece72 \
110 | --hash=sha256:f1d6bc9c23356908db712d282acb3eebd4ae5ec6d8b696aa40342b1d84f8e9e3
111 | h11==0.14.0 ; sys_platform != 'emscripten' \
112 | --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
113 | --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
114 | htmltools==0.6.0 \
115 | --hash=sha256:072a274ff5e2851e0acce13fc5bb2bbdbbad8268dc8b123f881c05012ce7dce0 \
116 | --hash=sha256:e8a3fb023d748935035db7ff17f620612ffc814a6a80b6ae388f7b7ab182adf7
117 | idna==3.10 \
118 | --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
119 | --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
120 | ipykernel==6.29.5 \
121 | --hash=sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5 \
122 | --hash=sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215
123 | ipython==9.0.2 \
124 | --hash=sha256:143ef3ea6fb1e1bffb4c74b114051de653ffb7737a3f7ab1670e657ca6ae8c44 \
125 | --hash=sha256:ec7b479e3e5656bf4f58c652c120494df1820f4f28f522fb7ca09e213c2aab52
126 | ipython-pygments-lexers==1.1.1 \
127 | --hash=sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81 \
128 | --hash=sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c
129 | ipywidgets==8.1.5 \
130 | --hash=sha256:3290f526f87ae6e77655555baba4f36681c555b8bdbbff430b70e52c34c86245 \
131 | --hash=sha256:870e43b1a35656a80c18c9503bbf2d16802db1cb487eec6fab27d683381dde17
132 | jedi==0.19.2 \
133 | --hash=sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0 \
134 | --hash=sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9
135 | jupyter-client==8.6.3 \
136 | --hash=sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419 \
137 | --hash=sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f
138 | jupyter-core==5.7.2 \
139 | --hash=sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409 \
140 | --hash=sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9
141 | jupyterlab-widgets==3.0.13 \
142 | --hash=sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed \
143 | --hash=sha256:e3cda2c233ce144192f1e29914ad522b2f4c40e77214b0cc97377ca3d323db54
144 | kiwisolver==1.4.8 \
145 | --hash=sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc \
146 | --hash=sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6 \
147 | --hash=sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09 \
148 | --hash=sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e \
149 | --hash=sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7 \
150 | --hash=sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880 \
151 | --hash=sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b \
152 | --hash=sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b \
153 | --hash=sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c \
154 | --hash=sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30 \
155 | --hash=sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47 \
156 | --hash=sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1 \
157 | --hash=sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90 \
158 | --hash=sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c \
159 | --hash=sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e \
160 | --hash=sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16 \
161 | --hash=sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712 \
162 | --hash=sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3 \
163 | --hash=sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc \
164 | --hash=sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed \
165 | --hash=sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957 \
166 | --hash=sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165 \
167 | --hash=sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2 \
168 | --hash=sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246 \
169 | --hash=sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d \
170 | --hash=sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85 \
171 | --hash=sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062 \
172 | --hash=sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb \
173 | --hash=sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794
174 | linkify-it-py==2.0.3 \
175 | --hash=sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048 \
176 | --hash=sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79
177 | markdown-it-py==3.0.0 \
178 | --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \
179 | --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb
180 | matplotlib==3.10.1 \
181 | --hash=sha256:01e63101ebb3014e6e9f80d9cf9ee361a8599ddca2c3e166c563628b39305dbb \
182 | --hash=sha256:19b06241ad89c3ae9469e07d77efa87041eac65d78df4fcf9cac318028009b01 \
183 | --hash=sha256:3f06bad951eea6422ac4e8bdebcf3a70c59ea0a03338c5d2b109f57b64eb3972 \
184 | --hash=sha256:4f0647b17b667ae745c13721602b540f7aadb2a32c5b96e924cd4fea5dcb90f1 \
185 | --hash=sha256:5d45d3f5245be5b469843450617dcad9af75ca50568acf59997bed9311131a0b \
186 | --hash=sha256:7e496c01441be4c7d5f96d4e40f7fca06e20dcb40e44c8daa2e740e1757ad9e6 \
187 | --hash=sha256:8e8e25b1209161d20dfe93037c8a7f7ca796ec9aa326e6e4588d8c4a5dd1e473 \
188 | --hash=sha256:a3dfb036f34873b46978f55e240cff7a239f6c4409eac62d8145bad3fc6ba5a3 \
189 | --hash=sha256:aa3854b5f9473564ef40a41bc922be978fab217776e9ae1545c9b3a5cf2092a3 \
190 | --hash=sha256:bc411ebd5889a78dabbc457b3fa153203e22248bfa6eedc6797be5df0164dbf9 \
191 | --hash=sha256:c42eee41e1b60fd83ee3292ed83a97a5f2a8239b10c26715d8a6172226988d7b \
192 | --hash=sha256:dc6ab14a7ab3b4d813b88ba957fc05c79493a037f54e246162033591e770de6f \
193 | --hash=sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba
194 | matplotlib-inline==0.1.7 \
195 | --hash=sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90 \
196 | --hash=sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca
197 | mdit-py-plugins==0.4.2 \
198 | --hash=sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636 \
199 | --hash=sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5
200 | mdurl==0.1.2 \
201 | --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
202 | --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
203 | narwhals==1.33.0 \
204 | --hash=sha256:6233d2457debf4b5fe4a1da54530c6fe2d84326f4a8e3bca35bbbff580a347cb \
205 | --hash=sha256:f653319112fd121a1f1c18a40cf70dada773cdacfd53e62c2aa0afae43c17129
206 | nest-asyncio==1.6.0 \
207 | --hash=sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe \
208 | --hash=sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c
209 | numpy==2.2.4 \
210 | --hash=sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286 \
211 | --hash=sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d \
212 | --hash=sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0 \
213 | --hash=sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7 \
214 | --hash=sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3 \
215 | --hash=sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6 \
216 | --hash=sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc \
217 | --hash=sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298 \
218 | --hash=sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392 \
219 | --hash=sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8 \
220 | --hash=sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd \
221 | --hash=sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0 \
222 | --hash=sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc \
223 | --hash=sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f \
224 | --hash=sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39 \
225 | --hash=sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd \
226 | --hash=sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7 \
227 | --hash=sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8 \
228 | --hash=sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff \
229 | --hash=sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960 \
230 | --hash=sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c
231 | orjson==3.10.16 \
232 | --hash=sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd \
233 | --hash=sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb \
234 | --hash=sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8 \
235 | --hash=sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6 \
236 | --hash=sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430 \
237 | --hash=sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5 \
238 | --hash=sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137 \
239 | --hash=sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90 \
240 | --hash=sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0 \
241 | --hash=sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56 \
242 | --hash=sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10 \
243 | --hash=sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b \
244 | --hash=sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e \
245 | --hash=sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652 \
246 | --hash=sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7
247 | packaging==24.2 \
248 | --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \
249 | --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f
250 | pandas==2.2.3 \
251 | --hash=sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d \
252 | --hash=sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4 \
253 | --hash=sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0 \
254 | --hash=sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28 \
255 | --hash=sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18 \
256 | --hash=sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468 \
257 | --hash=sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667 \
258 | --hash=sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d \
259 | --hash=sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb \
260 | --hash=sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659 \
261 | --hash=sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a \
262 | --hash=sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2 \
263 | --hash=sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015 \
264 | --hash=sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24
265 | parso==0.8.4 \
266 | --hash=sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18 \
267 | --hash=sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d
268 | pexpect==4.9.0 ; sys_platform != 'emscripten' and sys_platform != 'win32' \
269 | --hash=sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 \
270 | --hash=sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f
271 | pillow==11.1.0 \
272 | --hash=sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65 \
273 | --hash=sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352 \
274 | --hash=sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20 \
275 | --hash=sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114 \
276 | --hash=sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c \
277 | --hash=sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756 \
278 | --hash=sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861 \
279 | --hash=sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1 \
280 | --hash=sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081 \
281 | --hash=sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5 \
282 | --hash=sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c \
283 | --hash=sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe \
284 | --hash=sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc \
285 | --hash=sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec \
286 | --hash=sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3 \
287 | --hash=sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0 \
288 | --hash=sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547 \
289 | --hash=sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9 \
290 | --hash=sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab \
291 | --hash=sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9
292 | platformdirs==4.3.7 \
293 | --hash=sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94 \
294 | --hash=sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351
295 | plotly==5.24.1 \
296 | --hash=sha256:dbc8ac8339d248a4bcc36e08a5659bacfe1b079390b8953533f4eb22169b4bae \
297 | --hash=sha256:f67073a1e637eb0dc3e46324d9d51e2fe76e9727c892dde64ddf1e1b51f29089
298 | prompt-toolkit==3.0.50 \
299 | --hash=sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab \
300 | --hash=sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198
301 | psutil==7.0.0 \
302 | --hash=sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25 \
303 | --hash=sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91 \
304 | --hash=sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da \
305 | --hash=sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34 \
306 | --hash=sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553 \
307 | --hash=sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456 \
308 | --hash=sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993 \
309 | --hash=sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99
310 | psygnal==0.12.0 \
311 | --hash=sha256:15f39abd8bee2926e79da76bec31a258d03dbe3e61d22d6251f65caefbae5d54 \
312 | --hash=sha256:2d5a953a50fc8263bb23bc558b926cf691f70c9c781c68c64c983fb8cbead910 \
313 | --hash=sha256:2f4c1fed9337f57778109c397b6b9591961123ce4bbeb068115c0468964fc2b4 \
314 | --hash=sha256:742abb2d0e230521b208161eeab06abb682a19239e734e543a269214c84a54d2 \
315 | --hash=sha256:7a67ec8e0c8a6553dd56ed653f87c46ef652b0c512bb8c8f8c5adcff3907751f \
316 | --hash=sha256:8d2a99803f3152c469d3642d36c04d680213a20e114245558e026695adf9a9c2 \
317 | --hash=sha256:d779f20c6977ec9d5b9fece23b4b28bbcf0a7773539a4a176b5527aea5da27c7
318 | ptyprocess==0.7.0 ; sys_platform != 'emscripten' and sys_platform != 'win32' \
319 | --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \
320 | --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220
321 | pure-eval==0.2.3 \
322 | --hash=sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0 \
323 | --hash=sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42
324 | pycparser==2.22 ; implementation_name == 'pypy' \
325 | --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
326 | --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
327 | pygments==2.19.1 \
328 | --hash=sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f \
329 | --hash=sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c
330 | pyparsing==3.2.3 \
331 | --hash=sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf \
332 | --hash=sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be
333 | python-dateutil==2.9.0.post0 \
334 | --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
335 | --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
336 | python-multipart==0.0.20 ; sys_platform != 'emscripten' \
337 | --hash=sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 \
338 | --hash=sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13
339 | pytz==2025.2 \
340 | --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \
341 | --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00
342 | pywin32==310 ; platform_python_implementation != 'PyPy' and sys_platform == 'win32' \
343 | --hash=sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab \
344 | --hash=sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e \
345 | --hash=sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33
346 | pyzmq==26.3.0 \
347 | --hash=sha256:016d89bee8c7d566fad75516b4e53ec7c81018c062d4c51cd061badf9539be52 \
348 | --hash=sha256:04bfe59852d76d56736bfd10ac1d49d421ab8ed11030b4a0332900691507f557 \
349 | --hash=sha256:1fe05bd0d633a0f672bb28cb8b4743358d196792e1caf04973b7898a0d70b046 \
350 | --hash=sha256:209d09f0ab6ddbcebe64630d1e6ca940687e736f443c265ae15bc4bfad833597 \
351 | --hash=sha256:21399b31753bf321043ea60c360ed5052cc7be20739785b1dff1820f819e35b3 \
352 | --hash=sha256:240b1634b9e530ef6a277d95cbca1a6922f44dfddc5f0a3cd6c722a8de867f14 \
353 | --hash=sha256:2aa1a9f236d5b835fb8642f27de95f9edcfd276c4bc1b6ffc84f27c6fb2e2981 \
354 | --hash=sha256:6d64e74143587efe7c9522bb74d1448128fdf9897cc9b6d8b9927490922fd558 \
355 | --hash=sha256:73ca9ae9a9011b714cf7650450cd9c8b61a135180b708904f1f0a05004543dce \
356 | --hash=sha256:9b0137a1c40da3b7989839f9b78a44de642cdd1ce20dcef341de174c8d04aa53 \
357 | --hash=sha256:a995404bd3982c089e57b428c74edd5bfc3b0616b3dbcd6a8e270f1ee2110f36 \
358 | --hash=sha256:b380e9087078ba91e45fb18cdd0c25275ffaa045cf63c947be0ddae6186bc9d9 \
359 | --hash=sha256:c4430c7cba23bb0e2ee203eee7851c1654167d956fc6d4b3a87909ccaf3c5825 \
360 | --hash=sha256:d015efcd96aca8882057e7e6f06224f79eecd22cad193d3e6a0a91ec67590d1f \
361 | --hash=sha256:d35cc1086f1d4f907df85c6cceb2245cb39a04f69c3f375993363216134d76d4 \
362 | --hash=sha256:efba4f53ac7752eea6d8ca38a4ddac579e6e742fba78d1e99c12c95cd2acfc64 \
363 | --hash=sha256:f1cd68b8236faab78138a8fc703f7ca0ad431b17a3fcac696358600d4e6243b3 \
364 | --hash=sha256:fa85953df84beb7b8b73cb3ec3f5d92b62687a09a8e71525c6734e020edf56fd \
365 | --hash=sha256:fe67291775ea4c2883764ba467eb389c29c308c56b86c1e19e49c9e1ed0cbeca \
366 | --hash=sha256:fea7efbd7e49af9d7e5ed6c506dfc7de3d1a628790bd3a35fd0e3c904dc7d464
367 | questionary==2.1.0 ; sys_platform != 'emscripten' \
368 | --hash=sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec \
369 | --hash=sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587
370 | requests==2.32.3 \
371 | --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
372 | --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
373 | seaborn==0.13.2 \
374 | --hash=sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987 \
375 | --hash=sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7
376 | setuptools==78.1.0 \
377 | --hash=sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54 \
378 | --hash=sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8
379 | shiny==1.3.0 \
380 | --hash=sha256:0e29ccc9b642bf45409efc6a4e9751c29ab9378ac31f1b2f025552ebd7699c99 \
381 | --hash=sha256:c1a524f3512c02072f0fd124f7c13dfe03fe51f24ceda4ce9dab0f755deb891d
382 | shinywidgets==0.5.1 \
383 | --hash=sha256:5307974870b3854352ce6359e20a337d2ba840212edb0ad4114864b1c2eebeb3 \
384 | --hash=sha256:a866e9a7a2b05ded7b637943ea7b7dfd8975a3810dce475242f6d1e913613dd7
385 | six==1.17.0 \
386 | --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
387 | --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
388 | sniffio==1.3.1 \
389 | --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
390 | --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
391 | stack-data==0.6.3 \
392 | --hash=sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 \
393 | --hash=sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695
394 | starlette==0.46.1 \
395 | --hash=sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230 \
396 | --hash=sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227
397 | tenacity==9.1.2 \
398 | --hash=sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb \
399 | --hash=sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138
400 | tornado==6.4.2 \
401 | --hash=sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803 \
402 | --hash=sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec \
403 | --hash=sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482 \
404 | --hash=sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634 \
405 | --hash=sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38 \
406 | --hash=sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b \
407 | --hash=sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c \
408 | --hash=sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf \
409 | --hash=sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946 \
410 | --hash=sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73 \
411 | --hash=sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1
412 | traitlets==5.14.3 \
413 | --hash=sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7 \
414 | --hash=sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f
415 | typing-extensions==4.13.0 \
416 | --hash=sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b \
417 | --hash=sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5
418 | tzdata==2025.2 \
419 | --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \
420 | --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9
421 | uc-micro-py==1.0.3 \
422 | --hash=sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a \
423 | --hash=sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5
424 | urllib3==2.3.0 \
425 | --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \
426 | --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d
427 | uvicorn==0.34.0 ; sys_platform != 'emscripten' \
428 | --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \
429 | --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9
430 | watchfiles==1.0.4 ; sys_platform != 'emscripten' \
431 | --hash=sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1 \
432 | --hash=sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303 \
433 | --hash=sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590 \
434 | --hash=sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407 \
435 | --hash=sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205 \
436 | --hash=sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d \
437 | --hash=sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9 \
438 | --hash=sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b \
439 | --hash=sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60 \
440 | --hash=sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80 \
441 | --hash=sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc \
442 | --hash=sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902 \
443 | --hash=sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d
444 | wcwidth==0.2.13 \
445 | --hash=sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859 \
446 | --hash=sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5
447 | websockets==15.0.1 \
448 | --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \
449 | --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \
450 | --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \
451 | --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \
452 | --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \
453 | --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \
454 | --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \
455 | --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \
456 | --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \
457 | --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \
458 | --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \
459 | --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \
460 | --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f
461 | widgetsnbextension==4.0.13 \
462 | --hash=sha256:74b2692e8500525cc38c2b877236ba51d34541e6385eeed5aec15a70f88a6c71 \
463 | --hash=sha256:ffcb67bc9febd10234a362795f643927f4e0c05d9342c727b65d2384f8feacb6
464 |
--------------------------------------------------------------------------------
/solutions/part1_ex1_solution_app.py:
--------------------------------------------------------------------------------
1 | # PART 1 - Exercise 1 - Solution
2 | # //////////////////////////////
3 |
4 | from shiny import App, ui
5 |
6 | app_ui = ui.page_fluid(
7 | ui.panel_title("Shiny Q&A"),
8 | ui.input_text("name", "Your Name"),
9 | ui.input_select(
10 | "category", "Category", choices=["General", "Development", "Deployment"]
11 | ),
12 | ui.input_text_area("question", "Question"),
13 | ui.input_action_button("send", "Send"),
14 | )
15 |
16 |
17 | # You can ignore the sever function for this exercise
18 | def server(input, output, session):
19 | pass
20 |
21 |
22 | app = App(app_ui, server)
23 |
--------------------------------------------------------------------------------
/solutions/part1_ex2_solution_app.py:
--------------------------------------------------------------------------------
1 | # PART 1 - Exercise 2 - Solution
2 | # //////////////////////////////
3 |
4 | from shiny import App, ui
5 |
6 | app_ui = ui.page_fluid(
7 | ui.h1("Python Shiny Survey"),
8 | ui.row(
9 | ui.column(
10 | 5,
11 | ui.input_slider(
12 | "exp",
13 | "Experience from 0 (none) to 5 (expert)",
14 | min=0,
15 | max=5,
16 | value=2,
17 | ),
18 | ui.input_select(
19 | "usage",
20 | "I write Python Shiny apps ...",
21 | choices=["Daily", "Weekly", "Monthly", "Yearly"],
22 | ),
23 | ui.hr(),
24 | ui.input_checkbox("preference", "I prefer Python Shiny over R Shiny"),
25 | ui.input_text_area("learn", "What would you like to learn more about?"),
26 | ),
27 | ui.column(7, ui.output_plot("plt")),
28 | ),
29 | ui.input_action_button("submit", "Submit"),
30 | )
31 |
32 |
33 | # You can ignore the sever function for this exercise
34 | def server(input, output, session):
35 | pass
36 |
37 |
38 | app = App(app_ui, server)
39 |
--------------------------------------------------------------------------------
/solutions/part1_ex3_solution_app.py:
--------------------------------------------------------------------------------
1 | # PART 1 - Exercise 3 - Solution
2 | # //////////////////////////////
3 |
4 | from shiny import App, ui
5 | from pathlib import Path
6 |
7 | #UI
8 | app_ui = ui.page_fluid(
9 | ui.navset_tab(
10 | ui.nav_panel(
11 | "Tab 1",
12 | ui.layout_sidebar(
13 | ui.sidebar(
14 | ui.input_checkbox_group("cbx", "Features", choices=["A", "B", "C"]),
15 | title="Settings",
16 | ),
17 | ui.card(ui.card_header("Info"), ui.p("... some info ...")),
18 | ),
19 | ),
20 | ui.nav_panel("Tab 2", ui.img(src="image.jpg")),
21 | )
22 | )
23 |
24 |
25 | # Ignore for now
26 | def server(input, output, session):
27 | pass
28 |
29 |
30 | app = App(app_ui, server, static_assets=Path(__file__).parent / "www")
31 |
--------------------------------------------------------------------------------
/solutions/part2_ex1_solution_app.py:
--------------------------------------------------------------------------------
1 | # PART 2 - Exercise 1 - Solution
2 | # //////////////////////////////
3 |
4 | import requests
5 | import pandas as pd
6 | from io import StringIO
7 | from shiny import App, ui, render
8 |
9 | # Get the data and process it
10 | url = "https://data.opendatasoft.com/api/explore/v2.1/catalog/datasets/cats-in-movies@public/exports/csv"
11 | resp = requests.get(url)
12 | data = pd.read_csv(StringIO(resp.content.decode("UTF-8")), sep=";").sort_values(
13 | by=["title"]
14 | )
15 |
16 | # UI
17 | app_ui = ui.page_fluid(
18 | ui.panel_title("Movies with cats"),
19 | ui.row(
20 | ui.column(
21 | 4,
22 | ui.input_select("movie", "Movie", choices=data["title"].tolist()),
23 | ui.output_ui("img"),
24 | ),
25 | ui.column(
26 | 8,
27 | ui.input_slider(
28 | "era",
29 | "Era",
30 | min=min(data["year"]),
31 | max=max(data["year"]),
32 | value=[min(data["year"]), max(data["year"])],
33 | ),
34 | ui.output_data_frame("tbl"),
35 | ),
36 | ),
37 | )
38 |
39 |
40 | # SERVER
41 | def server(input, output, session):
42 | @render.ui
43 | def img():
44 | return ui.img(src=data[data["title"] == input.movie()]["url_poster"].values[0])
45 |
46 | @render.data_frame
47 | def tbl():
48 | table = data[
49 | (data["year"] >= input.era()[0]) & (data["year"] <= input.era()[1])
50 | ]
51 | table = table[["year", "title", "produced_by", "directed_by"]]
52 | return table
53 |
54 |
55 | app = App(app_ui, server)
56 |
--------------------------------------------------------------------------------
/solutions/part2_ex2_solution_app.py:
--------------------------------------------------------------------------------
1 | # PART 2 - Exercise 2 - Solution
2 | # //////////////////////////////
3 |
4 | import requests
5 | import string
6 | import random
7 | from shiny import App, ui, render, reactive
8 |
9 | # Get the data and process it
10 | url = "https://raw.githubusercontent.com/pkLazer/password_rank/refs/heads/master/4000-most-common-english-words-csv.csv"
11 | words = requests.get(url).text.splitlines()
12 | words = [word for word in words if len(word) == 6]
13 |
14 | # UI
15 | app_ui = ui.page_fluid(
16 | ui.panel_title("Hangman"),
17 | ui.output_ui("progress"),
18 | ui.input_select("letter", "Pick a letter", choices=list(string.ascii_lowercase)),
19 | ui.input_action_button("guess", "Guess"),
20 | )
21 |
22 |
23 | # SERVER
24 | def server(input, output, session):
25 | # Select a random word every time the app reloads
26 | word = random.choice(words)
27 | guesses = reactive.value([])
28 |
29 | @reactive.calc
30 | @reactive.event(input.guess)
31 | def result():
32 | # Add the new guess to the existing list
33 | x = guesses()
34 | x.append(input.letter())
35 |
36 | # Update the selection box
37 | remaining = [l for l in list(string.ascii_lowercase) if l not in x]
38 | ui.update_select("letter", choices=remaining)
39 |
40 | # Update the guessed letters reactive
41 | guesses.set(x)
42 |
43 | return " ".join([letter if letter in x else " - " for letter in list(word)])
44 |
45 | @render.ui
46 | def progress():
47 | return ui.h1(result(), style="font-family: monospace; color: #BF408B;")
48 |
49 |
50 | app = App(app_ui, server)
51 |
--------------------------------------------------------------------------------
/solutions/part2_ex3_solution_app.py:
--------------------------------------------------------------------------------
1 | # PART 2 - Exercise 3 - Solution
2 | # //////////////////////////////
3 |
4 | from shiny import App, ui, render, reactive
5 | import seaborn as sns
6 | import pandas as pd
7 | from pathlib import Path
8 |
9 | #data = pd.read_csv("PART_2_reactivity/exercise3/foods.csv")
10 | data = pd.read_csv(Path(__file__).parent / "foods.csv")
11 | data = data.sort_values("Food")
12 |
13 | # UI
14 | app_ui = ui.page_fluid(
15 | ui.panel_title("If I could only eat one thing ..."),
16 | ui.row(
17 | ui.column(
18 | 4,
19 | ui.card(
20 | ui.card_header("Selection"),
21 | ui.input_select("food", "Pick a Food", choices=list(data["Food"])),
22 | ui.input_select(
23 | "comp",
24 | "Daily intake component to match",
25 | choices=["Carbs", "Protein", "Fat", "Calories"],
26 | ),
27 | ),
28 | ),
29 | ui.column(
30 | 8,
31 | ui.card(
32 | ui.card_header("Target Daily intake"),
33 | ui.row(
34 | ui.column(
35 | 6,
36 | ui.input_slider(
37 | "Carbs", "Carbs (g)", min=10, max=500, value=250
38 | ),
39 | ui.input_slider(
40 | "Protein", "Protein (g)", min=10, max=200, value=50
41 | ),
42 | ),
43 | ui.column(
44 | 6,
45 | ui.input_slider("Fat", "Fat (g)", min=10, max=200, value=60),
46 | ui.input_slider(
47 | "Calories", "kCals", min=1000, max=4000, value=2000
48 | ),
49 | ),
50 | ),
51 | ),
52 | ),
53 | ),
54 | ui.card(ui.card_header("Nutritional values"), ui.output_plot("plt")),
55 | )
56 |
57 |
58 | # SERVER
59 | def server(input, output, session):
60 |
61 | @render.plot
62 | def plt():
63 |
64 | # Select food to focus e.g. Almonds
65 | food = data[data["Food"] == input.food()][
66 | ["Grams", "Calories", "Protein", "Fat", "Carbs"]
67 | ]
68 | # Get in long format
69 | food = pd.melt(food, var_name="name")
70 | # Adjust based on component to match and set daily target intake e.g. 250g of carbs
71 | food["value"] = (
72 | food["value"]
73 | / food.loc[food["name"] == input.comp(), "value"].values[0]
74 | * input[input.comp()]()
75 | )
76 |
77 | # Get the target daily intake values
78 | target = pd.DataFrame(
79 | {
80 | "name": ["Protein", "Fat", "Carbs"],
81 | "value": [input.Protein(), input.Fat(), input.Carbs()],
82 | }
83 | )
84 | # Create the bar plot showing consumed nutrients for chosen food
85 | plot = sns.barplot(
86 | x="name",
87 | y="value",
88 | data=food.iloc[2:5],
89 | color="#ff843d",
90 | label="Total Nutrients Consumed",
91 | )
92 | # Overlay barplot with target daily intake
93 | sns.barplot(
94 | x="name",
95 | y="value",
96 | data=target,
97 | color="gray",
98 | edgecolor="#007bc2",
99 | linewidth=2,
100 | facecolor="none",
101 | label="Recommended intake",
102 | )
103 |
104 | plot.set_ylabel("Grams")
105 | plot.set_xlabel("Nutrient")
106 |
107 | return plot
108 |
109 |
110 | app = App(app_ui, server)
111 |
--------------------------------------------------------------------------------
/solutions/part3_ex1_solution_app.py:
--------------------------------------------------------------------------------
1 | # PART 3 - Exercise 1 - Solution
2 | # //////////////////////////////
3 | from pathlib import Path
4 | import pandas as pd
5 | from datetime import datetime
6 |
7 | from shiny import reactive
8 | from shiny.express import input, render, ui, app_opts
9 |
10 | data = pd.read_csv(Path(__file__).parent / "extra-vehicular_activity.csv")
11 |
12 | # Data cleaning
13 | data.columns = data.columns.str.replace(" ", "")
14 | data["Date"] = pd.to_datetime(data["Date"])
15 | data["Duration"] = pd.to_datetime(data["Duration"], format="%H:%M")
16 | data["Duration"] = data["Duration"].dt.hour * 60 + data["Duration"].dt.minute
17 | data = data.drop(["EVA#", "Country"], axis=1)
18 | data = data.dropna()
19 |
20 | # Get a simplified list of vehicle types
21 | vehicleTypes = list(data["Vehicle"].str.extract(r"([^\s-]+)")[0].unique())
22 | vehicleTypes.sort()
23 |
24 | # Dropdown for vehicle type
25 | ui.input_select("vehicleType", "Vehicle Type", choices=vehicleTypes)
26 |
27 | # Slider for minimum duration
28 | ui.input_slider(
29 | "duration",
30 | "Minimum duration (min)",
31 | min=min(data["Duration"]),
32 | max=max(data["Duration"]),
33 | value=min(data["Duration"]),
34 | )
35 |
36 |
37 | # Slider update on vehicle type change
38 | @reactive.effect
39 | @reactive.event(input.vehicleType)
40 | def _():
41 | df = data
42 | ui.update_slider(
43 | "duration",
44 | min=min(df["Duration"]),
45 | max=max(df["Duration"]),
46 | value=min(df["Duration"]),
47 | )
48 |
49 |
50 | # Filtered table
51 | @render.data_frame
52 | def table():
53 | return data[
54 | (data["Duration"] >= input.duration())
55 | & data["Vehicle"].str.contains(str(input.vehicleType()))
56 | ]
57 |
--------------------------------------------------------------------------------
/solutions/part3_ex2_solution_app.py:
--------------------------------------------------------------------------------
1 | # PART 3 - Exercise 2 - Solution
2 | # //////////////////////////////
3 | from pathlib import Path
4 | from shiny.express import input, render, ui, app_opts, expressify
5 |
6 | # Set the www folder for static assets
7 | app_opts(static_assets=Path(__file__).parent / "www")
8 |
9 |
10 | # Needed for PART 2 only (not present in PART 1) ---
11 | @expressify
12 | def myTab(tab, image, text):
13 | with ui.nav_panel(tab):
14 | with ui.layout_columns(col_widths=[3, 9]):
15 | with ui.card():
16 | ui.img(src=image)
17 | with ui.card():
18 | ui.p(text)
19 |
20 |
21 | # ----
22 |
23 | with ui.navset_card_tab(id="tab"):
24 | # Solution for PART 1 ---
25 | # Tab 1
26 | with ui.nav_panel("YOUNG"):
27 | with ui.layout_columns(col_widths=[3, 9]):
28 | with ui.card():
29 | ui.img(src="young.jpg")
30 | with ui.card():
31 | ui.p("How it all began ...")
32 | # Tab 2
33 | with ui.nav_panel("ADULT"):
34 | with ui.layout_columns(col_widths=[3, 9]):
35 | with ui.card():
36 | ui.img(src="adult.jpg")
37 | with ui.card():
38 | ui.p("How it all began ...")
39 | # ---
40 |
41 | # PART 2 ...
42 | # Tab 3
43 | myTab("OLD", "old.jpg", "... what I have become")
44 | # ---
45 |
--------------------------------------------------------------------------------
/solutions/part4_ex1_solution_app.py:
--------------------------------------------------------------------------------
1 | # PART 4 - Exercise 1 - Solution
2 | # //////////////////////////////
3 |
4 | from shiny import App, ui, reactive, render, req
5 | import pandas as pd
6 | from datetime import datetime
7 |
8 | app_ui = ui.page_fluid(
9 | ui.card(
10 | ui.card_header("Create Task"),
11 | ui.input_text("task", "Description", width="auto"),
12 | ui.input_action_button("add", "Add task", width="150px"),
13 | ),
14 | ui.card(
15 | ui.card_header("ToDo list"),
16 | ui.output_data_frame("tbl"),
17 | ui.input_action_button(
18 | "completed", "Mark selected task as complete", width="300px"
19 | ),
20 | ),
21 | )
22 |
23 |
24 | def server(input, output, session):
25 | # Start with empty data frame
26 | todos = reactive.value(pd.DataFrame())
27 |
28 | # Render the todos in the table
29 | @render.data_frame
30 | def tbl():
31 | return render.DataTable(todos(), selection_mode="row", width="100%")
32 |
33 | # Add a new todo
34 | @reactive.effect
35 | @reactive.event(input.add)
36 | def _():
37 | req(input.task().strip())
38 | newTask = pd.DataFrame(
39 | {
40 | "created": [datetime.now().strftime("%Y-%m-%d %H:%M:%S")],
41 | "task": [input.task()],
42 | "completed": [None],
43 | }
44 | )
45 | todos.set(pd.concat([todos(), newTask], ignore_index=True))
46 | ui.update_text("task", value="")
47 |
48 | # Mark as completed based on selected row
49 | @reactive.effect
50 | @reactive.event(input.completed)
51 | def _():
52 | req(tbl.cell_selection()["rows"])
53 | updates = todos().copy()
54 | updates.at[tbl.cell_selection()["rows"][0], "completed"] = (
55 | datetime.now().strftime("%Y-%m-%d %H:%M:%S")
56 | )
57 | todos.set(updates)
58 |
59 |
60 | app = App(app_ui, server)
61 |
--------------------------------------------------------------------------------
/solutions/part4_ex2_solution_app.py:
--------------------------------------------------------------------------------
1 | # PART 4 - Exercise 2 - Solution
2 | # //////////////////////////////
3 |
4 | from shiny import App, ui, reactive, render, req
5 | from shiny import App, reactive, render, ui
6 | from shinywidgets import output_widget, render_widget
7 | import json
8 | import pandas as pd
9 | from pathlib import Path
10 | import plotly.express as px
11 | import plotly.graph_objects as go
12 |
13 | # --- PROCESSING DATA
14 |
15 | # Get the conference agenda
16 | # file = "PART_4_Plotly-DataTable\\exercise2\\agenda.json"
17 | file = Path(__file__).parent / "agenda.json"
18 | with open(file, "r") as file:
19 | data = list(json.load(file).values())
20 |
21 | # Function to convert JSON to text
22 | def format_dict_with_bullets(d, indent=0):
23 | result = []
24 | for key, value in d.items():
25 | # Create an indent for nested dictionaries
26 | space = " " * indent
27 | if isinstance(value, dict):
28 | # If the value is a dictionary, call the function recursively
29 | result.append(f"{space}- {key}:")
30 | result.append(format_dict_with_bullets(value, indent + 4))
31 | else:
32 | # Otherwise, format the key-value pair as a bullet point
33 | result.append(f"{space}- {key}: {value} ")
34 | return "\n".join(result)
35 |
36 | # Generate a data frame from the JSON file
37 | df = pd.DataFrame(
38 | [
39 | {
40 | "track": item["track"],
41 | "start": item["start_time"],
42 | "end": item["end_time"],
43 | "title": item["title"],
44 | "color": item["colour"],
45 | }
46 | for item in data
47 | ]
48 | )
49 |
50 | # Create dates and tracks
51 | df["start"] = pd.to_datetime(df["start"])
52 | df["track"] = df["start"].dt.day_name() + " - " + df["track"]
53 | df["end"] = pd.to_datetime(df["end"])
54 | df["end"] = df["end"].apply(lambda x: x.replace(day=1))
55 | df["start"] = df["start"].apply(lambda x: x.replace(day=1))
56 | df["start"] = df["start"].dt.tz_localize("UTC").dt.tz_convert("US/Eastern")
57 | df["end"] = df["end"].dt.tz_localize("UTC").dt.tz_convert("US/Eastern")
58 |
59 | # --- APP
60 |
61 | app_ui = ui.page_fluid(
62 | ui.panel_title("Shiny 2025 Conference Agenda"),
63 | output_widget("plt"),
64 | ui.output_ui("details")
65 | )
66 |
67 | def server(input, output, session):
68 |
69 | # Track which event has been clicked
70 | point_clicked = reactive.value()
71 |
72 | def click_data(trace, points, selector):
73 | point_clicked.set(points.point_inds[0])
74 |
75 | # Plotly timeline
76 | @render_widget
77 | def plt():
78 | # Create plotly timeline
79 | fig = px.timeline(
80 | df, x_start="start", x_end="end", y="track", hover_name="title"
81 | )
82 | # Format layout
83 | fig.update_xaxes(tickformat="%H:%M", type="date")
84 | fig.update_traces(marker=dict(color=df["color"]))
85 | fig.update_layout(plot_bgcolor="white", paper_bgcolor="white")
86 | # Create widget and add click function
87 | widget = go.FigureWidget(fig.data, fig.layout)
88 | widget.data[0].on_click(click_data)
89 | return widget
90 |
91 | # Metadata for selected event
92 | @render.ui
93 | def details():
94 | req(point_clicked() >= 0)
95 | return ui.h3(ui.HTML(format_dict_with_bullets(data[point_clicked()])))
96 |
97 |
98 | app = App(app_ui, server)
99 |
--------------------------------------------------------------------------------