├── .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 | ![screenshot](exercise1_screenshot.png) 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 | ![screenshot](exercise2_screenshot.png) 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 | ![screenshot](exercise3_screenshot.png) 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 | ![screenshot](exercise1_screenshot.png) 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 | ![screenshot](exercise2_screenshot.png) 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 | ![screenshot](exercise3_screenshot.png) 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 | ![screenshot](exercise1_screenshot.png) 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 | ![screenshot](exercise2_screenshot.png) 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 | ![screenshot](exercise1_screenshot.png) 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 | ![screenshot](exercise2_screenshot.png) 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 | [![shiny for python](assets/shiny-for-python.svg)](https://shiny.posit.co/py/) 15 | -------------------------------------------------------------------------------- /docs/assets/shiny-for-python.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 16 | 18 | 21 | 24 | 27 | 30 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 72 | 74 | 77 | 78 | 79 | 80 | 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 | --------------------------------------------------------------------------------