mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-11 07:58:11 -05:00
Compare commits
2049 Commits
v0.13.2
...
reactive_c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
550b679e61 | ||
|
|
8ada448c51 | ||
|
|
106ad74d2b | ||
|
|
0a6260259a | ||
|
|
78f9132eb3 | ||
|
|
15476ac32e | ||
|
|
17fb5b9eae | ||
|
|
fd27a0dfa2 | ||
|
|
5ffe69ec6c | ||
|
|
f5723b2a4d | ||
|
|
01491cc696 | ||
|
|
568a3f28cf | ||
|
|
02219df480 | ||
|
|
e006ca51ee | ||
|
|
86f651f3ec | ||
|
|
212b33a0ce | ||
|
|
6b7a121161 | ||
|
|
c89da718b1 | ||
|
|
eef3ae8387 | ||
|
|
0975a61725 | ||
|
|
0c53d54347 | ||
|
|
cbbb04cf69 | ||
|
|
120baf0a6e | ||
|
|
685dc7cc3a | ||
|
|
2fbb2ac77b | ||
|
|
2832db7aba | ||
|
|
18f2471d7c | ||
|
|
ea28f5a61b | ||
|
|
fe9cc6038e | ||
|
|
5ed335c499 | ||
|
|
fd04b97496 | ||
|
|
4c9d281b59 | ||
|
|
2ee06a7cbf | ||
|
|
cf2ba90b1d | ||
|
|
c69f34d1e2 | ||
|
|
ccfcc5d8b4 | ||
|
|
210c248264 | ||
|
|
e3258657d0 | ||
|
|
dbc518bf53 | ||
|
|
cdbdb4510e | ||
|
|
e7ec5e5ba4 | ||
|
|
03d8a7f296 | ||
|
|
480035c065 | ||
|
|
b32c18cf72 | ||
|
|
337a6b276a | ||
|
|
06cf1f9477 | ||
|
|
190cfd2b7a | ||
|
|
63035b4d66 | ||
|
|
6a11c8fcb1 | ||
|
|
33ffb006e3 | ||
|
|
162e7f63a9 | ||
|
|
bb581eeec4 | ||
|
|
272c555bc5 | ||
|
|
fb64caab23 | ||
|
|
6f2a74a46d | ||
|
|
ec65a74492 | ||
|
|
ba791c42fa | ||
|
|
5896667c36 | ||
|
|
003c949d38 | ||
|
|
d31394254c | ||
|
|
1a497e246c | ||
|
|
d24276aa54 | ||
|
|
6ed21a3e6b | ||
|
|
8066f9ce96 | ||
|
|
a0276ec1ce | ||
|
|
2ab925a24c | ||
|
|
78fbad7d8d | ||
|
|
89be4bdce9 | ||
|
|
d09a064471 | ||
|
|
2b18ca5a6c | ||
|
|
6bc2f18bbf | ||
|
|
fbb892d84e | ||
|
|
4efb7c20e4 | ||
|
|
4beb1f07a6 | ||
|
|
45e640e5f9 | ||
|
|
e84beffee3 | ||
|
|
e07c7483a7 | ||
|
|
34ec7bf5eb | ||
|
|
01b20a4829 | ||
|
|
45ea898da4 | ||
|
|
fd34c5070f | ||
|
|
6c409d96c1 | ||
|
|
0cbe4bb3d4 | ||
|
|
d04a990235 | ||
|
|
4747c87632 | ||
|
|
f57452c7bf | ||
|
|
9a8e2eb675 | ||
|
|
8ef7f3cbe2 | ||
|
|
de30a65f01 | ||
|
|
0bcf613195 | ||
|
|
89fd9004d0 | ||
|
|
b2be108db1 | ||
|
|
6102c44b70 | ||
|
|
327cdc8e41 | ||
|
|
0bc3613989 | ||
|
|
30cea871f9 | ||
|
|
5f332fe4db | ||
|
|
7ee7f2716b | ||
|
|
5e8c39cb1e | ||
|
|
ee355200b3 | ||
|
|
986fbe2254 | ||
|
|
32f93a2be1 | ||
|
|
ab79065c13 | ||
|
|
77171b7894 | ||
|
|
cce8ddb84f | ||
|
|
648b7e5911 | ||
|
|
67a66fdc93 | ||
|
|
5fbaa26d05 | ||
|
|
1f4a3c4fd2 | ||
|
|
959dc7ffd4 | ||
|
|
0e34221cac | ||
|
|
0cad13b3a3 | ||
|
|
0776f71ca3 | ||
|
|
5a74e369ce | ||
|
|
799c5ac662 | ||
|
|
1080cf0ef4 | ||
|
|
867d49e3fb | ||
|
|
c7be406099 | ||
|
|
37257e77ce | ||
|
|
270d9ff0fc | ||
|
|
34b48598d9 | ||
|
|
5105ecb148 | ||
|
|
f47b151458 | ||
|
|
d3f15a58fc | ||
|
|
42f6adb7fa | ||
|
|
263f8a8e7d | ||
|
|
3a42d30cfd | ||
|
|
9275217a5a | ||
|
|
1fed19ad68 | ||
|
|
6a8a78abd1 | ||
|
|
de69f51084 | ||
|
|
c81a3f39fd | ||
|
|
6fcb925e34 | ||
|
|
8823b7280a | ||
|
|
ebadad97a8 | ||
|
|
a095c39626 | ||
|
|
fb9bcb44c3 | ||
|
|
38f593450a | ||
|
|
6d44f2c5cb | ||
|
|
d1786a64c4 | ||
|
|
616ae99c0b | ||
|
|
4d2ff80788 | ||
|
|
005295fd4c | ||
|
|
d6b46f8243 | ||
|
|
bac35e8f1b | ||
|
|
a003c4da85 | ||
|
|
0ae8e4fe8a | ||
|
|
d3667dfc77 | ||
|
|
54c5467dc6 | ||
|
|
d01f0300a5 | ||
|
|
bff207008f | ||
|
|
ed739f95ff | ||
|
|
bb4de1336c | ||
|
|
f7205558d2 | ||
|
|
1318544ecf | ||
|
|
a81c161434 | ||
|
|
73acdc755f | ||
|
|
dd84ea8fda | ||
|
|
a2a4e40821 | ||
|
|
509f54d68c | ||
|
|
27ce460ea4 | ||
|
|
116794ad77 | ||
|
|
89feba870d | ||
|
|
2a980601c0 | ||
|
|
e1fd8ae910 | ||
|
|
9cb415008c | ||
|
|
26ba9bf94a | ||
|
|
fb091ca195 | ||
|
|
99a7dca3ce | ||
|
|
a1a03d94be | ||
|
|
85a2d41a72 | ||
|
|
89bd7e9011 | ||
|
|
ececdf42a7 | ||
|
|
2cf03de8b8 | ||
|
|
c8daa1730b | ||
|
|
d195b595dd | ||
|
|
ff3f7adff2 | ||
|
|
37781a9df7 | ||
|
|
ca1c60e00e | ||
|
|
649f382291 | ||
|
|
103a35c81b | ||
|
|
5af341bfdb | ||
|
|
7c7110cd83 | ||
|
|
c4ea489bff | ||
|
|
60b3b6ff03 | ||
|
|
1510dca065 | ||
|
|
2c49375928 | ||
|
|
f9fc22c48b | ||
|
|
8d14e7ab04 | ||
|
|
8f2a28a1f2 | ||
|
|
e8fb1faec0 | ||
|
|
0e4874c412 | ||
|
|
933630af28 | ||
|
|
ff87098102 | ||
|
|
6513a86bbd | ||
|
|
97e296c5d5 | ||
|
|
9f87adf4e8 | ||
|
|
6470b3f08c | ||
|
|
d1ba84525e | ||
|
|
05ad66c464 | ||
|
|
c41d38bf61 | ||
|
|
b155e8480b | ||
|
|
e94f687573 | ||
|
|
5883082d01 | ||
|
|
75b53ffda1 | ||
|
|
a8057b96f3 | ||
|
|
a89e809498 | ||
|
|
02f7a4fdc9 | ||
|
|
7c7c22a597 | ||
|
|
860fa525a2 | ||
|
|
9f0e38a28a | ||
|
|
f834b7befb | ||
|
|
7f3a45fb5b | ||
|
|
b0953e810b | ||
|
|
52a86012e5 | ||
|
|
2a06fe6baf | ||
|
|
6e688d2175 | ||
|
|
b610fd1f56 | ||
|
|
a4730096f4 | ||
|
|
6a02439944 | ||
|
|
b889b0d2b0 | ||
|
|
ba5733e4a4 | ||
|
|
2e0221ecfd | ||
|
|
aeded79544 | ||
|
|
c0a7958e77 | ||
|
|
431b194ec2 | ||
|
|
29d24d7e08 | ||
|
|
3b04c642ae | ||
|
|
609fc5b0c0 | ||
|
|
2a8c79b577 | ||
|
|
043316e40f | ||
|
|
c9d8b987d4 | ||
|
|
33c5a5c665 | ||
|
|
29c90ba163 | ||
|
|
8c19450b10 | ||
|
|
89c97458c4 | ||
|
|
02be516902 | ||
|
|
47ada300ea | ||
|
|
6f9c621774 | ||
|
|
324d9195c3 | ||
|
|
0310fe3b68 | ||
|
|
7144a6e4b7 | ||
|
|
1c8071a96f | ||
|
|
4ad115e024 | ||
|
|
f11d754cfe | ||
|
|
65019ce96f | ||
|
|
90e8fb2a57 | ||
|
|
ff5377da9e | ||
|
|
7aee84eb05 | ||
|
|
c0a7a6d0d6 | ||
|
|
29c48471f2 | ||
|
|
229e56464b | ||
|
|
769c32fd38 | ||
|
|
d05b89cfb3 | ||
|
|
f1f18a2334 | ||
|
|
afc556f801 | ||
|
|
7f240839fc | ||
|
|
8d0a6274cb | ||
|
|
91cab10ff8 | ||
|
|
5828ea7426 | ||
|
|
80ba147168 | ||
|
|
f85479ba11 | ||
|
|
a23c5f151f | ||
|
|
cab3601474 | ||
|
|
cf330fcd58 | ||
|
|
eb0162dccf | ||
|
|
a415aed7e6 | ||
|
|
9f6014dc0b | ||
|
|
21b0d38b57 | ||
|
|
1ec7f22b5f | ||
|
|
346c5e4a4c | ||
|
|
c9a0f0a713 | ||
|
|
8bbc38dc8a | ||
|
|
96494a22f9 | ||
|
|
0813789e2a | ||
|
|
98ca820ab1 | ||
|
|
81ca9d9f29 | ||
|
|
16fe0019f9 | ||
|
|
5fa650ab75 | ||
|
|
564c2a0f16 | ||
|
|
1685e1c310 | ||
|
|
332f5a1266 | ||
|
|
99ac85f06a | ||
|
|
fc30ad0935 | ||
|
|
aadf2eb609 | ||
|
|
68f778e423 | ||
|
|
0066cff652 | ||
|
|
f872a0c80a | ||
|
|
68d67a8194 | ||
|
|
756ac1514c | ||
|
|
d9478142b1 | ||
|
|
5eced59961 | ||
|
|
3e1862cd51 | ||
|
|
7271609850 | ||
|
|
f24337bb3b | ||
|
|
6167247ea2 | ||
|
|
0332e52501 | ||
|
|
0c23f78ab7 | ||
|
|
7624449644 | ||
|
|
97309e8c4c | ||
|
|
a1e78214db | ||
|
|
1a57b3296b | ||
|
|
7c10fc3514 | ||
|
|
494ef42aa8 | ||
|
|
8a54d216c6 | ||
|
|
896a20d76d | ||
|
|
a26510b02f | ||
|
|
1465f1d237 | ||
|
|
21b18d107a | ||
|
|
cc2173c587 | ||
|
|
71fe821ae9 | ||
|
|
3ffab69ad6 | ||
|
|
58a662bd35 | ||
|
|
eb55c256c7 | ||
|
|
b07e553b9e | ||
|
|
2d61709de3 | ||
|
|
1352e1d92d | ||
|
|
b595c3b902 | ||
|
|
76efb01c4c | ||
|
|
0078945b79 | ||
|
|
70d8ef0b8e | ||
|
|
9a1f7cba68 | ||
|
|
39e14acffe | ||
|
|
a6149390a0 | ||
|
|
33cdc75810 | ||
|
|
13f229089d | ||
|
|
2dbb0fca85 | ||
|
|
c7a8a4e30f | ||
|
|
dc6f1a0c10 | ||
|
|
178872d651 | ||
|
|
e3c15493a2 | ||
|
|
3f22e5da2d | ||
|
|
39ee4513c6 | ||
|
|
598898f0a1 | ||
|
|
052e783638 | ||
|
|
d2deda238a | ||
|
|
7317a8304f | ||
|
|
5ea9d70fb4 | ||
|
|
a73e0998bc | ||
|
|
51befe3e27 | ||
|
|
37569a291b | ||
|
|
7fe973145d | ||
|
|
da3fc276fd | ||
|
|
4c0af8b1c0 | ||
|
|
f65f7b2f1b | ||
|
|
33c86ed6a7 | ||
|
|
545b6c1247 | ||
|
|
1b0e37f371 | ||
|
|
97e00721e9 | ||
|
|
3c43301edb | ||
|
|
51cbb67a96 | ||
|
|
2e2bd80416 | ||
|
|
86389ff7a3 | ||
|
|
c2dfea18c4 | ||
|
|
4be6bbc681 | ||
|
|
cfc0ff9cc7 | ||
|
|
b4c6ba6962 | ||
|
|
dc3ed2f79b | ||
|
|
5d95c7a9cb | ||
|
|
6821ca6238 | ||
|
|
167dc0a259 | ||
|
|
fa9fa68693 | ||
|
|
353615da89 | ||
|
|
51de558675 | ||
|
|
174fc1dda1 | ||
|
|
7caeb60c47 | ||
|
|
e3aba1b5ff | ||
|
|
1a8b36f06d | ||
|
|
250303790c | ||
|
|
e20544659a | ||
|
|
ad7692ed34 | ||
|
|
f9144a4be3 | ||
|
|
0fc3b90efb | ||
|
|
25ccc8a77a | ||
|
|
da18390f3e | ||
|
|
b392bf8298 | ||
|
|
73c42ebeaf | ||
|
|
eb45f7fcba | ||
|
|
5fdca29448 | ||
|
|
048c4006e4 | ||
|
|
6d10a2dafb | ||
|
|
d6c421f8de | ||
|
|
ac4adcc62c | ||
|
|
bc8465d284 | ||
|
|
7fc497eeb8 | ||
|
|
633817e3d5 | ||
|
|
09dee9670a | ||
|
|
631debbec4 | ||
|
|
4e57bc2161 | ||
|
|
a111e36867 | ||
|
|
152bd5841c | ||
|
|
ecefdcd951 | ||
|
|
a976cfa98d | ||
|
|
df70d7708d | ||
|
|
c558d95e3b | ||
|
|
3bd4825d4b | ||
|
|
67cdcedd4e | ||
|
|
384116a76f | ||
|
|
12e91ae643 | ||
|
|
7c56d277da | ||
|
|
579a4592b8 | ||
|
|
9dad5e6362 | ||
|
|
f51b5421f2 | ||
|
|
387907ea32 | ||
|
|
b5ca1d48e0 | ||
|
|
396f170738 | ||
|
|
5514039d42 | ||
|
|
c90c4f3673 | ||
|
|
41758858cf | ||
|
|
3ff507e6b8 | ||
|
|
5199371025 | ||
|
|
26ad773f77 | ||
|
|
9e133e0ecc | ||
|
|
c4e7099229 | ||
|
|
56062628f2 | ||
|
|
48a3a1dabb | ||
|
|
ca3c2b3e26 | ||
|
|
d35c5c8320 | ||
|
|
749a582296 | ||
|
|
6310406430 | ||
|
|
d26d339f97 | ||
|
|
17afce6fa1 | ||
|
|
d3aa601798 | ||
|
|
8f24d667d6 | ||
|
|
5cd4588ef2 | ||
|
|
b0a1821d95 | ||
|
|
6b835f70e6 | ||
|
|
308bc76ac6 | ||
|
|
fd843509a1 | ||
|
|
7691cfdadb | ||
|
|
1aa9368e54 | ||
|
|
180e852fee | ||
|
|
547edd7e32 | ||
|
|
0b46c63c31 | ||
|
|
9b69ce1988 | ||
|
|
57cc44f662 | ||
|
|
4eaa9c7ea9 | ||
|
|
0b6cdcc826 | ||
|
|
7bc0a0ca39 | ||
|
|
1ef2074a10 | ||
|
|
0747b2a72a | ||
|
|
64b3095f2c | ||
|
|
ab82af122f | ||
|
|
54fccf2e7c | ||
|
|
05e953db3a | ||
|
|
f726835850 | ||
|
|
38d2809131 | ||
|
|
d7718991a6 | ||
|
|
32c2bff6eb | ||
|
|
555ede03ed | ||
|
|
2a6f218700 | ||
|
|
b087c19b52 | ||
|
|
6fed1c60ac | ||
|
|
b10f2a5291 | ||
|
|
a4a49a354e | ||
|
|
ead23528ca | ||
|
|
b8644949cc | ||
|
|
b88e3a64f2 | ||
|
|
2871b423fd | ||
|
|
562fafbc39 | ||
|
|
191e0874f8 | ||
|
|
fa5ff7bfa5 | ||
|
|
82e80ccdeb | ||
|
|
ff84cf5a18 | ||
|
|
44843a7768 | ||
|
|
68eeb338da | ||
|
|
ea54c17902 | ||
|
|
d5ad7eed40 | ||
|
|
c2430cd3f4 | ||
|
|
8a0731493f | ||
|
|
07e2b80b5d | ||
|
|
1311e1fca2 | ||
|
|
e6c2133520 | ||
|
|
3d6f734ff2 | ||
|
|
e0eaa58779 | ||
|
|
ced6622b25 | ||
|
|
2d2cf96f5e | ||
|
|
370f1b51ee | ||
|
|
67d3a504ae | ||
|
|
34ee48ef93 | ||
|
|
c61a585e79 | ||
|
|
09388c9f07 | ||
|
|
b1bc78dad3 | ||
|
|
a5a0f23c3a | ||
|
|
4c50c064d3 | ||
|
|
a63f271300 | ||
|
|
08b22ff550 | ||
|
|
b04133bf65 | ||
|
|
3602358d2c | ||
|
|
67b0416eba | ||
|
|
f8d69ecb1f | ||
|
|
5e8bc204c1 | ||
|
|
938332d646 | ||
|
|
386078d441 | ||
|
|
4d778faaf4 | ||
|
|
3055cf5602 | ||
|
|
36373ba28b | ||
|
|
1415b57181 | ||
|
|
65d4a4e906 | ||
|
|
0abe221227 | ||
|
|
1b8d822226 | ||
|
|
bc8fbd60d7 | ||
|
|
4c332eac9a | ||
|
|
f5392d77dc | ||
|
|
1e88990a0b | ||
|
|
de4c7567d0 | ||
|
|
aff33dd023 | ||
|
|
a287ebe324 | ||
|
|
583a8d1001 | ||
|
|
36a808add0 | ||
|
|
f651d4a274 | ||
|
|
f6e8e645f2 | ||
|
|
b4d2f88b74 | ||
|
|
c524a736bd | ||
|
|
cdf3bf18f0 | ||
|
|
b21bdacb4f | ||
|
|
92019b5ba3 | ||
|
|
908d635063 | ||
|
|
20329feb7f | ||
|
|
4cd92a1cd9 | ||
|
|
8ca3397c5d | ||
|
|
05cd79481e | ||
|
|
c0f1905785 | ||
|
|
9afc06028d | ||
|
|
7b6cc50238 | ||
|
|
722b1d0258 | ||
|
|
93d3b78ac1 | ||
|
|
69e82f6e0e | ||
|
|
1f83a6db7b | ||
|
|
8f37951e14 | ||
|
|
e1f4d43926 | ||
|
|
eb6139276f | ||
|
|
f18c426151 | ||
|
|
e46debb6d1 | ||
|
|
d8b8739cb8 | ||
|
|
9fd8eefa59 | ||
|
|
fd2af06a53 | ||
|
|
48f945ba7f | ||
|
|
6d59f88a76 | ||
|
|
8b94d4626d | ||
|
|
d7d8e78e42 | ||
|
|
9755f86f53 | ||
|
|
599a3ee82f | ||
|
|
c790346490 | ||
|
|
68cf3a9111 | ||
|
|
59221dfcf2 | ||
|
|
020413a206 | ||
|
|
a343e9ebdf | ||
|
|
c304efee36 | ||
|
|
95173f676d | ||
|
|
87d1db1f2b | ||
|
|
d445f384c7 | ||
|
|
59dd4b0721 | ||
|
|
d73c91d4a7 | ||
|
|
665a66522e | ||
|
|
ba1efa65fa | ||
|
|
64a74692b9 | ||
|
|
46cd285dd0 | ||
|
|
bcac115c3d | ||
|
|
77ddb2c8c2 | ||
|
|
8ae31eb998 | ||
|
|
7551a6ae1d | ||
|
|
93be659b1b | ||
|
|
3327878fc2 | ||
|
|
0b25c7f3c1 | ||
|
|
b606ba4dd7 | ||
|
|
0269bc810c | ||
|
|
f2775f2c1d | ||
|
|
f06274aec6 | ||
|
|
dfa686a3e0 | ||
|
|
fe679b5de5 | ||
|
|
aa1eb0410c | ||
|
|
1b06bab7ee | ||
|
|
0f13056aa2 | ||
|
|
beecf60db7 | ||
|
|
160a2013bc | ||
|
|
b8c636e87e | ||
|
|
add40e5926 | ||
|
|
960e7f3b24 | ||
|
|
3e749f36e8 | ||
|
|
8198d99309 | ||
|
|
81de1c8ed4 | ||
|
|
3eb55e9d9b | ||
|
|
6b6ac86aea | ||
|
|
1b45e70cbb | ||
|
|
929f7ec235 | ||
|
|
cf28d7e470 | ||
|
|
b0a00108f3 | ||
|
|
01151fc7f8 | ||
|
|
bf8dbc38c7 | ||
|
|
ae0d4d9353 | ||
|
|
43ec4ae238 | ||
|
|
c568a8cabe | ||
|
|
423bdd8b6b | ||
|
|
1e19ff65e6 | ||
|
|
a9cf632f53 | ||
|
|
fddf94a341 | ||
|
|
203168d261 | ||
|
|
0e3c3536f8 | ||
|
|
45b2b7e24f | ||
|
|
88f177b065 | ||
|
|
ea7a8dd3ad | ||
|
|
dda8f92494 | ||
|
|
26211802cd | ||
|
|
b4bef0d32c | ||
|
|
a8bf203067 | ||
|
|
624dd2e99d | ||
|
|
26a136a6e8 | ||
|
|
2d57ffa546 | ||
|
|
428b81a6d9 | ||
|
|
f24c12fdfb | ||
|
|
9a345d191b | ||
|
|
fec706d134 | ||
|
|
c338448997 | ||
|
|
956c1cb1a7 | ||
|
|
8831b4da9e | ||
|
|
f8bd60dcd7 | ||
|
|
6a373b585c | ||
|
|
54480e2510 | ||
|
|
83f73603db | ||
|
|
2b10f192ba | ||
|
|
775d5289cb | ||
|
|
e6c66352a7 | ||
|
|
77afd73ee1 | ||
|
|
5ac96a40aa | ||
|
|
2fea0e2598 | ||
|
|
2b64949cbe | ||
|
|
918d57f25e | ||
|
|
5e2b40d3a9 | ||
|
|
979ef4bd43 | ||
|
|
914baf594b | ||
|
|
02b0802886 | ||
|
|
0725239397 | ||
|
|
d72e8a06a7 | ||
|
|
cf79fec720 | ||
|
|
31dda45d1c | ||
|
|
9836b72661 | ||
|
|
6ede0194c6 | ||
|
|
5ec38581ca | ||
|
|
2629e59ace | ||
|
|
f3eb770e20 | ||
|
|
0683b79fac | ||
|
|
fcd09e2bae | ||
|
|
b25cb0f2d5 | ||
|
|
0704aec01b | ||
|
|
d38b939c63 | ||
|
|
112466de1e | ||
|
|
1d0edd2ad0 | ||
|
|
37736119be | ||
|
|
c5df150acb | ||
|
|
49a346334b | ||
|
|
e7c4656e8f | ||
|
|
85bed0582a | ||
|
|
b9e6f867c6 | ||
|
|
a5b80168bd | ||
|
|
3cea5fb2d0 | ||
|
|
c89d782048 | ||
|
|
1fd4179e07 | ||
|
|
3b62400298 | ||
|
|
ba0fe938a1 | ||
|
|
d4560171a8 | ||
|
|
9963ba6cf5 | ||
|
|
f5a23826c8 | ||
|
|
21ff005c1a | ||
|
|
206b9135f1 | ||
|
|
5449de1a67 | ||
|
|
47c61756e6 | ||
|
|
ef63679ff0 | ||
|
|
ef7e1c385a | ||
|
|
3a0a6cdbbb | ||
|
|
340df3e956 | ||
|
|
eeb264da8e | ||
|
|
00f08b8ec6 | ||
|
|
67ae2a39ba | ||
|
|
72dda25835 | ||
|
|
8c9ce1994a | ||
|
|
606b05fdaf | ||
|
|
420ba9549f | ||
|
|
51fbb5cfac | ||
|
|
ca2c2b60f2 | ||
|
|
d6064636d4 | ||
|
|
9646c9b0a0 | ||
|
|
f28900f8ca | ||
|
|
e0c15c42d7 | ||
|
|
7177618c25 | ||
|
|
3bdd4af75c | ||
|
|
98d4b5e487 | ||
|
|
8b5639bfdb | ||
|
|
1c70b8b1bf | ||
|
|
b5a7e03879 | ||
|
|
a6dade846e | ||
|
|
32913f9d95 | ||
|
|
cbabf9a2a3 | ||
|
|
03e92c3336 | ||
|
|
997c39fdc0 | ||
|
|
bba2d1ee18 | ||
|
|
a60301810f | ||
|
|
6b261f76b1 | ||
|
|
3db5f21d90 | ||
|
|
121bfcb984 | ||
|
|
265de66946 | ||
|
|
79c5c9f95e | ||
|
|
3354a47e8a | ||
|
|
a1e1416d7a | ||
|
|
24b7a9907f | ||
|
|
214abd0cd4 | ||
|
|
0bb53e8ca5 | ||
|
|
ec12caaeba | ||
|
|
5bbf2aa57a | ||
|
|
84ad9997da | ||
|
|
9f6ce87443 | ||
|
|
1ff6c382bf | ||
|
|
c366c10ae1 | ||
|
|
950df1e25c | ||
|
|
909bfa8c14 | ||
|
|
598b48d078 | ||
|
|
4c7b7f236a | ||
|
|
896c5b41cb | ||
|
|
205c35d5e5 | ||
|
|
bf0dd7d725 | ||
|
|
ba2b811172 | ||
|
|
be347c3ed4 | ||
|
|
c01abdb6a9 | ||
|
|
95a5a965a5 | ||
|
|
fc2849a8ff | ||
|
|
fcc900f3e0 | ||
|
|
9d0bcd5637 | ||
|
|
6ebbad5273 | ||
|
|
930459899a | ||
|
|
fe730e2d76 | ||
|
|
e58b2e9a47 | ||
|
|
719dbab0c2 | ||
|
|
86ea023e2e | ||
|
|
bc0fb3f44c | ||
|
|
6d37f6b4dd | ||
|
|
958ab85297 | ||
|
|
facef1d23c | ||
|
|
cdb446375c | ||
|
|
6f7b2887aa | ||
|
|
bc8ae063dd | ||
|
|
a23f973433 | ||
|
|
c124256bad | ||
|
|
f1b035bcca | ||
|
|
81cc7c591e | ||
|
|
a0ca560c3b | ||
|
|
d1f20a9c73 | ||
|
|
013059c5b9 | ||
|
|
fe6ad235ac | ||
|
|
67af26ffe6 | ||
|
|
0fce9de04f | ||
|
|
a8b8df21d6 | ||
|
|
ab2e304f02 | ||
|
|
574f2c53d4 | ||
|
|
bc85d812d2 | ||
|
|
364990a29f | ||
|
|
9ac9e36873 | ||
|
|
6745e09688 | ||
|
|
e758927c84 | ||
|
|
90fbf7d50f | ||
|
|
75f1ee0082 | ||
|
|
750aaf451a | ||
|
|
b44bfe9109 | ||
|
|
aa392f8563 | ||
|
|
ac7228f6c4 | ||
|
|
dcb12addaa | ||
|
|
ad398b5f8a | ||
|
|
803cb4806e | ||
|
|
1a468bbb61 | ||
|
|
c332c051f3 | ||
|
|
db48befcb7 | ||
|
|
b02edb05ac | ||
|
|
d7009fd1c8 | ||
|
|
ce3755676c | ||
|
|
db3c1b728d | ||
|
|
1761de4740 | ||
|
|
09d496925b | ||
|
|
3af5327f1c | ||
|
|
06cb14d7ec | ||
|
|
7be1a9d7fa | ||
|
|
95243fb35c | ||
|
|
26438a3979 | ||
|
|
28db097a71 | ||
|
|
76fdd8ae04 | ||
|
|
003dc39d76 | ||
|
|
3a73bfb142 | ||
|
|
a24bdabf08 | ||
|
|
8815f293a2 | ||
|
|
0b04c28011 | ||
|
|
9af2775539 | ||
|
|
ae5deae6e9 | ||
|
|
61c2126498 | ||
|
|
881fe0cfce | ||
|
|
a999bf389c | ||
|
|
ff3b97b630 | ||
|
|
639b520d39 | ||
|
|
31854ad9e8 | ||
|
|
4304e92f0d | ||
|
|
44736cefbf | ||
|
|
a807449f28 | ||
|
|
19dc29ea17 | ||
|
|
ae9d38b59c | ||
|
|
97bebae8d7 | ||
|
|
cf534ce6da | ||
|
|
f25f691a55 | ||
|
|
cbebf8be7b | ||
|
|
165ce26b2f | ||
|
|
05e50c1b98 | ||
|
|
e11004da7b | ||
|
|
97ee7b5d96 | ||
|
|
6c6e2573aa | ||
|
|
8992827f21 | ||
|
|
572c863bff | ||
|
|
d3c85d67b8 | ||
|
|
ff3434f77e | ||
|
|
762528c044 | ||
|
|
1891af0d4a | ||
|
|
583ad036f7 | ||
|
|
893b9c1b38 | ||
|
|
ac92bf98d4 | ||
|
|
fd90ff7ff7 | ||
|
|
d06dbbe5db | ||
|
|
bffc4995d7 | ||
|
|
4b8b406bed | ||
|
|
5641153272 | ||
|
|
08c6c7781f | ||
|
|
ad2ad391a7 | ||
|
|
caac88be0d | ||
|
|
10660aa373 | ||
|
|
cfaf97aee4 | ||
|
|
55f14576f0 | ||
|
|
4dca94ac99 | ||
|
|
14779d3d27 | ||
|
|
66d1e710b5 | ||
|
|
12ae3c17e9 | ||
|
|
36e4da0709 | ||
|
|
91631cb081 | ||
|
|
224f082e1f | ||
|
|
76b239a6ea | ||
|
|
cb476b510d | ||
|
|
334f233968 | ||
|
|
e1f21250b9 | ||
|
|
8d087e4f20 | ||
|
|
9e35e8c947 | ||
|
|
f98faef024 | ||
|
|
0f9346ead5 | ||
|
|
fc8118c694 | ||
|
|
6f8166ca0f | ||
|
|
026b7278c1 | ||
|
|
375a7e7e5c | ||
|
|
64db035d77 | ||
|
|
cb051e4254 | ||
|
|
20e9c2901d | ||
|
|
ce4b391495 | ||
|
|
7d932f5b18 | ||
|
|
56c8c08e08 | ||
|
|
13ef25c0b5 | ||
|
|
7a1aecb1a4 | ||
|
|
b3690e8680 | ||
|
|
97d490cfb4 | ||
|
|
2081dda6fc | ||
|
|
ea912fc50c | ||
|
|
b655fdf68f | ||
|
|
4749f46a4f | ||
|
|
f95bb9c82d | ||
|
|
6529529cdb | ||
|
|
3a2a3f21d4 | ||
|
|
631bc1c481 | ||
|
|
6abfa5bf80 | ||
|
|
20ae8e4f8b | ||
|
|
f595c5d504 | ||
|
|
972779253c | ||
|
|
9179a241e9 | ||
|
|
85e7e89ad9 | ||
|
|
9f5bc00c89 | ||
|
|
0ab842e3c5 | ||
|
|
3a0a3e49dc | ||
|
|
438b1c043e | ||
|
|
6d13b65e7c | ||
|
|
423d41ee0e | ||
|
|
1b61d9bc51 | ||
|
|
bf0c3d42db | ||
|
|
5394a68314 | ||
|
|
597af36759 | ||
|
|
691062f687 | ||
|
|
6651c4ea48 | ||
|
|
116559e5a0 | ||
|
|
7818e8ed64 | ||
|
|
b0063399bb | ||
|
|
724c6b7656 | ||
|
|
0530cbcd0f | ||
|
|
6e2bba1513 | ||
|
|
89ac5d7c42 | ||
|
|
dd68722b66 | ||
|
|
933d5db2ab | ||
|
|
0386ed6409 | ||
|
|
d3c14bf416 | ||
|
|
2a224ce9fb | ||
|
|
78322525b7 | ||
|
|
5b7c9c205e | ||
|
|
07ac70a460 | ||
|
|
3629f806a2 | ||
|
|
72fc43c738 | ||
|
|
2880391620 | ||
|
|
df38f0be3f | ||
|
|
808684c2a8 | ||
|
|
69ed3a7751 | ||
|
|
68556caa9a | ||
|
|
bb8ea8053b | ||
|
|
6f01e6edf1 | ||
|
|
66a74d16ff | ||
|
|
0e525f5aeb | ||
|
|
f742605a1b | ||
|
|
86007c466d | ||
|
|
7b39b79183 | ||
|
|
7f453aa6f6 | ||
|
|
f36052ffeb | ||
|
|
d35db11f43 | ||
|
|
173e5d3f97 | ||
|
|
bcebf737c3 | ||
|
|
2afff67e89 | ||
|
|
fe7bd53250 | ||
|
|
6df3509869 | ||
|
|
062dc771aa | ||
|
|
5280b72b85 | ||
|
|
a4dfe7138e | ||
|
|
b9960bad1a | ||
|
|
e1d7805396 | ||
|
|
ce6f993f0e | ||
|
|
aa1d94e6c9 | ||
|
|
00a6092836 | ||
|
|
f6372faa23 | ||
|
|
1a5e266d26 | ||
|
|
2e4a107201 | ||
|
|
d4688db31c | ||
|
|
c49a289619 | ||
|
|
2559496ded | ||
|
|
d3aa82fc5d | ||
|
|
704605918d | ||
|
|
7e8116888b | ||
|
|
e0f4bbd20d | ||
|
|
5ae2d5a24b | ||
|
|
8648737a7a | ||
|
|
6e090d5112 | ||
|
|
2207e561f2 | ||
|
|
b9cd5b572b | ||
|
|
344c6f3ee7 | ||
|
|
f6f2c0ed56 | ||
|
|
ec7a66a966 | ||
|
|
23ca428a01 | ||
|
|
eb9f251e34 | ||
|
|
9c3a0c86ca | ||
|
|
394d875eb4 | ||
|
|
4cc6403867 | ||
|
|
9d5fa773f3 | ||
|
|
075ca49a1f | ||
|
|
9564f1d871 | ||
|
|
cf546a47b6 | ||
|
|
d3a4f35170 | ||
|
|
f450aea449 | ||
|
|
aed308b259 | ||
|
|
714dffc943 | ||
|
|
f8a173efbd | ||
|
|
70e7822dd1 | ||
|
|
01b24e984c | ||
|
|
9dd4302fe9 | ||
|
|
c2f03aa833 | ||
|
|
2260459422 | ||
|
|
452631550a | ||
|
|
a14266b452 | ||
|
|
e838cc3fe9 | ||
|
|
74457b95e9 | ||
|
|
ceb19c7573 | ||
|
|
d5754515a6 | ||
|
|
4ed13c04f5 | ||
|
|
5a5294cc44 | ||
|
|
3a5d48ae7c | ||
|
|
6b605804d2 | ||
|
|
ffe883ab72 | ||
|
|
31c4e0fdfe | ||
|
|
66f970e0bd | ||
|
|
07b223dcb0 | ||
|
|
f1e27b6ffb | ||
|
|
389463aea5 | ||
|
|
b11ab9a31c | ||
|
|
5fe85b07b7 | ||
|
|
3c7b1e7d21 | ||
|
|
c556cf1e69 | ||
|
|
722e5fb5f7 | ||
|
|
e90cc591b7 | ||
|
|
7336d327b3 | ||
|
|
c555725201 | ||
|
|
c9c5225a6a | ||
|
|
e1060bf537 | ||
|
|
392e42a55d | ||
|
|
b974e41148 | ||
|
|
aa3e2a0b64 | ||
|
|
3df89dd9a3 | ||
|
|
cef1f3c7ee | ||
|
|
e5d1fa1ea4 | ||
|
|
3ccf2937b4 | ||
|
|
b7b696630f | ||
|
|
84aba546bc | ||
|
|
6ef751422a | ||
|
|
05d49ee45e | ||
|
|
3e4783c454 | ||
|
|
ce93201843 | ||
|
|
f9fc3a46b5 | ||
|
|
0467d6666a | ||
|
|
1f26b076a3 | ||
|
|
7944f21925 | ||
|
|
e91eda8eca | ||
|
|
d8ac84a5da | ||
|
|
3098a02b72 | ||
|
|
741236df56 | ||
|
|
e3584f0a61 | ||
|
|
432482c5a7 | ||
|
|
323ad46bba | ||
|
|
ace0fe1802 | ||
|
|
36f244fece | ||
|
|
99e5ef99ec | ||
|
|
d6d3ed5bbc | ||
|
|
49d09ecf30 | ||
|
|
c529a03096 | ||
|
|
101d9aa0fa | ||
|
|
b4864e1180 | ||
|
|
cba7304ab9 | ||
|
|
2d058b0519 | ||
|
|
eed9231884 | ||
|
|
5c84eaf2a5 | ||
|
|
2ef7226be0 | ||
|
|
e5d1c61cdf | ||
|
|
e635055ab8 | ||
|
|
d8d4e3b262 | ||
|
|
8f29543479 | ||
|
|
c11a8ea24b | ||
|
|
86646d7faa | ||
|
|
6e44915e08 | ||
|
|
f8b99cf4e9 | ||
|
|
0e7d6ff192 | ||
|
|
66501dac97 | ||
|
|
195907b2ec | ||
|
|
be11b44864 | ||
|
|
bc7cd21c13 | ||
|
|
0555cbdd28 | ||
|
|
97498451bb | ||
|
|
2e0d9b5475 | ||
|
|
62395f3103 | ||
|
|
6b31cd6aee | ||
|
|
e67a8ba369 | ||
|
|
133d301925 | ||
|
|
17c40a5d1d | ||
|
|
042211e5f6 | ||
|
|
d12830d700 | ||
|
|
b411c70280 | ||
|
|
2bc22cc7d5 | ||
|
|
b4c189c89b | ||
|
|
fe3f351a2d | ||
|
|
076be9cba7 | ||
|
|
f28dcd85fb | ||
|
|
8e0f17c9d7 | ||
|
|
d73817a0db | ||
|
|
11874db825 | ||
|
|
5d5a43ce90 | ||
|
|
75e548caab | ||
|
|
c901e7ba06 | ||
|
|
b1dc3dfca1 | ||
|
|
ce4ed20c69 | ||
|
|
d44df7f860 | ||
|
|
54353e0e1f | ||
|
|
1c042b6efb | ||
|
|
b8df1f29c4 | ||
|
|
18252f5b03 | ||
|
|
881370f284 | ||
|
|
35d1747bc3 | ||
|
|
91ac89a54e | ||
|
|
3c694d9bd9 | ||
|
|
6a78e9df77 | ||
|
|
078c6eb30a | ||
|
|
d35c6002a6 | ||
|
|
f23fc3beaa | ||
|
|
5a352e5ace | ||
|
|
27cae0065e | ||
|
|
50be2993fa | ||
|
|
d9ea15e9bc | ||
|
|
03b1d45d7e | ||
|
|
e48d6878c4 | ||
|
|
1a3b255848 | ||
|
|
f00aa94d7e | ||
|
|
f7980b19f4 | ||
|
|
6a1f9677a5 | ||
|
|
e844bb36a5 | ||
|
|
ae364adfc2 | ||
|
|
c14a382b90 | ||
|
|
da9c2beaaf | ||
|
|
a4a56476db | ||
|
|
39d3784b9b | ||
|
|
7d29df58f1 | ||
|
|
05aa413683 | ||
|
|
132f90f45b | ||
|
|
4526fd1917 | ||
|
|
2602dc15b0 | ||
|
|
2314f63424 | ||
|
|
c2410600ee | ||
|
|
f7e4702685 | ||
|
|
71682512c4 | ||
|
|
20b82fbf77 | ||
|
|
631f09847d | ||
|
|
671585f68a | ||
|
|
5feed888bb | ||
|
|
47bef0f1b0 | ||
|
|
c1dc662a40 | ||
|
|
16e1721fe8 | ||
|
|
f406e13600 | ||
|
|
9063133a7b | ||
|
|
3fbb436187 | ||
|
|
7c845d070b | ||
|
|
5e905aa73e | ||
|
|
e15654f265 | ||
|
|
369c067efc | ||
|
|
c037e69793 | ||
|
|
8c935ff44e | ||
|
|
74bf8b0554 | ||
|
|
6345972efe | ||
|
|
16242e87a1 | ||
|
|
8155320ba5 | ||
|
|
39a7f63972 | ||
|
|
7b72209277 | ||
|
|
cad20a0bfe | ||
|
|
ba8d79f202 | ||
|
|
176fe699b9 | ||
|
|
213ee7be13 | ||
|
|
48fd869c71 | ||
|
|
53e47484e2 | ||
|
|
dc18b20e5a | ||
|
|
b4c5debbdf | ||
|
|
771d3d52b9 | ||
|
|
2a53ac093d | ||
|
|
4fa2af72cc | ||
|
|
e512d3cd61 | ||
|
|
16b7ee3985 | ||
|
|
4f3d26c31b | ||
|
|
587bf94d69 | ||
|
|
635ad77e0d | ||
|
|
33258da6c3 | ||
|
|
c2b3c3379d | ||
|
|
e30fac02ed | ||
|
|
e74592a654 | ||
|
|
ebd47aa73b | ||
|
|
e2d19cbaba | ||
|
|
1f864a846f | ||
|
|
fc32c2c944 | ||
|
|
279e37f1cb | ||
|
|
3f9176176e | ||
|
|
b3201ccafd | ||
|
|
2a01a620a9 | ||
|
|
6f43cf7b82 | ||
|
|
1c6250f9c2 | ||
|
|
650075a9ab | ||
|
|
668ee6f24a | ||
|
|
c456ec2c4c | ||
|
|
3b0c390a9e | ||
|
|
b02eb11345 | ||
|
|
ed3ba303bc | ||
|
|
ee5da1410e | ||
|
|
494627c6e1 | ||
|
|
82ac112dec | ||
|
|
40cfff33ff | ||
|
|
c1c5873912 | ||
|
|
c090efd562 | ||
|
|
91dbb0e77b | ||
|
|
dde7b144f0 | ||
|
|
f1873a014c | ||
|
|
48b8923b67 | ||
|
|
6f9f3fea83 | ||
|
|
10f3320165 | ||
|
|
d57aa33b40 | ||
|
|
0e7c78bae3 | ||
|
|
e6602786ec | ||
|
|
31bbb3894c | ||
|
|
8bbf576807 | ||
|
|
1ecc9b9d0e | ||
|
|
3adbebc3d9 | ||
|
|
a4c086f51b | ||
|
|
0ecdcec698 | ||
|
|
ae7f026d46 | ||
|
|
2813e0b706 | ||
|
|
a409562d00 | ||
|
|
b6b6661ea1 | ||
|
|
fb7b6f667c | ||
|
|
b94efe81e4 | ||
|
|
72a1b3d2a0 | ||
|
|
20bff18bd4 | ||
|
|
ba5c5ef4fb | ||
|
|
aff3ac0bb3 | ||
|
|
2c350daf01 | ||
|
|
cb7627c736 | ||
|
|
f731a5cae4 | ||
|
|
07cb7c9305 | ||
|
|
86e9cc4896 | ||
|
|
12c9405257 | ||
|
|
4708b44c59 | ||
|
|
4cb428bb92 | ||
|
|
d7391b19bc | ||
|
|
db9e56d1ca | ||
|
|
e527af10f4 | ||
|
|
74c7be0a6d | ||
|
|
2d40e7b51a | ||
|
|
ea407fb2ea | ||
|
|
fca5b0529a | ||
|
|
65fd1dd2d8 | ||
|
|
0a7ede3818 | ||
|
|
24e84f3866 | ||
|
|
c1c8e46c09 | ||
|
|
8591e4f301 | ||
|
|
10db7ad89c | ||
|
|
4ca4f442b9 | ||
|
|
6d5ecbc9c4 | ||
|
|
ea685a5686 | ||
|
|
376d3b6e91 | ||
|
|
df7397af1f | ||
|
|
9ba9345b04 | ||
|
|
9fc5758ae0 | ||
|
|
25298a6182 | ||
|
|
246da1bff6 | ||
|
|
8b5d12b958 | ||
|
|
3817370d4e | ||
|
|
c29846a9da | ||
|
|
2158f906a7 | ||
|
|
008dd280d6 | ||
|
|
fb99db011c | ||
|
|
c0fbd9cb3c | ||
|
|
fb79b18002 | ||
|
|
3841f22108 | ||
|
|
379d523ac5 | ||
|
|
07ec7f8c13 | ||
|
|
d0f29cc7a2 | ||
|
|
0e23a487f7 | ||
|
|
ac10f7c426 | ||
|
|
852c00009e | ||
|
|
b365798e66 | ||
|
|
66a6097a49 | ||
|
|
0e529d3d92 | ||
|
|
06c75dd656 | ||
|
|
69c32d4d90 | ||
|
|
36ffebd975 | ||
|
|
deb56539fb | ||
|
|
af8d099b9f | ||
|
|
eed869d321 | ||
|
|
f8f2acf6c3 | ||
|
|
7be9f74827 | ||
|
|
ed77982330 | ||
|
|
e1b47eca90 | ||
|
|
bfa0b2d2bc | ||
|
|
d67783edbd | ||
|
|
77712b6664 | ||
|
|
1633e7faa6 | ||
|
|
2dc5ee5862 | ||
|
|
bbaea23eea | ||
|
|
d112ac7eef | ||
|
|
cf21e987f2 | ||
|
|
dae11765bc | ||
|
|
df30a3c7f4 | ||
|
|
aaa4600597 | ||
|
|
ba1730d26b | ||
|
|
d1b5c812f7 | ||
|
|
5bfe6d1c84 | ||
|
|
9804a794fd | ||
|
|
0344645208 | ||
|
|
f56ad6e787 | ||
|
|
7492db592b | ||
|
|
2e80ecf8a7 | ||
|
|
6993551a44 | ||
|
|
9bff15adfe | ||
|
|
0c24da2358 | ||
|
|
4ee4adb43d | ||
|
|
e33b028348 | ||
|
|
384d9c1841 | ||
|
|
f78fcd6b5f | ||
|
|
711a72989b | ||
|
|
d62a2fc1d5 | ||
|
|
f33f712a3a | ||
|
|
3315b3310b | ||
|
|
c7134b16ed | ||
|
|
f36f710661 | ||
|
|
00ab8681c7 | ||
|
|
4137bbac94 | ||
|
|
750b2ad599 | ||
|
|
511c833fbb | ||
|
|
29063a0c07 | ||
|
|
67909b3557 | ||
|
|
102c12d36c | ||
|
|
dc51651665 | ||
|
|
8b563d6d5f | ||
|
|
eb8b88027e | ||
|
|
a5b7f307ed | ||
|
|
45fca425aa | ||
|
|
a0bd9b5fd7 | ||
|
|
c12e24e3e3 | ||
|
|
d147c5a153 | ||
|
|
7a833456d9 | ||
|
|
306f33dfc4 | ||
|
|
a2745a4060 | ||
|
|
46b68c7b2a | ||
|
|
4264760113 | ||
|
|
42dedbbd9a | ||
|
|
ea99bfdb16 | ||
|
|
2ccb934338 | ||
|
|
367027cfbc | ||
|
|
c4ebd3b6d5 | ||
|
|
5f8cd82a09 | ||
|
|
0ef15fa662 | ||
|
|
c05452af91 | ||
|
|
4c8bafcf9a | ||
|
|
034f30a49a | ||
|
|
0f13075e17 | ||
|
|
ad274a5981 | ||
|
|
fdbcbaec8a | ||
|
|
9c09072ee6 | ||
|
|
0a4ca56da9 | ||
|
|
2b494398f2 | ||
|
|
95585c2264 | ||
|
|
92f9f0da9e | ||
|
|
fe943b5e95 | ||
|
|
3479a4661a | ||
|
|
7ba438cf7c | ||
|
|
c761e9fba0 | ||
|
|
deae31ea4a | ||
|
|
547355a163 | ||
|
|
9be4cb132c | ||
|
|
3e25c9f3f4 | ||
|
|
220c7e9139 | ||
|
|
79a085a9be | ||
|
|
b505c5a9d3 | ||
|
|
03ba660ea1 | ||
|
|
5aeb361f6d | ||
|
|
0e519a4e97 | ||
|
|
4feee00d34 | ||
|
|
ef5e4cdc0a | ||
|
|
67c599f50b | ||
|
|
5af9b61357 | ||
|
|
1d6771b4ed | ||
|
|
c55dc0a58e | ||
|
|
c525d55db8 | ||
|
|
408f66ef80 | ||
|
|
7f73a047a4 | ||
|
|
015bc98d60 | ||
|
|
5cd9ba609a | ||
|
|
c8ed6544db | ||
|
|
1162113d3b | ||
|
|
1612503e7b | ||
|
|
34ba85df3b | ||
|
|
8206e7d2a2 | ||
|
|
3e29672c70 | ||
|
|
f67aaafe4f | ||
|
|
ed704afc07 | ||
|
|
bbbfacb4b2 | ||
|
|
cf16d2e52d | ||
|
|
6268e6e1c9 | ||
|
|
99b8e5b303 | ||
|
|
73446af330 | ||
|
|
a0b917a207 | ||
|
|
53ec7edd06 | ||
|
|
ff804c0ff8 | ||
|
|
9d69ff01b3 | ||
|
|
61831f530f | ||
|
|
6065db1d24 | ||
|
|
270b8415e8 | ||
|
|
1987331a70 | ||
|
|
ab85216b96 | ||
|
|
b5cb78c77e | ||
|
|
e75c99672d | ||
|
|
7faba72ebe | ||
|
|
cbe8fc1bdf | ||
|
|
f66a7660e2 | ||
|
|
5f3159a203 | ||
|
|
76aeda4436 | ||
|
|
fa791cd28c | ||
|
|
d836c68ee5 | ||
|
|
519d90f0a7 | ||
|
|
26400be6f7 | ||
|
|
92ba7e9d54 | ||
|
|
25eafe1e69 | ||
|
|
118a9ca861 | ||
|
|
174a1fe834 | ||
|
|
1e0f3f40a9 | ||
|
|
19623694f5 | ||
|
|
55a16043e1 | ||
|
|
29943b7edd | ||
|
|
a1e2af9533 | ||
|
|
c350e2a668 | ||
|
|
e0868ba2ab | ||
|
|
bcefd1fbd8 | ||
|
|
f5fbad0abf | ||
|
|
95b1a197be | ||
|
|
39169a36f5 | ||
|
|
3b1a409f07 | ||
|
|
accd70d4b4 | ||
|
|
3c7f4b760f | ||
|
|
f7d7ccfd2c | ||
|
|
de98a03887 | ||
|
|
0e11c240cb | ||
|
|
c0a298e484 | ||
|
|
907b9a9862 | ||
|
|
8d70d91cf4 | ||
|
|
6fb86859ce | ||
|
|
fe733b319f | ||
|
|
08b58f3055 | ||
|
|
9f6659f526 | ||
|
|
d28397df93 | ||
|
|
2e1c37146b | ||
|
|
903adc8f97 | ||
|
|
fc7f454382 | ||
|
|
ef35fc63a1 | ||
|
|
52a193b183 | ||
|
|
dad401a6ec | ||
|
|
ec3f8118db | ||
|
|
cfc0194c00 | ||
|
|
dd28f52301 | ||
|
|
9dcbd532e6 | ||
|
|
16b4a2cad2 | ||
|
|
bd9d8a035a | ||
|
|
d55ffb0212 | ||
|
|
e76ddfd005 | ||
|
|
59145a3b40 | ||
|
|
c993f5343b | ||
|
|
b62acec5ee | ||
|
|
b34ab9cdd5 | ||
|
|
e0a8ab852e | ||
|
|
bd5ebd0e41 | ||
|
|
661e21d25b | ||
|
|
dc69a2bc94 | ||
|
|
e6fec6b27d | ||
|
|
27b92f9838 | ||
|
|
3446def4dd | ||
|
|
2700206715 | ||
|
|
fdfc6f70f3 | ||
|
|
065c288edb | ||
|
|
3121d2c23e | ||
|
|
7cd3bb524c | ||
|
|
6b8cc97779 | ||
|
|
b7112a1edd | ||
|
|
28965b7356 | ||
|
|
bd3aa28416 | ||
|
|
9fed4ce24c | ||
|
|
90383e30dd | ||
|
|
13f184e957 | ||
|
|
a7a2c6d7ff | ||
|
|
d1bf39d0ac | ||
|
|
7dff6b8415 | ||
|
|
656e019829 | ||
|
|
2133b0f498 | ||
|
|
bc4dcee2b1 | ||
|
|
0e8cf95739 | ||
|
|
e133290c57 | ||
|
|
1429b0677e | ||
|
|
d03ee36647 | ||
|
|
6e5880c642 | ||
|
|
fa93cffafb | ||
|
|
ce9af0fb57 | ||
|
|
95700d8d51 | ||
|
|
fb15e98519 | ||
|
|
3054cb7971 | ||
|
|
f84587cf5a | ||
|
|
538f38f314 | ||
|
|
06578349c7 | ||
|
|
a807476171 | ||
|
|
7aacf9ca89 | ||
|
|
50dae5fb83 | ||
|
|
0853c425fe | ||
|
|
edcc676693 | ||
|
|
c8a742a121 | ||
|
|
ee14a7e15f | ||
|
|
e1eaccf409 | ||
|
|
d2aae52868 | ||
|
|
9158fb4745 | ||
|
|
0ff5ef5337 | ||
|
|
1ace145f85 | ||
|
|
565eb4b450 | ||
|
|
f39861c43f | ||
|
|
72838c248f | ||
|
|
9be8765ccf | ||
|
|
48732c4393 | ||
|
|
5bf0b7c920 | ||
|
|
51a4580d0f | ||
|
|
266e611afa | ||
|
|
22598b693c | ||
|
|
008fe38f10 | ||
|
|
24e8123240 | ||
|
|
6054f03c0d | ||
|
|
476f6d83e2 | ||
|
|
ec57109f39 | ||
|
|
d73488f887 | ||
|
|
3201380c29 | ||
|
|
1f04b39ae3 | ||
|
|
9e2b47027c | ||
|
|
662149a98a | ||
|
|
fafa31589d | ||
|
|
43a5940b9e | ||
|
|
33908624fd | ||
|
|
ffef8a341f | ||
|
|
a48c5df844 | ||
|
|
37b6a668ab | ||
|
|
2a9a7cc897 | ||
|
|
c62e6b5734 | ||
|
|
6ec1d0b935 | ||
|
|
6c5769fdd8 | ||
|
|
09acc5920c | ||
|
|
9613c58bf8 | ||
|
|
147f9ac64b | ||
|
|
cc1e8961a1 | ||
|
|
3b1b2f401d | ||
|
|
58a87b9b61 | ||
|
|
f09475a6b5 | ||
|
|
750422d858 | ||
|
|
03d911d335 | ||
|
|
2747c11a46 | ||
|
|
5a9fe2637f | ||
|
|
a5787f9988 | ||
|
|
85e22bb515 | ||
|
|
5e1e90ad80 | ||
|
|
fe85421c7e | ||
|
|
38af6ce279 | ||
|
|
fe92f16da4 | ||
|
|
edc4b562f2 | ||
|
|
7b6a91064c | ||
|
|
a32414c6fc | ||
|
|
259b4e29de | ||
|
|
e56f80b546 | ||
|
|
1ff52c5290 | ||
|
|
70bd249f43 | ||
|
|
f2f7e43579 | ||
|
|
c36d60fcd4 | ||
|
|
0950f307d1 | ||
|
|
a9b7e4a85e | ||
|
|
912a886539 | ||
|
|
f7484f49e5 | ||
|
|
9f68be1925 | ||
|
|
ef298f8d7a | ||
|
|
c038f0e6ee | ||
|
|
3c53a93447 | ||
|
|
7e86e65cce | ||
|
|
ad171d6cbb | ||
|
|
76ffc20836 | ||
|
|
c4cc5b6dfc | ||
|
|
878c9210d3 | ||
|
|
35c982b367 | ||
|
|
9c4ff080af | ||
|
|
d32ca64a03 | ||
|
|
53b89390be | ||
|
|
a8e09d7fe6 | ||
|
|
0c7cf20e7e | ||
|
|
6ebcee33c5 | ||
|
|
c73544fb59 | ||
|
|
37c1f93bcb | ||
|
|
95aa2e10fc | ||
|
|
279e6e3925 | ||
|
|
8a661d5ee4 | ||
|
|
67fcb40455 | ||
|
|
641524c80e | ||
|
|
55802354d4 | ||
|
|
75f4f5c0bd | ||
|
|
382e9dee55 | ||
|
|
6861d4029e | ||
|
|
370ef16854 | ||
|
|
9dbe434792 | ||
|
|
21a0e95623 | ||
|
|
57c6307479 | ||
|
|
01d8b1f468 | ||
|
|
ef6b82a0a3 | ||
|
|
19b7d1a7c5 | ||
|
|
097d901191 | ||
|
|
a1b5846b29 | ||
|
|
dbdb353e69 | ||
|
|
4456eac1fd | ||
|
|
ba3f8f432e | ||
|
|
bab539f52c | ||
|
|
42dbb128be | ||
|
|
5e4a6cb15f | ||
|
|
73e45ce911 | ||
|
|
1e40043456 | ||
|
|
7f3b952ec7 | ||
|
|
82887dc1c1 | ||
|
|
71380ab37a | ||
|
|
5d00804758 | ||
|
|
84364c65b0 | ||
|
|
1b59b705ae | ||
|
|
bc90fe6f99 | ||
|
|
c8d6a0833e | ||
|
|
d8fc7d27ec | ||
|
|
1e44b19ff0 | ||
|
|
cc8b2cd20e | ||
|
|
057b1e294c | ||
|
|
0653e790c7 | ||
|
|
6d72bbcb76 | ||
|
|
59e6f08455 | ||
|
|
8fdccf50a8 | ||
|
|
1c7e11c5d9 | ||
|
|
1756fbbb23 | ||
|
|
7bb939ab7f | ||
|
|
4fa0abbd5a | ||
|
|
1e5f0266ef | ||
|
|
3dee62105e | ||
|
|
680b2323d5 | ||
|
|
562b4dad4d | ||
|
|
079a82dfe4 | ||
|
|
16f7eb43b0 | ||
|
|
4b0ed3f224 | ||
|
|
1d453b694d | ||
|
|
751e8c189e | ||
|
|
183e9a3d0b | ||
|
|
5f0f4dd485 | ||
|
|
20f05662aa | ||
|
|
963471b43f | ||
|
|
fdb52e0243 | ||
|
|
d1e4483f98 | ||
|
|
b194ada316 | ||
|
|
3a25a2dcbf | ||
|
|
85e4497fbe | ||
|
|
0bfa5e7ea6 | ||
|
|
013599890f | ||
|
|
519e552405 | ||
|
|
32a4ec49f7 | ||
|
|
3223332906 | ||
|
|
f78bd08440 | ||
|
|
99b5f92d7a | ||
|
|
1cc5e84104 | ||
|
|
8346f5ab08 | ||
|
|
dc60a39ba9 | ||
|
|
16f7872553 | ||
|
|
75e7c4b2ca | ||
|
|
c5d5ddd7d2 | ||
|
|
98a81e3708 | ||
|
|
ecdbdb944a | ||
|
|
8a2846461b | ||
|
|
6bfb9a2f57 | ||
|
|
85dfb2d4eb | ||
|
|
8f1d0c2b8f | ||
|
|
829494c03e | ||
|
|
f89fade28d | ||
|
|
9081b1dadd | ||
|
|
84dac544af | ||
|
|
49f1ac333d | ||
|
|
a2b761094b | ||
|
|
54bd3d480f | ||
|
|
b281f8fa32 | ||
|
|
a3732f845e | ||
|
|
38f6d0a020 | ||
|
|
a0e5da758a | ||
|
|
dcbe10c1c7 | ||
|
|
1f823d2a1b | ||
|
|
12d0a9e11a | ||
|
|
326b294c83 | ||
|
|
50f213ae71 | ||
|
|
91d4fd8849 | ||
|
|
3f1985a9dc | ||
|
|
573a71f09d | ||
|
|
57900fa287 | ||
|
|
fa721d9614 | ||
|
|
e64bbacf68 | ||
|
|
6ee2edc757 | ||
|
|
9ec4faf0d0 | ||
|
|
5be3ba2ffa | ||
|
|
a5ee96656b | ||
|
|
2db71d0323 | ||
|
|
dcf321047f | ||
|
|
4982110be9 | ||
|
|
28547e90d1 | ||
|
|
f70187597f | ||
|
|
333e454e78 | ||
|
|
61cfd11644 | ||
|
|
1c970c8176 | ||
|
|
1fea54ca5a | ||
|
|
faccc42b22 | ||
|
|
460f4769a5 | ||
|
|
f465643b75 | ||
|
|
a8afd71f96 | ||
|
|
2274d60207 | ||
|
|
57159bccfd | ||
|
|
2888124752 | ||
|
|
408e751dcf | ||
|
|
af5dcc38a4 | ||
|
|
81434640d6 | ||
|
|
a45b58d956 | ||
|
|
2c5e9a5e76 | ||
|
|
9fb847b179 | ||
|
|
b8341b2ba8 | ||
|
|
26d6e4da2c | ||
|
|
b16ed602d5 | ||
|
|
24aab4d5d3 | ||
|
|
2eb69d421a | ||
|
|
cb52706f2f | ||
|
|
f44d232e8b | ||
|
|
a0ac79b9dd | ||
|
|
177a2a8a1e | ||
|
|
8b21a87175 | ||
|
|
6d2dd8e315 | ||
|
|
8b3aff599b | ||
|
|
deb9b74f27 | ||
|
|
591de3cbe8 | ||
|
|
f7151e2132 | ||
|
|
44521be6dd | ||
|
|
30416cdbb5 | ||
|
|
d04da2d256 | ||
|
|
231d8a1949 | ||
|
|
3207bec805 | ||
|
|
425a71e382 | ||
|
|
daa12ab2ec | ||
|
|
4c652389c5 | ||
|
|
f69d88a656 | ||
|
|
098cbc1456 | ||
|
|
5f2da953a9 | ||
|
|
638d999fcc | ||
|
|
fa80fd64da | ||
|
|
e4dad82dde | ||
|
|
d65ff924c8 | ||
|
|
96e9661aaa | ||
|
|
8829d2ebd4 | ||
|
|
c019280d8a | ||
|
|
8d3e5fc160 | ||
|
|
d3f1312c0b | ||
|
|
c58f48a1e4 | ||
|
|
979e93509e | ||
|
|
135c3709b4 | ||
|
|
08400d3f18 | ||
|
|
bf52075d1b | ||
|
|
56befda288 | ||
|
|
3d68f1dc62 | ||
|
|
970036ce1a | ||
|
|
62108f28f4 | ||
|
|
66bbb072c3 | ||
|
|
6c52c26a62 | ||
|
|
d52943d1bf | ||
|
|
7cb1bbe3d6 | ||
|
|
2548c46b8b | ||
|
|
dd5118116b | ||
|
|
77a9b66028 | ||
|
|
e813dab81c | ||
|
|
6696880178 | ||
|
|
8e5952d9ae | ||
|
|
360c1d5953 | ||
|
|
a7aa6ced19 | ||
|
|
97eea669d4 | ||
|
|
c84777928e | ||
|
|
490064a953 | ||
|
|
d5975195b3 | ||
|
|
9588c36abb | ||
|
|
f9200ac135 | ||
|
|
fffb9606ec | ||
|
|
781e15cb84 | ||
|
|
9742001a71 | ||
|
|
e92eee5ffc | ||
|
|
293c1d471c | ||
|
|
384240b6a4 | ||
|
|
6fd626a3ec | ||
|
|
bb4ce2f978 | ||
|
|
2269e05058 | ||
|
|
ca2a07b816 | ||
|
|
38c7bb35e0 | ||
|
|
4f6408f3e1 | ||
|
|
7910d9fde4 | ||
|
|
0258d7e24f | ||
|
|
85556ed532 | ||
|
|
cecb04b097 | ||
|
|
8a7c5c18d0 | ||
|
|
14a1a3f574 | ||
|
|
c19f2a7499 | ||
|
|
df95be5455 | ||
|
|
00bef13f1c | ||
|
|
a6a35905a7 | ||
|
|
93f28ef55c | ||
|
|
bbcb9573cd | ||
|
|
43cc6e19d4 | ||
|
|
f4a44664c7 | ||
|
|
dd7a3269ad | ||
|
|
157d1b20c5 | ||
|
|
85fe0c00c2 | ||
|
|
91092b8a96 | ||
|
|
1ed237cfcc | ||
|
|
c7044498d5 | ||
|
|
1d2a2fbcae | ||
|
|
9b015e8cae | ||
|
|
0a8c26fff4 | ||
|
|
506de72666 | ||
|
|
a5b4156b56 | ||
|
|
da4b42cb1d | ||
|
|
53790f8247 | ||
|
|
69780d4727 | ||
|
|
aa2b644684 | ||
|
|
a12e8875a6 | ||
|
|
9e91b265ce | ||
|
|
8c12e3ab90 | ||
|
|
7e303b4fc0 | ||
|
|
40e0fcff30 | ||
|
|
3c9e74b23e | ||
|
|
6b001eb7c3 | ||
|
|
f81621aa66 | ||
|
|
08c7484087 | ||
|
|
a8c68f3e30 | ||
|
|
0e6698d760 | ||
|
|
f3d4f9ff23 | ||
|
|
d711f17081 | ||
|
|
d35eba45c5 | ||
|
|
cd53e79b19 | ||
|
|
3db7029534 | ||
|
|
ad1e52bf19 | ||
|
|
e08791a284 | ||
|
|
8d1deeb568 | ||
|
|
375c7789a2 | ||
|
|
ec8a81aedb | ||
|
|
033d513aee | ||
|
|
fb3e4e4881 | ||
|
|
8a30c006e7 | ||
|
|
3f76679673 | ||
|
|
1cee5d4b41 | ||
|
|
3107eec697 | ||
|
|
477d46316e | ||
|
|
3133693a0e | ||
|
|
bc7d701298 | ||
|
|
5d6d75b4f3 | ||
|
|
73d48a7b37 | ||
|
|
ed7b9a9989 | ||
|
|
e1a955752f | ||
|
|
0bdc8f0b2b | ||
|
|
a692b3ced8 | ||
|
|
2f5b93861d | ||
|
|
110183585c | ||
|
|
7eb29586a7 | ||
|
|
401065a23e | ||
|
|
4e5e0fb0ce | ||
|
|
d41a06611e | ||
|
|
26c3c27726 | ||
|
|
19ab63e041 | ||
|
|
5dafdab3d7 | ||
|
|
afbb17d428 | ||
|
|
8a721fbd25 | ||
|
|
5d91a409e7 | ||
|
|
8470f7caf8 | ||
|
|
67e279928e | ||
|
|
77ac3a62b7 | ||
|
|
12eaa3a162 | ||
|
|
bbd5dd7b4f | ||
|
|
38fcd6e267 | ||
|
|
fd7f683eaa | ||
|
|
e15f9acd91 | ||
|
|
7cb0882c73 | ||
|
|
486d4d1c88 | ||
|
|
ded8b13e96 | ||
|
|
c7eb7ba861 | ||
|
|
4920bff8fd | ||
|
|
d78edf5dda | ||
|
|
7510c02d83 | ||
|
|
2d7b729473 | ||
|
|
0495fe2d71 | ||
|
|
d7da5df734 | ||
|
|
4462b6bd39 | ||
|
|
80e1edeeb2 | ||
|
|
11af421f10 | ||
|
|
686ff235e7 | ||
|
|
31f76a6d4d | ||
|
|
50078078e0 | ||
|
|
be85e1e2f7 | ||
|
|
9ad1574292 | ||
|
|
4b71825707 | ||
|
|
fb1fd88947 | ||
|
|
dca527d8b6 | ||
|
|
3452a445fe | ||
|
|
a06e9d2bef | ||
|
|
7a3961a280 | ||
|
|
54729d8fb4 | ||
|
|
c2e17ee182 | ||
|
|
bc0064d4b9 | ||
|
|
03685dbb61 | ||
|
|
26fcba8ed5 | ||
|
|
bc15b65538 | ||
|
|
e9ab34a9c1 | ||
|
|
0bf512ebdd | ||
|
|
7646fbeaa0 | ||
|
|
84b4766013 | ||
|
|
3a48734b2f | ||
|
|
36ae332959 | ||
|
|
3e0d8da9d6 | ||
|
|
2fcb4dbe50 | ||
|
|
09c93bfb39 | ||
|
|
34068b1598 | ||
|
|
a67da1c99a | ||
|
|
0d6754761d | ||
|
|
898f7b66cf | ||
|
|
c18f3e86f0 | ||
|
|
de51922f10 | ||
|
|
be0cb18bfc | ||
|
|
39fd1db3c0 | ||
|
|
b4565e7354 | ||
|
|
e28cada4dd | ||
|
|
6daac65968 | ||
|
|
1ecc49c450 | ||
|
|
f96e7d9aaa | ||
|
|
c637bba867 | ||
|
|
bdc6554ca8 | ||
|
|
ecb59e9c31 | ||
|
|
1b39184e98 | ||
|
|
2a35ba64f7 | ||
|
|
3a5123627d | ||
|
|
a18eeecd59 | ||
|
|
85e3f04738 | ||
|
|
cc59864377 | ||
|
|
5b10cbf2e2 | ||
|
|
fc6b83bb5d | ||
|
|
bc509f55d9 | ||
|
|
f81301ece6 | ||
|
|
382e5c1f43 | ||
|
|
0243f74dcd | ||
|
|
58737ef454 | ||
|
|
940cea82ca | ||
|
|
5683e36733 | ||
|
|
f5137b7935 | ||
|
|
0c2af42c69 | ||
|
|
760dc5d0c6 | ||
|
|
5331aa08a7 | ||
|
|
375d7cc7b1 | ||
|
|
a05f3dd640 | ||
|
|
b91c1b44ba | ||
|
|
6efb01a397 | ||
|
|
1843eca6c0 | ||
|
|
506e3e8a48 | ||
|
|
0e5a3cc5aa | ||
|
|
d2dd76e13d | ||
|
|
470b82fd64 | ||
|
|
e04dd3a4b1 | ||
|
|
2d39e06c97 | ||
|
|
e1fc74bdc1 | ||
|
|
3ab5d7f861 | ||
|
|
d63dd6086a | ||
|
|
a8d9895a9b | ||
|
|
f8a7257af3 | ||
|
|
4703028988 | ||
|
|
87523cdbd5 | ||
|
|
d9567ed035 | ||
|
|
0ab277662a | ||
|
|
2eeb94e39c | ||
|
|
4b441d10b3 | ||
|
|
37a1d3d61e | ||
|
|
3839338c15 | ||
|
|
bdee5790e6 | ||
|
|
d0dab25dae | ||
|
|
b14b7b00c2 | ||
|
|
248bfcccda | ||
|
|
9b5833205b | ||
|
|
07f8589090 | ||
|
|
f77f83dfeb | ||
|
|
e3d3d916ba | ||
|
|
cccf219cd2 | ||
|
|
0896b2f7b8 | ||
|
|
cc406262ac | ||
|
|
0f20063eb8 | ||
|
|
5f32b165f2 | ||
|
|
3cadd1789b | ||
|
|
e486778b36 | ||
|
|
7fe6453bbb | ||
|
|
9f88d2b6d6 | ||
|
|
8f9d52699d | ||
|
|
0a774a8c55 | ||
|
|
d4ced34a11 | ||
|
|
85a762a0b9 | ||
|
|
b255fecc6e | ||
|
|
734d2e2594 | ||
|
|
2e292b4636 | ||
|
|
f0bc7356ac | ||
|
|
1bcb6ab931 | ||
|
|
ef65937662 | ||
|
|
3369b8b5b2 | ||
|
|
28db561cd9 | ||
|
|
0622326e1b | ||
|
|
c6e2593e4e | ||
|
|
d0e3279a67 | ||
|
|
aee5bda9ec | ||
|
|
979b4a8861 | ||
|
|
c10cd4b474 | ||
|
|
4aa1d19845 | ||
|
|
7ff51d89fc | ||
|
|
ea9d94e42f | ||
|
|
a9ba0fdb0b | ||
|
|
af19c3331c | ||
|
|
5e98b930ee | ||
|
|
057d160392 | ||
|
|
6b2899c219 | ||
|
|
85290e687c | ||
|
|
d778e81f42 | ||
|
|
2bfad21604 | ||
|
|
373e0d3a9f | ||
|
|
5e83403d0c | ||
|
|
cbe76aab83 | ||
|
|
26de088520 | ||
|
|
98430edb17 | ||
|
|
48c6784e51 | ||
|
|
dc0f5af3ef | ||
|
|
af85e6f2a6 | ||
|
|
4e91af4d64 | ||
|
|
faf87a5dee | ||
|
|
517c5d356f | ||
|
|
1bc3c90286 | ||
|
|
afd00edee3 | ||
|
|
b712398208 | ||
|
|
7586e91b4f | ||
|
|
9eba82c107 | ||
|
|
ccdc219a09 | ||
|
|
60d01e76e9 | ||
|
|
b5cfd4152e | ||
|
|
32c4c8ae32 | ||
|
|
bd4c506d22 | ||
|
|
476dd7cd56 | ||
|
|
8176f84715 | ||
|
|
6bd33721d8 | ||
|
|
c9d9671288 | ||
|
|
2a821edf5f | ||
|
|
68b85bdc87 | ||
|
|
83cf5907c3 | ||
|
|
c912b6547c | ||
|
|
bf04b74f87 | ||
|
|
9d1e008990 | ||
|
|
d9e5285a3b | ||
|
|
84937b7a0b | ||
|
|
924b3e16cf | ||
|
|
2a8cf01410 | ||
|
|
a3a5cfee6c | ||
|
|
2c04441591 | ||
|
|
a4eab8e216 | ||
|
|
189f9589d4 | ||
|
|
880721e0d0 | ||
|
|
6ab65e2031 | ||
|
|
e871934cfd | ||
|
|
686390c1f2 | ||
|
|
a8b9fb1708 | ||
|
|
55d3764169 | ||
|
|
cb5bc3d631 | ||
|
|
543e66eb00 | ||
|
|
b658983fb8 | ||
|
|
cfb3e42337 | ||
|
|
36815b5e43 | ||
|
|
897e077aca | ||
|
|
f395960ffa | ||
|
|
fb301717f5 | ||
|
|
d548b78dee | ||
|
|
d1353e8eae | ||
|
|
935a76d16b | ||
|
|
db4c41f420 | ||
|
|
62f5af8e0b | ||
|
|
ff9aefb649 | ||
|
|
2b10d03e1f | ||
|
|
a27efbd937 |
@@ -16,3 +16,8 @@
|
||||
^CONTRIBUTING.md$
|
||||
^cran-comments.md$
|
||||
^.*\.o$
|
||||
^appveyor\.yml$
|
||||
^revdep$
|
||||
^TODO-promises.md$
|
||||
^manualtests$
|
||||
^\.github$
|
||||
|
||||
40
.github/CONTRIBUTING.md
vendored
Normal file
40
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
We welcome contributions to the **shiny** package. To submit a contribution:
|
||||
|
||||
1. [Fork](https://github.com/rstudio/shiny/fork) the repository and make your changes.
|
||||
|
||||
2. Submit a [pull request](https://help.github.com/articles/using-pull-requests).
|
||||
|
||||
3. Ensure that you have signed the contributor license agreement. It will appear as a "Check"
|
||||
on your PR and a comment from "CLAassistant" will also appear explaining whether you have
|
||||
yet to sign. After you sign, you can click the "Recheck" link in that comment and the check
|
||||
will flip to reflect that you've signed.
|
||||
|
||||
We generally do not merge pull requests that update included web libraries (such as Bootstrap or jQuery) because it is difficult for us to verify that the update is done correctly; we prefer to update these libraries ourselves.
|
||||
|
||||
## How to make changes
|
||||
|
||||
Before you submit a pull request, please do the following:
|
||||
|
||||
* Add an entry to NEWS.md concisely describing what you changed.
|
||||
|
||||
* If appropriate, add unit tests in the tests/ directory.
|
||||
|
||||
* If you made any changes to the JavaScript files in the srcjs/ directory, make sure you build the output JavaScript files. See tools/README.md file for information on using the build system.
|
||||
|
||||
* Run Build->Check Package in the RStudio IDE, or `devtools::check()`, to make sure your change did not add any messages, warnings, or errors.
|
||||
|
||||
Doing these things will make it easier for the Shiny development team to evaluate your pull request. Even so, we may still decide to modify your code or even not merge it at all. Factors that may prevent us from merging the pull request include:
|
||||
|
||||
* breaking backward compatibility
|
||||
* adding a feature that we do not consider relevant for Shiny
|
||||
* is hard to understand
|
||||
* is hard to maintain in the future
|
||||
* is computationally expensive
|
||||
* is not intuitive for people to use
|
||||
|
||||
We will try to be responsive and provide feedback in case we decide not to merge your pull request.
|
||||
|
||||
|
||||
## Filing issues
|
||||
|
||||
If you find a bug in Shiny, you can also [file an issue](https://github.com/rstudio/shiny/issues/new). Please provide as much relevant information as you can, and include a minimal reproducible example if possible.
|
||||
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name : Bug report
|
||||
about : Report a bug in Shiny.
|
||||
---
|
||||
|
||||
<!--
|
||||
This issue tracker is for bugs and feature requests in the Shiny package. If you're having trouble with Shiny Server or a related package, please file an issue in the appropriate repository.
|
||||
|
||||
If you're having trouble with shinyapps.io, and you have a paid account (Starter, Basic, Standard, or Pro), please file a support ticket via https://support.rstudio.com. If you have a Free account, please post to the RStudio Community with the shinyappsio tag: https://community.rstudio.com/tags/shinyappsio.
|
||||
|
||||
Finally, if you are an RStudio customer and are having trouble with one of our Pro products, get in touch with our support team at support@rstudio.com.
|
||||
|
||||
Before you file an issue, please upgrade to the latest version of Shiny from CRAN and confirm that the problem persists.
|
||||
|
||||
# First, restart R.
|
||||
# To install latest shiny from CRAN:
|
||||
install.packages("shiny")
|
||||
|
||||
See our guide to writing good bug reports for further guidance: https://github.com/rstudio/shiny/wiki/Writing-Good-Bug-Reports. The better your report is, the likelier we are to be able to reproduce and ultimately solve it.
|
||||
-->
|
||||
|
||||
### System details
|
||||
|
||||
Browser Version: <!-- If applicable -->
|
||||
|
||||
Output of `sessionInfo()`:
|
||||
|
||||
```
|
||||
# sessionInfo() output goes here
|
||||
```
|
||||
|
||||
### Example application *or* steps to reproduce the problem
|
||||
|
||||
<!-- If you're able to create one, a reproducible example is extremely helpful to us. For instructions on how to create one, please see: https://github.com/rstudio/shiny/wiki/Creating-a-Reproducible-Example -->
|
||||
|
||||
```R
|
||||
# Minimal, self-contained example app code goes here
|
||||
```
|
||||
|
||||
### Describe the problem in detail
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name : Feature request
|
||||
about : Request a new feature.
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Thanks for taking the time to file a feature request! Please take the time to search for an existing feature request, to avoid creating duplicate requests. If you find an existing feature request, please give it a thumbs-up reaction, as we'll use these reactions to help prioritize the implementation of these features in the future.
|
||||
|
||||
If the feature has not yet been filed, then please describe the feature you'd like to see become a part of Shiny. See:
|
||||
|
||||
https://github.com/rstudio/shiny/wiki/Writing-Good-Feature-Requests
|
||||
|
||||
for a guide on how to write good feature requests.
|
||||
|
||||
-->
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name : Ask a Question
|
||||
about : The issue tracker is not for questions -- please ask questions at https://community.rstudio.com/c/shiny.
|
||||
---
|
||||
|
||||
The issue tracker is not for questions. If you have a question, please feel free to ask it on our community site, at https://community.rstudio.com/c/shiny.
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,3 +8,5 @@
|
||||
/src-x86_64/
|
||||
shinyapps/
|
||||
README.html
|
||||
.*.Rnb.cached
|
||||
tools/yarn-error.log
|
||||
|
||||
31
.travis.yml
31
.travis.yml
@@ -1,14 +1,27 @@
|
||||
language: r
|
||||
warnings_are_errors: true
|
||||
|
||||
r_binary_packages:
|
||||
- Rcpp
|
||||
- cairo
|
||||
- knitr
|
||||
|
||||
r_github_packages:
|
||||
- rstudio/htmltools
|
||||
matrix:
|
||||
include:
|
||||
- name: "Roxygen check"
|
||||
r: release
|
||||
r_packages:
|
||||
- devtools
|
||||
- rprojroot
|
||||
script: ./tools/checkDocsCurrent.sh
|
||||
- name: "Javascript check"
|
||||
language: node_js
|
||||
cache: yarn
|
||||
script: ./tools/checkJSCurrent.sh
|
||||
node_js:
|
||||
- "12"
|
||||
- r: 3.2
|
||||
- r: 3.3
|
||||
- r: 3.4
|
||||
- r: 3.5
|
||||
- r: release
|
||||
- r: devel
|
||||
|
||||
sudo: false
|
||||
cache: packages
|
||||
notifications:
|
||||
email:
|
||||
on_success: change
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
|
||||
We welcome contributions to the **shiny** package. To submit a contribution:
|
||||
|
||||
1. [Fork](https://github.com/rstudio/shiny/fork) the repository and make your changes.
|
||||
|
||||
2. Ensure that you have signed the [individual](http://www.rstudio.com/wp-content/uploads/2014/06/RStudioIndividualContributorAgreement.pdf) or [corporate](http://www.rstudio.com/wp-content/uploads/2014/06/RStudioCorporateContributorAgreement.pdf) contributor agreement as appropriate. You can send the signed copy to jj@rstudio.com.
|
||||
|
||||
3. Submit a [pull request](https://help.github.com/articles/using-pull-requests).
|
||||
|
||||
We'll try to be as responsive as possible in reviewing and accepting pull requests. We appreciate your contributions!
|
||||
75
DESCRIPTION
75
DESCRIPTION
@@ -1,8 +1,7 @@
|
||||
Package: shiny
|
||||
Type: Package
|
||||
Title: Web Application Framework for R
|
||||
Version: 0.13.1
|
||||
Date: 2016-02-17
|
||||
Version: 1.4.0.9002
|
||||
Authors@R: c(
|
||||
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
|
||||
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
|
||||
@@ -15,7 +14,7 @@ Authors@R: c(
|
||||
person(family = "jQuery contributors", role = c("ctb", "cph"),
|
||||
comment = "jQuery library; authors listed in inst/www/shared/jquery-AUTHORS.txt"),
|
||||
person(family = "jQuery UI contributors", role = c("ctb", "cph"),
|
||||
comment = "jQuery UI library; authors listed in inst/www/shared/jqueryui/1.10.4/AUTHORS.txt"),
|
||||
comment = "jQuery UI library; authors listed in inst/www/shared/jqueryui/AUTHORS.txt"),
|
||||
person("Mark", "Otto", role = "ctb",
|
||||
comment = "Bootstrap library"),
|
||||
person("Jacob", "Thornton", role = "ctb",
|
||||
@@ -57,44 +56,71 @@ Authors@R: c(
|
||||
)
|
||||
Description: Makes it incredibly easy to build interactive web
|
||||
applications with R. Automatic "reactive" binding between inputs and
|
||||
outputs and extensive pre-built widgets make it possible to build
|
||||
outputs and extensive prebuilt widgets make it possible to build
|
||||
beautiful, responsive, and powerful applications with minimal effort.
|
||||
License: GPL-3 | file LICENSE
|
||||
Depends:
|
||||
R (>= 3.0.0),
|
||||
R (>= 3.0.2),
|
||||
methods
|
||||
Imports:
|
||||
utils,
|
||||
httpuv (>= 1.3.3),
|
||||
grDevices,
|
||||
httpuv (>= 1.5.2),
|
||||
mime (>= 0.3),
|
||||
jsonlite (>= 0.9.16),
|
||||
xtable,
|
||||
digest,
|
||||
htmltools (>= 0.3),
|
||||
R6 (>= 2.0)
|
||||
htmltools (>= 0.4.0.9001),
|
||||
R6 (>= 2.0),
|
||||
sourcetools,
|
||||
later (>= 1.0.0),
|
||||
promises (>= 1.1.0),
|
||||
tools,
|
||||
crayon,
|
||||
rlang (>= 0.4.0),
|
||||
fastmap (>= 1.0.0),
|
||||
withr
|
||||
Suggests:
|
||||
datasets,
|
||||
Cairo (>= 1.5-5),
|
||||
testthat,
|
||||
testthat (>= 2.1.1),
|
||||
knitr (>= 1.6),
|
||||
markdown,
|
||||
rmarkdown,
|
||||
ggplot2
|
||||
ggplot2,
|
||||
reactlog (>= 1.0.0),
|
||||
magrittr,
|
||||
shinytest,
|
||||
yaml,
|
||||
future,
|
||||
dygraphs
|
||||
Remotes:
|
||||
rstudio/htmltools
|
||||
URL: http://shiny.rstudio.com
|
||||
BugReports: https://github.com/rstudio/shiny/issues
|
||||
VignetteBuilder: knitr
|
||||
Collate:
|
||||
'app.R'
|
||||
'bookmark-state-local.R'
|
||||
'stack.R'
|
||||
'bookmark-state.R'
|
||||
'bootstrap-deprecated.R'
|
||||
'bootstrap-layout.R'
|
||||
'globals.R'
|
||||
'conditions.R'
|
||||
'map.R'
|
||||
'globals.R'
|
||||
'utils.R'
|
||||
'bootstrap.R'
|
||||
'cache.R'
|
||||
'cache-context.R'
|
||||
'cache-disk.R'
|
||||
'cache-memory.R'
|
||||
'cache-utils.R'
|
||||
'diagnose.R'
|
||||
'fileupload.R'
|
||||
'stack.R'
|
||||
'font-awesome.R'
|
||||
'graph.R'
|
||||
'reactives.R'
|
||||
'reactive-domains.R'
|
||||
'history.R'
|
||||
'hooks.R'
|
||||
'html-deps.R'
|
||||
'htmltools.R'
|
||||
@@ -114,26 +140,39 @@ Collate:
|
||||
'input-slider.R'
|
||||
'input-submit.R'
|
||||
'input-text.R'
|
||||
'input-textarea.R'
|
||||
'input-utils.R'
|
||||
'insert-tab.R'
|
||||
'insert-ui.R'
|
||||
'jqueryui.R'
|
||||
'middleware-shiny.R'
|
||||
'middleware.R'
|
||||
'timer.R'
|
||||
'mock-session.R'
|
||||
'modal.R'
|
||||
'modules.R'
|
||||
'notifications.R'
|
||||
'priorityqueue.R'
|
||||
'progress.R'
|
||||
'react.R'
|
||||
'reactive-domains.R'
|
||||
'reactives.R'
|
||||
'render-cached-plot.R'
|
||||
'render-plot.R'
|
||||
'render-table.R'
|
||||
'run-url.R'
|
||||
'serializers.R'
|
||||
'server-input-handlers.R'
|
||||
'server.R'
|
||||
'shiny-options.R'
|
||||
'shiny.R'
|
||||
'shinyui.R'
|
||||
'shinywrappers.R'
|
||||
'showcase.R'
|
||||
'snapshot.R'
|
||||
'tar.R'
|
||||
'timer.R'
|
||||
'test-export.R'
|
||||
'test-module.R'
|
||||
'test.R'
|
||||
'update-input.R'
|
||||
RoxygenNote: 5.0.1
|
||||
RoxygenNote: 7.1.0
|
||||
Encoding: UTF-8
|
||||
Roxygen: list(markdown = TRUE)
|
||||
|
||||
328
LICENSE
328
LICENSE
@@ -12,7 +12,7 @@ these components are included below):
|
||||
- Respond.js, https://github.com/scottjehl/Respond
|
||||
- bootstrap-datepicker, https://github.com/eternicode/bootstrap-datepicker
|
||||
- Font Awesome, https://github.com/FortAwesome/Font-Awesome
|
||||
- selectize.js, https://github.com/brianreavis/selectize.js
|
||||
- selectize.js, https://github.com/selectize/selectize.js
|
||||
- es5-shim, https://github.com/es-shims/es5-shim
|
||||
- ion.rangeSlider, https://github.com/IonDen/ion.rangeSlider
|
||||
- strftime for Javascript, https://github.com/samsonjs/strftime
|
||||
@@ -25,7 +25,7 @@ these components are included below):
|
||||
jQuery license and license for included components from jQuery UI
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Copyright jQuery Foundation and other contributors, https://jquery.org/
|
||||
Copyright JS Foundation and other contributors, https://js.foundation/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
@@ -51,7 +51,7 @@ Bootstrap License
|
||||
----------------------------------------------------------------------
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011-2014 Twitter, Inc
|
||||
Copyright (c) 2011-2019 Twitter, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -673,7 +673,7 @@ bootstrap-datepicker
|
||||
limitations under the License.
|
||||
|
||||
|
||||
Font-Awesome (CSS file is MIT licensed; font has SIL Open Font License 1.1)
|
||||
Font Awesome (CSS files are MIT licensed; fonts have SIL Open Font License 1.1, svgs have CC BY 4.0 License)
|
||||
----------------------------------------------------------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
@@ -795,6 +795,326 @@ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution 4.0 International Public License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution 4.0 International Public License ("Public License"). To the
|
||||
extent this Public License may be interpreted as a contract, You are
|
||||
granted the Licensed Rights in consideration of Your acceptance of
|
||||
these terms and conditions, and the Licensor grants You such rights in
|
||||
consideration of benefits the Licensor receives from making the
|
||||
Licensed Material available under these terms and conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
d. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
e. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
f. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
g. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
h. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
i. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
j. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
k. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
4. If You Share Adapted Material You produce, the Adapter's
|
||||
License You apply must not prevent recipients of the Adapted
|
||||
Material from complying with this Public License.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material; and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
selectize.js
|
||||
----------------------------------------------------------------------
|
||||
|
||||
96
NAMESPACE
96
NAMESPACE
@@ -1,16 +1,20 @@
|
||||
# Generated by roxygen2: do not edit by hand
|
||||
|
||||
S3method("$",mockclientdata)
|
||||
S3method("$",reactivevalues)
|
||||
S3method("$",session_proxy)
|
||||
S3method("$",shinyoutput)
|
||||
S3method("$<-",reactivevalues)
|
||||
S3method("$<-",session_proxy)
|
||||
S3method("$<-",shinyoutput)
|
||||
S3method("[",mockclientdata)
|
||||
S3method("[",reactivevalues)
|
||||
S3method("[",shinyoutput)
|
||||
S3method("[<-",reactivevalues)
|
||||
S3method("[<-",shinyoutput)
|
||||
S3method("[[",mockclientdata)
|
||||
S3method("[[",reactivevalues)
|
||||
S3method("[[",session_proxy)
|
||||
S3method("[[",shinyoutput)
|
||||
S3method("[[<-",reactivevalues)
|
||||
S3method("[[<-",shinyoutput)
|
||||
@@ -21,14 +25,19 @@ S3method(as.shiny.appobj,list)
|
||||
S3method(as.shiny.appobj,shiny.appobj)
|
||||
S3method(as.tags,shiny.appobj)
|
||||
S3method(as.tags,shiny.render.function)
|
||||
S3method(format,reactiveExpr)
|
||||
S3method(format,reactiveVal)
|
||||
S3method(names,reactivevalues)
|
||||
S3method(print,reactive)
|
||||
S3method(print,reactivevalues)
|
||||
S3method(print,shiny.appobj)
|
||||
S3method(print,shiny.render.function)
|
||||
S3method(str,reactivevalues)
|
||||
export("conditionStackTrace<-")
|
||||
export(..stacktraceoff..)
|
||||
export(..stacktraceon..)
|
||||
export(HTML)
|
||||
export(MockShinySession)
|
||||
export(NS)
|
||||
export(Progress)
|
||||
export(a)
|
||||
@@ -37,8 +46,10 @@ export(actionButton)
|
||||
export(actionLink)
|
||||
export(addResourcePath)
|
||||
export(animationOptions)
|
||||
export(appendTab)
|
||||
export(as.shiny.appobj)
|
||||
export(basicPage)
|
||||
export(bookmarkButton)
|
||||
export(bootstrapLib)
|
||||
export(bootstrapPage)
|
||||
export(br)
|
||||
@@ -54,18 +65,23 @@ export(code)
|
||||
export(column)
|
||||
export(conditionStackTrace)
|
||||
export(conditionalPanel)
|
||||
export(createRenderFunction)
|
||||
export(createWebDependency)
|
||||
export(dataTableOutput)
|
||||
export(dateInput)
|
||||
export(dateRangeInput)
|
||||
export(dblclickOpts)
|
||||
export(debounce)
|
||||
export(dialogViewer)
|
||||
export(diskCache)
|
||||
export(div)
|
||||
export(downloadButton)
|
||||
export(downloadHandler)
|
||||
export(downloadLink)
|
||||
export(em)
|
||||
export(enableBookmarking)
|
||||
export(eventReactive)
|
||||
export(exportTestValues)
|
||||
export(exprToFunction)
|
||||
export(extractStackTrace)
|
||||
export(fileInput)
|
||||
@@ -79,7 +95,13 @@ export(flowLayout)
|
||||
export(fluidPage)
|
||||
export(fluidRow)
|
||||
export(formatStackTrace)
|
||||
export(freezeReactiveVal)
|
||||
export(freezeReactiveValue)
|
||||
export(getCurrentOutputInfo)
|
||||
export(getDefaultReactiveDomain)
|
||||
export(getQueryString)
|
||||
export(getShinyOption)
|
||||
export(getUrlHash)
|
||||
export(h1)
|
||||
export(h2)
|
||||
export(h3)
|
||||
@@ -88,6 +110,7 @@ export(h5)
|
||||
export(h6)
|
||||
export(headerPanel)
|
||||
export(helpText)
|
||||
export(hideTab)
|
||||
export(hoverOpts)
|
||||
export(hr)
|
||||
export(htmlOutput)
|
||||
@@ -102,23 +125,28 @@ export(includeMarkdown)
|
||||
export(includeScript)
|
||||
export(includeText)
|
||||
export(inputPanel)
|
||||
export(insertTab)
|
||||
export(insertUI)
|
||||
export(installExprFunction)
|
||||
export(invalidateLater)
|
||||
export(is.key_missing)
|
||||
export(is.reactive)
|
||||
export(is.reactivevalues)
|
||||
export(is.shiny.appobj)
|
||||
export(is.singleton)
|
||||
export(isRunning)
|
||||
export(isTruthy)
|
||||
export(isolate)
|
||||
export(knit_print.html)
|
||||
export(knit_print.reactive)
|
||||
export(knit_print.shiny.appobj)
|
||||
export(knit_print.shiny.render.function)
|
||||
export(knit_print.shiny.tag)
|
||||
export(knit_print.shiny.tag.list)
|
||||
export(key_missing)
|
||||
export(loadSupport)
|
||||
export(mainPanel)
|
||||
export(makeReactiveBinding)
|
||||
export(markRenderFunction)
|
||||
export(maskReactiveContext)
|
||||
export(memoryCache)
|
||||
export(modalButton)
|
||||
export(modalDialog)
|
||||
export(moduleServer)
|
||||
export(navbarMenu)
|
||||
export(navbarPage)
|
||||
export(navlistPanel)
|
||||
@@ -128,7 +156,15 @@ export(ns.sep)
|
||||
export(numericInput)
|
||||
export(observe)
|
||||
export(observeEvent)
|
||||
export(onBookmark)
|
||||
export(onBookmarked)
|
||||
export(onFlush)
|
||||
export(onFlushed)
|
||||
export(onReactiveDomainEnded)
|
||||
export(onRestore)
|
||||
export(onRestored)
|
||||
export(onSessionEnded)
|
||||
export(onStop)
|
||||
export(outputOptions)
|
||||
export(p)
|
||||
export(pageWithSidebar)
|
||||
@@ -138,6 +174,7 @@ export(passwordInput)
|
||||
export(plotOutput)
|
||||
export(plotPNG)
|
||||
export(pre)
|
||||
export(prependTab)
|
||||
export(printError)
|
||||
export(printStackTrace)
|
||||
export(radioButtons)
|
||||
@@ -150,10 +187,20 @@ export(reactiveTable)
|
||||
export(reactiveText)
|
||||
export(reactiveTimer)
|
||||
export(reactiveUI)
|
||||
export(reactiveVal)
|
||||
export(reactiveValues)
|
||||
export(reactiveValuesToList)
|
||||
export(reactlog)
|
||||
export(reactlogReset)
|
||||
export(reactlogShow)
|
||||
export(registerInputHandler)
|
||||
export(removeInputHandler)
|
||||
export(removeModal)
|
||||
export(removeNotification)
|
||||
export(removeResourcePath)
|
||||
export(removeTab)
|
||||
export(removeUI)
|
||||
export(renderCachedPlot)
|
||||
export(renderDataTable)
|
||||
export(renderImage)
|
||||
export(renderPlot)
|
||||
@@ -163,26 +210,41 @@ export(renderText)
|
||||
export(renderUI)
|
||||
export(repeatable)
|
||||
export(req)
|
||||
export(resourcePaths)
|
||||
export(restoreInput)
|
||||
export(runApp)
|
||||
export(runExample)
|
||||
export(runGadget)
|
||||
export(runGist)
|
||||
export(runGitHub)
|
||||
export(runTests)
|
||||
export(runUrl)
|
||||
export(safeError)
|
||||
export(selectInput)
|
||||
export(selectizeInput)
|
||||
export(serverInfo)
|
||||
export(setBookmarkExclude)
|
||||
export(setProgress)
|
||||
export(setSerializer)
|
||||
export(shinyApp)
|
||||
export(shinyAppDir)
|
||||
export(shinyAppFile)
|
||||
export(shinyOptions)
|
||||
export(shinyServer)
|
||||
export(shinyUI)
|
||||
export(showBookmarkUrlModal)
|
||||
export(showModal)
|
||||
export(showNotification)
|
||||
export(showReactLog)
|
||||
export(showTab)
|
||||
export(sidebarLayout)
|
||||
export(sidebarPanel)
|
||||
export(singleton)
|
||||
export(sizeGrowthRatio)
|
||||
export(sliderInput)
|
||||
export(snapshotExclude)
|
||||
export(snapshotPreprocessInput)
|
||||
export(snapshotPreprocessOutput)
|
||||
export(span)
|
||||
export(splitLayout)
|
||||
export(stopApp)
|
||||
@@ -196,13 +258,20 @@ export(tag)
|
||||
export(tagAppendAttributes)
|
||||
export(tagAppendChild)
|
||||
export(tagAppendChildren)
|
||||
export(tagGetAttribute)
|
||||
export(tagHasAttribute)
|
||||
export(tagList)
|
||||
export(tagSetChildren)
|
||||
export(tags)
|
||||
export(testModule)
|
||||
export(testServer)
|
||||
export(textAreaInput)
|
||||
export(textInput)
|
||||
export(textOutput)
|
||||
export(throttle)
|
||||
export(titlePanel)
|
||||
export(uiOutput)
|
||||
export(updateActionButton)
|
||||
export(updateCheckboxGroupInput)
|
||||
export(updateCheckboxInput)
|
||||
export(updateDateInput)
|
||||
@@ -210,14 +279,21 @@ export(updateDateRangeInput)
|
||||
export(updateNavbarPage)
|
||||
export(updateNavlistPanel)
|
||||
export(updateNumericInput)
|
||||
export(updateQueryString)
|
||||
export(updateRadioButtons)
|
||||
export(updateSelectInput)
|
||||
export(updateSelectizeInput)
|
||||
export(updateSliderInput)
|
||||
export(updateTabsetPanel)
|
||||
export(updateTextAreaInput)
|
||||
export(updateTextInput)
|
||||
export(updateVarSelectInput)
|
||||
export(updateVarSelectizeInput)
|
||||
export(urlModal)
|
||||
export(validate)
|
||||
export(validateCssUnit)
|
||||
export(varSelectInput)
|
||||
export(varSelectizeInput)
|
||||
export(verbatimTextOutput)
|
||||
export(verticalLayout)
|
||||
export(wellPanel)
|
||||
@@ -233,3 +309,11 @@ import(httpuv)
|
||||
import(methods)
|
||||
import(mime)
|
||||
import(xtable)
|
||||
importFrom(fastmap,fastmap)
|
||||
importFrom(fastmap,is.key_missing)
|
||||
importFrom(fastmap,key_missing)
|
||||
importFrom(grDevices,dev.cur)
|
||||
importFrom(grDevices,dev.set)
|
||||
importFrom(promises,"%...!%")
|
||||
importFrom(promises,"%...>%")
|
||||
importFrom(withr,with_options)
|
||||
|
||||
965
NEWS
965
NEWS
@@ -1,965 +0,0 @@
|
||||
shiny 0.13.1
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* `flexCol` did not work on RStudio for Windows or Linux.
|
||||
|
||||
* Fixed #561: DataTables might pop up a warning when the data is updated
|
||||
extremely frequently.
|
||||
|
||||
* Fixed RStudio debugger integration.
|
||||
|
||||
* BREAKING CHANGE: The long-deprecated ability to pass functions (rather than
|
||||
expressions) to reactive() and observe() has finally been removed.
|
||||
|
||||
shiny 0.13.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Fixed #962: plot interactions did not work with the development version of
|
||||
ggplot2 (after ggplot2 1.0.1).
|
||||
|
||||
* Fixed #902: the `drag_drop` plugin of the selectize input did not work.
|
||||
|
||||
* Fixed #933: `updateSliderInput()` does not work when only the label is
|
||||
updated.
|
||||
|
||||
* Multiple imageOutput/plotOutput calls can now share the same brush id. Shiny
|
||||
will ensure that performing a brush operation will clear any other brush with
|
||||
the same id.
|
||||
|
||||
* Added `placeholder` option to `textInput`.
|
||||
|
||||
* Improved support for Unicode characters on Windows (#968).
|
||||
|
||||
* Fixed bug in `selectInput` and `selectizeInput` where values with double
|
||||
quotes were not properly escaped.
|
||||
|
||||
* `runApp()` can now take a path to any .R file that yields a `shinyApp` object;
|
||||
previously, the path had to be a directory that contained an app.R file (or
|
||||
server.R if using separately defined server and UI). Similarly, introduced
|
||||
`shinyAppFile()` function which creates a `shinyApp` object for an .R file
|
||||
path, just as `shinyAppDir()` does for a directory path.
|
||||
|
||||
* Introduced Shiny Modules, which are designed to 1) simplify the reuse of
|
||||
Shiny UI/server logic and 2) make authoring and maintaining complex Shiny
|
||||
apps much easier. See the article linked from `?callModule`.
|
||||
|
||||
* `invalidateLater` and `reactiveTimer` no longer require an explicit `session`
|
||||
argument; the default value uses the current session.
|
||||
|
||||
* Added `session$reload()` method, the equivalent of hitting the browser's
|
||||
Reload button.
|
||||
|
||||
* Added `shiny.autoreload` option, which will automatically cause browsers to
|
||||
reload whenever Shiny app files change on disk. This is intended to shorten
|
||||
the feedback cycle when tweaking UI code.
|
||||
|
||||
* Errors are now printed with stack traces! This should make it tremendously
|
||||
easier to track down the causes of errors in Shiny. Try it by calling
|
||||
`stop("message")` from within an output, reactive, or observer. Shiny itself
|
||||
adds a lot of noise to the call stack, so by default, it attempts to hide all
|
||||
but the relevant levels of the call stack. You can turn off this behavior by
|
||||
setting `options(shiny.fullstacktrace=TRUE)` before or during app startup.
|
||||
|
||||
* Fixed #1018: the selected value of a selectize input is guaranteed to be
|
||||
selected in server-side mode.
|
||||
|
||||
* Added `req` function, which provides a simple way to prevent a reactive,
|
||||
observer, or output from executing until all required inputs and values are
|
||||
available. (Similar functionality has been available for a while using
|
||||
validate/need, but req provides a much simpler and more direct interface.)
|
||||
|
||||
* Improve stability with Shiny Server when many subapps are used, by deferring
|
||||
the loading of subapp iframes until a connection has first been established
|
||||
with the server.
|
||||
|
||||
* Upgrade to Font Awesome 4.5.0.
|
||||
|
||||
* Upgraded to Bootstrap 3.3.5.
|
||||
|
||||
* Switched to an almost-complete build of jQuery UI with the exception of the
|
||||
datepicker extension, which conflicts with Shiny's date picker.
|
||||
|
||||
* Added `fillPage` function, an alternative to `fluidPage`, `fixedPage`, etc.
|
||||
that is designed for apps that fill the entire available page width/height.
|
||||
|
||||
* Added `fillRow` and `fillCol` functions, for laying out proportional grids in
|
||||
`fillPage`. For modern browsers only.
|
||||
|
||||
* Added `runGadget`, `paneViewer`, `dialogViewer`, and `browserViewer`
|
||||
functions to support Shiny Gadgets. More detailed docs about gadgets coming
|
||||
soon.
|
||||
|
||||
* Added support for the new htmltools 0.3 feature `htmlTemplate`. It's now
|
||||
possible to use regular HTML markup to design your UI, but still use R
|
||||
expressions to define inputs, outputs, and HTML widgets.
|
||||
|
||||
shiny 0.12.2
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* GitHub changed URLs for gists from .tar.gz to .zip, so `runGist` was updated
|
||||
to work with the new URLs.
|
||||
|
||||
* Callbacks from the session object are now guaranteed to execute in the order
|
||||
in which registration occurred.
|
||||
|
||||
* Minor bugs in sliderInput's animation behavior have been fixed. (#852)
|
||||
|
||||
* Updated to ion.rangeSlider to 2.0.12.
|
||||
|
||||
* Added `shiny.minified` option, which controls whether the minified version
|
||||
of shiny.js is used. Setting it to FALSe can be useful for debugging. (#826,
|
||||
#850)
|
||||
|
||||
* Fixed an issue for outputting plots from ggplot objects which also have an
|
||||
additional class whose print method takes precedence over `print.ggplot`.
|
||||
(#840, 841)
|
||||
|
||||
* Added `width` option to Shiny's input functions. (#589, #834)
|
||||
|
||||
* Added two alias functions of `updateTabsetPanel()` to update the selected tab:
|
||||
`updateNavbarPage()` and `updateNavlistPanel()`. (#881)
|
||||
|
||||
* All non-base functions are now explicitly namespaced, to pass checks in
|
||||
R-devel.
|
||||
|
||||
* Shiny now correctly handles HTTP HEAD requests. (#876)
|
||||
|
||||
shiny 0.12.1
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Fixed an issue where unbindAll() causes subsequent bindAll() to be ignored for
|
||||
previously bound outputs. (#856)
|
||||
|
||||
* Undeprecate `dataTableOutput` and `renderDataTable`, which had been deprecated
|
||||
in favor of the new DT package. The DT package is a bit too new and has a
|
||||
slightly different API, we were too hasty in deprecating the existing Shiny
|
||||
functions.
|
||||
|
||||
shiny 0.12.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Switched from RJSONIO to jsonlite. This improves consistency and speed when
|
||||
converting between R data structures and JSON. One notable change is that
|
||||
POSIXt objects are now serialized to JSON in UTC8601 format (like
|
||||
"2015-03-20T20:00:00Z"), instead of as seconds from the epoch).
|
||||
|
||||
* In addition to the existing support for clicking and hovering on plots
|
||||
created by base graphics, added support for double-clicking and brushing.
|
||||
(#769)
|
||||
|
||||
* Added support for clicking, hovering, double-clicking, and brushing for
|
||||
plots created by ggplot2, including support for facets. (#802)
|
||||
|
||||
* Added `nearPoints` and `brushedPoints` functions for easily selecting rows of
|
||||
data that are clicked/hovered, or brushed. (#802)
|
||||
|
||||
* Added `shiny.port` option. If this is option is set, `runApp()` will listen on
|
||||
this port by default. (#756)
|
||||
|
||||
* `runUrl`, `runGist`, and `runGitHub` now can save downloaded applications,
|
||||
with the `destdir` argument. (#688)
|
||||
|
||||
* Restored ability to set labels for `selectInput`. (#741)
|
||||
|
||||
* Travis continuous integration now uses Travis's native R support.
|
||||
|
||||
* Fixed encoding issue when the server receives data from the client browser.
|
||||
(#742)
|
||||
|
||||
* The `session` object now has class `ShinySession`, making it easier to test
|
||||
whether an object is indeed a session object. (#720, #746)
|
||||
|
||||
* Fix JavaScript error when an output appears in nested uiOutputs. (Thanks,
|
||||
Gregory Zhang. #749)
|
||||
|
||||
* Eliminate delay on receiving new value when `updateSliderInput(value=...)` is
|
||||
called.
|
||||
|
||||
* Updated to DataTables (Javascript library) 1.10.5.
|
||||
|
||||
* Fixed downloading of files that have no filename extension. (#575, #753)
|
||||
|
||||
* Fixed bug where nested UI outputs broke outputs. (#749, #750)
|
||||
|
||||
* Removed unneeded HTML ID attributes for `checkboxGroupInputs` and
|
||||
`radioButtons`. (#684)
|
||||
|
||||
* Fixed bug where checkboxes were still active even after `Shiny.unbindAll()`
|
||||
was called. (#206)
|
||||
|
||||
* The server side selectize input will load the first 1000 options by default
|
||||
before users start to type and search in the box. (#823)
|
||||
|
||||
* renderDataTable() and dataTableOutput() have been deprecated in shiny and will
|
||||
be removed in future versions of shiny. Please use the DT package instead:
|
||||
http://rstudio.github.io/DT/ (#807)
|
||||
|
||||
shiny 0.11.1
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Major client-side performance improvements for pages that have many
|
||||
conditionalPanels, tabPanels, and plotOutputs. (#693, #717, #723)
|
||||
|
||||
* `tabPanel`s now use the `title` for `value` by default. This fixes a bug
|
||||
in which an icon in the title caused problems with a conditionalPanel's test
|
||||
condition. (#725, #728)
|
||||
|
||||
* `selectInput` now has a `size` argument to control the height of the input
|
||||
box. (#729)
|
||||
|
||||
* `navbarPage` no longer includes a first row of extra whitespace when
|
||||
`header=NULL`. (#722)
|
||||
|
||||
* `selectInput`s now use Bootstrap styling when `selectize=FALSE`. (#724)
|
||||
|
||||
* Better vertical spacing of label for checkbox groups and radio buttons.
|
||||
|
||||
* `selectInput` correctly uses width for both selectize and non-selectize
|
||||
inputs. (#702)
|
||||
|
||||
* The wrapper tag generated by `htmlOutput` and `uiOutput` can now be any type
|
||||
of HTML tag, instead of just span and div. Also, custom classes are now
|
||||
allowed on the tag. (#704)
|
||||
|
||||
* Slider problems in IE 11 and Chrome on touchscreen-equipped Windows computers
|
||||
have been fixed. (#700)
|
||||
|
||||
* Sliders now work correctly with draggable panels. (#711)
|
||||
|
||||
* Fixed arguments in `fixedPanel`. (#709)
|
||||
|
||||
* downloadHandler content callback functions are now invoked with a temp file
|
||||
name that has the same extension as the final filename that will be used by
|
||||
the download. This is to deal with the fact that some file writing functions
|
||||
in R will auto-append the extension for their file type (pdf, zip).
|
||||
|
||||
shiny 0.11
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Changed sliders from jquery-slider to ion.rangeSlider. These sliders have
|
||||
an improved appearance, support updating more properties from the server,
|
||||
and can be controlled with keyboard input.
|
||||
|
||||
* Switched from Bootstrap 2 to Bootstrap 3. For most users, this will work
|
||||
seamlessly, but some users may need to use the shinybootstrap2 package for
|
||||
backward compatibility.
|
||||
|
||||
* The UI of a Shiny app can now have a body tag. This is useful for CSS
|
||||
templates that use classes on the body tag.
|
||||
|
||||
* `actionButton` and `actionLink` now pass their `...` arguments to the
|
||||
underlying tag function. (#607)
|
||||
|
||||
* Added `observeEvent` and `eventReactive` functions for clearer, more concise
|
||||
handling of `actionButton`, plot clicks, and other naturally-imperative
|
||||
inputs.
|
||||
|
||||
* Errors that happen in reactives no longer prevent any remaining pending
|
||||
observers from executing. It is also now possible for users to control how
|
||||
errors are handled, with the 'shiny.observer.error' global option. (#603,
|
||||
#604)
|
||||
|
||||
* Added an `escape` argument to `renderDataTable()` to escape the HTML entities
|
||||
in the data table for security reasons. This might break tables from previous
|
||||
versions of shiny that use raw HTML in the table content, and the old behavior
|
||||
can be brought back by `escape = FALSE` if you are aware of the security
|
||||
implications. (#627)
|
||||
|
||||
* Changed the URI encoding/decoding functions internally to use `encodeURI()`,
|
||||
`encodeURIComponent()`, and `decodeURIComponent()` from the httpuv package
|
||||
instead of `utils::URLencode()` and `utils::URLdecode()`. (#630)
|
||||
|
||||
* Shiny's web assets are now minified.
|
||||
|
||||
* The default reactive domain is now available in event handler functions. (#669)
|
||||
|
||||
* Password input fields can now be used, with `passwordInput()`. (#672)
|
||||
|
||||
shiny 0.10.2.2
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Remove use of `rstudio::viewer` in a code example, for R CMD check.
|
||||
|
||||
shiny 0.10.2.1
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Changed some examples to use \donttest instead of \dontrun.
|
||||
|
||||
shiny 0.10.2
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* The minimal version of R required for the shiny package is 3.0.0 now.
|
||||
|
||||
* Shiny apps can now consist of a single file, app.R, instead of ui.R and
|
||||
server.R.
|
||||
|
||||
* Upgraded DataTables from 1.9.4 to 1.10.2. This might be a breaking change if
|
||||
you have customized the DataTables options in your apps. (More info:
|
||||
https://github.com/rstudio/shiny/pull/558)
|
||||
|
||||
* File uploading via `fileInput()` works for Internet Explorer 8 and 9 now. Note
|
||||
IE8/9 do not support multiple files from a single file input. If you need to
|
||||
upload multiple files, you have to use one file input for each file.
|
||||
|
||||
* Switched away from reference classes to R6.
|
||||
|
||||
* Reactive log performance has been greatly improved.
|
||||
|
||||
* Added `Progress` and `withProgress`, to display the progress of computation
|
||||
on the client browser.
|
||||
|
||||
* Fixed #557: updateSelectizeInput(choices, server = TRUE) did not work when
|
||||
`choices` is a character vector.
|
||||
|
||||
* Searching in DataTables is case-insensitive and the search strings are not
|
||||
treated as regular expressions by default now. If you want case-sensitive
|
||||
searching or regular expressions, you can use the configuration options
|
||||
`search$caseInsensitive` and `search$regex`, e.g. `renderDataTable(...,
|
||||
options = list(search = list(caseInsensitve = FALSE, regex = TRUE)))`.
|
||||
|
||||
* Added support for `htmltools::htmlDependency`'s new `attachment` parameter to
|
||||
`renderUI`/`uiOutput`.
|
||||
|
||||
* Exported `createWebDependency`. It takes an `htmltools::htmlDependency` object
|
||||
and makes it available over Shiny's built-in web server.
|
||||
|
||||
* Custom output bindings can now render `htmltools::htmlDependency` objects at
|
||||
runtime using `Shiny.renderDependencies()`.
|
||||
|
||||
* Fixes to rounding behavior of sliderInput. (#301, #502)
|
||||
|
||||
* Updated selectize.js to version 0.11.2. (#596)
|
||||
* Added `position` parameter to `navbarPage`.
|
||||
|
||||
shiny 0.10.1
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Added Unicode support for Windows. Shiny apps running on Windows must use the
|
||||
UTF-8 encoding for ui.R and server.R (also the optional global.R) if they
|
||||
contain non-ASCII characters. See this article for details and examples:
|
||||
http://shiny.rstudio.com/gallery/unicode-characters.html (#516)
|
||||
|
||||
* `runGitHub()` also allows the 'username/repo' syntax now, which is equivalent
|
||||
to `runGitHub('repo', 'username')`. (#427)
|
||||
|
||||
* `navbarPage()` now accepts a `windowTitle` parameter to set the web browser
|
||||
page title to something other than the title displayed in the navbar.
|
||||
|
||||
* Added an `inline` argument to `textOutput()`, `imageOutput()`, `plotOutput()`,
|
||||
and `htmlOutput()`. When `inline = TRUE`, these outputs will be put in
|
||||
`span()` instead of the default `div()`. This occurs automatically when these
|
||||
outputs are created via the inline expressions (e.g. `r renderText(expr)`) in
|
||||
R Markdown documents. See an R Markdown example at
|
||||
http://shiny.rstudio.com/gallery/inline-output.html (#512)
|
||||
|
||||
* Added support for option groups in the select/selectize inputs. When the
|
||||
`choices` argument for `selectInput()`/`selectizeInput()` is a list of
|
||||
sub-lists and any sub-list is of length greater than 1, the HTML tag
|
||||
`<optgroup>` will be used. See an example at
|
||||
http://shiny.rstudio.com/gallery/option-groups-for-selectize-input.html (#542)
|
||||
|
||||
shiny 0.10.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* BREAKING CHANGE: By default, observers now terminate themselves if they were
|
||||
created during a session and that session ends. See ?domains for more details.
|
||||
|
||||
* Shiny can now be used in R Markdown v2 documents, to create "Shiny Docs":
|
||||
reports and presentations that combine narrative, statically computed output,
|
||||
and fully dynamic inputs and outputs. For more info, including examples, see
|
||||
http://rmarkdown.rstudio.com/authoring_shiny.html.
|
||||
|
||||
* The `session` object that can be passed into a server function (e.g.
|
||||
shinyServer(function(input, output, session) {...})) is now documented: see
|
||||
`?session`.
|
||||
|
||||
* Most inputs can now accept `NULL` label values to omit the label altogether.
|
||||
|
||||
* New `actionLink` input control; like `actionButton`, but with the appearance
|
||||
of a normal link.
|
||||
|
||||
* `renderPlot` now calls `print` on its result if it's visible (i.e. no more
|
||||
explicit `print()` required for ggplot2).
|
||||
|
||||
* Introduced Shiny app objects (see `?shinyApp`). These essentially replace the
|
||||
little-advertised ability for `runApp` to take a `list(ui=..., server=...)`
|
||||
as the first argument instead of a directory (though that ability remains for
|
||||
backward compatibility). Unlike those lists, Shiny app objects are tagged with
|
||||
class `shiny.appobj` so they can be run simply by printing them.
|
||||
|
||||
* Added `maskReactiveContext` function. It blocks the current reactive context,
|
||||
to evaluate expressions that shouldn't use reactive sources directly. (This
|
||||
should not be commonly needed.)
|
||||
|
||||
* Added `flowLayout`, `splitLayout`, and `inputPanel` functions for putting UI
|
||||
elements side by side. `flowLayout` lays out its children in a left-to-right,
|
||||
top-to-bottom arrangement. `splitLayout` evenly divides its horizontal space
|
||||
among its children (or unevenly divides if `cellWidths` argument is provided).
|
||||
`inputPanel` is like `flowPanel`, but with a light grey background, and is
|
||||
intended to be used to encapsulate small input controls wherever vertical
|
||||
space is at a premium.
|
||||
|
||||
* Added `serverInfo` to obtain info about the Shiny Server if the app is served
|
||||
through it.
|
||||
|
||||
* Added an `inline` argument (TRUE/FALSE) in `checkboxGroupInput()` and
|
||||
`radioButtons()` to allow the horizontal layout (inline = TRUE) of checkboxes
|
||||
or radio buttons. (Thanks, @saurfang, #481)
|
||||
|
||||
* `sliderInput` and `selectizeInput`/`selectInput` now use a standard horizontal
|
||||
size instead of filling up all available horizontal space. Pass `width="100%"`
|
||||
explicitly for the old behavior.
|
||||
|
||||
* Added the `updateSelectizeInput()` function to make it possible to process
|
||||
searching on the server side (i.e. using R), which can be much faster than the
|
||||
client side processing (i.e. using HTML and JavaScript). See the article at
|
||||
http://shiny.rstudio.com/articles/selectize.html for a detailed introduction.
|
||||
|
||||
* Fixed a bug of renderDataTable() when the data object only has 1 row and 1
|
||||
column. (Thanks, ZJ Dai, #429)
|
||||
|
||||
* `renderPrint` gained a new argument 'width' to control the width of the text
|
||||
output, e.g. renderPrint({mtcars}, width = 40).
|
||||
|
||||
* Fixed #220: the zip file for a directory created by some programs may not have
|
||||
the directory name as its first entry, in which case runUrl() can fail. (#220)
|
||||
|
||||
* `runGitHub()` can also take a value of the form "username/repo" in its first
|
||||
argument, e.g. both runGitHub("shiny_example", "rstudio") and
|
||||
runGitHub("rstudio/shiny_example") are valid ways to run the GitHub repo.
|
||||
|
||||
shiny 0.9.1
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Fixed warning 'Error in Context$new : could not find function "loadMethod"'
|
||||
that was happening to dependent packages on "R CMD check".
|
||||
|
||||
shiny 0.9.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* BREAKING CHANGE: Added a `host` parameter to runApp() and runExample(),
|
||||
which defaults to the shiny.host option if it is non-NULL, or "127.0.0.1"
|
||||
otherwise. This means that by default, Shiny applications can only be
|
||||
accessed on the same machine from which they are served. To allow other
|
||||
clients to connect, as in previous versions of Shiny, use "0.0.0.0"
|
||||
(or the IP address of one of your network interfaces, if you care to be
|
||||
explicit about it).
|
||||
|
||||
* Added a new function `selectizeInput()` to use the JavaScript library
|
||||
selectize.js (https://github.com/brianreavis/selectize.js), which extends
|
||||
the basic select input in many aspects.
|
||||
|
||||
* The `selectInput()` function also gained a new argument `selectize = TRUE`
|
||||
to makes use of selectize.js by default. If you want to revert back to the
|
||||
original select input, you have to call selectInput(..., selectize = FALSE).
|
||||
|
||||
* Added Showcase mode, which displays the R code for an app right in the app
|
||||
itself. You can invoke Showcase mode by passing `display.mode="showcase"`
|
||||
to the `runApp()` function. Or, if an app is designed to run in Showcase
|
||||
mode by default, add a DESCRIPTION file in the app dir with Title, Author,
|
||||
and License fields; with "Type: Shiny"; and with "DisplayMode: Showcase".
|
||||
|
||||
* Upgraded to Bootstrap 2.3.2 and jQuery 1.11.0.
|
||||
|
||||
* Make `tags$head()` and `singleton()` behave correctly when used with
|
||||
`renderUI()` and `uiOutput()`. Previously, "hoisting content to the head"
|
||||
and "only rendering items a single time" were features that worked only
|
||||
when the page was initially loading, not in dynamic rendering.
|
||||
|
||||
* Files are now sourced with the `keep.source` option, to help with debugging
|
||||
and profiling.
|
||||
|
||||
* Support user-defined input parsers for data coming in from JavaScript using
|
||||
the parseShinyInput method.
|
||||
|
||||
* Fixed the bug #299: renderDataTable() can deal with 0-row data frames now.
|
||||
(reported by Harlan Harris)
|
||||
|
||||
* Added `navbarPage()` and `navbarMenu()` functions to create applications
|
||||
with multiple top level panels.
|
||||
|
||||
* Added `navlistPanel()` function to create layouts with a a bootstrap
|
||||
navlist on the left and tabPanels on the right
|
||||
|
||||
* Added `type` parameter to `tabsetPanel()` to enable the use of pill
|
||||
style tabs in addition to the standard ones.
|
||||
|
||||
* Added `position` paramter to `tabsetPanel()` to enable positioning of tabs
|
||||
above, below, left, or right of tab content.
|
||||
|
||||
* Added `fluidPage()` and `fixedPage()` functions as well as related row and
|
||||
column layout functions for creating arbitrary bootstrap grid layouts.
|
||||
|
||||
* Added `hr()` builder function for creating horizontal rules.
|
||||
|
||||
* Automatically concatenate duplicate attributes in tag definitions
|
||||
|
||||
* Added `responsive` parameter to page building functions for opting-out of
|
||||
bootstrap responsive css.
|
||||
|
||||
* Added `theme` parameter to page building functions for specifying alternate
|
||||
bootstrap css styles.
|
||||
|
||||
* Added `icon()` function for embedding icons from the
|
||||
[font awesome](http://fontawesome.io/) icon library
|
||||
|
||||
* Added `makeReactiveBinding` function to turn a "regular" variable into a
|
||||
reactive one (i.e. reading the variable makes the current reactive context
|
||||
dependent on it, and setting the variable is a source of reactivity).
|
||||
|
||||
* Added a function `withMathJax()` to include the MathJax library in an app.
|
||||
|
||||
* The argument `selected` in checkboxGroupInput(), selectInput(), and
|
||||
radioButtons() refers to the value(s) instead of the name(s) of the
|
||||
argument `choices` now. For example, the value of the `selected` argument
|
||||
in selectInput(..., choices = c('Label 1' = 'x1', 'Label 2' = 'x2'),
|
||||
selected = 'Label 2') must be updated to 'x2', although names/labels will
|
||||
be automatically converted to values internally for backward
|
||||
compatibility. The same change applies to updateCheckboxGroupInput(),
|
||||
updateSelectInput(), and updateRadioButtons() as well. (#340)
|
||||
|
||||
* Now it is possible to only update the value of a checkbox group, select input,
|
||||
or radio buttons using the `selected` argument without providing the
|
||||
`choices` argument in updateCheckboxGroupInput(), updateSelectInput(), and
|
||||
updateRadioButtons(), respectively. (#340)
|
||||
|
||||
* Added `absolutePanel` and `fixedPanel` functions for creating absolute-
|
||||
and fixed-position panels. They can be easily made user-draggable by
|
||||
specifying `draggable = TRUE`.
|
||||
|
||||
* For the `options` argument of the function `renderDataTable()`, we can
|
||||
pass literal JavaScript code to the DataTables library via `I()`. This
|
||||
makes it possible to use any JavaScript object in the options, e.g. a
|
||||
JavaScript function (which is not supported in JSON). See
|
||||
`?renderDataTable` for details and examples.
|
||||
|
||||
* DataTables also works under IE8 now.
|
||||
|
||||
* Fixed a bug in DataTables pagination when searching is turned on, which
|
||||
caused failures for matrices as well as empty rows when displaying data
|
||||
frames using renderDataTable().
|
||||
|
||||
* The `options` argument in `renderDataTable()` can also take a function
|
||||
that returns a list. This makes it possible to use reactive values in the
|
||||
options. (#392)
|
||||
|
||||
* `renderDataTable()` respects more DataTables options now: (1) either
|
||||
bPaginate = FALSE or iDisplayLength = -1 will disable pagination (i.e. all
|
||||
rows are returned from the data); besides, this means we can also use -1
|
||||
in the length menu, e.g. aLengthMenu = list(c(10, 30, -1), list(10, 30,
|
||||
'All')); (2) we can disable searching for individual columns through the
|
||||
bSearchable option, e.g. aoColumns = list(list(bSearchable = FALSE),
|
||||
list(bSearchable = TRUE),...) (the search box for the first column is
|
||||
hidden); (3) we can turn off searching entirely (for both global searching
|
||||
and individual columns) using the option bFilter = FALSE.
|
||||
|
||||
* Added an argument `callback` in `renderDataTable()` so that a custom
|
||||
JavaScript function can be applied to the DataTable object. This makes it
|
||||
much easier to use DataTables plug-ins.
|
||||
|
||||
* For numeric columns in a DataTable, the search boxes support lower and
|
||||
upper bounds now: a search query of the form "lower,upper" (without
|
||||
quotes) indicates the limits [lower, upper]. For a column X, this means
|
||||
the rows corresponding to X >= lower & X <= upper are returned. If we omit
|
||||
either the lower limit or the upper limit, only the other limit will be
|
||||
used, e.g. ",upper" means X <= upper.
|
||||
|
||||
* `updateNumericInput(value)` tries to preserve numeric precision by avoiding
|
||||
scientific notation when possible, e.g. 102145 is no longer rounded to
|
||||
1.0214e+05 = 102140. (Thanks, Martin Loos. #401)
|
||||
|
||||
* `sliderInput()` no longer treats a label wrapped in HTML() as plain text,
|
||||
e.g. the label in sliderInput(..., label = HTML('<em>A Label</em>')) will
|
||||
not be escaped any more. (#119)
|
||||
|
||||
* Fixed #306: the trailing slash in a path could fail `addResourcePath()`
|
||||
under Windows. (Thanks, ZJ Dai)
|
||||
|
||||
* Dots are now legal characters for inputId/outputId. (Thanks, Kevin
|
||||
Lindquist. #358)
|
||||
|
||||
shiny 0.8.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Debug hooks are registered on all user-provided functions and (reactive)
|
||||
expressions (e.g., in renderPlot()), which makes it possible to set
|
||||
breakpoints in these functions using the latest version of the RStudio
|
||||
IDE, and the RStudio visual debugging tools can be used to debug Shiny
|
||||
apps. Internally, the registration is done via installExprFunction(),
|
||||
which is a new function introduced in this version to replace
|
||||
exprToFunction() so that the registration can be automatically done.
|
||||
|
||||
* Added a new function renderDataTable() to display tables using the
|
||||
JavaScript library DataTables. It includes basic features like pagination,
|
||||
searching (global search or search by individual columns), sorting (by
|
||||
single or multiple columns). All these features are implemented on the R
|
||||
side; for example, we can use R regular expressions for searching.
|
||||
Besides, it also uses the Bootstrap CSS style. See the full
|
||||
documentation and examples in the tutorial:
|
||||
http://rstudio.github.io/shiny/tutorial/#datatables
|
||||
|
||||
* Added a new option `shiny.error` which can take a function as an error
|
||||
handler. It is called when an error occurs in an app (in user-provided
|
||||
code), e.g., after we set options(shiny.error = recover), we can enter a
|
||||
specified environment in the call stack to debug our code after an error
|
||||
occurs.
|
||||
|
||||
* The argument `launch.browser` in runApp() can also be a function,
|
||||
which takes the URL of the shiny app as its input value.
|
||||
|
||||
* runApp() uses a random port between 3000 and 8000 instead of 8100 now. It
|
||||
will try up to 20 ports in case certain ports are not available.
|
||||
|
||||
* Fixed a bug for conditional panels: the value `input.id` in the condition
|
||||
was not correctly retrieved when the input widget had a type, such as
|
||||
numericInput(). (reported by Jason Bryer)
|
||||
|
||||
* Fixed two bugs in plotOutput(); clickId and hoverId did not give correct
|
||||
coordinates in Firefox, or when the axis limits of the plot were changed.
|
||||
(reported by Chris Warth and Greg D)
|
||||
|
||||
* The minimal required version for the httpuv package was increased to 1.2
|
||||
(on CRAN now).
|
||||
|
||||
|
||||
shiny 0.7.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Stopped sending websocket subprotocol. This fixes a compatibility issue with
|
||||
Google Chrome 30.
|
||||
|
||||
* The `input` and `output` objects are now also accessible via `session$input`
|
||||
and `session$output`.
|
||||
|
||||
* Added click and hover events for static plots; see `?plotOutput` for details.
|
||||
|
||||
* Added optional logging of the execution states of a reactive program, and
|
||||
tools for visualizing the log data. To use, start a new R session and call
|
||||
`options(shiny.reactlog=TRUE)`. Then launch a Shiny app and interact with it.
|
||||
Press Ctrl+F3 (or for Mac, Cmd+F3) in the browser to launch an interactive
|
||||
visualization of the reactivity that has occurred. See `?showReactLog` for
|
||||
more information.
|
||||
|
||||
* Added `includeScript()` and `includeCSS()` functions.
|
||||
|
||||
* Reactive expressions now have class="reactive" attribute. Also added
|
||||
`is.reactive()` and `is.reactivevalues()` functions.
|
||||
|
||||
* New `stopApp()` function, which stops an app and returns a value to the caller
|
||||
of `runApp()`.
|
||||
|
||||
* Added the `shiny.usecairo` option, which can be used to tell Shiny not to use
|
||||
Cairo for PNG output even when it is installed. (Defaults to `TRUE`.)
|
||||
|
||||
* Speed increases for `selectInput()` and `radioButtons()`, and their
|
||||
corresponding updater functions, for when they have many options.
|
||||
|
||||
* Added `tagSetChildren()` and `tagAppendChildren()` functions.
|
||||
|
||||
* The HTTP request object that created the websocket is now accessible from the
|
||||
`session` object, as `session$request`. This is a Rook-like request
|
||||
environment that can be used to access HTTP headers, among other things.
|
||||
(Note: When running in a Shiny Server environment, the request will reflect
|
||||
the proxy HTTP request that was made from the Shiny Server process to the R
|
||||
process, not the request that was made from the web browser to Shiny Server.)
|
||||
|
||||
* Fix `getComputedStyle` issue, for IE8 browser compatibility (#196). Note:
|
||||
Shiny Server is still required for IE8/9 compatibility.
|
||||
|
||||
* Add shiny.sharedSecret option, to require the HTTP header Shiny-Shared-Secret
|
||||
to be set to the given value.
|
||||
|
||||
shiny 0.6.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* `tabsetPanel()` can be directed to start with a specific tab selected.
|
||||
|
||||
* Fix bug where multiple file uploads with 3 or more files result in incorrect
|
||||
data.
|
||||
|
||||
* Add `withTags()` function.
|
||||
|
||||
* Add dateInput and dateRangeInput.
|
||||
|
||||
* `shinyServer()` now takes an optional `session` argument, which is used for
|
||||
communication with the session object.
|
||||
|
||||
* Add functions to update values of existing inputs on a page, instead of
|
||||
replacing them entirely.
|
||||
|
||||
* Allow listening on domain sockets.
|
||||
|
||||
* Added `actionButton()` to Shiny.
|
||||
|
||||
* The server can now send custom JSON messages to the client. On the client
|
||||
side, functions can be registered to handle these messages.
|
||||
|
||||
* Callbacks can be registered to be called at the end of a client session.
|
||||
|
||||
* Add ability to set priority of observers and outputs. Each priority level
|
||||
gets its own queue.
|
||||
|
||||
* Fix bug where the presence of a submit button would prevent sending of
|
||||
metadata until the button was clicked.
|
||||
|
||||
* `reactiveTimer()` and `invalidateLater()` by default no longer invalidate
|
||||
reactive objects after the client session has closed.
|
||||
|
||||
* Shiny apps can be run without a server.r and ui.r file.
|
||||
|
||||
shiny 0.5.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Switch from websockets package for handling websocket connections to httpuv.
|
||||
|
||||
* New method for detecting hidden output objects. Instead of checking that
|
||||
height and width are 0, it checks that the object or any ancestor in the DOM
|
||||
has style display:none.
|
||||
|
||||
* Add `clientData` reactive values object, which carries information about the
|
||||
client. This includes the hidden status of output objects, height/width plot
|
||||
output objects, and the URL of the browser.
|
||||
|
||||
* Add `parseQueryString()` function.
|
||||
|
||||
* Add `renderImage()` function for sending arbitrary image files to the client,
|
||||
and its counterpart, `imageOutput()`.
|
||||
|
||||
* Add support for high-resolution (Retina) displays.
|
||||
|
||||
* Fix bug #55, where `renderTable()` would throw error with an empty data frame.
|
||||
|
||||
shiny 0.4.1
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Fix bug where width and height weren't passed along properly from
|
||||
`reactivePlot` to `renderPlot`.
|
||||
|
||||
* Fix bug where infinite recursion would happen when `reactivePlot` was passed
|
||||
a function for width or height.
|
||||
|
||||
shiny 0.4.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Added suspend/resume capability to observers.
|
||||
|
||||
* Output objects are automatically suspended when they are hidden on the user's
|
||||
web browser.
|
||||
|
||||
* `runGist()` accepts GitHub's new URL format, which includes the username.
|
||||
|
||||
* `reactive()` and `observe()` now take expressions instead of functions.
|
||||
|
||||
* `reactiveText()`, `reactivePlot()`, and so on, have been renamed to
|
||||
`renderText()`, `renderPlot()`, etc. They also now take expressions instead
|
||||
of functions.
|
||||
|
||||
* Fixed a bug where empty values in a numericInput were sent to the R process
|
||||
as 0. They are now sent as NA.
|
||||
|
||||
shiny 0.3.1
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Fix issue #91: bug where downloading files did not work.
|
||||
|
||||
* Add [[<- operator for shinyoutput object, making it possible to assign values
|
||||
with `output[['plot1']] <- ...`.
|
||||
|
||||
* Reactive functions now preserve the visible/invisible state of their returned
|
||||
values.
|
||||
|
||||
shiny 0.3.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Reactive functions are now evaluated lazily.
|
||||
|
||||
* Add `reactiveValues()`.
|
||||
|
||||
* Using `as.list()` to convert a reactivevalues object (like `input`) to a list
|
||||
is deprecated. The new function `reactiveValuesToList()` should be used
|
||||
instead.
|
||||
|
||||
* Add `isolate()`. This function is used for accessing reactive functions,
|
||||
without them invalidating their parent contexts.
|
||||
|
||||
* Fix issue #58: bug where reactive functions are not re-run when all items in
|
||||
a checkboxGroup are unchecked.
|
||||
|
||||
* Fix issue #71, where `reactiveTable()` would return blank if the first
|
||||
element of a data frame was NA.
|
||||
|
||||
* In `plotOutput`, better validation for CSS units when specifying width and
|
||||
height.
|
||||
|
||||
* `reactivePrint()` no longer displays invisible output.
|
||||
|
||||
* `reactiveText()` no longer displays printed output, only the return value
|
||||
from a function.
|
||||
|
||||
* The `runGitHub()` and `runUrl()` functions have been added, for running
|
||||
Shiny apps from GitHub repositories and zip/tar files at remote URLs.
|
||||
|
||||
* Fix issue #64, where pressing Enter in a textbox would cause a form to
|
||||
submit.
|
||||
|
||||
shiny 0.2.4
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* `runGist` has been updated to use the new download URLs from
|
||||
https://gist.github.com.
|
||||
|
||||
* Shiny now uses `CairoPNG()` for output, when the Cairo package is available.
|
||||
This provides better-looking output on Linux and Windows.
|
||||
|
||||
shiny 0.2.3
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Ignore request variables for routing purposes
|
||||
|
||||
shiny 0.2.2
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Fix CRAN warning (assigning to global environment)
|
||||
|
||||
|
||||
shiny 0.2.1
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* [BREAKING] Modify API of `downloadHandler`: The `content` function now takes
|
||||
a file path, not writable connection, as an argument. This makes it much
|
||||
easier to work with APIs that only write to file paths, not connections.
|
||||
|
||||
|
||||
shiny 0.2.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Fix subtle name resolution bug--the usual symptom being S4 methods not being
|
||||
invoked correctly when called from inside of ui.R or server.R
|
||||
|
||||
|
||||
shiny 0.1.14
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Fix slider animator, which broke in 0.1.10
|
||||
|
||||
|
||||
shiny 0.1.13
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Fix temp file leak in reactivePlot
|
||||
|
||||
|
||||
shiny 0.1.12
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Fix problems with runGist on Windows
|
||||
* Add feature for on-the-fly file downloads (e.g. CSV data, PDFs)
|
||||
* Add CSS hooks for app-wide busy indicators
|
||||
|
||||
|
||||
shiny 0.1.11
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Fix input binding with IE8 on Shiny Server
|
||||
* Fix issue #41: reactiveTable should allow print options too
|
||||
* Allow dynamic sizing of reactivePlot (i.e. using a function instead of a fixed
|
||||
value)
|
||||
|
||||
|
||||
shiny 0.1.10
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Support more MIME types when serving out of www
|
||||
* Fix issue #35: Allow modification of untar args
|
||||
* headerPanel can take an explicit window title parameter
|
||||
* checkboxInput uses correct attribute `checked` instead of `selected`
|
||||
* Fix plot rendering with IE8 on Shiny Server
|
||||
|
||||
|
||||
shiny 0.1.9
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Much less flicker when updating plots
|
||||
* More customizable error display
|
||||
* Add `includeText`, `includeHTML`, and `includeMarkdown` functions for putting
|
||||
text, HTML, and Markdown content from external files in the application's UI.
|
||||
|
||||
|
||||
shiny 0.1.8
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Add `runGist` function for conveniently running a Shiny app that is published
|
||||
on gist.github.com.
|
||||
* Fix issue #27: Warnings cause reactive functions to stop executing.
|
||||
* The server.R and ui.R filenames are now case insensitive.
|
||||
* Add `wellPanel` function for creating inset areas on the page.
|
||||
* Add `bootstrapPage` function for creating new Bootstrap based
|
||||
layouts from scratch.
|
||||
|
||||
|
||||
shiny 0.1.7
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Fix issue #26: Shiny.OutputBindings not correctly exported.
|
||||
* Add `repeatable` function for making easily repeatable versions of random
|
||||
number generating functions.
|
||||
* Transcode JSON into UTF-8 (prevents non-ASCII reactivePrint values from
|
||||
causing errors on Windows).
|
||||
|
||||
|
||||
shiny 0.1.6
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Import package dependencies, instead of attaching them (with the exception of
|
||||
websockets, which doesn't currently work unless attached).
|
||||
* conditionalPanel was animated, now it is not.
|
||||
* bindAll was not correctly sending initial values to the server; fixed.
|
||||
|
||||
|
||||
shiny 0.1.5
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* BREAKING CHANGE: JS APIs Shiny.bindInput and Shiny.bindOutput removed and
|
||||
replaced with Shiny.bindAll; Shiny.unbindInput and Shiny.unbindOutput removed
|
||||
and replaced with Shiny.unbindAll.
|
||||
* Add file upload support (currently only works with Chrome and Firefox). Use
|
||||
a normal HTML file input, or call the `fileInput` UI function.
|
||||
* Shiny.unbindOutputs did not work, now it does.
|
||||
* Generally improved robustness of dynamic input/output bindings.
|
||||
* Add conditionalPanel UI function to allow showing/hiding UI based on a JS
|
||||
expression; for example, whether an input is a particular value. Also works in
|
||||
raw HTML (add the `data-display-if` attribute to the element that should be
|
||||
shown/hidden).
|
||||
* htmlOutput (CSS class `shiny-html-output`) can contain inputs and outputs.
|
||||
|
||||
|
||||
shiny 0.1.4
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Allow Bootstrap tabsets to act as reactive inputs; their value indicates which
|
||||
tab is active
|
||||
* Upgrade to Bootstrap 2.1
|
||||
* Add `checkboxGroupInput` control, which presents a list of checkboxes and
|
||||
returns a vector of the selected values
|
||||
* Add `addResourcePath`, intended for reusable component authors to access CSS,
|
||||
JavaScript, image files, etc. from their package directories
|
||||
* Add Shiny.bindInputs(scope), .unbindInputs(scope), .bindOutputs(scope), and
|
||||
.unbindOutputs(scope) JS API calls to allow dynamic binding/unbinding of HTML
|
||||
elements
|
||||
|
||||
|
||||
shiny 0.1.3
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Introduce Shiny.inputBindings.register JS API and InputBinding class, for
|
||||
creating custom input controls
|
||||
* Add `step` parameter to numericInput
|
||||
* Read names of input using `names(input)`
|
||||
* Access snapshot of input as a list using `as.list(input)`
|
||||
* Fix issue #10: Plots in tabsets not rendered
|
||||
|
||||
|
||||
shiny 0.1.2
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Initial private beta release!
|
||||
264
R/app.R
264
R/app.R
@@ -3,36 +3,47 @@
|
||||
#' Create a Shiny app object
|
||||
#'
|
||||
#' These functions create Shiny app objects from either an explicit UI/server
|
||||
#' pair (\code{shinyApp}), or by passing the path of a directory that contains a
|
||||
#' Shiny app (\code{shinyAppDir}). You generally shouldn't need to use these
|
||||
#' functions to create/run applications; they are intended for interoperability
|
||||
#' purposes, such as embedding Shiny apps inside a \pkg{knitr} document.
|
||||
#' pair (`shinyApp`), or by passing the path of a directory that contains a
|
||||
#' Shiny app (`shinyAppDir`).
|
||||
#'
|
||||
#' Normally when this function is used at the R console, the Shiny app object is
|
||||
#' automatically passed to the \code{print()} function, which runs the app. If
|
||||
#' automatically passed to the `print()` function, which runs the app. If
|
||||
#' this is called in the middle of a function, the value will not be passed to
|
||||
#' \code{print()} and the app will not be run. To make the app run, pass the app
|
||||
#' object to \code{print()} or \code{\link{runApp}()}.
|
||||
#' `print()` and the app will not be run. To make the app run, pass the app
|
||||
#' object to `print()` or [runApp()].
|
||||
#'
|
||||
#' @param ui The UI definition of the app (for example, a call to
|
||||
#' \code{fluidPage()} with nested controls)
|
||||
#' @param server A server function
|
||||
#' `fluidPage()` with nested controls).
|
||||
#'
|
||||
#' If bookmarking is enabled (see `enableBookmarking`), this must be
|
||||
#' a single argument function that returns the UI definition.
|
||||
#' @param server A function with three parameters: `input`, `output`, and
|
||||
#' `session`. The function is called once for each session ensuring that each
|
||||
#' app is independent.
|
||||
#' @param onStart A function that will be called before the app is actually run.
|
||||
#' This is only needed for \code{shinyAppObj}, since in the \code{shinyAppDir}
|
||||
#' case, a \code{global.R} file can be used for this purpose.
|
||||
#' @param options Named options that should be passed to the `runApp` call. You
|
||||
#' can also specify \code{width} and \code{height} parameters which provide a
|
||||
#' hint to the embedding environment about the ideal height/width for the app.
|
||||
#' @param uiPattern A regular expression that will be applied to each \code{GET}
|
||||
#' request to determine whether the \code{ui} should be used to handle the
|
||||
#' This is only needed for `shinyAppObj`, since in the `shinyAppDir`
|
||||
#' case, a `global.R` file can be used for this purpose.
|
||||
#' @param options Named options that should be passed to the `runApp` call
|
||||
#' (these can be any of the following: "port", "launch.browser", "host", "quiet",
|
||||
#' "display.mode" and "test.mode"). You can also specify `width` and
|
||||
#' `height` parameters which provide a hint to the embedding environment
|
||||
#' about the ideal height/width for the app.
|
||||
#' @param uiPattern A regular expression that will be applied to each `GET`
|
||||
#' request to determine whether the `ui` should be used to handle the
|
||||
#' request. Note that the entire request path must match the regular
|
||||
#' expression in order for the match to be considered successful.
|
||||
#' @param enableBookmarking Can be one of `"url"`, `"server"`, or
|
||||
#' `"disable"`. The default value, `NULL`, will respect the setting from
|
||||
#' any previous calls to [enableBookmarking()]. See [enableBookmarking()]
|
||||
#' for more information on bookmarking your app.
|
||||
#' @return An object that represents the app. Printing the object or passing it
|
||||
#' to \code{\link{runApp}} will run the app.
|
||||
#' to [runApp()] will run the app.
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' numericInput("n", "n", 1),
|
||||
@@ -59,12 +70,11 @@
|
||||
#'
|
||||
#' runApp(app)
|
||||
#' }
|
||||
#'
|
||||
#' @export
|
||||
shinyApp <- function(ui=NULL, server=NULL, onStart=NULL, options=list(),
|
||||
uiPattern="/") {
|
||||
if (is.null(server)) {
|
||||
stop("`server` missing from shinyApp")
|
||||
shinyApp <- function(ui, server, onStart=NULL, options=list(),
|
||||
uiPattern="/", enableBookmarking=NULL) {
|
||||
if (!is.function(server)) {
|
||||
stop("`server` must be a function", call. = FALSE)
|
||||
}
|
||||
|
||||
# Ensure that the entire path is a match
|
||||
@@ -76,12 +86,24 @@ shinyApp <- function(ui=NULL, server=NULL, onStart=NULL, options=list(),
|
||||
server
|
||||
}
|
||||
|
||||
if (!is.null(enableBookmarking)) {
|
||||
bookmarkStore <- match.arg(enableBookmarking, c("url", "server", "disable"))
|
||||
enableBookmarking(bookmarkStore)
|
||||
}
|
||||
|
||||
# Store the appDir and bookmarking-related options, so that we can read them
|
||||
# from within the app.
|
||||
shinyOptions(appDir = getwd())
|
||||
appOptions <- consumeAppOptions()
|
||||
|
||||
structure(
|
||||
list(
|
||||
httpHandler = httpHandler,
|
||||
serverFuncSource = serverFuncSource,
|
||||
onStart = onStart,
|
||||
options = options),
|
||||
options = options,
|
||||
appOptions = appOptions
|
||||
),
|
||||
class = "shiny.appobj"
|
||||
)
|
||||
}
|
||||
@@ -113,15 +135,28 @@ shinyAppDir <- function(appDir, options=list()) {
|
||||
#' @export
|
||||
shinyAppFile <- function(appFile, options=list()) {
|
||||
appFile <- normalizePath(appFile, mustWork = TRUE)
|
||||
shinyAppDir_appR(basename(appFile), dirname(appFile), options = options)
|
||||
appDir <- dirname(appFile)
|
||||
|
||||
shinyAppDir_appR(basename(appFile), appDir, options = options)
|
||||
}
|
||||
|
||||
# This reads in an app dir in the case that there's a server.R (and ui.R/www)
|
||||
# present, and returns a shiny.appobj.
|
||||
# appDir must be a normalized (absolute) path, not a relative one
|
||||
shinyAppDir_serverR <- function(appDir, options=list()) {
|
||||
# Most of the complexity here comes from needing to hot-reload if the .R files
|
||||
# change on disk, or are created, or are removed.
|
||||
|
||||
# In an upcoming version of shiny, this option will go away.
|
||||
if (getOption("shiny.autoload.r", TRUE)) {
|
||||
# Create a child env which contains all the helpers and will be the shared parent
|
||||
# of the ui.R and server.R load.
|
||||
sharedEnv <- new.env(parent = globalenv())
|
||||
} else {
|
||||
# old behavior
|
||||
sharedEnv <- globalenv()
|
||||
}
|
||||
|
||||
# uiHandlerSource is a function that returns an HTTP handler for serving up
|
||||
# ui.R as a webpage. The "cachedFuncWithFile" call makes sure that the closure
|
||||
# we're creating here only gets executed when ui.R's contents change.
|
||||
@@ -132,7 +167,7 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
|
||||
# If not, then take the last expression that's returned from ui.R.
|
||||
.globals$ui <- NULL
|
||||
on.exit(.globals$ui <- NULL, add = FALSE)
|
||||
ui <- sourceUTF8(uiR, envir = new.env(parent = globalenv()))
|
||||
ui <- sourceUTF8(uiR, envir = new.env(parent = sharedEnv))
|
||||
if (!is.null(.globals$ui)) {
|
||||
ui <- .globals$ui[[1]]
|
||||
}
|
||||
@@ -147,7 +182,14 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
|
||||
}
|
||||
|
||||
wwwDir <- file.path.ci(appDir, "www")
|
||||
if (dirExists(wwwDir)) {
|
||||
staticPaths <- list("/" = staticPath(wwwDir, indexhtml = FALSE, fallthrough = TRUE))
|
||||
} else {
|
||||
staticPaths <- list()
|
||||
}
|
||||
|
||||
fallbackWWWDir <- system.file("www-dir", package = "shiny")
|
||||
|
||||
serverSource <- cachedFuncWithFile(appDir, "server.R", case.sensitive = FALSE,
|
||||
function(serverR) {
|
||||
# If server.R contains a call to shinyServer (which sets .globals$server),
|
||||
@@ -155,7 +197,7 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
|
||||
# server.R.
|
||||
.globals$server <- NULL
|
||||
on.exit(.globals$server <- NULL, add = TRUE)
|
||||
result <- sourceUTF8(serverR, envir = new.env(parent = globalenv()))
|
||||
result <- sourceUTF8(serverR, envir = new.env(parent = sharedEnv))
|
||||
if (!is.null(.globals$server)) {
|
||||
result <- .globals$server[[1]]
|
||||
}
|
||||
@@ -178,16 +220,23 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
|
||||
}
|
||||
}
|
||||
|
||||
shinyOptions(appDir = appDir)
|
||||
|
||||
oldwd <- NULL
|
||||
monitorHandle <- NULL
|
||||
onStart <- function() {
|
||||
oldwd <<- getwd()
|
||||
setwd(appDir)
|
||||
monitorHandle <<- initAutoReloadMonitor(appDir)
|
||||
if (file.exists(file.path.ci(appDir, "global.R")))
|
||||
sourceUTF8(file.path.ci(appDir, "global.R"))
|
||||
# TODO: we should support hot reloading on global.R and R/*.R changes.
|
||||
if (getOption("shiny.autoload.r", TRUE)) {
|
||||
loadSupport(appDir, renv=sharedEnv, globalrenv=globalenv())
|
||||
} else {
|
||||
if (file.exists(file.path.ci(appDir, "global.R")))
|
||||
sourceUTF8(file.path.ci(appDir, "global.R"))
|
||||
}
|
||||
}
|
||||
onEnd <- function() {
|
||||
onStop <- function() {
|
||||
setwd(oldwd)
|
||||
monitorHandle()
|
||||
monitorHandle <<- NULL
|
||||
@@ -195,11 +244,19 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
|
||||
|
||||
structure(
|
||||
list(
|
||||
staticPaths = staticPaths,
|
||||
# Even though the wwwDir is handled as a static path, we need to include
|
||||
# it here to be handled by R as well. This is because the special case
|
||||
# of index.html: it is specifically not handled as a staticPath for
|
||||
# reasons explained above, but if someone does want to serve up an
|
||||
# index.html, we need to handle it, and we do it by using the
|
||||
# staticHandler in the R code path. (#2380)
|
||||
httpHandler = joinHandlers(c(uiHandler, wwwDir, fallbackWWWDir)),
|
||||
serverFuncSource = serverFuncSource,
|
||||
onStart = onStart,
|
||||
onEnd = onEnd,
|
||||
options = options),
|
||||
onStop = onStop,
|
||||
options = options
|
||||
),
|
||||
class = "shiny.appobj"
|
||||
)
|
||||
}
|
||||
@@ -209,13 +266,13 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
|
||||
# ignored when checking extensions. If any changes are detected, all connected
|
||||
# Shiny sessions are reloaded.
|
||||
#
|
||||
# Use option(shiny.autoreload = TRUE) to enable this behavior. Since monitoring
|
||||
# Use options(shiny.autoreload = TRUE) to enable this behavior. Since monitoring
|
||||
# for changes is expensive (we are polling for mtimes here, nothing fancy) this
|
||||
# feature is intended only for development.
|
||||
#
|
||||
# You can customize the file patterns Shiny will monitor by setting the
|
||||
# shiny.autoreload.pattern option. For example, to monitor only ui.R:
|
||||
# option(shiny.autoreload.pattern = glob2rx("ui.R"))
|
||||
# options(shiny.autoreload.pattern = glob2rx("ui.R"))
|
||||
#
|
||||
# The return value is a function that halts monitoring when called.
|
||||
initAutoReloadMonitor <- function(dir) {
|
||||
@@ -227,7 +284,8 @@ initAutoReloadMonitor <- function(dir) {
|
||||
".*\\.(r|html?|js|css|png|jpe?g|gif)$")
|
||||
|
||||
lastValue <- NULL
|
||||
obs <- observe({
|
||||
observeLabel <- paste0("File Auto-Reload - '", basename(dir), "'")
|
||||
obs <- observe(label = observeLabel, {
|
||||
files <- sort(list.files(dir, pattern = filePattern, recursive = TRUE,
|
||||
ignore.case = TRUE))
|
||||
times <- file.info(files)$mtime
|
||||
@@ -250,21 +308,87 @@ initAutoReloadMonitor <- function(dir) {
|
||||
obs$destroy
|
||||
}
|
||||
|
||||
#' Load an app's supporting R files
|
||||
#'
|
||||
#' Loads all of the supporting R files of a Shiny application. Specifically,
|
||||
#' this function loads any top-level supporting `.R` files in the `R/` directory
|
||||
#' adjacent to the `app.R`/`server.R`/`ui.R` files.
|
||||
#'
|
||||
#' Since Shiny 1.5.0, this function is called by default when running an
|
||||
#' application. If it causes problems, there are two ways to opt out. You can
|
||||
#' either place a file named `_disable_autoload.R` in your R/ directory, or
|
||||
#' set `options(shiny.autoload.r=FALSE)`. If you set this option, it will
|
||||
#' affect any application that runs later in the same R session, potentially
|
||||
#' breaking it, so after running your application, you should unset option with
|
||||
#' `options(shiny.autoload.r=NULL)`
|
||||
#'
|
||||
#' @details The files are sourced in alphabetical order (as determined by
|
||||
#' [list.files]). `global.R` is evaluated before the supporting R files in the
|
||||
#' `R/` directory.
|
||||
#' @param appDir The application directory
|
||||
#' @param renv The environmeny in which the files in the `R/` directory should
|
||||
#' be evaluated.
|
||||
#' @param globalrenv The environment in which `global.R` should be evaluated. If
|
||||
#' `NULL`, `global.R` will not be evaluated at all.
|
||||
#' @export
|
||||
loadSupport <- function(appDir, renv=new.env(parent=globalenv()), globalrenv=globalenv()){
|
||||
if (!is.null(globalrenv)){
|
||||
# Evaluate global.R, if it exists.
|
||||
if (file.exists(file.path.ci(appDir, "global.R"))){
|
||||
sourceUTF8(file.path.ci(appDir, "global.R"), envir=globalrenv)
|
||||
}
|
||||
}
|
||||
|
||||
helpersDir <- file.path(appDir, "R")
|
||||
|
||||
disabled <- list.files(helpersDir, pattern="^_disable_autoload\\.r$", recursive=FALSE, ignore.case=TRUE)
|
||||
if (length(disabled) > 0){
|
||||
message("R/_disable_autoload.R detected; not loading the R/ directory automatically")
|
||||
return(invisible(renv))
|
||||
}
|
||||
|
||||
helpers <- list.files(helpersDir, pattern="\\.[rR]$", recursive=FALSE, full.names=TRUE)
|
||||
|
||||
if (length(helpers) > 0){
|
||||
message("Automatically loading ", length(helpers), " .R file",
|
||||
ifelse(length(helpers) != 1, "s", ""),
|
||||
" found in the R/ directory.\nSee https://rstd.io/shiny-autoload for more info.")
|
||||
}
|
||||
|
||||
lapply(helpers, sourceUTF8, envir=renv)
|
||||
|
||||
invisible(renv)
|
||||
}
|
||||
|
||||
# This reads in an app dir for a single-file application (e.g. app.R), and
|
||||
# returns a shiny.appobj.
|
||||
shinyAppDir_appR <- function(fileName, appDir, options=list()) {
|
||||
# appDir must be a normalized (absolute) path, not a relative one
|
||||
shinyAppDir_appR <- function(fileName, appDir, options=list())
|
||||
{
|
||||
fullpath <- file.path.ci(appDir, fileName)
|
||||
|
||||
# In an upcoming version of shiny, this option will go away.
|
||||
if (getOption("shiny.autoload.r", TRUE)) {
|
||||
# Create a child env which contains all the helpers and will be the shared parent
|
||||
# of the ui.R and server.R load.
|
||||
sharedEnv <- new.env(parent = globalenv())
|
||||
} else {
|
||||
sharedEnv <- globalenv()
|
||||
}
|
||||
|
||||
|
||||
# This sources app.R and caches the content. When appObj() is called but
|
||||
# app.R hasn't changed, it won't re-source the file. But if called and
|
||||
# app.R has changed, it'll re-source the file and return the result.
|
||||
appObj <- cachedFuncWithFile(appDir, fileName, case.sensitive = FALSE,
|
||||
function(appR) {
|
||||
result <- sourceUTF8(fullpath, envir = new.env(parent = globalenv()))
|
||||
result <- sourceUTF8(fullpath, envir = new.env(parent = sharedEnv))
|
||||
|
||||
if (!is.shiny.appobj(result))
|
||||
stop("app.R did not return a shiny.appobj object.")
|
||||
|
||||
unconsumeAppOptions(result$appOptions)
|
||||
|
||||
return(result)
|
||||
}
|
||||
)
|
||||
@@ -280,6 +404,20 @@ shinyAppDir_appR <- function(fileName, appDir, options=list()) {
|
||||
}
|
||||
|
||||
wwwDir <- file.path.ci(appDir, "www")
|
||||
if (dirExists(wwwDir)) {
|
||||
# wwwDir is a static path served by httpuv. It does _not_ serve up
|
||||
# index.html, for two reasons. (1) It's possible that the user's
|
||||
# www/index.html file is not actually used as the index, but as a template
|
||||
# that gets processed before being sent; and (2) the index content may be
|
||||
# modified by the hosting environment (as in SockJSAdapter.R).
|
||||
#
|
||||
# The call to staticPath normalizes the path, so that if the working dir
|
||||
# later changes, it will continue to point to the right place.
|
||||
staticPaths <- list("/" = staticPath(wwwDir, indexhtml = FALSE, fallthrough = TRUE))
|
||||
} else {
|
||||
staticPaths <- list()
|
||||
}
|
||||
|
||||
fallbackWWWDir <- system.file("www-dir", package = "shiny")
|
||||
|
||||
oldwd <- NULL
|
||||
@@ -287,9 +425,14 @@ shinyAppDir_appR <- function(fileName, appDir, options=list()) {
|
||||
onStart <- function() {
|
||||
oldwd <<- getwd()
|
||||
setwd(appDir)
|
||||
# TODO: we should support hot reloading on R/*.R changes.
|
||||
if (getOption("shiny.autoload.r", TRUE)) {
|
||||
loadSupport(appDir, renv=sharedEnv, globalrenv=NULL)
|
||||
}
|
||||
monitorHandle <<- initAutoReloadMonitor(appDir)
|
||||
if (!is.null(appObj()$onStart)) appObj()$onStart()
|
||||
}
|
||||
onEnd <- function() {
|
||||
onStop <- function() {
|
||||
setwd(oldwd)
|
||||
monitorHandle()
|
||||
monitorHandle <<- NULL
|
||||
@@ -297,10 +440,22 @@ shinyAppDir_appR <- function(fileName, appDir, options=list()) {
|
||||
|
||||
structure(
|
||||
list(
|
||||
# fallbackWWWDir is _not_ listed in staticPaths, because it needs to
|
||||
# come after the uiHandler. It also does not need to be fast, since it
|
||||
# should rarely be hit. The order is wwwDir (in staticPaths), then
|
||||
# uiHandler, then falbackWWWDir (which is served up by the R
|
||||
# staticHandler function).
|
||||
staticPaths = staticPaths,
|
||||
# Even though the wwwDir is handled as a static path, we need to include
|
||||
# it here to be handled by R as well. This is because the special case
|
||||
# of index.html: it is specifically not handled as a staticPath for
|
||||
# reasons explained above, but if someone does want to serve up an
|
||||
# index.html, we need to handle it, and we do it by using the
|
||||
# staticHandler in the R code path. (#2380)
|
||||
httpHandler = joinHandlers(c(dynHttpHandler, wwwDir, fallbackWWWDir)),
|
||||
serverFuncSource = dynServerFuncSource,
|
||||
onStart = onStart,
|
||||
onEnd = onEnd,
|
||||
onStop = onStop,
|
||||
options = options
|
||||
),
|
||||
class = "shiny.appobj"
|
||||
@@ -308,26 +463,34 @@ shinyAppDir_appR <- function(fileName, appDir, options=list()) {
|
||||
}
|
||||
|
||||
|
||||
#' @rdname shinyApp
|
||||
#' Shiny App object
|
||||
#'
|
||||
#' Internal methods for the `shiny.appobj` S3 class.
|
||||
#'
|
||||
#' @keywords internal
|
||||
#' @name shiny.appobj
|
||||
NULL
|
||||
|
||||
#' @rdname shiny.appobj
|
||||
#' @param x Object to convert to a Shiny app.
|
||||
#' @export
|
||||
as.shiny.appobj <- function(x) {
|
||||
UseMethod("as.shiny.appobj", x)
|
||||
}
|
||||
|
||||
#' @rdname shinyApp
|
||||
#' @rdname shiny.appobj
|
||||
#' @export
|
||||
as.shiny.appobj.shiny.appobj <- function(x) {
|
||||
x
|
||||
}
|
||||
|
||||
#' @rdname shinyApp
|
||||
#' @rdname shiny.appobj
|
||||
#' @export
|
||||
as.shiny.appobj.list <- function(x) {
|
||||
shinyApp(ui = x$ui, server = x$server)
|
||||
}
|
||||
|
||||
#' @rdname shinyApp
|
||||
#' @rdname shiny.appobj
|
||||
#' @export
|
||||
as.shiny.appobj.character <- function(x) {
|
||||
if (identical(tolower(tools::file_ext(x)), "r"))
|
||||
@@ -336,26 +499,28 @@ as.shiny.appobj.character <- function(x) {
|
||||
shinyAppDir(x)
|
||||
}
|
||||
|
||||
#' @rdname shinyApp
|
||||
#' @rdname shiny.appobj
|
||||
#' @export
|
||||
is.shiny.appobj <- function(x) {
|
||||
inherits(x, "shiny.appobj")
|
||||
}
|
||||
|
||||
#' @rdname shinyApp
|
||||
#' @rdname shiny.appobj
|
||||
#' @param ... Additional parameters to be passed to print.
|
||||
#' @export
|
||||
print.shiny.appobj <- function(x, ...) {
|
||||
opts <- x$options %OR% list()
|
||||
opts <- opts[names(opts) %in%
|
||||
c("port", "launch.browser", "host", "quiet", "display.mode")]
|
||||
c("port", "launch.browser", "host", "quiet",
|
||||
"display.mode", "test.mode")]
|
||||
|
||||
args <- c(list(x), opts)
|
||||
# Quote x and put runApp in quotes so that there's a nicer stack trace (#1851)
|
||||
args <- c(list(quote(x)), opts)
|
||||
|
||||
do.call(runApp, args)
|
||||
do.call("runApp", args)
|
||||
}
|
||||
|
||||
#' @rdname shinyApp
|
||||
#' @rdname shiny.appobj
|
||||
#' @method as.tags shiny.appobj
|
||||
#' @export
|
||||
as.tags.shiny.appobj <- function(x, ...) {
|
||||
@@ -409,7 +574,6 @@ shiny_rmd_warning <- function() {
|
||||
}
|
||||
|
||||
#' @rdname knitr_methods
|
||||
#' @export
|
||||
knit_print.shiny.appobj <- function(x, ...) {
|
||||
opts <- x$options %OR% list()
|
||||
width <- if (is.null(opts$width)) "100%" else opts$width
|
||||
@@ -446,7 +610,6 @@ knit_print.shiny.appobj <- function(x, ...) {
|
||||
# calling output$value <- renderFoo(...) and fooOutput().
|
||||
#' @rdname knitr_methods
|
||||
#' @param inline Whether the object is printed inline.
|
||||
#' @export
|
||||
knit_print.shiny.render.function <- function(x, ..., inline = FALSE) {
|
||||
x <- htmltools::as.tags(x, inline = inline)
|
||||
output <- knitr::knit_print(tagList(x))
|
||||
@@ -459,7 +622,6 @@ knit_print.shiny.render.function <- function(x, ..., inline = FALSE) {
|
||||
# Lets us drop reactive expressions directly into a knitr chunk and have the
|
||||
# value printed out! Nice for teaching if nothing else.
|
||||
#' @rdname knitr_methods
|
||||
#' @export
|
||||
knit_print.reactive <- function(x, ..., inline = FALSE) {
|
||||
renderFunc <- if (inline) renderText else renderPrint
|
||||
knitr::knit_print(renderFunc({
|
||||
|
||||
28
R/bookmark-state-local.R
Normal file
28
R/bookmark-state-local.R
Normal file
@@ -0,0 +1,28 @@
|
||||
# Function wrappers for saving and restoring state to/from disk when running
|
||||
# Shiny locally.
|
||||
#
|
||||
# These functions provide a directory to the callback function.
|
||||
#
|
||||
# @param id A session ID to save.
|
||||
# @param callback A callback function that saves state to or restores state from
|
||||
# a directory. It must take one argument, \code{stateDir}, which is a
|
||||
# directory to which it writes/reads.
|
||||
|
||||
saveInterfaceLocal <- function(id, callback) {
|
||||
# Try to save in app directory
|
||||
appDir <- getShinyOption("appDir", default = getwd())
|
||||
|
||||
stateDir <- file.path(appDir, "shiny_bookmarks", id)
|
||||
if (!dirExists(stateDir))
|
||||
dir.create(stateDir, recursive = TRUE)
|
||||
|
||||
callback(stateDir)
|
||||
}
|
||||
|
||||
loadInterfaceLocal <- function(id, callback) {
|
||||
# Try to load from app directory
|
||||
appDir <- getShinyOption("appDir", default = getwd())
|
||||
|
||||
stateDir <- file.path(appDir, "shiny_bookmarks", id)
|
||||
callback(stateDir)
|
||||
}
|
||||
1207
R/bookmark-state.R
Normal file
1207
R/bookmark-state.R
Normal file
File diff suppressed because it is too large
Load Diff
47
R/bootstrap-deprecated.R
Normal file
47
R/bootstrap-deprecated.R
Normal file
@@ -0,0 +1,47 @@
|
||||
#' Create a page with a sidebar
|
||||
#'
|
||||
#' **DEPRECATED**: use [fluidPage()] and [sidebarLayout()] instead.
|
||||
#'
|
||||
#' @param headerPanel The [headerPanel] with the application title
|
||||
#' @param sidebarPanel The [sidebarPanel] containing input controls
|
||||
#' @param mainPanel The [mainPanel] containing outputs
|
||||
#' @keywords internal
|
||||
#' @return A UI defintion that can be passed to the [shinyUI] function
|
||||
#' @export
|
||||
pageWithSidebar <- function(headerPanel,
|
||||
sidebarPanel,
|
||||
mainPanel) {
|
||||
|
||||
bootstrapPage(
|
||||
# basic application container divs
|
||||
div(
|
||||
class="container-fluid",
|
||||
div(class="row",
|
||||
headerPanel
|
||||
),
|
||||
div(class="row",
|
||||
sidebarPanel,
|
||||
mainPanel
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#' Create a header panel
|
||||
#'
|
||||
#' **DEPRECATED**: use [titlePanel()] instead.
|
||||
#'
|
||||
#' @param title An application title to display
|
||||
#' @param windowTitle The title that should be displayed by the browser window.
|
||||
#' Useful if `title` is not a string.
|
||||
#' @return A headerPanel that can be passed to [pageWithSidebar]
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
headerPanel <- function(title, windowTitle=title) {
|
||||
tagList(
|
||||
tags$head(tags$title(windowTitle)),
|
||||
div(class="col-sm-12",
|
||||
h1(title)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -10,28 +10,33 @@
|
||||
#'
|
||||
#' @param ... Elements to include within the page
|
||||
#' @param title The browser window title (defaults to the host URL of the page).
|
||||
#' Can also be set as a side effect of the \code{\link{titlePanel}} function.
|
||||
#' Can also be set as a side effect of the [titlePanel()] function.
|
||||
#' @param responsive This option is deprecated; it is no longer optional with
|
||||
#' Bootstrap 3.
|
||||
#' @param theme Alternative Bootstrap stylesheet (normally a css file within the
|
||||
#' www directory). For example, to use the theme located at
|
||||
#' \code{www/bootstrap.css} you would use \code{theme = "bootstrap.css"}.
|
||||
#' `www/bootstrap.css` you would use `theme = "bootstrap.css"`.
|
||||
#'
|
||||
#' @return A UI defintion that can be passed to the \link{shinyUI} function.
|
||||
#' @return A UI defintion that can be passed to the [shinyUI] function.
|
||||
#'
|
||||
#' @details To create a fluid page use the \code{fluidPage} function and include
|
||||
#' instances of \code{fluidRow} and \code{\link{column}} within it. As an
|
||||
#' @details To create a fluid page use the `fluidPage` function and include
|
||||
#' instances of `fluidRow` and [column()] within it. As an
|
||||
#' alternative to low-level row and column functions you can also use
|
||||
#' higher-level layout functions like \code{\link{sidebarLayout}}.
|
||||
#' higher-level layout functions like [sidebarLayout()].
|
||||
#'
|
||||
#' @note See the \href{http://shiny.rstudio.com/articles/layout-guide.html}{
|
||||
#' Shiny-Application-Layout-Guide} for additional details on laying out fluid
|
||||
#' @note See the [
|
||||
#' Shiny-Application-Layout-Guide](http://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fluid
|
||||
#' pages.
|
||||
#'
|
||||
#' @seealso \code{\link{column}}, \code{\link{sidebarLayout}}
|
||||
#' @family layout functions
|
||||
#' @seealso [column()]
|
||||
#'
|
||||
#' @examples
|
||||
#' shinyUI(fluidPage(
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # Example of UI with fluidPage
|
||||
#' ui <- fluidPage(
|
||||
#'
|
||||
#' # Application title
|
||||
#' titlePanel("Hello Shiny!"),
|
||||
@@ -52,9 +57,21 @@
|
||||
#' plotOutput("distPlot")
|
||||
#' )
|
||||
#' )
|
||||
#' ))
|
||||
#' )
|
||||
#'
|
||||
#' shinyUI(fluidPage(
|
||||
#' # Server logic
|
||||
#' server <- function(input, output) {
|
||||
#' output$distPlot <- renderPlot({
|
||||
#' hist(rnorm(input$obs))
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' # Complete app with UI and server components
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#'
|
||||
#' # UI demonstrating column layouts
|
||||
#' ui <- fluidPage(
|
||||
#' title = "Hello Shiny!",
|
||||
#' fluidRow(
|
||||
#' column(width = 4,
|
||||
@@ -64,8 +81,10 @@
|
||||
#' "3 offset 2"
|
||||
#' )
|
||||
#' )
|
||||
#' ))
|
||||
#' )
|
||||
#'
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#' @rdname fluidPage
|
||||
#' @export
|
||||
fluidPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
|
||||
@@ -98,24 +117,29 @@ fluidRow <- function(...) {
|
||||
#' Bootstrap 3.
|
||||
#' @param theme Alternative Bootstrap stylesheet (normally a css file within the
|
||||
#' www directory). For example, to use the theme located at
|
||||
#' \code{www/bootstrap.css} you would use \code{theme = "bootstrap.css"}.
|
||||
#' `www/bootstrap.css` you would use `theme = "bootstrap.css"`.
|
||||
#'
|
||||
#' @return A UI defintion that can be passed to the \link{shinyUI} function.
|
||||
#' @return A UI defintion that can be passed to the [shinyUI] function.
|
||||
#'
|
||||
#' @details To create a fixed page use the \code{fixedPage} function and include
|
||||
#' instances of \code{fixedRow} and \code{\link{column}} within it. Note that
|
||||
#' unlike \code{\link{fluidPage}}, fixed pages cannot make use of higher-level
|
||||
#' layout functions like \code{sidebarLayout}, rather, all layout must be done
|
||||
#' with \code{fixedRow} and \code{column}.
|
||||
#' @details To create a fixed page use the `fixedPage` function and include
|
||||
#' instances of `fixedRow` and [column()] within it. Note that
|
||||
#' unlike [fluidPage()], fixed pages cannot make use of higher-level
|
||||
#' layout functions like `sidebarLayout`, rather, all layout must be done
|
||||
#' with `fixedRow` and `column`.
|
||||
#'
|
||||
#' @note See the \href{http://shiny.rstudio.com/articles/layout-guide.html}{
|
||||
#' Shiny Application Layout Guide} for additional details on laying out fixed
|
||||
#' @note See the [
|
||||
#' Shiny Application Layout Guide](http://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fixed
|
||||
#' pages.
|
||||
#'
|
||||
#' @seealso \code{\link{column}}
|
||||
#' @family layout functions
|
||||
#'
|
||||
#' @seealso [column()]
|
||||
#'
|
||||
#' @examples
|
||||
#' shinyUI(fixedPage(
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fixedPage(
|
||||
#' title = "Hello, Shiny!",
|
||||
#' fixedRow(
|
||||
#' column(width = 4,
|
||||
@@ -125,7 +149,10 @@ fluidRow <- function(...) {
|
||||
#' "3 offset 2"
|
||||
#' )
|
||||
#' )
|
||||
#' ))
|
||||
#' )
|
||||
#'
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#'
|
||||
#' @rdname fixedPage
|
||||
#' @export
|
||||
@@ -145,8 +172,8 @@ fixedRow <- function(...) {
|
||||
|
||||
#' Create a column within a UI definition
|
||||
#'
|
||||
#' Create a column for use within a \code{\link{fluidRow}} or
|
||||
#' \code{\link{fixedRow}}
|
||||
#' Create a column for use within a [fluidRow()] or
|
||||
#' [fixedRow()]
|
||||
#'
|
||||
#' @param width The grid width of the column (must be between 1 and 12)
|
||||
#' @param ... Elements to include within the column
|
||||
@@ -154,30 +181,49 @@ fixedRow <- function(...) {
|
||||
#' previous column.
|
||||
#'
|
||||
#' @return A column that can be included within a
|
||||
#' \code{\link{fluidRow}} or \code{\link{fixedRow}}.
|
||||
#' [fluidRow()] or [fixedRow()].
|
||||
#'
|
||||
#'
|
||||
#' @seealso \code{\link{fluidRow}}, \code{\link{fixedRow}}.
|
||||
#' @seealso [fluidRow()], [fixedRow()].
|
||||
#'
|
||||
#' @examples
|
||||
#' fluidRow(
|
||||
#' column(4,
|
||||
#' sliderInput("obs", "Number of observations:",
|
||||
#' min = 1, max = 1000, value = 500)
|
||||
#' ),
|
||||
#' column(8,
|
||||
#' plotOutput("distPlot")
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' fluidRow(
|
||||
#' column(4,
|
||||
#' sliderInput("obs", "Number of observations:",
|
||||
#' min = 1, max = 1000, value = 500)
|
||||
#' ),
|
||||
#' column(8,
|
||||
#' plotOutput("distPlot")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' fluidRow(
|
||||
#' column(width = 4,
|
||||
#' "4"
|
||||
#' ),
|
||||
#' column(width = 3, offset = 2,
|
||||
#' "3 offset 2"
|
||||
#' server <- function(input, output) {
|
||||
#' output$distPlot <- renderPlot({
|
||||
#' hist(rnorm(input$obs))
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#'
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' fluidRow(
|
||||
#' column(width = 4,
|
||||
#' "4"
|
||||
#' ),
|
||||
#' column(width = 3, offset = 2,
|
||||
#' "3 offset 2"
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#' @export
|
||||
column <- function(width, ..., offset = 0) {
|
||||
|
||||
@@ -185,8 +231,12 @@ column <- function(width, ..., offset = 0) {
|
||||
stop("column width must be between 1 and 12")
|
||||
|
||||
colClass <- paste0("col-sm-", width)
|
||||
if (offset > 0)
|
||||
colClass <- paste0(colClass, " col-sm-offset-", offset)
|
||||
if (offset > 0) {
|
||||
# offset-md-x is for bootstrap 4 forward compat
|
||||
# (every size tier has been bumped up one level)
|
||||
# https://github.com/twbs/bootstrap/blob/74b8fe7/docs/4.3/migration/index.html#L659
|
||||
colClass <- paste0(colClass, " offset-md-", offset, " col-sm-offset-", offset)
|
||||
}
|
||||
div(class = colClass, ...)
|
||||
}
|
||||
|
||||
@@ -197,13 +247,18 @@ column <- function(width, ..., offset = 0) {
|
||||
#' @param windowTitle The title that should be displayed by the browser window.
|
||||
#'
|
||||
#' @details Calling this function has the side effect of including a
|
||||
#' \code{title} tag within the head. You can also specify a page title
|
||||
#' `title` tag within the head. You can also specify a page title
|
||||
#' explicitly using the `title` parameter of the top-level page function.
|
||||
#'
|
||||
#'
|
||||
#' @examples
|
||||
#' titlePanel("Hello Shiny!")
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' titlePanel("Hello Shiny!")
|
||||
#' )
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#' @export
|
||||
titlePanel <- function(title, windowTitle=title) {
|
||||
tagList(
|
||||
@@ -214,20 +269,31 @@ titlePanel <- function(title, windowTitle=title) {
|
||||
|
||||
#' Layout a sidebar and main area
|
||||
#'
|
||||
#' Create a layout with a sidebar and main area. The sidebar is displayed with a
|
||||
#' distinct background color and typically contains input controls. The main
|
||||
#' Create a layout (`sidebarLayout()`) with a sidebar (`sidebarPanel()`) and
|
||||
#' main area (`mainPanel()`). The sidebar is displayed with a distinct
|
||||
#' background color and typically contains input controls. The main
|
||||
#' area occupies 2/3 of the horizontal width and typically contains outputs.
|
||||
#'
|
||||
#' @param sidebarPanel The \link{sidebarPanel} containing input controls
|
||||
#' @param mainPanel The \link{mainPanel} containing outputs
|
||||
#' @param sidebarPanel The `sidebarPanel()` containing input controls.
|
||||
#' @param mainPanel The `mainPanel()` containing outputs.
|
||||
#' @param position The position of the sidebar relative to the main area ("left"
|
||||
#' or "right")
|
||||
#' @param fluid \code{TRUE} to use fluid layout; \code{FALSE} to use fixed
|
||||
#' or "right").
|
||||
#' @param fluid `TRUE` to use fluid layout; `FALSE` to use fixed
|
||||
#' layout.
|
||||
#' @param width The width of the sidebar and main panel. By default, the
|
||||
#' sidebar takes up 1/3 of the width, and the main panel 2/3. The total
|
||||
#' width must be 12 or less.
|
||||
#' @param ... Output elements to include in the sidebar/main panel.
|
||||
#'
|
||||
#' @family layout functions
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' # Define UI
|
||||
#' shinyUI(fluidPage(
|
||||
#' ui <- fluidPage(
|
||||
#'
|
||||
#' # Application title
|
||||
#' titlePanel("Hello Shiny!"),
|
||||
@@ -248,8 +314,18 @@ titlePanel <- function(title, windowTitle=title) {
|
||||
#' plotOutput("distPlot")
|
||||
#' )
|
||||
#' )
|
||||
#' ))
|
||||
#' )
|
||||
#'
|
||||
#' # Server logic
|
||||
#' server <- function(input, output) {
|
||||
#' output$distPlot <- renderPlot({
|
||||
#' hist(rnorm(input$obs))
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' # Complete app with UI and server components
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
sidebarLayout <- function(sidebarPanel,
|
||||
mainPanel,
|
||||
@@ -274,25 +350,48 @@ sidebarLayout <- function(sidebarPanel,
|
||||
fixedRow(firstPanel, secondPanel)
|
||||
}
|
||||
|
||||
#' @export
|
||||
#' @rdname sidebarLayout
|
||||
sidebarPanel <- function(..., width = 4) {
|
||||
div(class=paste0("col-sm-", width),
|
||||
tags$form(class="well",
|
||||
...
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#' @export
|
||||
#' @rdname sidebarLayout
|
||||
mainPanel <- function(..., width = 8) {
|
||||
div(class=paste0("col-sm-", width),
|
||||
...
|
||||
)
|
||||
}
|
||||
|
||||
#' Lay out UI elements vertically
|
||||
#'
|
||||
#' Create a container that includes one or more rows of content (each element
|
||||
#' passed to the container will appear on it's own line in the UI)
|
||||
#'
|
||||
#' @param ... Elements to include within the container
|
||||
#' @param fluid \code{TRUE} to use fluid layout; \code{FALSE} to use fixed
|
||||
#' @param fluid `TRUE` to use fluid layout; `FALSE` to use fixed
|
||||
#' layout.
|
||||
#'
|
||||
#' @seealso \code{\link{fluidPage}}, \code{\link{flowLayout}}
|
||||
#' @family layout functions
|
||||
#'
|
||||
#' @examples
|
||||
#' shinyUI(fluidPage(
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' verticalLayout(
|
||||
#' a(href="http://example.com/link1", "Link One"),
|
||||
#' a(href="http://example.com/link2", "Link Two"),
|
||||
#' a(href="http://example.com/link3", "Link Three")
|
||||
#' )
|
||||
#' ))
|
||||
#' )
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#' @export
|
||||
verticalLayout <- function(..., fluid = TRUE) {
|
||||
lapply(list(...), function(row) {
|
||||
@@ -309,21 +408,26 @@ verticalLayout <- function(..., fluid = TRUE) {
|
||||
#' Lays out elements in a left-to-right, top-to-bottom arrangement. The elements
|
||||
#' on a given row will be top-aligned with each other. This layout will not work
|
||||
#' well with elements that have a percentage-based width (e.g.
|
||||
#' \code{\link{plotOutput}} at its default setting of \code{width = "100\%"}).
|
||||
#' [plotOutput()] at its default setting of `width = "100%"`).
|
||||
#'
|
||||
#' @param ... Unnamed arguments will become child elements of the layout. Named
|
||||
#' arguments will become HTML attributes on the outermost tag.
|
||||
#' @param cellArgs Any additional attributes that should be used for each cell
|
||||
#' of the layout.
|
||||
#'
|
||||
#' @seealso \code{\link{verticalLayout}}
|
||||
#' @family layout functions
|
||||
#'
|
||||
#' @examples
|
||||
#' flowLayout(
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- flowLayout(
|
||||
#' numericInput("rows", "How many rows?", 5),
|
||||
#' selectInput("letter", "Which letter?", LETTERS),
|
||||
#' sliderInput("value", "What value?", 0, 100, 50)
|
||||
#' )
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#' @export
|
||||
flowLayout <- function(..., cellArgs = list()) {
|
||||
|
||||
@@ -342,11 +446,10 @@ flowLayout <- function(..., cellArgs = list()) {
|
||||
|
||||
#' Input panel
|
||||
#'
|
||||
#' A \code{\link{flowLayout}} with a grey border and light grey background,
|
||||
#' A [flowLayout()] with a grey border and light grey background,
|
||||
#' suitable for wrapping inputs.
|
||||
#'
|
||||
#' @param ... Input controls or other HTML elements.
|
||||
#'
|
||||
#' @export
|
||||
inputPanel <- function(...) {
|
||||
div(class = "shiny-input-panel",
|
||||
@@ -363,27 +466,42 @@ inputPanel <- function(...) {
|
||||
#' arguments will become HTML attributes on the outermost tag.
|
||||
#' @param cellWidths Character or numeric vector indicating the widths of the
|
||||
#' individual cells. Recycling will be used if needed. Character values will
|
||||
#' be interpreted as CSS lengths (see \code{\link{validateCssUnit}}), numeric
|
||||
#' be interpreted as CSS lengths (see [validateCssUnit()]), numeric
|
||||
#' values as pixels.
|
||||
#' @param cellArgs Any additional attributes that should be used for each cell
|
||||
#' of the layout.
|
||||
#'
|
||||
#' @family layout functions
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' # Server code used for all examples
|
||||
#' server <- function(input, output) {
|
||||
#' output$plot1 <- renderPlot(plot(cars))
|
||||
#' output$plot2 <- renderPlot(plot(pressure))
|
||||
#' output$plot3 <- renderPlot(plot(AirPassengers))
|
||||
#' }
|
||||
#'
|
||||
#' # Equal sizing
|
||||
#' splitLayout(
|
||||
#' ui <- splitLayout(
|
||||
#' plotOutput("plot1"),
|
||||
#' plotOutput("plot2")
|
||||
#' )
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' # Custom widths
|
||||
#' splitLayout(cellWidths = c("25%", "75%"),
|
||||
#' ui <- splitLayout(cellWidths = c("25%", "75%"),
|
||||
#' plotOutput("plot1"),
|
||||
#' plotOutput("plot2")
|
||||
#' )
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' # All cells at 300 pixels wide, with cell padding
|
||||
#' # and a border around everything
|
||||
#' splitLayout(
|
||||
#' ui <- splitLayout(
|
||||
#' style = "border: 1px solid silver;",
|
||||
#' cellWidths = 300,
|
||||
#' cellArgs = list(style = "padding: 6px"),
|
||||
@@ -391,6 +509,8 @@ inputPanel <- function(...) {
|
||||
#' plotOutput("plot2"),
|
||||
#' plotOutput("plot3")
|
||||
#' )
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
|
||||
|
||||
@@ -422,48 +542,45 @@ splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
|
||||
#'
|
||||
#' Creates row and column layouts with proportionally-sized cells, using the
|
||||
#' Flex Box layout model of CSS3. These can be nested to create arbitrary
|
||||
#' proportional-grid layouts. \strong{Warning:} Flex Box is not well supported
|
||||
#' proportional-grid layouts. **Warning:** Flex Box is not well supported
|
||||
#' by Internet Explorer, so these functions should only be used where modern
|
||||
#' browsers can be assumed.
|
||||
#'
|
||||
#' @details If you try to use \code{fillRow} and \code{fillCol} inside of other
|
||||
#' Shiny containers, such as \code{\link{sidebarLayout}},
|
||||
#' \code{\link{navbarPage}}, or even \code{tags$div}, you will probably find
|
||||
#' that they will not appear. This is due to \code{fillRow} and \code{fillCol}
|
||||
#' defaulting to \code{height="100\%"}, which will only work inside of
|
||||
#' @details If you try to use `fillRow` and `fillCol` inside of other
|
||||
#' Shiny containers, such as [sidebarLayout()],
|
||||
#' [navbarPage()], or even `tags$div`, you will probably find
|
||||
#' that they will not appear. This is due to `fillRow` and `fillCol`
|
||||
#' defaulting to `height="100%"`, which will only work inside of
|
||||
#' containers that have determined their own size (rather than shrinking to
|
||||
#' the size of their contents, as is usually the case in HTML).
|
||||
#'
|
||||
#' To avoid this problem, you have two options:
|
||||
#' \itemize{
|
||||
#' \item only use \code{fillRow}/\code{fillCol} inside of \code{fillPage},
|
||||
#' \code{fillRow}, or \code{fillCol}
|
||||
#' \item provide an explicit \code{height} argument to
|
||||
#' \code{fillRow}/\code{fillCol}
|
||||
#' \item only use `fillRow`/`fillCol` inside of `fillPage`,
|
||||
#' `fillRow`, or `fillCol`
|
||||
#' \item provide an explicit `height` argument to
|
||||
#' `fillRow`/`fillCol`
|
||||
#' }
|
||||
#'
|
||||
#' @param ... UI objects to put in each row/column cell; each argument will
|
||||
#' occupy a single cell. (To put multiple items in a single cell, you can use
|
||||
#' \code{\link{tagList}} or \code{\link{div}} to combine them.) Named
|
||||
#' arguments will be used as attributes on the \code{div} element that
|
||||
#' [tagList()] or [div()] to combine them.) Named
|
||||
#' arguments will be used as attributes on the `div` element that
|
||||
#' encapsulates the row/column.
|
||||
#' @param flex Determines how space should be distributed to the cells. Can be a
|
||||
#' single value like \code{1} or \code{2} to evenly distribute the available
|
||||
#' single value like `1` or `2` to evenly distribute the available
|
||||
#' space; or use a vector of numbers to specify the proportions. For example,
|
||||
#' \code{flex = c(2, 3)} would cause the space to be split 40\%/60\% between
|
||||
#' `flex = c(2, 3)` would cause the space to be split 40%/60% between
|
||||
#' two cells. NA values will cause the corresponding cell to be sized
|
||||
#' according to its contents (without growing or shrinking).
|
||||
#' @param width,height The total amount of width and height to use for the
|
||||
#' entire row/column. For the default height of \code{"100\%"} to be
|
||||
#' effective, the parent must be \code{fillPage}, another
|
||||
#' \code{fillRow}/\code{fillCol}, or some other HTML element whose height is
|
||||
#' entire row/column. For the default height of `"100%"` to be
|
||||
#' effective, the parent must be `fillPage`, another
|
||||
#' `fillRow`/`fillCol`, or some other HTML element whose height is
|
||||
#' not determined by the height of its contents.
|
||||
#'
|
||||
#' @examples
|
||||
#' \donttest{
|
||||
#' # Only run this example in interactive R sessions.
|
||||
#' # NOTE: This example should be run with example(fillRow, ask = FALSE) to
|
||||
#' # avoid being prompted to hit Enter during plot rendering.
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fillPage(fillRow(
|
||||
@@ -483,7 +600,6 @@ splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' }
|
||||
#' }
|
||||
#' @export
|
||||
fillRow <- function(..., flex = 1, width = "100%", height = "100%") {
|
||||
flexfill(..., direction = "row", flex = flex, width = width, height = height)
|
||||
@@ -505,7 +621,7 @@ flexfill <- function(..., direction, flex, width = width, height = height) {
|
||||
}
|
||||
|
||||
if (length(flex) > length(children)) {
|
||||
flex <- flex[1:length(children)]
|
||||
flex <- flex[seq_along(children)]
|
||||
}
|
||||
|
||||
# The dimension along the main axis
|
||||
|
||||
927
R/bootstrap.R
927
R/bootstrap.R
File diff suppressed because it is too large
Load Diff
561
R/cache-disk.R
Normal file
561
R/cache-disk.R
Normal file
@@ -0,0 +1,561 @@
|
||||
#' Create a disk cache object
|
||||
#'
|
||||
#' A disk cache object is a key-value store that saves the values as files in a
|
||||
#' directory on disk. Objects can be stored and retrieved using the `get()`
|
||||
#' and `set()` methods. Objects are automatically pruned from the cache
|
||||
#' according to the parameters `max_size`, `max_age`, `max_n`,
|
||||
#' and `evict`.
|
||||
#'
|
||||
#'
|
||||
#' @section Missing Keys:
|
||||
#'
|
||||
#' The `missing` and `exec_missing` parameters controls what happens
|
||||
#' when `get()` is called with a key that is not in the cache (a cache
|
||||
#' miss). The default behavior is to return a [key_missing()]
|
||||
#' object. This is a *sentinel value* that indicates that the key was not
|
||||
#' present in the cache. You can test if the returned value represents a
|
||||
#' missing key by using the [is.key_missing()] function. You can
|
||||
#' also have `get()` return a different sentinel value, like `NULL`.
|
||||
#' If you want to throw an error on a cache miss, you can do so by providing a
|
||||
#' function for `missing` that takes one argument, the key, and also use
|
||||
#' `exec_missing=TRUE`.
|
||||
#'
|
||||
#' When the cache is created, you can supply a value for `missing`, which
|
||||
#' sets the default value to be returned for missing values. It can also be
|
||||
#' overridden when `get()` is called, by supplying a `missing`
|
||||
#' argument. For example, if you use `cache$get("mykey", missing =
|
||||
#' NULL)`, it will return `NULL` if the key is not in the cache.
|
||||
#'
|
||||
#' If your cache is configured so that `get()` returns a sentinel value
|
||||
#' to represent a cache miss, then `set` will also not allow you to store
|
||||
#' the sentinel value in the cache. It will throw an error if you attempt to
|
||||
#' do so.
|
||||
#'
|
||||
#' Instead of returning the same sentinel value each time there is cache miss,
|
||||
#' the cache can execute a function each time `get()` encounters missing
|
||||
#' key. If the function returns a value, then `get()` will in turn return
|
||||
#' that value. However, a more common use is for the function to throw an
|
||||
#' error. If an error is thrown, then `get()` will not return a value.
|
||||
#'
|
||||
#' To do this, pass a one-argument function to `missing`, and use
|
||||
#' `exec_missing=TRUE`. For example, if you want to throw an error that
|
||||
#' prints the missing key, you could do this:
|
||||
#'
|
||||
#' \preformatted{
|
||||
#' diskCache(
|
||||
#' missing = function(key) {
|
||||
#' stop("Attempted to get missing key: ", key)
|
||||
#' },
|
||||
#' exec_missing = TRUE
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' If you use this, the code that calls `get()` should be wrapped with
|
||||
#' [tryCatch()] to gracefully handle missing keys.
|
||||
#'
|
||||
#' @section Cache pruning:
|
||||
#'
|
||||
#' Cache pruning occurs when `set()` is called, or it can be invoked
|
||||
#' manually by calling `prune()`.
|
||||
#'
|
||||
#' The disk cache will throttle the pruning so that it does not happen on
|
||||
#' every call to `set()`, because the filesystem operations for checking
|
||||
#' the status of files can be slow. Instead, it will prune once in every 20
|
||||
#' calls to `set()`, or if at least 5 seconds have elapsed since the last
|
||||
#' prune occurred, whichever is first. These parameters are currently not
|
||||
#' customizable, but may be in the future.
|
||||
#'
|
||||
#' When a pruning occurs, if there are any objects that are older than
|
||||
#' `max_age`, they will be removed.
|
||||
#'
|
||||
#' The `max_size` and `max_n` parameters are applied to the cache as
|
||||
#' a whole, in contrast to `max_age`, which is applied to each object
|
||||
#' individually.
|
||||
#'
|
||||
#' If the number of objects in the cache exceeds `max_n`, then objects
|
||||
#' will be removed from the cache according to the eviction policy, which is
|
||||
#' set with the `evict` parameter. Objects will be removed so that the
|
||||
#' number of items is `max_n`.
|
||||
#'
|
||||
#' If the size of the objects in the cache exceeds `max_size`, then
|
||||
#' objects will be removed from the cache. Objects will be removed from the
|
||||
#' cache so that the total size remains under `max_size`. Note that the
|
||||
#' size is calculated using the size of the files, not the size of disk space
|
||||
#' used by the files --- these two values can differ because of files are
|
||||
#' stored in blocks on disk. For example, if the block size is 4096 bytes,
|
||||
#' then a file that is one byte in size will take 4096 bytes on disk.
|
||||
#'
|
||||
#' Another time that objects can be removed from the cache is when
|
||||
#' `get()` is called. If the target object is older than `max_age`,
|
||||
#' it will be removed and the cache will report it as a missing value.
|
||||
#'
|
||||
#' @section Eviction policies:
|
||||
#'
|
||||
#' If `max_n` or `max_size` are used, then objects will be removed
|
||||
#' from the cache according to an eviction policy. The available eviction
|
||||
#' policies are:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`"lru"`}{
|
||||
#' Least Recently Used. The least recently used objects will be removed.
|
||||
#' This uses the filesystem's mtime property. When "lru" is used, each
|
||||
#' `get()` is called, it will update the file's mtime.
|
||||
#' }
|
||||
#' \item{`"fifo"`}{
|
||||
#' First-in-first-out. The oldest objects will be removed.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' Both of these policies use files' mtime. Note that some filesystems (notably
|
||||
#' FAT) have poor mtime resolution. (atime is not used because support for
|
||||
#' atime is worse than mtime.)
|
||||
#'
|
||||
#'
|
||||
#' @section Sharing among multiple processes:
|
||||
#'
|
||||
#' The directory for a DiskCache can be shared among multiple R processes. To
|
||||
#' do this, each R process should have a DiskCache object that uses the same
|
||||
#' directory. Each DiskCache will do pruning independently of the others, so if
|
||||
#' they have different pruning parameters, then one DiskCache may remove cached
|
||||
#' objects before another DiskCache would do so.
|
||||
#'
|
||||
#' Even though it is possible for multiple processes to share a DiskCache
|
||||
#' directory, this should not be done on networked file systems, because of
|
||||
#' slow performance of networked file systems can cause problems. If you need
|
||||
#' a high-performance shared cache, you can use one built on a database like
|
||||
#' Redis, SQLite, mySQL, or similar.
|
||||
#'
|
||||
#' When multiple processes share a cache directory, there are some potential
|
||||
#' race conditions. For example, if your code calls `exists(key)` to check
|
||||
#' if an object is in the cache, and then call `get(key)`, the object may
|
||||
#' be removed from the cache in between those two calls, and `get(key)`
|
||||
#' will throw an error. Instead of calling the two functions, it is better to
|
||||
#' simply call `get(key)`, and use `tryCatch()` to handle the error
|
||||
#' that is thrown if the object is not in the cache. This effectively tests for
|
||||
#' existence and gets the object in one operation.
|
||||
#'
|
||||
#' It is also possible for one processes to prune objects at the same time that
|
||||
#' another processes is trying to prune objects. If this happens, you may see
|
||||
#' a warning from `file.remove()` failing to remove a file that has
|
||||
#' already been deleted.
|
||||
#'
|
||||
#'
|
||||
#' @section Methods:
|
||||
#'
|
||||
#' A disk cache object has the following methods:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`get(key, missing, exec_missing)`}{
|
||||
#' Returns the value associated with `key`. If the key is not in the
|
||||
#' cache, then it returns the value specified by `missing` or,
|
||||
#' `missing` is a function and `exec_missing=TRUE`, then
|
||||
#' executes `missing`. The function can throw an error or return the
|
||||
#' value. If either of these parameters are specified here, then they
|
||||
#' will override the defaults that were set when the DiskCache object was
|
||||
#' created. See section Missing Keys for more information.
|
||||
#' }
|
||||
#' \item{`set(key, value)`}{
|
||||
#' Stores the `key`-`value` pair in the cache.
|
||||
#' }
|
||||
#' \item{`exists(key)`}{
|
||||
#' Returns `TRUE` if the cache contains the key, otherwise
|
||||
#' `FALSE`.
|
||||
#' }
|
||||
#' \item{`size()`}{
|
||||
#' Returns the number of items currently in the cache.
|
||||
#' }
|
||||
#' \item{`keys()`}{
|
||||
#' Returns a character vector of all keys currently in the cache.
|
||||
#' }
|
||||
#' \item{`reset()`}{
|
||||
#' Clears all objects from the cache.
|
||||
#' }
|
||||
#' \item{`destroy()`}{
|
||||
#' Clears all objects in the cache, and removes the cache directory from
|
||||
#' disk.
|
||||
#' }
|
||||
#' \item{`prune()`}{
|
||||
#' Prunes the cache, using the parameters specified by `max_size`,
|
||||
#' `max_age`, `max_n`, and `evict`.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @param dir Directory to store files for the cache. If `NULL` (the
|
||||
#' default) it will create and use a temporary directory.
|
||||
#' @param max_age Maximum age of files in cache before they are evicted, in
|
||||
#' seconds. Use `Inf` for no age limit.
|
||||
#' @param max_size Maximum size of the cache, in bytes. If the cache exceeds
|
||||
#' this size, cached objects will be removed according to the value of the
|
||||
#' `evict`. Use `Inf` for no size limit.
|
||||
#' @param max_n Maximum number of objects in the cache. If the number of objects
|
||||
#' exceeds this value, then cached objects will be removed according to the
|
||||
#' value of `evict`. Use `Inf` for no limit of number of items.
|
||||
#' @param evict The eviction policy to use to decide which objects are removed
|
||||
#' when a cache pruning occurs. Currently, `"lru"` and `"fifo"` are
|
||||
#' supported.
|
||||
#' @param destroy_on_finalize If `TRUE`, then when the DiskCache object is
|
||||
#' garbage collected, the cache directory and all objects inside of it will be
|
||||
#' deleted from disk. If `FALSE` (the default), it will do nothing when
|
||||
#' finalized.
|
||||
#' @param missing A value to return or a function to execute when
|
||||
#' `get(key)` is called but the key is not present in the cache. The
|
||||
#' default is a [key_missing()] object. If it is a function to
|
||||
#' execute, the function must take one argument (the key), and you must also
|
||||
#' use `exec_missing = TRUE`. If it is a function, it is useful in most
|
||||
#' cases for it to throw an error, although another option is to return a
|
||||
#' value. If a value is returned, that value will in turn be returned by
|
||||
#' `get()`. See section Missing keys for more information.
|
||||
#' @param exec_missing If `FALSE` (the default), then treat `missing`
|
||||
#' as a value to return when `get()` results in a cache miss. If
|
||||
#' `TRUE`, treat `missing` as a function to execute when
|
||||
#' `get()` results in a cache miss.
|
||||
#' @param logfile An optional filename or connection object to where logging
|
||||
#' information will be written. To log to the console, use `stdout()`.
|
||||
#'
|
||||
#' @export
|
||||
diskCache <- function(
|
||||
dir = NULL,
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
destroy_on_finalize = FALSE,
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
DiskCache$new(dir, max_size, max_age, max_n, evict, destroy_on_finalize,
|
||||
missing, exec_missing, logfile)
|
||||
}
|
||||
|
||||
|
||||
DiskCache <- R6Class("DiskCache",
|
||||
public = list(
|
||||
initialize = function(
|
||||
dir = NULL,
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
destroy_on_finalize = FALSE,
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
if (exec_missing && (!is.function(missing) || length(formals(missing)) == 0)) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
if (is.null(dir)) {
|
||||
dir <- tempfile("DiskCache-")
|
||||
}
|
||||
if (!is.numeric(max_size)) stop("max_size must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_age)) stop("max_age must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_n)) stop("max_n must be a number. Use `Inf` for no limit.")
|
||||
|
||||
if (!dirExists(dir)) {
|
||||
private$log(paste0("initialize: Creating ", dir))
|
||||
dir.create(dir, recursive = TRUE)
|
||||
}
|
||||
|
||||
private$dir <- normalizePath(dir)
|
||||
private$max_size <- max_size
|
||||
private$max_age <- max_age
|
||||
private$max_n <- max_n
|
||||
private$evict <- match.arg(evict)
|
||||
private$destroy_on_finalize <- destroy_on_finalize
|
||||
private$missing <- missing
|
||||
private$exec_missing <- exec_missing
|
||||
private$logfile <- logfile
|
||||
|
||||
private$prune_last_time <- as.numeric(Sys.time())
|
||||
},
|
||||
|
||||
get = function(key, missing = private$missing, exec_missing = private$exec_missing) {
|
||||
private$log(paste0('get: key "', key, '"'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
|
||||
private$maybe_prune_single(key)
|
||||
|
||||
filename <- private$key_to_filename(key)
|
||||
|
||||
# Instead of calling exists() before fetching the value, just try to
|
||||
# fetch the value. This reduces the risk of a race condition when
|
||||
# multiple processes share a cache.
|
||||
read_error <- FALSE
|
||||
tryCatch(
|
||||
{
|
||||
value <- suppressWarnings(readRDS(filename))
|
||||
if (private$evict == "lru"){
|
||||
Sys.setFileTime(filename, Sys.time())
|
||||
}
|
||||
},
|
||||
error = function(e) {
|
||||
read_error <<- TRUE
|
||||
}
|
||||
)
|
||||
if (read_error) {
|
||||
private$log(paste0('get: key "', key, '" is missing'))
|
||||
|
||||
if (exec_missing) {
|
||||
if (!is.function(missing) || length(formals(missing)) == 0) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
return(missing(key))
|
||||
} else {
|
||||
return(missing)
|
||||
}
|
||||
}
|
||||
|
||||
private$log(paste0('get: key "', key, '" found'))
|
||||
value
|
||||
},
|
||||
|
||||
set = function(key, value) {
|
||||
private$log(paste0('set: key "', key, '"'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
|
||||
file <- private$key_to_filename(key)
|
||||
temp_file <- paste0(file, "-temp-", createUniqueId(8))
|
||||
|
||||
save_error <- FALSE
|
||||
ref_object <- FALSE
|
||||
tryCatch(
|
||||
{
|
||||
saveRDS(value, file = temp_file,
|
||||
refhook = function(x) {
|
||||
ref_object <<- TRUE
|
||||
NULL
|
||||
}
|
||||
)
|
||||
file.rename(temp_file, file)
|
||||
},
|
||||
error = function(e) {
|
||||
save_error <<- TRUE
|
||||
# Unlike file.remove(), unlink() does not raise warning if file does
|
||||
# not exist.
|
||||
unlink(temp_file)
|
||||
}
|
||||
)
|
||||
if (save_error) {
|
||||
private$log(paste0('set: key "', key, '" error'))
|
||||
stop('Error setting value for key "', key, '".')
|
||||
}
|
||||
if (ref_object) {
|
||||
private$log(paste0('set: value is a reference object'))
|
||||
warning("A reference object was cached in a serialized format. The restored object may not work as expected.")
|
||||
}
|
||||
|
||||
private$prune_throttled()
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
exists = function(key) {
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
file.exists(private$key_to_filename(key))
|
||||
},
|
||||
|
||||
# Return all keys in the cache
|
||||
keys = function() {
|
||||
self$is_destroyed(throw = TRUE)
|
||||
files <- dir(private$dir, "\\.rds$")
|
||||
sub("\\.rds$", "", files)
|
||||
},
|
||||
|
||||
remove = function(key) {
|
||||
private$log(paste0('remove: key "', key, '"'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
file.remove(private$key_to_filename(key))
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
reset = function() {
|
||||
private$log(paste0('reset'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
file.remove(dir(private$dir, "\\.rds$", full.names = TRUE))
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
prune = function() {
|
||||
# TODO: It would be good to add parameters `n` and `size`, so that the
|
||||
# cache can be pruned to `max_n - n` and `max_size - size` before adding
|
||||
# an object. Right now we prune after adding the object, so the cache
|
||||
# can temporarily grow past the limits. The reason we don't do this now
|
||||
# is because it is expensive to find the size of the serialized object
|
||||
# before adding it.
|
||||
|
||||
private$log(paste0('prune'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
|
||||
current_time <- Sys.time()
|
||||
|
||||
filenames <- dir(private$dir, "\\.rds$", full.names = TRUE)
|
||||
info <- file.info(filenames)
|
||||
info <- info[info$isdir == FALSE, ]
|
||||
info$name <- rownames(info)
|
||||
rownames(info) <- NULL
|
||||
# Files could be removed between the dir() and file.info() calls. The
|
||||
# entire row for such files will have NA values. Remove those rows.
|
||||
info <- info[!is.na(info$size), ]
|
||||
|
||||
# 1. Remove any files where the age exceeds max age.
|
||||
if (is.finite(private$max_age)) {
|
||||
timediff <- as.numeric(current_time - info$mtime, units = "secs")
|
||||
rm_idx <- timediff > private$max_age
|
||||
if (any(rm_idx)) {
|
||||
private$log(paste0("prune max_age: Removing ", paste(info$name[rm_idx], collapse = ", ")))
|
||||
file.remove(info$name[rm_idx])
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
}
|
||||
|
||||
# Sort objects by priority. The sorting is done in a function which can be
|
||||
# called multiple times but only does the work the first time.
|
||||
info_is_sorted <- FALSE
|
||||
ensure_info_is_sorted <- function() {
|
||||
if (info_is_sorted) return()
|
||||
|
||||
info <<- info[order(info$mtime, decreasing = TRUE), ]
|
||||
info_is_sorted <<- TRUE
|
||||
}
|
||||
|
||||
# 2. Remove files if there are too many.
|
||||
if (is.finite(private$max_n) && nrow(info) > private$max_n) {
|
||||
ensure_info_is_sorted()
|
||||
rm_idx <- seq_len(nrow(info)) > private$max_n
|
||||
private$log(paste0("prune max_n: Removing ", paste(info$name[rm_idx], collapse = ", ")))
|
||||
rm_success <- file.remove(info$name[rm_idx])
|
||||
info <- info[!rm_success, ]
|
||||
}
|
||||
|
||||
# 3. Remove files if cache is too large.
|
||||
if (is.finite(private$max_size) && sum(info$size) > private$max_size) {
|
||||
ensure_info_is_sorted()
|
||||
cum_size <- cumsum(info$size)
|
||||
rm_idx <- cum_size > private$max_size
|
||||
private$log(paste0("prune max_size: Removing ", paste(info$name[rm_idx], collapse = ", ")))
|
||||
rm_success <- file.remove(info$name[rm_idx])
|
||||
info <- info[!rm_success, ]
|
||||
}
|
||||
|
||||
private$prune_last_time <- as.numeric(current_time)
|
||||
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
size = function() {
|
||||
self$is_destroyed(throw = TRUE)
|
||||
length(dir(private$dir, "\\.rds$"))
|
||||
},
|
||||
|
||||
destroy = function() {
|
||||
if (self$is_destroyed()) {
|
||||
return(invisible(self))
|
||||
}
|
||||
|
||||
private$log(paste0("destroy: Removing ", private$dir))
|
||||
# First create a sentinel file so that other processes sharing this
|
||||
# cache know that the cache is to be destroyed. This is needed because
|
||||
# the recursive unlink is not atomic: another process can add a file to
|
||||
# the directory after unlink starts removing files but before it removes
|
||||
# the directory, and when that happens, the directory removal will fail.
|
||||
file.create(file.path(private$dir, "__destroyed__"))
|
||||
# Remove all the .rds files. This will not remove the setinel file.
|
||||
file.remove(dir(private$dir, "\\.rds$", full.names = TRUE))
|
||||
# Next remove dir recursively, including sentinel file.
|
||||
unlink(private$dir, recursive = TRUE)
|
||||
private$destroyed <- TRUE
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
is_destroyed = function(throw = FALSE) {
|
||||
if (!dirExists(private$dir) ||
|
||||
file.exists(file.path(private$dir, "__destroyed__")))
|
||||
{
|
||||
# It's possible for another process to destroy a shared cache directory
|
||||
private$destroyed <- TRUE
|
||||
}
|
||||
|
||||
if (throw) {
|
||||
if (private$destroyed) {
|
||||
stop("Attempted to use cache which has been destroyed:\n ", private$dir)
|
||||
}
|
||||
|
||||
} else {
|
||||
private$destroyed
|
||||
}
|
||||
},
|
||||
|
||||
finalize = function() {
|
||||
if (private$destroy_on_finalize) {
|
||||
self$destroy()
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
dir = NULL,
|
||||
max_age = NULL,
|
||||
max_size = NULL,
|
||||
max_n = NULL,
|
||||
evict = NULL,
|
||||
destroy_on_finalize = NULL,
|
||||
destroyed = FALSE,
|
||||
missing = NULL,
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL,
|
||||
|
||||
prune_throttle_counter = 0,
|
||||
prune_last_time = NULL,
|
||||
|
||||
key_to_filename = function(key) {
|
||||
validate_key(key)
|
||||
# Additional validation. This 80-char limit is arbitrary, and is
|
||||
# intended to avoid hitting a filename length limit on Windows.
|
||||
if (nchar(key) > 80) {
|
||||
stop("Invalid key: key must have fewer than 80 characters.")
|
||||
}
|
||||
file.path(private$dir, paste0(key, ".rds"))
|
||||
},
|
||||
|
||||
# A wrapper for prune() that throttles it, because prune() can be
|
||||
# expensive due to filesystem operations. This function will prune only
|
||||
# once every 20 times it is called, or if it has been more than 5 seconds
|
||||
# since the last time the cache was actually pruned, whichever is first.
|
||||
# In the future, the behavior may be customizable.
|
||||
prune_throttled = function() {
|
||||
# Count the number of times prune() has been called.
|
||||
private$prune_throttle_counter <- private$prune_throttle_counter + 1
|
||||
|
||||
if (private$prune_throttle_counter > 20 ||
|
||||
private$prune_last_time - as.numeric(Sys.time()) > 5)
|
||||
{
|
||||
self$prune()
|
||||
private$prune_throttle_counter <- 0
|
||||
}
|
||||
},
|
||||
|
||||
# Prunes a single object if it exceeds max_age. If the object does not
|
||||
# exceed max_age, or if the object doesn't exist, do nothing.
|
||||
maybe_prune_single = function(key) {
|
||||
obj <- private$cache[[key]]
|
||||
if (is.null(obj)) return()
|
||||
|
||||
timediff <- as.numeric(Sys.time()) - obj$mtime
|
||||
if (timediff > private$max_age) {
|
||||
private$log(paste0("pruning single object exceeding max_age: Removing ", key))
|
||||
rm(list = key, envir = private$cache)
|
||||
}
|
||||
},
|
||||
|
||||
log = function(text) {
|
||||
if (is.null(private$logfile)) return()
|
||||
|
||||
text <- paste0(format(Sys.time(), "[%Y-%m-%d %H:%M:%OS3] DiskCache "), text)
|
||||
writeLines(text, private$logfile)
|
||||
}
|
||||
)
|
||||
)
|
||||
365
R/cache-memory.R
Normal file
365
R/cache-memory.R
Normal file
@@ -0,0 +1,365 @@
|
||||
#' Create a memory cache object
|
||||
#'
|
||||
#' A memory cache object is a key-value store that saves the values in an
|
||||
#' environment. Objects can be stored and retrieved using the `get()` and
|
||||
#' `set()` methods. Objects are automatically pruned from the cache
|
||||
#' according to the parameters `max_size`, `max_age`, `max_n`,
|
||||
#' and `evict`.
|
||||
#'
|
||||
#' In a `MemoryCache`, R objects are stored directly in the cache; they are
|
||||
#' not *not* serialized before being stored in the cache. This contrasts
|
||||
#' with other cache types, like [diskCache()], where objects are
|
||||
#' serialized, and the serialized object is cached. This can result in some
|
||||
#' differences of behavior. For example, as long as an object is stored in a
|
||||
#' MemoryCache, it will not be garbage collected.
|
||||
#'
|
||||
#'
|
||||
#' @section Missing keys:
|
||||
#' The `missing` and `exec_missing` parameters controls what happens
|
||||
#' when `get()` is called with a key that is not in the cache (a cache
|
||||
#' miss). The default behavior is to return a [key_missing()]
|
||||
#' object. This is a *sentinel value* that indicates that the key was not
|
||||
#' present in the cache. You can test if the returned value represents a
|
||||
#' missing key by using the [is.key_missing()] function. You can
|
||||
#' also have `get()` return a different sentinel value, like `NULL`.
|
||||
#' If you want to throw an error on a cache miss, you can do so by providing a
|
||||
#' function for `missing` that takes one argument, the key, and also use
|
||||
#' `exec_missing=TRUE`.
|
||||
#'
|
||||
#' When the cache is created, you can supply a value for `missing`, which
|
||||
#' sets the default value to be returned for missing values. It can also be
|
||||
#' overridden when `get()` is called, by supplying a `missing`
|
||||
#' argument. For example, if you use `cache$get("mykey", missing =
|
||||
#' NULL)`, it will return `NULL` if the key is not in the cache.
|
||||
#'
|
||||
#' If your cache is configured so that `get()` returns a sentinel value
|
||||
#' to represent a cache miss, then `set` will also not allow you to store
|
||||
#' the sentinel value in the cache. It will throw an error if you attempt to
|
||||
#' do so.
|
||||
#'
|
||||
#' Instead of returning the same sentinel value each time there is cache miss,
|
||||
#' the cache can execute a function each time `get()` encounters missing
|
||||
#' key. If the function returns a value, then `get()` will in turn return
|
||||
#' that value. However, a more common use is for the function to throw an
|
||||
#' error. If an error is thrown, then `get()` will not return a value.
|
||||
#'
|
||||
#' To do this, pass a one-argument function to `missing`, and use
|
||||
#' `exec_missing=TRUE`. For example, if you want to throw an error that
|
||||
#' prints the missing key, you could do this:
|
||||
#'
|
||||
#' \preformatted{
|
||||
#' diskCache(
|
||||
#' missing = function(key) {
|
||||
#' stop("Attempted to get missing key: ", key)
|
||||
#' },
|
||||
#' exec_missing = TRUE
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' If you use this, the code that calls `get()` should be wrapped with
|
||||
#' [tryCatch()] to gracefully handle missing keys.
|
||||
#'
|
||||
#' @section Cache pruning:
|
||||
#'
|
||||
#' Cache pruning occurs when `set()` is called, or it can be invoked
|
||||
#' manually by calling `prune()`.
|
||||
#'
|
||||
#' When a pruning occurs, if there are any objects that are older than
|
||||
#' `max_age`, they will be removed.
|
||||
#'
|
||||
#' The `max_size` and `max_n` parameters are applied to the cache as
|
||||
#' a whole, in contrast to `max_age`, which is applied to each object
|
||||
#' individually.
|
||||
#'
|
||||
#' If the number of objects in the cache exceeds `max_n`, then objects
|
||||
#' will be removed from the cache according to the eviction policy, which is
|
||||
#' set with the `evict` parameter. Objects will be removed so that the
|
||||
#' number of items is `max_n`.
|
||||
#'
|
||||
#' If the size of the objects in the cache exceeds `max_size`, then
|
||||
#' objects will be removed from the cache. Objects will be removed from the
|
||||
#' cache so that the total size remains under `max_size`. Note that the
|
||||
#' size is calculated using the size of the files, not the size of disk space
|
||||
#' used by the files --- these two values can differ because of files are
|
||||
#' stored in blocks on disk. For example, if the block size is 4096 bytes,
|
||||
#' then a file that is one byte in size will take 4096 bytes on disk.
|
||||
#'
|
||||
#' Another time that objects can be removed from the cache is when
|
||||
#' `get()` is called. If the target object is older than `max_age`,
|
||||
#' it will be removed and the cache will report it as a missing value.
|
||||
#'
|
||||
#' @section Eviction policies:
|
||||
#'
|
||||
#' If `max_n` or `max_size` are used, then objects will be removed
|
||||
#' from the cache according to an eviction policy. The available eviction
|
||||
#' policies are:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`"lru"`}{
|
||||
#' Least Recently Used. The least recently used objects will be removed.
|
||||
#' This uses the filesystem's atime property. Some filesystems do not
|
||||
#' support atime, or have a very low atime resolution. The DiskCache will
|
||||
#' check for atime support, and if the filesystem does not support atime,
|
||||
#' a warning will be issued and the "fifo" policy will be used instead.
|
||||
#' }
|
||||
#' \item{`"fifo"`}{
|
||||
#' First-in-first-out. The oldest objects will be removed.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @section Methods:
|
||||
#'
|
||||
#' A disk cache object has the following methods:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`get(key, missing, exec_missing)`}{
|
||||
#' Returns the value associated with `key`. If the key is not in the
|
||||
#' cache, then it returns the value specified by `missing` or,
|
||||
#' `missing` is a function and `exec_missing=TRUE`, then
|
||||
#' executes `missing`. The function can throw an error or return the
|
||||
#' value. If either of these parameters are specified here, then they
|
||||
#' will override the defaults that were set when the DiskCache object was
|
||||
#' created. See section Missing Keys for more information.
|
||||
#' }
|
||||
#' \item{`set(key, value)`}{
|
||||
#' Stores the `key`-`value` pair in the cache.
|
||||
#' }
|
||||
#' \item{`exists(key)`}{
|
||||
#' Returns `TRUE` if the cache contains the key, otherwise
|
||||
#' `FALSE`.
|
||||
#' }
|
||||
#' \item{`size()`}{
|
||||
#' Returns the number of items currently in the cache.
|
||||
#' }
|
||||
#' \item{`keys()`}{
|
||||
#' Returns a character vector of all keys currently in the cache.
|
||||
#' }
|
||||
#' \item{`reset()`}{
|
||||
#' Clears all objects from the cache.
|
||||
#' }
|
||||
#' \item{`destroy()`}{
|
||||
#' Clears all objects in the cache, and removes the cache directory from
|
||||
#' disk.
|
||||
#' }
|
||||
#' \item{`prune()`}{
|
||||
#' Prunes the cache, using the parameters specified by `max_size`,
|
||||
#' `max_age`, `max_n`, and `evict`.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @inheritParams diskCache
|
||||
#'
|
||||
#' @export
|
||||
memoryCache <- function(
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
MemoryCache$new(max_size, max_age, max_n, evict, missing, exec_missing, logfile)
|
||||
}
|
||||
|
||||
MemoryCache <- R6Class("MemoryCache",
|
||||
public = list(
|
||||
initialize = function(
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
if (exec_missing && (!is.function(missing) || length(formals(missing)) == 0)) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
if (!is.numeric(max_size)) stop("max_size must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_age)) stop("max_age must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_n)) stop("max_n must be a number. Use `Inf` for no limit.")
|
||||
private$cache <- fastmap()
|
||||
private$max_size <- max_size
|
||||
private$max_age <- max_age
|
||||
private$max_n <- max_n
|
||||
private$evict <- match.arg(evict)
|
||||
private$missing <- missing
|
||||
private$exec_missing <- exec_missing
|
||||
private$logfile <- logfile
|
||||
},
|
||||
|
||||
get = function(key, missing = private$missing, exec_missing = private$exec_missing) {
|
||||
private$log(paste0('get: key "', key, '"'))
|
||||
validate_key(key)
|
||||
|
||||
private$maybe_prune_single(key)
|
||||
|
||||
if (!self$exists(key)) {
|
||||
private$log(paste0('get: key "', key, '" is missing'))
|
||||
if (exec_missing) {
|
||||
if (!is.function(missing) || length(formals(missing)) == 0) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
return(missing(key))
|
||||
} else {
|
||||
return(missing)
|
||||
}
|
||||
}
|
||||
|
||||
private$log(paste0('get: key "', key, '" found'))
|
||||
value <- private$cache$get(key)$value
|
||||
value
|
||||
},
|
||||
|
||||
set = function(key, value) {
|
||||
private$log(paste0('set: key "', key, '"'))
|
||||
validate_key(key)
|
||||
|
||||
time <- as.numeric(Sys.time())
|
||||
|
||||
# Only record size if we're actually using max_size for pruning.
|
||||
if (is.finite(private$max_size)) {
|
||||
# Reported size is rough! See ?object.size.
|
||||
size <- as.numeric(object.size(value))
|
||||
} else {
|
||||
size <- NULL
|
||||
}
|
||||
|
||||
private$cache$set(key, list(
|
||||
key = key,
|
||||
value = value,
|
||||
size = size,
|
||||
mtime = time,
|
||||
atime = time
|
||||
))
|
||||
self$prune()
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
exists = function(key) {
|
||||
validate_key(key)
|
||||
private$cache$has(key)
|
||||
},
|
||||
|
||||
keys = function() {
|
||||
private$cache$keys()
|
||||
},
|
||||
|
||||
remove = function(key) {
|
||||
private$log(paste0('remove: key "', key, '"'))
|
||||
validate_key(key)
|
||||
private$cache$remove(key)
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
reset = function() {
|
||||
private$log(paste0('reset'))
|
||||
private$cache$reset()
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
prune = function() {
|
||||
private$log(paste0('prune'))
|
||||
info <- private$object_info()
|
||||
|
||||
# 1. Remove any objects where the age exceeds max age.
|
||||
if (is.finite(private$max_age)) {
|
||||
time <- as.numeric(Sys.time())
|
||||
timediff <- time - info$mtime
|
||||
rm_idx <- timediff > private$max_age
|
||||
if (any(rm_idx)) {
|
||||
private$log(paste0("prune max_age: Removing ", paste(info$key[rm_idx], collapse = ", ")))
|
||||
private$cache$remove(info$key[rm_idx])
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
}
|
||||
|
||||
# Sort objects by priority, according to eviction policy. The sorting is
|
||||
# done in a function which can be called multiple times but only does
|
||||
# the work the first time.
|
||||
info_is_sorted <- FALSE
|
||||
ensure_info_is_sorted <- function() {
|
||||
if (info_is_sorted) return()
|
||||
|
||||
if (private$evict == "lru") {
|
||||
info <<- info[order(info$atime, decreasing = TRUE), ]
|
||||
} else if (private$evict == "fifo") {
|
||||
info <<- info[order(info$mtime, decreasing = TRUE), ]
|
||||
} else {
|
||||
stop('Unknown eviction policy "', private$evict, '"')
|
||||
}
|
||||
info_is_sorted <<- TRUE
|
||||
}
|
||||
|
||||
# 2. Remove objects if there are too many.
|
||||
if (is.finite(private$max_n) && nrow(info) > private$max_n) {
|
||||
ensure_info_is_sorted()
|
||||
rm_idx <- seq_len(nrow(info)) > private$max_n
|
||||
private$log(paste0("prune max_n: Removing ", paste(info$key[rm_idx], collapse = ", ")))
|
||||
private$cache$remove(info$key[rm_idx])
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
|
||||
# 3. Remove objects if cache is too large.
|
||||
if (is.finite(private$max_size) && sum(info$size) > private$max_size) {
|
||||
ensure_info_is_sorted()
|
||||
cum_size <- cumsum(info$size)
|
||||
rm_idx <- cum_size > private$max_size
|
||||
private$log(paste0("prune max_size: Removing ", paste(info$key[rm_idx], collapse = ", ")))
|
||||
private$cache$remove(info$key[rm_idx])
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
size = function() {
|
||||
length(self$keys())
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
cache = NULL,
|
||||
max_age = NULL,
|
||||
max_size = NULL,
|
||||
max_n = NULL,
|
||||
evict = NULL,
|
||||
missing = NULL,
|
||||
exec_missing = NULL,
|
||||
logfile = NULL,
|
||||
|
||||
# Prunes a single object if it exceeds max_age. If the object does not
|
||||
# exceed max_age, or if the object doesn't exist, do nothing.
|
||||
maybe_prune_single = function(key) {
|
||||
if (!is.finite(private$max_age)) return()
|
||||
|
||||
obj <- private$cache$get(key)
|
||||
if (is.null(obj)) return()
|
||||
|
||||
timediff <- as.numeric(Sys.time()) - obj$mtime
|
||||
if (timediff > private$max_age) {
|
||||
private$log(paste0("pruning single object exceeding max_age: Removing ", key))
|
||||
private$cache$remove(key)
|
||||
}
|
||||
},
|
||||
|
||||
object_info = function() {
|
||||
keys <- private$cache$keys()
|
||||
data.frame(
|
||||
key = keys,
|
||||
size = vapply(keys, function(key) private$cache$get(key)$size, 0),
|
||||
mtime = vapply(keys, function(key) private$cache$get(key)$mtime, 0),
|
||||
atime = vapply(keys, function(key) private$cache$get(key)$atime, 0),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
},
|
||||
|
||||
log = function(text) {
|
||||
if (is.null(private$logfile)) return()
|
||||
|
||||
text <- paste0(format(Sys.time(), "[%Y-%m-%d %H:%M:%OS3] MemoryCache "), text)
|
||||
writeLines(text, private$logfile)
|
||||
}
|
||||
)
|
||||
)
|
||||
18
R/cache-utils.R
Normal file
18
R/cache-utils.R
Normal file
@@ -0,0 +1,18 @@
|
||||
#' @importFrom fastmap key_missing
|
||||
#' @export
|
||||
fastmap::key_missing
|
||||
|
||||
#' @importFrom fastmap is.key_missing
|
||||
#' @export
|
||||
fastmap::is.key_missing
|
||||
|
||||
|
||||
validate_key <- function(key) {
|
||||
if (!is.character(key) || length(key) != 1 || nchar(key) == 0) {
|
||||
stop("Invalid key: key must be single non-empty string.")
|
||||
}
|
||||
if (grepl("[^a-z0-9]", key)) {
|
||||
stop("Invalid key: ", key, ". Only lowercase letters and numbers are allowed.")
|
||||
}
|
||||
}
|
||||
|
||||
436
R/conditions.R
436
R/conditions.R
@@ -3,9 +3,9 @@
|
||||
#' Advanced (borderline internal) functions for capturing, printing, and
|
||||
#' manipulating stack traces.
|
||||
#'
|
||||
#' @return \code{printError} and \code{printStackTrace} return
|
||||
#' \code{invisible()}. The other functions pass through the results of
|
||||
#' \code{expr}.
|
||||
#' @return `printError` and `printStackTrace` return
|
||||
#' `invisible()`. The other functions pass through the results of
|
||||
#' `expr`.
|
||||
#'
|
||||
#' @examples
|
||||
#' # Keeps tryCatch and withVisible related calls off the
|
||||
@@ -76,7 +76,7 @@ getCallNames <- function(calls) {
|
||||
}
|
||||
|
||||
getLocs <- function(calls) {
|
||||
sapply(calls, function(call) {
|
||||
vapply(calls, function(call) {
|
||||
srcref <- attr(call, "srcref", exact = TRUE)
|
||||
if (!is.null(srcref)) {
|
||||
srcfile <- attr(srcref, "srcfile", exact = TRUE)
|
||||
@@ -86,41 +86,134 @@ getLocs <- function(calls) {
|
||||
}
|
||||
}
|
||||
return("")
|
||||
})
|
||||
}, character(1))
|
||||
}
|
||||
|
||||
#' @details \code{captureStackTraces} runs the given \code{expr} and if any
|
||||
#' \emph{uncaught} errors occur, annotates them with stack trace info for use
|
||||
#' by \code{printError} and \code{printStackTrace}. It is not necessary to use
|
||||
#' \code{captureStackTraces} around the same expression as
|
||||
#' \code{withLogErrors}, as the latter includes a call to the former. Note
|
||||
#' that if \code{expr} contains calls (either directly or indirectly) to
|
||||
#' \code{try}, or \code{tryCatch} with an error handler, stack traces therein
|
||||
#' cannot be captured unless another \code{captureStackTraces} call is
|
||||
#' inserted in the interior of the \code{try} or \code{tryCatch}. This is
|
||||
getCallCategories <- function(calls) {
|
||||
vapply(calls, function(call) {
|
||||
srcref <- attr(call, "srcref", exact = TRUE)
|
||||
if (!is.null(srcref)) {
|
||||
srcfile <- attr(srcref, "srcfile", exact = TRUE)
|
||||
if (!is.null(srcfile)) {
|
||||
if (!is.null(srcfile$original)) {
|
||||
return("pkg")
|
||||
} else {
|
||||
return("user")
|
||||
}
|
||||
}
|
||||
}
|
||||
return("")
|
||||
}, character(1))
|
||||
}
|
||||
|
||||
#' @details `captureStackTraces` runs the given `expr` and if any
|
||||
#' *uncaught* errors occur, annotates them with stack trace info for use
|
||||
#' by `printError` and `printStackTrace`. It is not necessary to use
|
||||
#' `captureStackTraces` around the same expression as
|
||||
#' `withLogErrors`, as the latter includes a call to the former. Note
|
||||
#' that if `expr` contains calls (either directly or indirectly) to
|
||||
#' `try`, or `tryCatch` with an error handler, stack traces therein
|
||||
#' cannot be captured unless another `captureStackTraces` call is
|
||||
#' inserted in the interior of the `try` or `tryCatch`. This is
|
||||
#' because these calls catch the error and prevent it from traveling up to the
|
||||
#' condition handler installed by \code{captureStackTraces}.
|
||||
#' condition handler installed by `captureStackTraces`.
|
||||
#'
|
||||
#' @param expr The expression to wrap.
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
captureStackTraces <- function(expr) {
|
||||
withCallingHandlers(expr,
|
||||
error = function(e) {
|
||||
if (is.null(attr(e, "stack.trace", exact = TRUE))) {
|
||||
calls <- sys.calls()
|
||||
attr(e, "stack.trace") <- calls
|
||||
stop(e)
|
||||
}
|
||||
}
|
||||
promises::with_promise_domain(createStackTracePromiseDomain(),
|
||||
expr
|
||||
)
|
||||
}
|
||||
|
||||
#' @details \code{withLogErrors} captures stack traces and logs errors that
|
||||
#' occur in \code{expr}, but does allow errors to propagate beyond this point
|
||||
#' @include globals.R
|
||||
.globals$deepStack <- NULL
|
||||
|
||||
createStackTracePromiseDomain <- function() {
|
||||
# These are actually stateless, we wouldn't have to create a new one each time
|
||||
# if we didn't want to. They're pretty cheap though.
|
||||
|
||||
d <- promises::new_promise_domain(
|
||||
wrapOnFulfilled = function(onFulfilled) {
|
||||
force(onFulfilled)
|
||||
# Subscription time
|
||||
if (deepStacksEnabled()) {
|
||||
currentStack <- sys.calls()
|
||||
currentParents <- sys.parents()
|
||||
attr(currentStack, "parents") <- currentParents
|
||||
currentDeepStack <- .globals$deepStack
|
||||
}
|
||||
function(...) {
|
||||
# Fulfill time
|
||||
if (deepStacksEnabled()) {
|
||||
origDeepStack <- .globals$deepStack
|
||||
.globals$deepStack <- c(currentDeepStack, list(currentStack))
|
||||
on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
|
||||
}
|
||||
|
||||
withCallingHandlers(
|
||||
onFulfilled(...),
|
||||
error = doCaptureStack
|
||||
)
|
||||
}
|
||||
},
|
||||
wrapOnRejected = function(onRejected) {
|
||||
force(onRejected)
|
||||
# Subscription time
|
||||
if (deepStacksEnabled()) {
|
||||
currentStack <- sys.calls()
|
||||
currentParents <- sys.parents()
|
||||
attr(currentStack, "parents") <- currentParents
|
||||
currentDeepStack <- .globals$deepStack
|
||||
}
|
||||
function(...) {
|
||||
# Fulfill time
|
||||
if (deepStacksEnabled()) {
|
||||
origDeepStack <- .globals$deepStack
|
||||
.globals$deepStack <- c(currentDeepStack, list(currentStack))
|
||||
on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
|
||||
}
|
||||
|
||||
withCallingHandlers(
|
||||
onRejected(...),
|
||||
error = doCaptureStack
|
||||
)
|
||||
}
|
||||
},
|
||||
wrapSync = function(expr) {
|
||||
withCallingHandlers(expr,
|
||||
error = doCaptureStack
|
||||
)
|
||||
},
|
||||
onError = doCaptureStack
|
||||
)
|
||||
}
|
||||
|
||||
deepStacksEnabled <- function() {
|
||||
getOption("shiny.deepstacktrace", TRUE)
|
||||
}
|
||||
|
||||
doCaptureStack <- function(e) {
|
||||
if (is.null(attr(e, "stack.trace", exact = TRUE))) {
|
||||
calls <- sys.calls()
|
||||
parents <- sys.parents()
|
||||
attr(calls, "parents") <- parents
|
||||
attr(e, "stack.trace") <- calls
|
||||
}
|
||||
if (deepStacksEnabled()) {
|
||||
if (is.null(attr(e, "deep.stack.trace", exact = TRUE)) && !is.null(.globals$deepStack)) {
|
||||
attr(e, "deep.stack.trace") <- .globals$deepStack
|
||||
}
|
||||
}
|
||||
stop(e)
|
||||
}
|
||||
|
||||
#' @details `withLogErrors` captures stack traces and logs errors that
|
||||
#' occur in `expr`, but does allow errors to propagate beyond this point
|
||||
#' (i.e. it doesn't catch the error). The same caveats that apply to
|
||||
#' \code{captureStackTraces} with regard to \code{try}/\code{tryCatch} apply
|
||||
#' to \code{withLogErrors}.
|
||||
#' `captureStackTraces` with regard to `try`/`tryCatch` apply
|
||||
#' to `withLogErrors`.
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
withLogErrors <- function(expr,
|
||||
@@ -128,40 +221,56 @@ withLogErrors <- function(expr,
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
withCallingHandlers(
|
||||
captureStackTraces(expr),
|
||||
{
|
||||
result <- captureStackTraces(expr)
|
||||
|
||||
# Handle expr being an async operation
|
||||
if (promises::is.promise(result)) {
|
||||
result <- promises::catch(result, function(cond) {
|
||||
# Don't print shiny.silent.error (i.e. validation errors)
|
||||
if (inherits(cond, "shiny.silent.error")) return()
|
||||
if (isTRUE(getOption("show.error.messages"))) {
|
||||
printError(cond, full = full, offset = offset)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
result
|
||||
},
|
||||
error = function(cond) {
|
||||
# Don't print shiny.silent.error (i.e. validation errors)
|
||||
if (inherits(cond, "shiny.silent.error"))
|
||||
return()
|
||||
printError(cond, full = full, offset = offset)
|
||||
if (inherits(cond, "shiny.silent.error")) return()
|
||||
if (isTRUE(getOption("show.error.messages"))) {
|
||||
printError(cond, full = full, offset = offset)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#' @details \code{printError} prints the error and stack trace (if any) using
|
||||
#' \code{warning(immediate.=TRUE)}. \code{printStackTrace} prints the stack
|
||||
#' @details `printError` prints the error and stack trace (if any) using
|
||||
#' `warning(immediate.=TRUE)`. `printStackTrace` prints the stack
|
||||
#' trace only.
|
||||
#'
|
||||
#' @param cond An condition object (generally, an error).
|
||||
#' @param full If \code{TRUE}, then every element of \code{sys.calls()} will be
|
||||
#' included in the stack trace. By default (\code{FALSE}), calls that Shiny
|
||||
#' @param full If `TRUE`, then every element of `sys.calls()` will be
|
||||
#' included in the stack trace. By default (`FALSE`), calls that Shiny
|
||||
#' deems uninteresting will be hidden.
|
||||
#' @param offset If \code{TRUE} (the default), srcrefs will be reassigned from
|
||||
#' @param offset If `TRUE` (the default), srcrefs will be reassigned from
|
||||
#' the calls they originated from, to the destinations of those calls. If
|
||||
#' you're used to stack traces from other languages, this feels more
|
||||
#' intuitive, as the definition of the function indicated in the call and the
|
||||
#' location specified by the srcref match up. If \code{FALSE}, srcrefs will be
|
||||
#' location specified by the srcref match up. If `FALSE`, srcrefs will be
|
||||
#' left alone (traditional R treatment where the srcref is of the callsite).
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
printError <- function(cond,
|
||||
full = getOption("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
warning(call. = FALSE, immediate. = TRUE, sprintf("Error in %s: %s",
|
||||
|
||||
warning(call. = FALSE, immediate. = TRUE, sprintf("Error in %s: %s",
|
||||
getCallNames(list(conditionCall(cond))), conditionMessage(cond)))
|
||||
|
||||
printStackTrace(cond, full = full, offset = offset)
|
||||
invisible()
|
||||
}
|
||||
|
||||
#' @rdname stacktrace
|
||||
@@ -170,37 +279,103 @@ printStackTrace <- function(cond,
|
||||
full = getOption("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
stackTrace <- attr(cond, "stack.trace", exact = TRUE)
|
||||
tryCatch(
|
||||
if (!is.null(stackTrace)) {
|
||||
message(paste0(
|
||||
"Stack trace (innermost first):\n",
|
||||
paste0(collapse = "\n",
|
||||
formatStackTrace(stackTrace, full = full, offset = offset,
|
||||
indent = " ")
|
||||
)
|
||||
))
|
||||
} else {
|
||||
message("No stack trace available")
|
||||
},
|
||||
|
||||
error = function(cond) {
|
||||
warning("Failed to write stack trace: ", cond)
|
||||
}
|
||||
should_drop <- !full
|
||||
should_strip <- !full
|
||||
should_prune <- !full
|
||||
|
||||
stackTraceCalls <- c(
|
||||
attr(cond, "deep.stack.trace", exact = TRUE),
|
||||
list(attr(cond, "stack.trace", exact = TRUE))
|
||||
)
|
||||
|
||||
stackTraceParents <- lapply(stackTraceCalls, attr, which = "parents", exact = TRUE)
|
||||
stackTraceCallNames <- lapply(stackTraceCalls, getCallNames)
|
||||
stackTraceCalls <- lapply(stackTraceCalls, offsetSrcrefs, offset = offset)
|
||||
|
||||
# Use dropTrivialFrames logic to remove trailing bits (.handleSimpleError, h)
|
||||
if (should_drop) {
|
||||
# toKeep is a list of logical vectors, of which elements (stack frames) to keep
|
||||
toKeep <- lapply(stackTraceCallNames, dropTrivialFrames)
|
||||
# We apply the list of logical vector indices to each data structure
|
||||
stackTraceCalls <- mapply(stackTraceCalls, FUN = `[`, toKeep, SIMPLIFY = FALSE)
|
||||
stackTraceCallNames <- mapply(stackTraceCallNames, FUN = `[`, toKeep, SIMPLIFY = FALSE)
|
||||
stackTraceParents <- mapply(stackTraceParents, FUN = `[`, toKeep, SIMPLIFY = FALSE)
|
||||
}
|
||||
|
||||
delayedAssign("all_true", {
|
||||
# List of logical vectors that are all TRUE, the same shape as
|
||||
# stackTraceCallNames. Delay the evaluation so we don't create it unless
|
||||
# we need it, but if we need it twice then we don't pay to create it twice.
|
||||
lapply(stackTraceCallNames, function(st) {
|
||||
rep_len(TRUE, length(st))
|
||||
})
|
||||
})
|
||||
|
||||
# stripStackTraces and lapply(stackTraceParents, pruneStackTrace) return lists
|
||||
# of logical vectors. Use mapply(FUN = `&`) to boolean-and each pair of the
|
||||
# logical vectors.
|
||||
toShow <- mapply(
|
||||
if (should_strip) stripStackTraces(stackTraceCallNames) else all_true,
|
||||
if (should_prune) lapply(stackTraceParents, pruneStackTrace) else all_true,
|
||||
FUN = `&`,
|
||||
SIMPLIFY = FALSE
|
||||
)
|
||||
|
||||
dfs <- mapply(seq_along(stackTraceCalls), rev(stackTraceCalls), rev(stackTraceCallNames), rev(toShow), FUN = function(i, calls, nms, index) {
|
||||
st <- data.frame(
|
||||
num = rev(which(index)),
|
||||
call = rev(nms[index]),
|
||||
loc = rev(getLocs(calls[index])),
|
||||
category = rev(getCallCategories(calls[index])),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
|
||||
if (i != 1) {
|
||||
message("From earlier call:")
|
||||
}
|
||||
|
||||
if (nrow(st) == 0) {
|
||||
message(" [No stack trace available]")
|
||||
} else {
|
||||
width <- floor(log10(max(st$num))) + 1
|
||||
formatted <- paste0(
|
||||
" ",
|
||||
formatC(st$num, width = width),
|
||||
": ",
|
||||
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
|
||||
if (category == "pkg")
|
||||
crayon::silver(name)
|
||||
else if (category == "user")
|
||||
crayon::blue$bold(name)
|
||||
else
|
||||
crayon::white(name)
|
||||
}),
|
||||
"\n"
|
||||
)
|
||||
cat(file = stderr(), formatted, sep = "")
|
||||
}
|
||||
|
||||
st
|
||||
}, SIMPLIFY = FALSE)
|
||||
|
||||
invisible()
|
||||
}
|
||||
|
||||
#' @details \code{extractStackTrace} takes a list of calls (e.g. as returned
|
||||
#' from \code{conditionStackTrace(cond)}) and returns a data frame with one
|
||||
#' row for each stack frame and the columns \code{num} (stack frame number),
|
||||
#' \code{call} (a function name or similar), and \code{loc} (source file path
|
||||
#' and line number, if available).
|
||||
#' @details `extractStackTrace` takes a list of calls (e.g. as returned
|
||||
#' from `conditionStackTrace(cond)`) and returns a data frame with one
|
||||
#' row for each stack frame and the columns `num` (stack frame number),
|
||||
#' `call` (a function name or similar), and `loc` (source file path
|
||||
#' and line number, if available). It was deprecated after shiny 1.0.5 because
|
||||
#' it doesn't support deep stack traces.
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
extractStackTrace <- function(calls,
|
||||
full = getOption("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
shinyDeprecated(NULL,
|
||||
"extractStackTrace is deprecated. Please contact the Shiny team if you were using this functionality.",
|
||||
version = "1.0.5")
|
||||
|
||||
srcrefs <- getSrcRefs(calls)
|
||||
if (offset) {
|
||||
@@ -240,7 +415,11 @@ extractStackTrace <- function(calls,
|
||||
score <- rep.int(0, length(callnames))
|
||||
score[callnames == "..stacktraceoff.."] <- -1
|
||||
score[callnames == "..stacktraceon.."] <- 1
|
||||
toShow <- (1 + cumsum(score)) > 0 & !(callnames %in% c("..stacktraceon..", "..stacktraceoff.."))
|
||||
toShow <- (1 + cumsum(score)) > 0 & !(callnames %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
|
||||
|
||||
# doTryCatch, tryCatchOne, and tryCatchList are not informative--they're
|
||||
# just internals for tryCatch
|
||||
toShow <- toShow & !(callnames %in% c("doTryCatch", "tryCatchOne", "tryCatchList"))
|
||||
}
|
||||
calls <- calls[toShow]
|
||||
|
||||
@@ -252,12 +431,115 @@ extractStackTrace <- function(calls,
|
||||
num = index,
|
||||
call = getCallNames(calls),
|
||||
loc = getLocs(calls),
|
||||
category = getCallCategories(calls),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
}
|
||||
|
||||
#' @details \code{formatStackTrace} is similar to \code{extractStackTrace}, but
|
||||
#' it returns a preformatted character vector instead of a data frame.
|
||||
stripStackTraces <- function(stackTraces, values = FALSE) {
|
||||
score <- 1L # >=1: show, <=0: hide
|
||||
lapply(seq_along(stackTraces), function(i) {
|
||||
res <- stripOneStackTrace(stackTraces[[i]], i != 1, score)
|
||||
score <<- res$score
|
||||
toShow <- as.logical(res$trace)
|
||||
if (values) {
|
||||
as.character(stackTraces[[i]][toShow])
|
||||
} else {
|
||||
as.logical(toShow)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stripOneStackTrace <- function(stackTrace, truncateFloor, startingScore) {
|
||||
prefix <- logical(0)
|
||||
if (truncateFloor) {
|
||||
indexOfFloor <- utils::tail(which(stackTrace == "..stacktracefloor.."), 1)
|
||||
if (length(indexOfFloor)) {
|
||||
stackTrace <- stackTrace[(indexOfFloor+1L):length(stackTrace)]
|
||||
prefix <- rep_len(FALSE, indexOfFloor)
|
||||
}
|
||||
}
|
||||
|
||||
if (length(stackTrace) == 0) {
|
||||
return(list(score = startingScore, character(0)))
|
||||
}
|
||||
|
||||
score <- rep.int(0L, length(stackTrace))
|
||||
score[stackTrace == "..stacktraceon.."] <- 1L
|
||||
score[stackTrace == "..stacktraceoff.."] <- -1L
|
||||
score <- startingScore + cumsum(score)
|
||||
|
||||
toShow <- score > 0 & !(stackTrace %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
|
||||
|
||||
|
||||
list(score = utils::tail(score, 1), trace = c(prefix, toShow))
|
||||
}
|
||||
|
||||
# Given sys.parents() (which corresponds to sys.calls()), return a logical index
|
||||
# that prunes each subtree so that only the final branch remains. The result,
|
||||
# when applied to sys.calls(), is a linear list of calls without any "wrapper"
|
||||
# functions like tryCatch, try, with, hybrid_chain, etc. While these are often
|
||||
# part of the active call stack, they rarely are helpful when trying to identify
|
||||
# a broken bit of code.
|
||||
pruneStackTrace <- function(parents) {
|
||||
# Detect nodes that are not the last child. This is necessary, but not
|
||||
# sufficient; we also need to drop nodes that are the last child, but one of
|
||||
# their ancestors is not.
|
||||
is_dupe <- duplicated(parents, fromLast = TRUE)
|
||||
|
||||
# The index of the most recently seen node that was actually kept instead of
|
||||
# dropped.
|
||||
current_node <- 0
|
||||
|
||||
# Loop over the parent indices. Anything that is not parented by current_node
|
||||
# (a.k.a. last-known-good node), or is a dupe, can be discarded. Anything that
|
||||
# is kept becomes the new current_node.
|
||||
include <- vapply(seq_along(parents), function(i) {
|
||||
if (!is_dupe[[i]] && parents[[i]] == current_node) {
|
||||
current_node <<- i
|
||||
TRUE
|
||||
} else {
|
||||
FALSE
|
||||
}
|
||||
}, FUN.VALUE = logical(1))
|
||||
|
||||
include
|
||||
}
|
||||
|
||||
dropTrivialFrames <- function(callnames) {
|
||||
# Remove stop(), .handleSimpleError(), and h() calls from the end of
|
||||
# the calls--they don't add any helpful information. But only remove
|
||||
# the last *contiguous* block of them, and then, only if they are the
|
||||
# last thing in the calls list.
|
||||
hideable <- callnames %in% c(".handleSimpleError", "h", "base$wrapOnFulfilled")
|
||||
# What's the last that *didn't* match stop/.handleSimpleError/h?
|
||||
lastGoodCall <- max(which(!hideable))
|
||||
toRemove <- length(callnames) - lastGoodCall
|
||||
|
||||
c(
|
||||
rep_len(TRUE, length(callnames) - toRemove),
|
||||
rep_len(FALSE, toRemove)
|
||||
)
|
||||
}
|
||||
|
||||
offsetSrcrefs <- function(calls, offset = TRUE) {
|
||||
if (offset) {
|
||||
srcrefs <- getSrcRefs(calls)
|
||||
|
||||
# Offset calls vs. srcrefs by 1 to make them more intuitive.
|
||||
# E.g. for "foo [bar.R:10]", line 10 of bar.R will be part of
|
||||
# the definition of foo().
|
||||
srcrefs <- c(utils::tail(srcrefs, -1), list(NULL))
|
||||
|
||||
calls <- setSrcRefs(calls, srcrefs)
|
||||
}
|
||||
|
||||
calls
|
||||
}
|
||||
|
||||
#' @details `formatStackTrace` is similar to `extractStackTrace`, but
|
||||
#' it returns a preformatted character vector instead of a data frame. It was
|
||||
#' deprecated after shiny 1.0.5 because it doesn't support deep stack traces.
|
||||
#' @param indent A string to prefix every line of the stack trace.
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
@@ -265,6 +547,10 @@ formatStackTrace <- function(calls, indent = " ",
|
||||
full = getOption("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
shinyDeprecated(NULL,
|
||||
"extractStackTrace is deprecated. Please contact the Shiny team if you were using this functionality.",
|
||||
version = "1.0.5")
|
||||
|
||||
st <- extractStackTrace(calls, full = full, offset = offset)
|
||||
if (nrow(st) == 0) {
|
||||
return(character(0))
|
||||
@@ -275,8 +561,14 @@ formatStackTrace <- function(calls, indent = " ",
|
||||
indent,
|
||||
formatC(st$num, width = width),
|
||||
": ",
|
||||
st$call,
|
||||
st$loc
|
||||
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
|
||||
if (category == "pkg")
|
||||
crayon::silver(name)
|
||||
else if (category == "user")
|
||||
crayon::blue$bold(name)
|
||||
else
|
||||
crayon::white(name)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -296,11 +588,11 @@ stripStackTrace <- function(cond) {
|
||||
conditionStackTrace(cond) <- NULL
|
||||
}
|
||||
|
||||
#' @details \code{conditionStackTrace} and \code{conditionStackTrace<-} are
|
||||
#' @details `conditionStackTrace` and `conditionStackTrace<-` are
|
||||
#' accessor functions for getting/setting stack traces on conditions.
|
||||
#'
|
||||
#' @param cond A condition that may have previously been annotated by
|
||||
#' \code{captureStackTraces} (or \code{withLogErrors}).
|
||||
#' `captureStackTraces` (or `withLogErrors`).
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
conditionStackTrace <- function(cond) {
|
||||
@@ -315,8 +607,8 @@ conditionStackTrace <- function(cond) {
|
||||
invisible(cond)
|
||||
}
|
||||
|
||||
#' @details The two functions \code{..stacktraceon..} and
|
||||
#' \code{..stacktraceoff..} have no runtime behavior during normal execution;
|
||||
#' @details The two functions `..stacktraceon..` and
|
||||
#' `..stacktraceoff..` have no runtime behavior during normal execution;
|
||||
#' they exist only to create artifacts on the stack trace (sys.call()) that
|
||||
#' instruct the stack trace pretty printer what parts of the stack trace are
|
||||
#' interesting or not. The initial state is 1 and we walk from the outermost
|
||||
@@ -331,3 +623,5 @@ conditionStackTrace <- function(cond) {
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
..stacktraceoff.. <- function(expr) expr
|
||||
|
||||
..stacktracefloor.. <- function(expr) expr
|
||||
|
||||
157
R/diagnose.R
Normal file
157
R/diagnose.R
Normal file
@@ -0,0 +1,157 @@
|
||||
# Analyze an R file for possible extra or missing commas. Returns FALSE if any
|
||||
# problems detected, TRUE otherwise.
|
||||
diagnoseCode <- function(path = NULL, text = NULL) {
|
||||
if (!xor(is.null(path), is.null(text))) {
|
||||
stop("Must specify `path` or `text`, but not both.")
|
||||
}
|
||||
|
||||
if (!is.null(path)) {
|
||||
tokens <- sourcetools::tokenize_file(path)
|
||||
} else {
|
||||
tokens <- sourcetools::tokenize_string(text)
|
||||
}
|
||||
|
||||
find_scopes <- function(tokens) {
|
||||
# Strip whitespace and comments
|
||||
tokens <- tokens[!(tokens$type %in% c("whitespace", "comment")),]
|
||||
|
||||
# Replace various types of things with "value"
|
||||
tokens$type[tokens$type %in% c("string", "number", "symbol", "keyword")] <- "value"
|
||||
|
||||
# Record types for close and open brace/bracket/parens, and commas
|
||||
brace_idx <- tokens$value %in% c("(", ")", "{", "}", "[", "]", ",")
|
||||
tokens$type[brace_idx] <- tokens$value[brace_idx]
|
||||
|
||||
# Stack-related function for recording scope. Starting scope is "{"
|
||||
stack <- "{"
|
||||
push <- function(x) {
|
||||
stack <<- c(stack, x)
|
||||
}
|
||||
pop <- function() {
|
||||
if (length(stack) == 1) {
|
||||
# Stack underflow, but we need to keep going
|
||||
return(NA_character_)
|
||||
}
|
||||
res <- stack[length(stack)]
|
||||
stack <<- stack[-length(stack)]
|
||||
res
|
||||
}
|
||||
peek <- function() {
|
||||
stack[length(stack)]
|
||||
}
|
||||
|
||||
# First, establish a scope for each token. For opening and closing
|
||||
# braces/brackets/parens, the scope at that location is the *surrounding*
|
||||
# scope, not the new scope created by the brace/bracket/paren.
|
||||
for (i in seq_len(nrow(tokens))) {
|
||||
value <- tokens$value[i]
|
||||
|
||||
tokens$scope[i] <- peek()
|
||||
if (value %in% c("{", "(", "[")) {
|
||||
push(value)
|
||||
|
||||
} else if (value == "}") {
|
||||
if (!identical(pop(), "{"))
|
||||
tokens$err[i] <- "unmatched_brace"
|
||||
# For closing brace/paren/bracket, get the scope after popping
|
||||
tokens$scope[i] <- peek()
|
||||
|
||||
} else if (value == ")") {
|
||||
if (!identical(pop(), "("))
|
||||
tokens$err[i] <- "unmatched_paren"
|
||||
tokens$scope[i] <- peek()
|
||||
|
||||
} else if (value == "]") {
|
||||
if (!identical(pop(), "["))
|
||||
tokens$err[i] <- "unmatched_bracket"
|
||||
tokens$scope[i] <- peek()
|
||||
}
|
||||
}
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
check_commas <- function(tokens) {
|
||||
# Find extra and missing commas
|
||||
tokens$err <- mapply(
|
||||
tokens$type,
|
||||
c("", tokens$type[-length(tokens$type)]),
|
||||
c(tokens$type[-1], ""),
|
||||
tokens$scope,
|
||||
tokens$err,
|
||||
SIMPLIFY = FALSE,
|
||||
FUN = function(type, prevType, nextType, scope, err) {
|
||||
# If an error was already found, just return it. This could have
|
||||
# happened in the brace/paren/bracket matching phase.
|
||||
if (!is.na(err)) {
|
||||
return(err)
|
||||
}
|
||||
if (scope == "(") {
|
||||
if (type == "," &&
|
||||
(prevType == "(" || prevType == "," || nextType == ")"))
|
||||
{
|
||||
return("extra_comma")
|
||||
}
|
||||
|
||||
if ((prevType == ")" && type == "value") ||
|
||||
(prevType == "value" && type == "value")) {
|
||||
return("missing_comma")
|
||||
}
|
||||
}
|
||||
|
||||
NA_character_
|
||||
}
|
||||
)
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
|
||||
tokens$err <- NA_character_
|
||||
tokens <- find_scopes(tokens)
|
||||
tokens <- check_commas(tokens)
|
||||
|
||||
# No errors found
|
||||
if (all(is.na(tokens$err))) {
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
# If we got here, errors were found; print messages.
|
||||
if (!is.null(path)) {
|
||||
lines <- readLines(path)
|
||||
} else {
|
||||
lines <- strsplit(text, "\n")[[1]]
|
||||
}
|
||||
|
||||
# Print out the line of code with the error, and point to the column with
|
||||
# the error.
|
||||
show_code_error <- function(msg, lines, row, col) {
|
||||
message(paste0(
|
||||
msg, "\n",
|
||||
row, ":", lines[row], "\n",
|
||||
paste0(rep.int(" ", nchar(as.character(row)) + 1), collapse = ""),
|
||||
gsub(perl = TRUE, "[^\\s]", " ", substr(lines[row], 1, col-1)), "^"
|
||||
))
|
||||
}
|
||||
|
||||
err_idx <- which(!is.na(tokens$err))
|
||||
msg <- ""
|
||||
for (i in err_idx) {
|
||||
row <- tokens$row[i]
|
||||
col <- tokens$column[i]
|
||||
err <- tokens$err[i]
|
||||
|
||||
if (err == "missing_comma") {
|
||||
show_code_error("Possible missing comma at:", lines, row, col)
|
||||
} else if (err == "extra_comma") {
|
||||
show_code_error("Possible extra comma at:", lines, row, col)
|
||||
} else if (err == "unmatched_brace") {
|
||||
show_code_error("Possible unmatched '}' at:", lines, row, col)
|
||||
} else if (err == "unmatched_paren") {
|
||||
show_code_error("Possible unmatched ')' at:", lines, row, col)
|
||||
} else if (err == "unmatched_bracket") {
|
||||
show_code_error("Possible unmatched ']' at:", lines, row, col)
|
||||
}
|
||||
}
|
||||
return(FALSE)
|
||||
}
|
||||
@@ -20,6 +20,18 @@
|
||||
# form upload, i.e. traditional HTTP POST-based file upload) doesn't work with
|
||||
# the websockets package's HTTP server at the moment.
|
||||
|
||||
# @description Returns a file's extension, with a leading dot, if one can be
|
||||
# found. A valid extension contains only alphanumeric characters. If there is
|
||||
# no extension, or if it contains non-alphanumeric characters, an empty
|
||||
# string is returned.
|
||||
# @param x character vector giving file paths.
|
||||
# @return The extension of \code{x}, with a leading dot, if one was found.
|
||||
# Otherwise, an empty character vector.
|
||||
maybeGetExtension <- function(x) {
|
||||
ext <- tools::file_ext(x)
|
||||
ifelse(ext == "", ext, paste0(".", ext))
|
||||
}
|
||||
|
||||
FileUploadOperation <- R6Class(
|
||||
'FileUploadOperation',
|
||||
portable = FALSE,
|
||||
@@ -52,8 +64,9 @@ FileUploadOperation <- R6Class(
|
||||
.currentFileInfo <<- file
|
||||
.pendingFileInfos <<- tail(.pendingFileInfos, -1)
|
||||
|
||||
filename <- file.path(.dir, as.character(length(.files$name)))
|
||||
row <- data.frame(name=file$name, size=file$size, type=file$type,
|
||||
fileBasename <- basename(.currentFileInfo$name)
|
||||
filename <- file.path(.dir, paste0(as.character(length(.files$name)), maybeGetExtension(fileBasename)))
|
||||
row <- data.frame(name=fileBasename, size=file$size, type=file$type,
|
||||
datapath=filename, stringsAsFactors=FALSE)
|
||||
|
||||
if (length(.files$name) == 0)
|
||||
@@ -94,7 +107,7 @@ FileUploadContext <- R6Class(
|
||||
},
|
||||
createUploadOperation = function(fileInfos) {
|
||||
while (TRUE) {
|
||||
id <- paste(as.raw(p_runif(12, min=0, max=0xFF)), collapse='')
|
||||
id <- createUniqueId(12)
|
||||
private$ids <- c(private$ids, id)
|
||||
dir <- file.path(private$basedir, id)
|
||||
if (!dir.create(dir))
|
||||
|
||||
75
R/font-awesome.R
Normal file
75
R/font-awesome.R
Normal file
@@ -0,0 +1,75 @@
|
||||
font_awesome_brands <- c(
|
||||
"500px", "accessible-icon", "accusoft", "adn", "adversal",
|
||||
"affiliatetheme", "algolia", "alipay", "amazon", "amazon-pay",
|
||||
"amilia", "android", "angellist", "angrycreative", "angular",
|
||||
"app-store", "app-store-ios", "apper", "apple", "apple-pay",
|
||||
"asymmetrik", "audible", "autoprefixer", "avianex", "aviato",
|
||||
"aws", "bandcamp", "behance", "behance-square", "bimobject",
|
||||
"bitbucket", "bitcoin", "bity", "black-tie", "blackberry", "blogger",
|
||||
"blogger-b", "bluetooth", "bluetooth-b", "btc", "buromobelexperte",
|
||||
"buysellads", "cc-amazon-pay", "cc-amex", "cc-apple-pay", "cc-diners-club",
|
||||
"cc-discover", "cc-jcb", "cc-mastercard", "cc-paypal", "cc-stripe",
|
||||
"cc-visa", "centercode", "chrome", "cloudscale", "cloudsmith",
|
||||
"cloudversify", "codepen", "codiepie", "connectdevelop", "contao",
|
||||
"cpanel", "creative-commons", "creative-commons-by", "creative-commons-nc",
|
||||
"creative-commons-nc-eu", "creative-commons-nc-jp", "creative-commons-nd",
|
||||
"creative-commons-pd", "creative-commons-pd-alt", "creative-commons-remix",
|
||||
"creative-commons-sa", "creative-commons-sampling", "creative-commons-sampling-plus",
|
||||
"creative-commons-share", "css3", "css3-alt", "cuttlefish", "d-and-d",
|
||||
"dashcube", "delicious", "deploydog", "deskpro", "deviantart",
|
||||
"digg", "digital-ocean", "discord", "discourse", "dochub", "docker",
|
||||
"draft2digital", "dribbble", "dribbble-square", "dropbox", "drupal",
|
||||
"dyalog", "earlybirds", "ebay", "edge", "elementor", "ello",
|
||||
"ember", "empire", "envira", "erlang", "ethereum", "etsy", "expeditedssl",
|
||||
"facebook", "facebook-f", "facebook-messenger", "facebook-square",
|
||||
"firefox", "first-order", "first-order-alt", "firstdraft", "flickr",
|
||||
"flipboard", "fly", "font-awesome", "font-awesome-alt", "font-awesome-flag",
|
||||
"font-awesome-logo-full", "fonticons", "fonticons-fi", "fort-awesome",
|
||||
"fort-awesome-alt", "forumbee", "foursquare", "free-code-camp",
|
||||
"freebsd", "fulcrum", "galactic-republic", "galactic-senate",
|
||||
"get-pocket", "gg", "gg-circle", "git", "git-square", "github",
|
||||
"github-alt", "github-square", "gitkraken", "gitlab", "gitter",
|
||||
"glide", "glide-g", "gofore", "goodreads", "goodreads-g", "google",
|
||||
"google-drive", "google-play", "google-plus", "google-plus-g",
|
||||
"google-plus-square", "google-wallet", "gratipay", "grav", "gripfire",
|
||||
"grunt", "gulp", "hacker-news", "hacker-news-square", "hackerrank",
|
||||
"hips", "hire-a-helper", "hooli", "hornbill", "hotjar", "houzz",
|
||||
"html5", "hubspot", "imdb", "instagram", "internet-explorer",
|
||||
"ioxhost", "itunes", "itunes-note", "java", "jedi-order", "jenkins",
|
||||
"joget", "joomla", "js", "js-square", "jsfiddle", "kaggle", "keybase",
|
||||
"keycdn", "kickstarter", "kickstarter-k", "korvue", "laravel",
|
||||
"lastfm", "lastfm-square", "leanpub", "less", "line", "linkedin",
|
||||
"linkedin-in", "linode", "linux", "lyft", "magento", "mailchimp",
|
||||
"mandalorian", "markdown", "mastodon", "maxcdn", "medapps", "medium",
|
||||
"medium-m", "medrt", "meetup", "megaport", "microsoft", "mix",
|
||||
"mixcloud", "mizuni", "modx", "monero", "napster", "neos", "nimblr",
|
||||
"nintendo-switch", "node", "node-js", "npm", "ns8", "nutritionix",
|
||||
"odnoklassniki", "odnoklassniki-square", "old-republic", "opencart",
|
||||
"openid", "opera", "optin-monster", "osi", "page4", "pagelines",
|
||||
"palfed", "patreon", "paypal", "periscope", "phabricator", "phoenix-framework",
|
||||
"phoenix-squadron", "php", "pied-piper", "pied-piper-alt", "pied-piper-hat",
|
||||
"pied-piper-pp", "pinterest", "pinterest-p", "pinterest-square",
|
||||
"playstation", "product-hunt", "pushed", "python", "qq", "quinscape",
|
||||
"quora", "r-project", "ravelry", "react", "readme", "rebel",
|
||||
"red-river", "reddit", "reddit-alien", "reddit-square", "rendact",
|
||||
"renren", "replyd", "researchgate", "resolving", "rev", "rocketchat",
|
||||
"rockrms", "safari", "sass", "schlix", "scribd", "searchengin",
|
||||
"sellcast", "sellsy", "servicestack", "shirtsinbulk", "shopware",
|
||||
"simplybuilt", "sistrix", "sith", "skyatlas", "skype", "slack",
|
||||
"slack-hash", "slideshare", "snapchat", "snapchat-ghost", "snapchat-square",
|
||||
"soundcloud", "speakap", "spotify", "squarespace", "stack-exchange",
|
||||
"stack-overflow", "staylinked", "steam", "steam-square", "steam-symbol",
|
||||
"sticker-mule", "strava", "stripe", "stripe-s", "studiovinari",
|
||||
"stumbleupon", "stumbleupon-circle", "superpowers", "supple",
|
||||
"teamspeak", "telegram", "telegram-plane", "tencent-weibo", "the-red-yeti",
|
||||
"themeco", "themeisle", "trade-federation", "trello", "tripadvisor",
|
||||
"tumblr", "tumblr-square", "twitch", "twitter", "twitter-square",
|
||||
"typo3", "uber", "uikit", "uniregistry", "untappd", "usb", "ussunnah",
|
||||
"vaadin", "viacoin", "viadeo", "viadeo-square", "viber", "vimeo",
|
||||
"vimeo-square", "vimeo-v", "vine", "vk", "vnv", "vuejs", "weebly",
|
||||
"weibo", "weixin", "whatsapp", "whatsapp-square", "whmcs", "wikipedia-w",
|
||||
"windows", "wix", "wolf-pack-battalion", "wordpress", "wordpress-simple",
|
||||
"wpbeginner", "wpexplorer", "wpforms", "xbox", "xing", "xing-square",
|
||||
"y-combinator", "yahoo", "yandex", "yandex-international", "yelp",
|
||||
"yoast", "youtube", "youtube-square", "zhihu"
|
||||
)
|
||||
83
R/globals.R
83
R/globals.R
@@ -1,23 +1,78 @@
|
||||
# A scope where we can put mutable global state
|
||||
.globals <- new.env(parent = emptyenv())
|
||||
|
||||
register_s3_method <- function(pkg, generic, class, fun = NULL) {
|
||||
stopifnot(is.character(pkg), length(pkg) == 1)
|
||||
stopifnot(is.character(generic), length(generic) == 1)
|
||||
stopifnot(is.character(class), length(class) == 1)
|
||||
|
||||
if (is.null(fun)) {
|
||||
fun <- get(paste0(generic, ".", class), envir = parent.frame())
|
||||
} else {
|
||||
stopifnot(is.function(fun))
|
||||
}
|
||||
|
||||
if (pkg %in% loadedNamespaces()) {
|
||||
registerS3method(generic, class, fun, envir = asNamespace(pkg))
|
||||
}
|
||||
|
||||
# Always register hook in case pkg is loaded at some
|
||||
# point the future (or, potentially, but less commonly,
|
||||
# unloaded & reloaded)
|
||||
setHook(
|
||||
packageEvent(pkg, "onLoad"),
|
||||
function(...) {
|
||||
registerS3method(generic, class, fun, envir = asNamespace(pkg))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
register_upgrade_message <- function(pkg, version) {
|
||||
# Is an out-dated version of this package installed?
|
||||
needs_upgrade <- function() {
|
||||
if (system.file(package = pkg) == "")
|
||||
return(FALSE)
|
||||
if (utils::packageVersion(pkg) >= version)
|
||||
return(FALSE)
|
||||
TRUE
|
||||
}
|
||||
|
||||
msg <- sprintf(
|
||||
"This version of Shiny is designed to work with '%s' >= %s.
|
||||
Please upgrade via install.packages('%s').",
|
||||
pkg, version, pkg
|
||||
)
|
||||
|
||||
if (pkg %in% loadedNamespaces() && needs_upgrade()) {
|
||||
packageStartupMessage(msg)
|
||||
}
|
||||
|
||||
# Always register hook in case pkg is loaded at some
|
||||
# point the future (or, potentially, but less commonly,
|
||||
# unloaded & reloaded)
|
||||
setHook(
|
||||
packageEvent(pkg, "onLoad"),
|
||||
function(...) {
|
||||
if (needs_upgrade()) packageStartupMessage(msg)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
.onLoad <- function(libname, pkgname) {
|
||||
# R's lazy-loading package scheme causes the private seed to be cached in the
|
||||
# package itself, making our PRNG completely deterministic. This line resets
|
||||
# the private seed during load.
|
||||
withPrivateSeed(reinitializeSeed())
|
||||
}
|
||||
withPrivateSeed(set.seed(NULL))
|
||||
|
||||
.onAttach <- function(libname, pkgname) {
|
||||
# Check for htmlwidgets version, if installed. As of Shiny 0.12.0 and
|
||||
# htmlwidgets 0.4, both packages switched from RJSONIO to jsonlite. Because of
|
||||
# this change, Shiny 0.12.0 will work only with htmlwidgets >= 0.4, and vice
|
||||
# versa.
|
||||
if (system.file(package = "htmlwidgets") != "" &&
|
||||
utils::packageVersion("htmlwidgets") < "0.4") {
|
||||
packageStartupMessage(
|
||||
"This version of Shiny is designed to work with htmlwidgets >= 0.4. ",
|
||||
"Please upgrade your version of htmlwidgets."
|
||||
)
|
||||
}
|
||||
# Make sure these methods are available to knitr if shiny is loaded but not
|
||||
# attached.
|
||||
register_s3_method("knitr", "knit_print", "reactive")
|
||||
register_s3_method("knitr", "knit_print", "shiny.appobj")
|
||||
register_s3_method("knitr", "knit_print", "shiny.render.function")
|
||||
|
||||
# Shiny 1.4.0 bumps jQuery 1.x to 3.x, which caused a problem
|
||||
# with static-rendering of htmlwidgets, and htmlwidgets 1.5
|
||||
# includes a fix for this problem
|
||||
# https://github.com/rstudio/shiny/issues/2630
|
||||
register_upgrade_message("htmlwidgets", 1.5)
|
||||
}
|
||||
|
||||
593
R/graph.R
593
R/graph.R
@@ -1,21 +1,66 @@
|
||||
writeReactLog <- function(file=stdout(), sessionToken = NULL) {
|
||||
log <- .graphStack$as_list()
|
||||
if (!is.null(sessionToken)) {
|
||||
log <- Filter(function(x) {
|
||||
is.null(x$session) || identical(x$session, sessionToken)
|
||||
}, log)
|
||||
}
|
||||
cat(toJSON(log, pretty=TRUE), file=file)
|
||||
is_installed <- function(package, version) {
|
||||
installedVersion <- tryCatch(utils::packageVersion(package), error = function(e) NA)
|
||||
!is.na(installedVersion) && installedVersion >= version
|
||||
}
|
||||
|
||||
# Check that the version of an suggested package satisfies the requirements
|
||||
#
|
||||
# @param package The name of the suggested package
|
||||
# @param version The version of the package
|
||||
check_suggested <- function(package, version, location) {
|
||||
|
||||
if (is_installed(package, version)) {
|
||||
return()
|
||||
}
|
||||
|
||||
missing_location <- missing(location)
|
||||
msg <- paste0(
|
||||
sQuote(package),
|
||||
if (is.na(version)) "" else paste0("(>= ", version, ")"),
|
||||
" must be installed for this functionality.",
|
||||
if (!missing_location)
|
||||
paste0(
|
||||
"\nPlease install the missing package: \n",
|
||||
" source(\"https://install-github.me/", location, "\")"
|
||||
)
|
||||
)
|
||||
|
||||
if (interactive() && missing_location) {
|
||||
message(msg, "\nWould you like to install it?")
|
||||
if (utils::menu(c("Yes", "No")) == 1) {
|
||||
return(utils::install.packages(package))
|
||||
}
|
||||
}
|
||||
|
||||
stop(msg, call. = FALSE)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
# domain is like session
|
||||
|
||||
|
||||
# used to help define truly global react id's.
|
||||
# should work across session and in global namespace
|
||||
.globals$reactIdCounter <- 0L
|
||||
nextGlobalReactId <- function() {
|
||||
.globals$reactIdCounter <- .globals$reactIdCounter + 1L
|
||||
reactIdStr(.globals$reactIdCounter)
|
||||
}
|
||||
reactIdStr <- function(num) {
|
||||
paste0("r", num)
|
||||
}
|
||||
|
||||
|
||||
#' Reactive Log Visualizer
|
||||
#'
|
||||
#' Provides an interactive browser-based tool for visualizing reactive
|
||||
#' dependencies and execution in your application.
|
||||
#'
|
||||
#' To use the reactive log visualizer, start with a fresh R session and
|
||||
#' run the command \code{options(shiny.reactlog=TRUE)}; then launch your
|
||||
#' application in the usual way (e.g. using \code{\link{runApp}}). At
|
||||
#' run the command `options(shiny.reactlog=TRUE)`; then launch your
|
||||
#' application in the usual way (e.g. using [runApp()]). At
|
||||
#' any time you can hit Ctrl+F3 (or for Mac users, Command+F3) in your
|
||||
#' web browser to launch the reactive log visualization.
|
||||
#'
|
||||
@@ -30,85 +75,499 @@ writeReactLog <- function(file=stdout(), sessionToken = NULL) {
|
||||
#'
|
||||
#' As an alternative to pressing Ctrl/Command+F3--for example, if you
|
||||
#' are using reactives outside of the context of a Shiny
|
||||
#' application--you can run the \code{showReactLog} function, which will
|
||||
#' application--you can run the `reactlogShow` function, which will
|
||||
#' generate the reactive log visualization as a static HTML file and
|
||||
#' launch it in your default browser. In this case, refreshing your
|
||||
#' browser will not load new activity into the report; you will need to
|
||||
#' call \code{showReactLog()} explicitly.
|
||||
#' call `reactlogShow()` explicitly.
|
||||
#'
|
||||
#' For security and performance reasons, do not enable
|
||||
#' \code{shiny.reactlog} in production environments. When the option is
|
||||
#' `shiny.reactlog` in production environments. When the option is
|
||||
#' enabled, it's possible for any user of your app to see at least some
|
||||
#' of the source code of your reactive expressions and observers.
|
||||
#'
|
||||
#' @name reactlog
|
||||
NULL
|
||||
|
||||
|
||||
#' @describeIn reactlog Return a list of reactive information. Can be used in conjunction with
|
||||
#' [reactlog::reactlog_show] to later display the reactlog graph.
|
||||
#' @export
|
||||
showReactLog <- function() {
|
||||
utils::browseURL(renderReactLog())
|
||||
reactlog <- function() {
|
||||
rLog$asList()
|
||||
}
|
||||
|
||||
renderReactLog <- function(sessionToken = NULL) {
|
||||
templateFile <- system.file('www/reactive-graph.html', package='shiny')
|
||||
html <- paste(readLines(templateFile, warn=FALSE), collapse='\r\n')
|
||||
tc <- textConnection(NULL, 'w')
|
||||
on.exit(close(tc))
|
||||
writeReactLog(tc, sessionToken)
|
||||
cat('\n', file=tc)
|
||||
flush(tc)
|
||||
html <- sub('__DATA__', paste(textConnectionValue(tc), collapse='\r\n'), html, fixed=TRUE)
|
||||
file <- tempfile(fileext = '.html')
|
||||
writeLines(html, file)
|
||||
return(file)
|
||||
#' @describeIn reactlog Display a full reactlog graph for all sessions.
|
||||
#' @inheritParams reactlog::reactlog_show
|
||||
#' @export
|
||||
reactlogShow <- function(time = TRUE) {
|
||||
check_reactlog()
|
||||
reactlog::reactlog_show(reactlog(), time = time)
|
||||
}
|
||||
#' @describeIn reactlog This function is deprecated. You should use [reactlogShow()]
|
||||
#' @export
|
||||
# legacy purposes
|
||||
showReactLog <- function(time = TRUE) {
|
||||
shinyDeprecated(new = "`reactlogShow`", version = "1.2.0")
|
||||
reactlogShow(time = time)
|
||||
}
|
||||
#' @describeIn reactlog Resets the entire reactlog stack. Useful for debugging and removing all prior reactive history.
|
||||
#' @export
|
||||
reactlogReset <- function() {
|
||||
rLog$reset()
|
||||
}
|
||||
|
||||
.graphAppend <- function(logEntry, domain = getDefaultReactiveDomain()) {
|
||||
if (isTRUE(getOption('shiny.reactlog'))) {
|
||||
sessionToken <- if (is.null(domain)) NULL else domain$token
|
||||
.graphStack$push(c(logEntry, list(
|
||||
session = sessionToken,
|
||||
time = as.numeric(Sys.time())
|
||||
)))
|
||||
# called in "/reactlog" middleware
|
||||
renderReactlog <- function(sessionToken = NULL, time = TRUE) {
|
||||
check_reactlog()
|
||||
reactlog::reactlog_render(
|
||||
reactlog(),
|
||||
session_token = sessionToken,
|
||||
time = time
|
||||
)
|
||||
}
|
||||
check_reactlog <- function() {
|
||||
check_suggested("reactlog", reactlog_version())
|
||||
}
|
||||
# read reactlog version from description file
|
||||
# prevents version mismatch in code and description file
|
||||
reactlog_version <- function() {
|
||||
desc <- read.dcf(system.file("DESCRIPTION", package = "shiny", mustWork = TRUE))
|
||||
suggests <- desc[1,"Suggests"][[1]]
|
||||
suggests_pkgs <- strsplit(suggests, "\n")[[1]]
|
||||
|
||||
reactlog_info <- suggests_pkgs[grepl("reactlog", suggests_pkgs)]
|
||||
if (length(reactlog_info) == 0) {
|
||||
stop("reactlog can not be found in shiny DESCRIPTION file")
|
||||
}
|
||||
|
||||
if (!is.null(domain)) {
|
||||
domain$reactlog(logEntry)
|
||||
}
|
||||
reactlog_info <- sub("^[^\\(]*\\(", "", reactlog_info)
|
||||
reactlog_info <- sub("\\)[^\\)]*$", "", reactlog_info)
|
||||
reactlog_info <- sub("^[>= ]*", "", reactlog_info)
|
||||
|
||||
package_version(reactlog_info)
|
||||
}
|
||||
|
||||
.graphDependsOn <- function(id, label) {
|
||||
.graphAppend(list(action='dep', id=id, dependsOn=label))
|
||||
}
|
||||
|
||||
.graphDependsOnId <- function(id, dependee) {
|
||||
.graphAppend(list(action='depId', id=id, dependsOn=dependee))
|
||||
}
|
||||
RLog <- R6Class(
|
||||
"RLog",
|
||||
portable = FALSE,
|
||||
private = list(
|
||||
option = "shiny.reactlog",
|
||||
msgOption = "shiny.reactlog.console",
|
||||
|
||||
.graphCreateContext <- function(id, label, type, prevId, domain) {
|
||||
.graphAppend(list(
|
||||
action='ctx', id=id, label=paste(label, collapse='\n'),
|
||||
srcref=as.vector(attr(label, "srcref")), srcfile=attr(label, "srcfile"),
|
||||
type=type, prevId=prevId
|
||||
), domain = domain)
|
||||
}
|
||||
appendEntry = function(domain, logEntry) {
|
||||
if (self$isLogging()) {
|
||||
sessionToken <- if (is.null(domain)) NULL else domain$token
|
||||
logStack$push(c(logEntry, list(
|
||||
session = sessionToken,
|
||||
time = as.numeric(Sys.time())
|
||||
)))
|
||||
}
|
||||
if (!is.null(domain)) domain$reactlog(logEntry)
|
||||
}
|
||||
),
|
||||
public = list(
|
||||
msg = "<MessageLogger>",
|
||||
logStack = "<Stack>",
|
||||
|
||||
.graphEnterContext <- function(id) {
|
||||
.graphAppend(list(action='enter', id=id))
|
||||
}
|
||||
noReactIdLabel = "NoCtxReactId",
|
||||
noReactId = reactIdStr("NoCtxReactId"),
|
||||
dummyReactIdLabel = "DummyReactId",
|
||||
dummyReactId = reactIdStr("DummyReactId"),
|
||||
|
||||
.graphExitContext <- function(id) {
|
||||
.graphAppend(list(action='exit', id=id))
|
||||
}
|
||||
asList = function() {
|
||||
ret <- self$logStack$as_list()
|
||||
attr(ret, "version") <- "1"
|
||||
ret
|
||||
},
|
||||
|
||||
.graphValueChange <- function(label, value) {
|
||||
.graphAppend(list(
|
||||
action = 'valueChange',
|
||||
id = label,
|
||||
value = paste(utils::capture.output(utils::str(value)), collapse='\n')
|
||||
))
|
||||
}
|
||||
ctxIdStr = function(ctxId) {
|
||||
if (is.null(ctxId) || identical(ctxId, "")) return(NULL)
|
||||
paste0("ctx", ctxId)
|
||||
},
|
||||
namesIdStr = function(reactId) {
|
||||
paste0("names(", reactId, ")")
|
||||
},
|
||||
asListIdStr = function(reactId) {
|
||||
paste0("as.list(", reactId, ")")
|
||||
},
|
||||
asListAllIdStr = function(reactId) {
|
||||
paste0("as.list(", reactId, ", all.names = TRUE)")
|
||||
},
|
||||
keyIdStr = function(reactId, key) {
|
||||
paste0(reactId, "$", key)
|
||||
},
|
||||
|
||||
.graphInvalidate <- function(id, domain) {
|
||||
.graphAppend(list(action='invalidate', id=id), domain)
|
||||
}
|
||||
valueStr = function(value, n = 200) {
|
||||
if (!self$isLogging()) {
|
||||
# return a placeholder string to avoid calling str
|
||||
return("<reactlog is turned off>")
|
||||
}
|
||||
output <- try(silent = TRUE, {
|
||||
# only capture the first level of the object
|
||||
utils::capture.output(utils::str(value, max.level = 1))
|
||||
})
|
||||
outputTxt <- paste0(output, collapse="\n")
|
||||
msg$shortenString(outputTxt, n = n)
|
||||
},
|
||||
|
||||
initialize = function(rlogOption = "shiny.reactlog", msgOption = "shiny.reactlog.console") {
|
||||
private$option <- rlogOption
|
||||
private$msgOption <- msgOption
|
||||
|
||||
self$reset()
|
||||
},
|
||||
reset = function() {
|
||||
.globals$reactIdCounter <- 0L
|
||||
|
||||
self$logStack <- Stack$new()
|
||||
self$msg <- MessageLogger$new(option = private$msgOption)
|
||||
|
||||
# setup dummy and missing react information
|
||||
self$msg$setReact(force = TRUE, list(reactId = self$noReactId, label = self$noReactIdLabel))
|
||||
self$msg$setReact(force = TRUE, list(reactId = self$dummyReactId, label = self$dummyReactIdLabel))
|
||||
},
|
||||
isLogging = function() {
|
||||
isTRUE(getOption(private$option, FALSE))
|
||||
},
|
||||
|
||||
define = function(reactId, value, label, type, domain) {
|
||||
valueStr <- self$valueStr(value)
|
||||
if (msg$hasReact(reactId)) {
|
||||
stop("react definition for id: ", reactId, " already found!!", "Label: ", label, "Type: ", type)
|
||||
}
|
||||
msg$setReact(list(reactId = reactId, label = label))
|
||||
msg$log("define:", msg$reactStr(reactId), msg$typeStr(type = type), msg$valueStr(valueStr))
|
||||
private$appendEntry(domain, list(
|
||||
action = "define",
|
||||
reactId = reactId,
|
||||
label = msg$shortenString(label),
|
||||
type = type,
|
||||
value = valueStr
|
||||
))
|
||||
},
|
||||
defineNames = function(reactId, value, label, domain) {
|
||||
self$define(self$namesIdStr(reactId), value, self$namesIdStr(label), "reactiveValuesNames", domain)
|
||||
},
|
||||
defineAsList = function(reactId, value, label, domain) {
|
||||
self$define(self$asListIdStr(reactId), value, self$asListIdStr(label), "reactiveValuesAsList", domain)
|
||||
},
|
||||
defineAsListAll = function(reactId, value, label, domain) {
|
||||
self$define(self$asListAllIdStr(reactId), value, self$asListAllIdStr(label), "reactiveValuesAsListAll", domain)
|
||||
},
|
||||
defineKey = function(reactId, value, key, label, domain) {
|
||||
self$define(self$keyIdStr(reactId, key), value, self$keyIdStr(label, key), "reactiveValuesKey", domain)
|
||||
},
|
||||
defineObserver = function(reactId, label, domain) {
|
||||
self$define(reactId, value = NULL, label, "observer", domain)
|
||||
},
|
||||
|
||||
dependsOn = function(reactId, depOnReactId, ctxId, domain) {
|
||||
if (is.null(reactId)) return()
|
||||
ctxId <- ctxIdStr(ctxId)
|
||||
msg$log("dependsOn:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId))
|
||||
private$appendEntry(domain, list(
|
||||
action = "dependsOn",
|
||||
reactId = reactId,
|
||||
depOnReactId = depOnReactId,
|
||||
ctxId = ctxId
|
||||
))
|
||||
},
|
||||
dependsOnKey = function(reactId, depOnReactId, key, ctxId, domain) {
|
||||
self$dependsOn(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain)
|
||||
},
|
||||
|
||||
dependsOnRemove = function(reactId, depOnReactId, ctxId, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
msg$log("dependsOnRemove:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId))
|
||||
private$appendEntry(domain, list(
|
||||
action = "dependsOnRemove",
|
||||
reactId = reactId,
|
||||
depOnReactId = depOnReactId,
|
||||
ctxId = ctxId
|
||||
))
|
||||
},
|
||||
dependsOnKeyRemove = function(reactId, depOnReactId, key, ctxId, domain) {
|
||||
self$dependsOnRemove(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain)
|
||||
},
|
||||
|
||||
createContext = function(ctxId, label, type, prevCtxId, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
prevCtxId <- self$ctxIdStr(prevCtxId)
|
||||
msg$log("createContext:", msg$ctxPrevCtxStr(preCtxIdTxt = " ", ctxId, prevCtxId, type))
|
||||
private$appendEntry(domain, list(
|
||||
action = "createContext",
|
||||
ctxId = ctxId,
|
||||
label = msg$shortenString(label),
|
||||
type = type,
|
||||
prevCtxId = prevCtxId,
|
||||
srcref = as.vector(attr(label, "srcref")), srcfile=attr(label, "srcfile")
|
||||
))
|
||||
},
|
||||
|
||||
enter = function(reactId, ctxId, type, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
if (identical(type, "isolate")) {
|
||||
msg$log("isolateEnter:", msg$reactStr(reactId), msg$ctxStr(ctxId))
|
||||
msg$depthIncrement()
|
||||
private$appendEntry(domain, list(
|
||||
action = "isolateEnter",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId
|
||||
))
|
||||
} else {
|
||||
msg$log("enter:", msg$reactStr(reactId), msg$ctxStr(ctxId, type))
|
||||
msg$depthIncrement()
|
||||
private$appendEntry(domain, list(
|
||||
action = "enter",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId,
|
||||
type = type
|
||||
))
|
||||
}
|
||||
},
|
||||
exit = function(reactId, ctxId, type, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
if (identical(type, "isolate")) {
|
||||
msg$depthDecrement()
|
||||
msg$log("isolateExit:", msg$reactStr(reactId), msg$ctxStr(ctxId))
|
||||
private$appendEntry(domain, list(
|
||||
action = "isolateExit",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId
|
||||
))
|
||||
} else {
|
||||
msg$depthDecrement()
|
||||
msg$log("exit:", msg$reactStr(reactId), msg$ctxStr(ctxId, type))
|
||||
private$appendEntry(domain, list(
|
||||
action = "exit",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId,
|
||||
type = type
|
||||
))
|
||||
}
|
||||
},
|
||||
|
||||
valueChange = function(reactId, value, domain) {
|
||||
valueStr <- self$valueStr(value)
|
||||
msg$log("valueChange:", msg$reactStr(reactId), msg$valueStr(valueStr))
|
||||
private$appendEntry(domain, list(
|
||||
action = "valueChange",
|
||||
reactId = reactId,
|
||||
value = valueStr
|
||||
))
|
||||
},
|
||||
valueChangeNames = function(reactId, nameValues, domain) {
|
||||
self$valueChange(self$namesIdStr(reactId), nameValues, domain)
|
||||
},
|
||||
valueChangeAsList = function(reactId, listValue, domain) {
|
||||
self$valueChange(self$asListIdStr(reactId), listValue, domain)
|
||||
},
|
||||
valueChangeAsListAll = function(reactId, listValue, domain) {
|
||||
self$valueChange(self$asListAllIdStr(reactId), listValue, domain)
|
||||
},
|
||||
valueChangeKey = function(reactId, key, value, domain) {
|
||||
self$valueChange(self$keyIdStr(reactId, key), value, domain)
|
||||
},
|
||||
|
||||
|
||||
invalidateStart = function(reactId, ctxId, type, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
if (identical(type, "isolate")) {
|
||||
msg$log("isolateInvalidateStart:", msg$reactStr(reactId), msg$ctxStr(ctxId))
|
||||
msg$depthIncrement()
|
||||
private$appendEntry(domain, list(
|
||||
action = "isolateInvalidateStart",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId
|
||||
))
|
||||
} else {
|
||||
msg$log("invalidateStart:", msg$reactStr(reactId), msg$ctxStr(ctxId, type))
|
||||
msg$depthIncrement()
|
||||
private$appendEntry(domain, list(
|
||||
action = "invalidateStart",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId,
|
||||
type = type
|
||||
))
|
||||
}
|
||||
},
|
||||
invalidateEnd = function(reactId, ctxId, type, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
if (identical(type, "isolate")) {
|
||||
msg$depthDecrement()
|
||||
msg$log("isolateInvalidateEnd:", msg$reactStr(reactId), msg$ctxStr(ctxId))
|
||||
private$appendEntry(domain, list(
|
||||
action = "isolateInvalidateEnd",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId
|
||||
))
|
||||
} else {
|
||||
msg$depthDecrement()
|
||||
msg$log("invalidateEnd:", msg$reactStr(reactId), msg$ctxStr(ctxId, type))
|
||||
private$appendEntry(domain, list(
|
||||
action = "invalidateEnd",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId,
|
||||
type = type
|
||||
))
|
||||
}
|
||||
},
|
||||
|
||||
invalidateLater = function(reactId, runningCtx, millis, domain) {
|
||||
msg$log("invalidateLater: ", millis, "ms", msg$reactStr(reactId), msg$ctxStr(runningCtx))
|
||||
private$appendEntry(domain, list(
|
||||
action = "invalidateLater",
|
||||
reactId = reactId,
|
||||
ctxId = runningCtx,
|
||||
millis = millis
|
||||
))
|
||||
},
|
||||
|
||||
idle = function(domain = NULL) {
|
||||
msg$log("idle")
|
||||
private$appendEntry(domain, list(
|
||||
action = "idle"
|
||||
))
|
||||
},
|
||||
|
||||
asyncStart = function(domain = NULL) {
|
||||
msg$log("asyncStart")
|
||||
private$appendEntry(domain, list(
|
||||
action = "asyncStart"
|
||||
))
|
||||
},
|
||||
asyncStop = function(domain = NULL) {
|
||||
msg$log("asyncStop")
|
||||
private$appendEntry(domain, list(
|
||||
action = "asyncStop"
|
||||
))
|
||||
},
|
||||
|
||||
freezeReactiveVal = function(reactId, domain) {
|
||||
msg$log("freeze:", msg$reactStr(reactId))
|
||||
private$appendEntry(domain, list(
|
||||
action = "freeze",
|
||||
reactId = reactId
|
||||
))
|
||||
},
|
||||
freezeReactiveKey = function(reactId, key, domain) {
|
||||
self$freezeReactiveVal(self$keyIdStr(reactId, key), domain)
|
||||
},
|
||||
|
||||
thawReactiveVal = function(reactId, domain) {
|
||||
msg$log("thaw:", msg$reactStr(reactId))
|
||||
private$appendEntry(domain, list(
|
||||
action = "thaw",
|
||||
reactId = reactId
|
||||
))
|
||||
},
|
||||
thawReactiveKey = function(reactId, key, domain) {
|
||||
self$thawReactiveVal(self$keyIdStr(reactId, key), domain)
|
||||
},
|
||||
|
||||
userMark = function(domain = NULL) {
|
||||
msg$log("userMark")
|
||||
private$appendEntry(domain, list(
|
||||
action = "userMark"
|
||||
))
|
||||
}
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
MessageLogger = R6Class(
|
||||
"MessageLogger",
|
||||
portable = FALSE,
|
||||
public = list(
|
||||
depth = 0L,
|
||||
reactCache = list(),
|
||||
option = "shiny.reactlog.console",
|
||||
|
||||
initialize = function(option = "shiny.reactlog.console", depth = 0L) {
|
||||
if (!missing(depth)) self$depth <- depth
|
||||
if (!missing(option)) self$option <- option
|
||||
},
|
||||
|
||||
isLogging = function() {
|
||||
isTRUE(getOption(self$option))
|
||||
},
|
||||
isNotLogging = function() {
|
||||
! isTRUE(getOption(self$option))
|
||||
},
|
||||
depthIncrement = function() {
|
||||
if (self$isNotLogging()) return(NULL)
|
||||
self$depth <- self$depth + 1L
|
||||
},
|
||||
depthDecrement = function() {
|
||||
if (self$isNotLogging()) return(NULL)
|
||||
self$depth <- self$depth - 1L
|
||||
},
|
||||
hasReact = function(reactId) {
|
||||
if (self$isNotLogging()) return(FALSE)
|
||||
!is.null(self$getReact(reactId))
|
||||
},
|
||||
getReact = function(reactId, force = FALSE) {
|
||||
if (identical(force, FALSE) && self$isNotLogging()) return(NULL)
|
||||
self$reactCache[[reactId]]
|
||||
},
|
||||
setReact = function(reactObj, force = FALSE) {
|
||||
if (identical(force, FALSE) && self$isNotLogging()) return(NULL)
|
||||
self$reactCache[[reactObj$reactId]] <- reactObj
|
||||
},
|
||||
shortenString = function(txt, n = 250) {
|
||||
if (is.null(txt) || isTRUE(is.na(txt))) {
|
||||
return("")
|
||||
}
|
||||
if (nchar(txt) > n) {
|
||||
return(
|
||||
paste0(substr(txt, 1, n - 3), "...")
|
||||
)
|
||||
}
|
||||
return(txt)
|
||||
},
|
||||
singleLine = function(txt) {
|
||||
gsub("[^\\]\\n", "\\\\n", txt)
|
||||
},
|
||||
valueStr = function(valueStr) {
|
||||
paste0(
|
||||
" '", self$shortenString(self$singleLine(valueStr)), "'"
|
||||
)
|
||||
},
|
||||
reactStr = function(reactId) {
|
||||
if (self$isNotLogging()) return(NULL)
|
||||
reactInfo <- self$getReact(reactId)
|
||||
if (is.null(reactInfo)) return(" <UNKNOWN_REACTID>")
|
||||
paste0(
|
||||
" ", reactInfo$reactId, ":'", self$shortenString(self$singleLine(reactInfo$label)), "'"
|
||||
)
|
||||
},
|
||||
typeStr = function(type = NULL) {
|
||||
self$ctxStr(ctxId = NULL, type = type)
|
||||
},
|
||||
ctxStr = function(ctxId = NULL, type = NULL) {
|
||||
if (self$isNotLogging()) return(NULL)
|
||||
self$ctxPrevCtxStr(ctxId = ctxId, prevCtxId = NULL, type = type)
|
||||
},
|
||||
ctxPrevCtxStr = function(ctxId = NULL, prevCtxId = NULL, type = NULL, preCtxIdTxt = " in ") {
|
||||
if (self$isNotLogging()) return(NULL)
|
||||
paste0(
|
||||
if (!is.null(ctxId)) paste0(preCtxIdTxt, ctxId),
|
||||
if (!is.null(prevCtxId)) paste0(" from ", prevCtxId),
|
||||
if (!is.null(type) && !identical(type, "other")) paste0(" - ", type)
|
||||
)
|
||||
},
|
||||
log = function(...) {
|
||||
if (self$isNotLogging()) return(NULL)
|
||||
msg <- paste0(
|
||||
paste0(rep("= ", depth), collapse = ""), "- ", paste0(..., collapse = ""),
|
||||
collapse = ""
|
||||
)
|
||||
message(msg)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
#' @include stack.R
|
||||
.graphStack <- Stack$new()
|
||||
rLog <- RLog$new("shiny.reactlog", "shiny.reactlog.console")
|
||||
|
||||
95
R/history.R
Normal file
95
R/history.R
Normal file
@@ -0,0 +1,95 @@
|
||||
|
||||
#' @include reactive-domains.R
|
||||
NULL
|
||||
|
||||
#' @include reactives.R
|
||||
NULL
|
||||
|
||||
#' Get the query string / hash component from the URL
|
||||
#'
|
||||
#' Two user friendly wrappers for getting the query string and the hash
|
||||
#' component from the app's URL.
|
||||
#'
|
||||
#' These can be particularly useful if you want to display different content
|
||||
#' depending on the values in the query string / hash (e.g. instead of basing
|
||||
#' the conditional on an input or a calculated reactive, you can base it on the
|
||||
#' query string). However, note that, if you're changing the query string / hash
|
||||
#' programatically from within the server code, you must use
|
||||
#' `updateQueryString(_yourNewQueryString_, mode = "push")`. The default
|
||||
#' `mode` for `updateQueryString` is `"replace"`, which doesn't
|
||||
#' raise any events, so any observers or reactives that depend on it will
|
||||
#' *not* get triggered. However, if you're changing the query string / hash
|
||||
#' directly by typing directly in the browser and hitting enter, you don't have
|
||||
#' to worry about this.
|
||||
#'
|
||||
#' @param session A Shiny session object.
|
||||
#'
|
||||
#' @return For `getQueryString`, a named list. For example, the query
|
||||
#' string `?param1=value1¶m2=value2` becomes `list(param1 =
|
||||
#' value1, param2 = value2)`. For `getUrlHash`, a character vector with
|
||||
#' the hash (including the leading `#` symbol).
|
||||
#'
|
||||
#' @seealso [updateQueryString()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ## App 1: getQueryString
|
||||
#' ## Printing the value of the query string
|
||||
#' ## (Use the back and forward buttons to see how the browser
|
||||
#' ## keeps a record of each state)
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' textInput("txt", "Enter new query string"),
|
||||
#' helpText("Format: ?param1=val1¶m2=val2"),
|
||||
#' actionButton("go", "Update"),
|
||||
#' hr(),
|
||||
#' verbatimTextOutput("query")
|
||||
#' ),
|
||||
#' server = function(input, output, session) {
|
||||
#' observeEvent(input$go, {
|
||||
#' updateQueryString(input$txt, mode = "push")
|
||||
#' })
|
||||
#' output$query <- renderText({
|
||||
#' query <- getQueryString()
|
||||
#' queryText <- paste(names(query), query,
|
||||
#' sep = "=", collapse=", ")
|
||||
#' paste("Your query string is:\n", queryText)
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' ## App 2: getUrlHash
|
||||
#' ## Printing the value of the URL hash
|
||||
#' ## (Use the back and forward buttons to see how the browser
|
||||
#' ## keeps a record of each state)
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' textInput("txt", "Enter new hash"),
|
||||
#' helpText("Format: #hash"),
|
||||
#' actionButton("go", "Update"),
|
||||
#' hr(),
|
||||
#' verbatimTextOutput("hash")
|
||||
#' ),
|
||||
#' server = function(input, output, session) {
|
||||
#' observeEvent(input$go, {
|
||||
#' updateQueryString(input$txt, mode = "push")
|
||||
#' })
|
||||
#' output$hash <- renderText({
|
||||
#' hash <- getUrlHash()
|
||||
#' paste("Your hash is:\n", hash)
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#' @export
|
||||
getQueryString <- function(session = getDefaultReactiveDomain()) {
|
||||
parseQueryString(session$clientData$url_search)
|
||||
}
|
||||
|
||||
#' @rdname getQueryString
|
||||
#' @export
|
||||
getUrlHash <- function(session = getDefaultReactiveDomain()) {
|
||||
session$clientData$url_hash
|
||||
}
|
||||
@@ -2,17 +2,22 @@
|
||||
#'
|
||||
#' Ensure that a file-based HTML dependency (from the htmltools package) can be
|
||||
#' served over Shiny's HTTP server. This function works by using
|
||||
#' \code{\link{addResourcePath}} to map the HTML dependency's directory to a
|
||||
#' [addResourcePath()] to map the HTML dependency's directory to a
|
||||
#' URL.
|
||||
#'
|
||||
#' @param dependency A single HTML dependency object, created using
|
||||
#' \code{\link{htmlDependency}}. If the \code{src} value is named, then
|
||||
#' \code{href} and/or \code{file} names must be present.
|
||||
#' [htmltools::htmlDependency()]. If the `src` value is named,
|
||||
#' then `href` and/or `file` names must be present.
|
||||
#' @param scrubFile If TRUE (the default), remove `src$file` for the
|
||||
#' dependency. This prevents the local file path from being sent to the client
|
||||
#' when dynamic web dependencies are used. If FALSE, don't remove
|
||||
#' `src$file`. Setting it to FALSE should be needed only in very unusual
|
||||
#' cases.
|
||||
#'
|
||||
#' @return A single HTML dependency object that has an \code{href}-named element
|
||||
#' in its \code{src}.
|
||||
#' @return A single HTML dependency object that has an `href`-named element
|
||||
#' in its `src`.
|
||||
#' @export
|
||||
createWebDependency <- function(dependency) {
|
||||
createWebDependency <- function(dependency, scrubFile = TRUE) {
|
||||
if (is.null(dependency))
|
||||
return(NULL)
|
||||
|
||||
@@ -25,5 +30,27 @@ createWebDependency <- function(dependency) {
|
||||
dependency$src$href <- prefix
|
||||
}
|
||||
|
||||
# Don't leak local file path to client
|
||||
if (scrubFile)
|
||||
dependency$src$file <- NULL
|
||||
|
||||
return(dependency)
|
||||
}
|
||||
|
||||
|
||||
# Given a Shiny tag object, process singletons and dependencies. Returns a list
|
||||
# with rendered HTML and dependency objects.
|
||||
processDeps <- function(tags, session) {
|
||||
ui <- takeSingletons(tags, session$singletons, desingleton=FALSE)$ui
|
||||
ui <- surroundSingletons(ui)
|
||||
dependencies <- lapply(
|
||||
resolveDependencies(findDependencies(ui)),
|
||||
createWebDependency
|
||||
)
|
||||
names(dependencies) <- NULL
|
||||
|
||||
list(
|
||||
html = doRenderTags(ui),
|
||||
deps = dependencies
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#' @export a br code div em h1 h2 h3 h4 h5 h6 hr HTML img p pre span strong
|
||||
#' @export includeCSS includeHTML includeMarkdown includeScript includeText
|
||||
#' @export is.singleton singleton
|
||||
#' @export tag tagAppendAttributes tagAppendChild tagAppendChildren tagList tags tagSetChildren withTags
|
||||
#' @import htmltools
|
||||
#' @export tags p h1 h2 h3 h4 h5 h6 a br div span pre code img strong em hr
|
||||
#' @export tag tagList tagAppendAttributes tagHasAttribute tagGetAttribute tagAppendChild tagAppendChildren tagSetChildren
|
||||
#' @export HTML
|
||||
#' @export includeHTML includeText includeMarkdown includeCSS includeScript
|
||||
#' @export singleton is.singleton
|
||||
#' @export validateCssUnit
|
||||
#' @export knit_print.html knit_print.shiny.tag knit_print.shiny.tag.list
|
||||
#' @export htmlTemplate suppressDependencies
|
||||
#' @export htmlTemplate
|
||||
#' @export suppressDependencies
|
||||
#' @export withTags
|
||||
NULL
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#' Create an object representing click options
|
||||
#'
|
||||
#' This generates an object representing click options, to be passed as the
|
||||
#' \code{click} argument of \code{\link{imageOutput}} or
|
||||
#' \code{\link{plotOutput}}.
|
||||
#' `click` argument of [imageOutput()] or
|
||||
#' [plotOutput()].
|
||||
#'
|
||||
#' @param id Input value name. For example, if the value is \code{"plot_click"},
|
||||
#' then the click coordinates will be available as \code{input$plot_click}.
|
||||
#' @param id Input value name. For example, if the value is `"plot_click"`,
|
||||
#' then the click coordinates will be available as `input$plot_click`.
|
||||
#' @param clip Should the click area be clipped to the plotting area? If FALSE,
|
||||
#' then the server will receive click events even when the mouse is outside
|
||||
#' the plotting area, as long as it is still inside the image.
|
||||
@@ -24,12 +24,12 @@ clickOpts <- function(id = NULL, clip = TRUE) {
|
||||
#' Create an object representing double-click options
|
||||
#'
|
||||
#' This generates an object representing dobule-click options, to be passed as
|
||||
#' the \code{dblclick} argument of \code{\link{imageOutput}} or
|
||||
#' \code{\link{plotOutput}}.
|
||||
#' the `dblclick` argument of [imageOutput()] or
|
||||
#' [plotOutput()].
|
||||
#'
|
||||
#' @param id Input value name. For example, if the value is
|
||||
#' \code{"plot_dblclick"}, then the click coordinates will be available as
|
||||
#' \code{input$plot_dblclick}.
|
||||
#' `"plot_dblclick"`, then the click coordinates will be available as
|
||||
#' `input$plot_dblclick`.
|
||||
#' @param clip Should the click area be clipped to the plotting area? If FALSE,
|
||||
#' then the server will receive double-click events even when the mouse is
|
||||
#' outside the plotting area, as long as it is still inside the image.
|
||||
@@ -50,23 +50,23 @@ dblclickOpts <- function(id = NULL, clip = TRUE, delay = 400) {
|
||||
#' Create an object representing hover options
|
||||
#'
|
||||
#' This generates an object representing hovering options, to be passed as the
|
||||
#' \code{hover} argument of \code{\link{imageOutput}} or
|
||||
#' \code{\link{plotOutput}}.
|
||||
#' `hover` argument of [imageOutput()] or
|
||||
#' [plotOutput()].
|
||||
#'
|
||||
#' @param id Input value name. For example, if the value is \code{"plot_hover"},
|
||||
#' then the hover coordinates will be available as \code{input$plot_hover}.
|
||||
#' @param id Input value name. For example, if the value is `"plot_hover"`,
|
||||
#' then the hover coordinates will be available as `input$plot_hover`.
|
||||
#' @param delay How long to delay (in milliseconds) when debouncing or
|
||||
#' throttling, before sending the mouse location to the server.
|
||||
#' @param delayType The type of algorithm for limiting the number of hover
|
||||
#' events. Use \code{"throttle"} to limit the number of hover events to one
|
||||
#' every \code{delay} milliseconds. Use \code{"debounce"} to suspend events
|
||||
#' events. Use `"throttle"` to limit the number of hover events to one
|
||||
#' every `delay` milliseconds. Use `"debounce"` to suspend events
|
||||
#' while the cursor is moving, and wait until the cursor has been at rest for
|
||||
#' \code{delay} milliseconds before sending an event.
|
||||
#' `delay` milliseconds before sending an event.
|
||||
#' @param clip Should the hover area be clipped to the plotting area? If FALSE,
|
||||
#' then the server will receive hover events even when the mouse is outside
|
||||
#' the plotting area, as long as it is still inside the image.
|
||||
#' @param nullOutside If \code{TRUE} (the default), the value will be set to
|
||||
#' \code{NULL} when the mouse exits the plotting area. If \code{FALSE}, the
|
||||
#' @param nullOutside If `TRUE` (the default), the value will be set to
|
||||
#' `NULL` when the mouse exits the plotting area. If `FALSE`, the
|
||||
#' value will stop changing when the cursor exits the plotting area.
|
||||
#' @export
|
||||
hoverOpts <- function(id = NULL, delay = 300,
|
||||
@@ -87,34 +87,34 @@ hoverOpts <- function(id = NULL, delay = 300,
|
||||
#' Create an object representing brushing options
|
||||
#'
|
||||
#' This generates an object representing brushing options, to be passed as the
|
||||
#' \code{brush} argument of \code{\link{imageOutput}} or
|
||||
#' \code{\link{plotOutput}}.
|
||||
#' `brush` argument of [imageOutput()] or
|
||||
#' [plotOutput()].
|
||||
#'
|
||||
#' @param id Input value name. For example, if the value is \code{"plot_brush"},
|
||||
#' then the coordinates will be available as \code{input$plot_brush}. Multiple
|
||||
#' \code{imageOutput}/\code{plotOutput} calls may share the same \code{id}
|
||||
#' @param id Input value name. For example, if the value is `"plot_brush"`,
|
||||
#' then the coordinates will be available as `input$plot_brush`. Multiple
|
||||
#' `imageOutput`/`plotOutput` calls may share the same `id`
|
||||
#' value; brushing one image or plot will cause any other brushes with the
|
||||
#' same \code{id} to disappear.
|
||||
#' same `id` to disappear.
|
||||
#' @param fill Fill color of the brush.
|
||||
#' @param stroke Outline color of the brush.
|
||||
#' @param opacity Opacity of the brush
|
||||
#' @param delay How long to delay (in milliseconds) when debouncing or
|
||||
#' throttling, before sending the brush data to the server.
|
||||
#' @param delayType The type of algorithm for limiting the number of brush
|
||||
#' events. Use \code{"throttle"} to limit the number of brush events to one
|
||||
#' every \code{delay} milliseconds. Use \code{"debounce"} to suspend events
|
||||
#' events. Use `"throttle"` to limit the number of brush events to one
|
||||
#' every `delay` milliseconds. Use `"debounce"` to suspend events
|
||||
#' while the cursor is moving, and wait until the cursor has been at rest for
|
||||
#' \code{delay} milliseconds before sending an event.
|
||||
#' `delay` milliseconds before sending an event.
|
||||
#' @param clip Should the brush area be clipped to the plotting area? If FALSE,
|
||||
#' then the user will be able to brush outside the plotting area, as long as
|
||||
#' it is still inside the image.
|
||||
#' @param direction The direction for brushing. If \code{"xy"}, the brush can be
|
||||
#' drawn and moved in both x and y directions. If \code{"x"}, or \code{"y"},
|
||||
#' @param direction The direction for brushing. If `"xy"`, the brush can be
|
||||
#' drawn and moved in both x and y directions. If `"x"`, or `"y"`,
|
||||
#' the brush wil work horizontally or vertically.
|
||||
#' @param resetOnNew When a new image is sent to the browser (via
|
||||
#' \code{\link{renderImage}}), should the brush be reset? The default,
|
||||
#' \code{FALSE}, is useful if you want to update the plot while keeping the
|
||||
#' brush. Using \code{TRUE} is useful if you want to clear the brush whenever
|
||||
#' [renderImage()]), should the brush be reset? The default,
|
||||
#' `FALSE`, is useful if you want to update the plot while keeping the
|
||||
#' brush. Using `TRUE` is useful if you want to clear the brush whenever
|
||||
#' the plot is updated.
|
||||
#' @export
|
||||
brushOpts <- function(id = NULL, fill = "#9cf", stroke = "#036",
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
#' Find rows of data that are selected by a brush
|
||||
#'
|
||||
#' This function returns rows from a data frame which are under a brush used
|
||||
#' with \code{\link{plotOutput}}.
|
||||
#' with [plotOutput()].
|
||||
#'
|
||||
#' It is also possible for this function to return all rows from the input data
|
||||
#' frame, but with an additional column \code{selected_}, which indicates which
|
||||
#' rows of the input data frame are selected by the brush (\code{TRUE} for
|
||||
#' selected, \code{FALSE} for not-selected). This is enabled by setting
|
||||
#' \code{allRows=TRUE} option.
|
||||
#' frame, but with an additional column `selected_`, which indicates which
|
||||
#' rows of the input data frame are selected by the brush (`TRUE` for
|
||||
#' selected, `FALSE` for not-selected). This is enabled by setting
|
||||
#' `allRows=TRUE` option.
|
||||
#'
|
||||
#' The \code{xvar}, \code{yvar}, \code{panelvar1}, and \code{panelvar2}
|
||||
#' The `xvar`, `yvar`, `panelvar1`, and `panelvar2`
|
||||
#' arguments specify which columns in the data correspond to the x variable, y
|
||||
#' variable, and panel variables of the plot. For example, if your plot is
|
||||
#' \code{plot(x=cars$speed, y=cars$dist)}, and your brush is named
|
||||
#' \code{"cars_brush"}, then you would use \code{brushedPoints(cars,
|
||||
#' input$cars_brush, "speed", "dist")}.
|
||||
#' `plot(x=cars$speed, y=cars$dist)`, and your brush is named
|
||||
#' `"cars_brush"`, then you would use `brushedPoints(cars,
|
||||
#' input$cars_brush, "speed", "dist")`.
|
||||
#'
|
||||
#' For plots created with ggplot2, it should not be necessary to specify the
|
||||
#' column names; that information will already be contained in the brush,
|
||||
#' provided that variables are in the original data, and not computed. For
|
||||
#' example, with \code{ggplot(cars, aes(x=speed, y=dist)) + geom_point()}, you
|
||||
#' could use \code{brushedPoints(cars, input$cars_brush)}. If, however, you use
|
||||
#' a computed column, like \code{ggplot(cars, aes(x=speed/2, y=dist)) +
|
||||
#' geom_point()}, then it will not be able to automatically extract column names
|
||||
#' example, with `ggplot(cars, aes(x=speed, y=dist)) + geom_point()`, you
|
||||
#' could use `brushedPoints(cars, input$cars_brush)`. If, however, you use
|
||||
#' a computed column, like `ggplot(cars, aes(x=speed/2, y=dist)) +
|
||||
#' geom_point()`, then it will not be able to automatically extract column names
|
||||
#' and filter on them. If you want to use this function to filter data, it is
|
||||
#' recommended that you not use computed columns; instead, modify the data
|
||||
#' first, and then make the plot with "raw" columns in the modified data.
|
||||
@@ -33,26 +33,26 @@
|
||||
#' to cover a given character/factor value when it covers the center value.
|
||||
#'
|
||||
#' If the brush is operating in just the x or y directions (e.g., with
|
||||
#' \code{brushOpts(direction = "x")}, then this function will filter out points
|
||||
#' `brushOpts(direction = "x")`, then this function will filter out points
|
||||
#' using just the x or y variable, whichever is appropriate.
|
||||
#'
|
||||
#' @param brush The data from a brush, such as \code{input$plot_brush}.
|
||||
#' @param brush The data from a brush, such as `input$plot_brush`.
|
||||
#' @param df A data frame from which to select rows.
|
||||
#' @param xvar,yvar A string with the name of the variable on the x or y axis.
|
||||
#' This must also be the name of a column in \code{df}. If absent, then this
|
||||
#' This must also be the name of a column in `df`. If absent, then this
|
||||
#' function will try to infer the variable from the brush (only works for
|
||||
#' ggplot2).
|
||||
#' @param panelvar1,panelvar2 Each of these is a string with the name of a panel
|
||||
#' variable. For example, if with ggplot2, you facet on a variable called
|
||||
#' \code{cyl}, then you can use \code{"cyl"} here. However, specifying the
|
||||
#' `cyl`, then you can use `"cyl"` here. However, specifying the
|
||||
#' panel variable should not be necessary with ggplot2; Shiny should be able
|
||||
#' to auto-detect the panel variable.
|
||||
#' @param allRows If \code{FALSE} (the default) return a data frame containing
|
||||
#' the selected rows. If \code{TRUE}, the input data frame will have a new
|
||||
#' column, \code{selected_}, which indicates whether the row was inside the
|
||||
#' brush (\code{TRUE}) or outside the brush (\code{FALSE}).
|
||||
#' @param allRows If `FALSE` (the default) return a data frame containing
|
||||
#' the selected rows. If `TRUE`, the input data frame will have a new
|
||||
#' column, `selected_`, which indicates whether the row was inside the
|
||||
#' brush (`TRUE`) or outside the brush (`FALSE`).
|
||||
#'
|
||||
#' @seealso \code{\link{plotOutput}} for example usage.
|
||||
#' @seealso [plotOutput()] for example usage.
|
||||
#' @export
|
||||
brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
|
||||
panelvar1 = NULL, panelvar2 = NULL,
|
||||
@@ -86,15 +86,16 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
|
||||
if (use_x) {
|
||||
if (is.null(xvar))
|
||||
stop("brushedPoints: not able to automatically infer `xvar` from brush")
|
||||
# Extract data values from the data frame
|
||||
x <- asNumber(df[[xvar]])
|
||||
keep_rows <- keep_rows & (x >= brush$xmin & x <= brush$xmax)
|
||||
if (!(xvar %in% names(df)))
|
||||
stop("brushedPoints: `xvar` ('", xvar ,"') not in names of input")
|
||||
keep_rows <- keep_rows & within_brush(df[[xvar]], brush, "x")
|
||||
}
|
||||
if (use_y) {
|
||||
if (is.null(yvar))
|
||||
stop("brushedPoints: not able to automatically infer `yvar` from brush")
|
||||
y <- asNumber(df[[yvar]])
|
||||
keep_rows <- keep_rows & (y >= brush$ymin & y <= brush$ymax)
|
||||
if (!(yvar %in% names(df)))
|
||||
stop("brushedPoints: `yvar` ('", yvar ,"') not in names of input")
|
||||
keep_rows <- keep_rows & within_brush(df[[yvar]], brush, "y")
|
||||
}
|
||||
|
||||
# Find which rows are matches for the panel vars (if present)
|
||||
@@ -118,6 +119,19 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
|
||||
# $ xmax : num 4.22
|
||||
# $ ymin : num 13.9
|
||||
# $ ymax : num 19.8
|
||||
# $ coords_css:List of 4
|
||||
# ..$ xmin: int 260
|
||||
# ..$ xmax: int 298
|
||||
# ..$ ymin: num 112
|
||||
# ..$ ymax: num 205
|
||||
# $ coords_img:List of 4
|
||||
# ..$ xmin: int 325
|
||||
# ..$ xmax: num 372
|
||||
# ..$ ymin: num 140
|
||||
# ..$ ymax: num 257
|
||||
# $ img_css_ratio:List of 2
|
||||
# ..$ x: num 1.25
|
||||
# ..$ y: num 1.25
|
||||
# $ mapping: Named list()
|
||||
# $ domain :List of 4
|
||||
# ..$ left : num 1.36
|
||||
@@ -143,6 +157,19 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
|
||||
# $ ymax : num 20.4
|
||||
# $ panelvar1: int 6
|
||||
# $ panelvar2: int 0
|
||||
# $ coords_css:List of 4
|
||||
# ..$ xmin: int 260
|
||||
# ..$ xmax: int 298
|
||||
# ..$ ymin: num 112
|
||||
# ..$ ymax: num 205
|
||||
# $ coords_img:List of 4
|
||||
# ..$ xmin: int 325
|
||||
# ..$ xmax: num 372
|
||||
# ..$ ymin: num 140
|
||||
# ..$ ymax: num 257
|
||||
# $ img_css_ratio:List of 2
|
||||
# ..$ x: num 1.25
|
||||
# ..$ y: num 1.25
|
||||
# $ mapping :List of 4
|
||||
# ..$ x : chr "wt"
|
||||
# ..$ y : chr "mpg"
|
||||
@@ -167,39 +194,39 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
|
||||
#'Find rows of data that are near a click/hover/double-click
|
||||
#'
|
||||
#'This function returns rows from a data frame which are near a click, hover, or
|
||||
#'double-click, when used with \code{\link{plotOutput}}. The rows will be sorted
|
||||
#'double-click, when used with [plotOutput()]. The rows will be sorted
|
||||
#'by their distance to the mouse event.
|
||||
#'
|
||||
#'It is also possible for this function to return all rows from the input data
|
||||
#'frame, but with an additional column \code{selected_}, which indicates which
|
||||
#'rows of the input data frame are selected by the brush (\code{TRUE} for
|
||||
#'selected, \code{FALSE} for not-selected). This is enabled by setting
|
||||
#'\code{allRows=TRUE} option. If this is used, the resulting data frame will not
|
||||
#'frame, but with an additional column `selected_`, which indicates which
|
||||
#'rows of the input data frame are selected by the brush (`TRUE` for
|
||||
#'selected, `FALSE` for not-selected). This is enabled by setting
|
||||
#'`allRows=TRUE` option. If this is used, the resulting data frame will not
|
||||
#'be sorted by distance to the mouse event.
|
||||
#'
|
||||
#'The \code{xvar}, \code{yvar}, \code{panelvar1}, and \code{panelvar2} arguments
|
||||
#'The `xvar`, `yvar`, `panelvar1`, and `panelvar2` arguments
|
||||
#'specify which columns in the data correspond to the x variable, y variable,
|
||||
#'and panel variables of the plot. For example, if your plot is
|
||||
#'\code{plot(x=cars$speed, y=cars$dist)}, and your click variable is named
|
||||
#'\code{"cars_click"}, then you would use \code{nearPoints(cars,
|
||||
#'input$cars_brush, "speed", "dist")}.
|
||||
#'`plot(x=cars$speed, y=cars$dist)`, and your click variable is named
|
||||
#'`"cars_click"`, then you would use `nearPoints(cars,
|
||||
#'input$cars_brush, "speed", "dist")`.
|
||||
#'
|
||||
#'@inheritParams brushedPoints
|
||||
#'@param coordinfo The data from a mouse event, such as \code{input$plot_click}.
|
||||
#'@param coordinfo The data from a mouse event, such as `input$plot_click`.
|
||||
#'@param threshold A maxmimum distance to the click point; rows in the data
|
||||
#' frame where the distance to the click is less than \code{threshold} will be
|
||||
#' frame where the distance to the click is less than `threshold` will be
|
||||
#' returned.
|
||||
#'@param maxpoints Maximum number of rows to return. If NULL (the default),
|
||||
#' return all rows that are within the threshold distance.
|
||||
#'@param addDist If TRUE, add a column named \code{dist_} that contains the
|
||||
#'@param addDist If TRUE, add a column named `dist_` that contains the
|
||||
#' distance from the coordinate to the point, in pixels. When no mouse event
|
||||
#' has yet occured, the value of \code{dist_} will be \code{NA}.
|
||||
#'@param allRows If \code{FALSE} (the default) return a data frame containing
|
||||
#' the selected rows. If \code{TRUE}, the input data frame will have a new
|
||||
#' column, \code{selected_}, which indicates whether the row was inside the
|
||||
#' selected by the mouse event (\code{TRUE}) or not (\code{FALSE}).
|
||||
#' has yet occured, the value of `dist_` will be `NA`.
|
||||
#'@param allRows If `FALSE` (the default) return a data frame containing
|
||||
#' the selected rows. If `TRUE`, the input data frame will have a new
|
||||
#' column, `selected_`, which indicates whether the row was inside the
|
||||
#' selected by the mouse event (`TRUE`) or not (`FALSE`).
|
||||
#'
|
||||
#'@seealso \code{\link{plotOutput}} for more examples.
|
||||
#'@seealso [plotOutput()] for more examples.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
@@ -245,18 +272,29 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
|
||||
if (is.null(yvar))
|
||||
stop("nearPoints: not able to automatically infer `yvar` from coordinfo")
|
||||
|
||||
if (!(xvar %in% names(df)))
|
||||
stop("nearPoints: `xvar` ('", xvar ,"') not in names of input")
|
||||
if (!(yvar %in% names(df)))
|
||||
stop("nearPoints: `yvar` ('", yvar ,"') not in names of input")
|
||||
|
||||
# Extract data values from the data frame
|
||||
x <- asNumber(df[[xvar]])
|
||||
y <- asNumber(df[[yvar]])
|
||||
x <- asNumber(df[[xvar]], coordinfo$domain$discrete_limits$x)
|
||||
y <- asNumber(df[[yvar]], coordinfo$domain$discrete_limits$y)
|
||||
|
||||
# Get the pixel coordinates of the point
|
||||
coordPx <- scaleCoords(coordinfo$x, coordinfo$y, coordinfo)
|
||||
# Get the coordinates of the point (in img pixel coordinates)
|
||||
point_img <- coordinfo$coords_img
|
||||
|
||||
# Get pixel coordinates of data points
|
||||
dataPx <- scaleCoords(x, y, coordinfo)
|
||||
# Get coordinates of data points (in img pixel coordinates)
|
||||
data_img <- scaleCoords(x, y, coordinfo)
|
||||
|
||||
# Distances of data points to coordPx
|
||||
dists <- sqrt((dataPx$x - coordPx$x) ^ 2 + (dataPx$y - coordPx$y) ^ 2)
|
||||
# Get x/y distances (in css coordinates)
|
||||
dist_css <- list(
|
||||
x = (data_img$x - point_img$x) / coordinfo$img_css_ratio$x,
|
||||
y = (data_img$y - point_img$y) / coordinfo$img_css_ratio$y
|
||||
)
|
||||
|
||||
# Distances of data points to the target point, in css pixels.
|
||||
dists <- sqrt(dist_css$x^2 + dist_css$y^2)
|
||||
|
||||
if (addDist)
|
||||
df$dist_ <- dists
|
||||
@@ -298,56 +336,90 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
|
||||
# The coordinfo data structure will look something like the examples below.
|
||||
# For base graphics, `mapping` is empty, and there are no panelvars:
|
||||
# List of 7
|
||||
# $ x : num 4.37
|
||||
# $ y : num 12
|
||||
# $ mapping: Named list()
|
||||
# $ domain :List of 4
|
||||
# $ x : num 4.37
|
||||
# $ y : num 12
|
||||
# $ coords_css:List of 2
|
||||
# ..$ x: int 286
|
||||
# ..$ y: int 192
|
||||
# $ coords_img:List of 2
|
||||
# ..$ x: num 358
|
||||
# ..$ y: int 240
|
||||
# $ img_css_ratio:List of 2
|
||||
# ..$ x: num 1.25
|
||||
# ..$ y: num 1.25
|
||||
# $ mapping : Named list()
|
||||
# $ domain :List of 4
|
||||
# ..$ left : num 1.36
|
||||
# ..$ right : num 5.58
|
||||
# ..$ bottom: num 9.46
|
||||
# ..$ top : num 34.8
|
||||
# $ range :List of 4
|
||||
# $ range :List of 4
|
||||
# ..$ left : num 58
|
||||
# ..$ right : num 429
|
||||
# ..$ bottom: num 226
|
||||
# ..$ top : num 58
|
||||
# $ log :List of 2
|
||||
# $ log :List of 2
|
||||
# ..$ x: NULL
|
||||
# ..$ y: NULL
|
||||
# $ .nonce : num 0.343
|
||||
# $ .nonce : num 0.343
|
||||
#
|
||||
# For ggplot2, the mapping vars usually will be included, and if faceting is
|
||||
# used, they will be listed as panelvars:
|
||||
# List of 9
|
||||
# $ x : num 3.78
|
||||
# $ y : num 17.1
|
||||
# $ panelvar1: int 6
|
||||
# $ panelvar2: int 0
|
||||
# $ mapping :List of 4
|
||||
# $ x : num 3.78
|
||||
# $ y : num 17.1
|
||||
# $ coords_css:List of 2
|
||||
# ..$ x: int 286
|
||||
# ..$ y: int 192
|
||||
# $ coords_img:List of 2
|
||||
# ..$ x: num 358
|
||||
# ..$ y: int 240
|
||||
# $ img_css_ratio:List of 2
|
||||
# ..$ x: num 1.25
|
||||
# ..$ y: num 1.25
|
||||
# $ panelvar1 : int 6
|
||||
# $ panelvar2 : int 0
|
||||
# $ mapping :List of 4
|
||||
# ..$ x : chr "wt"
|
||||
# ..$ y : chr "mpg"
|
||||
# ..$ panelvar1: chr "cyl"
|
||||
# ..$ panelvar2: chr "am"
|
||||
# $ domain :List of 4
|
||||
# $ domain :List of 4
|
||||
# ..$ left : num 1.32
|
||||
# ..$ right : num 5.62
|
||||
# ..$ bottom: num 9.22
|
||||
# ..$ top : num 35.1
|
||||
# $ range :List of 4
|
||||
# $ range :List of 4
|
||||
# ..$ left : num 172
|
||||
# ..$ right : num 300
|
||||
# ..$ bottom: num 144
|
||||
# ..$ top : num 28.5
|
||||
# $ log :List of 2
|
||||
# $ log :List of 2
|
||||
# ..$ x: NULL
|
||||
# ..$ y: NULL
|
||||
# $ .nonce : num 0.603
|
||||
|
||||
# $ .nonce : num 0.603
|
||||
|
||||
# Helper to determine if data values are within the limits of
|
||||
# an input brush
|
||||
within_brush <- function(vals, brush, var = "x") {
|
||||
var <- match.arg(var, c("x", "y"))
|
||||
vals <- asNumber(vals, brush$domain$discrete_limits[[var]])
|
||||
# It's possible for a non-missing data values to not
|
||||
# map to the axis limits, for example:
|
||||
# https://github.com/rstudio/shiny/pull/2410#issuecomment-488100881
|
||||
!is.na(vals) &
|
||||
vals >= brush[[paste0(var, "min")]] &
|
||||
vals <= brush[[paste0(var, "max")]]
|
||||
}
|
||||
|
||||
# Coerce various types of variables to numbers. This works for Date, POSIXt,
|
||||
# characters, and factors. Used because the mouse coords are numeric.
|
||||
asNumber <- function(x) {
|
||||
# The `levels` argument should be used when mapping this variable to
|
||||
# a known set of discrete levels, which is needed for ggplot2 since
|
||||
# it allows you to control ordering and possible values of a discrete
|
||||
# positional scale (#2410)
|
||||
asNumber <- function(x, levels = NULL) {
|
||||
if (length(levels)) return(match(x, levels))
|
||||
if (is.character(x)) x <- as.factor(x)
|
||||
if (is.factor(x)) x <- as.integer(x)
|
||||
as.numeric(x)
|
||||
|
||||
104
R/imageutils.R
104
R/imageutils.R
@@ -1,41 +1,11 @@
|
||||
#' Run a plotting function and save the output as a PNG
|
||||
#'
|
||||
#' This function returns the name of the PNG file that it generates. In
|
||||
#' essence, it calls \code{png()}, then \code{func()}, then \code{dev.off()}.
|
||||
#' So \code{func} must be a function that will generate a plot when used this
|
||||
#' way.
|
||||
#'
|
||||
#' For output, it will try to use the following devices, in this order:
|
||||
#' quartz (via \code{\link[grDevices]{png}}), then \code{\link[Cairo]{CairoPNG}},
|
||||
#' and finally \code{\link[grDevices]{png}}. This is in order of quality of
|
||||
#' output. Notably, plain \code{png} output on Linux and Windows may not
|
||||
#' antialias some point shapes, resulting in poor quality output.
|
||||
#'
|
||||
#' In some cases, \code{Cairo()} provides output that looks worse than
|
||||
#' \code{png()}. To disable Cairo output for an app, use
|
||||
#' \code{options(shiny.usecairo=FALSE)}.
|
||||
#'
|
||||
#' @param func A function that generates a plot.
|
||||
#' @param filename The name of the output file. Defaults to a temp file with
|
||||
#' extension \code{.png}.
|
||||
#' @param width Width in pixels.
|
||||
#' @param height Height in pixels.
|
||||
#' @param res Resolution in pixels per inch. This value is passed to
|
||||
#' \code{\link{png}}. Note that this affects the resolution of PNG rendering in
|
||||
#' R; it won't change the actual ppi of the browser.
|
||||
#' @param ... Arguments to be passed through to \code{\link[grDevices]{png}}.
|
||||
#' These can be used to set the width, height, background color, etc.
|
||||
#'
|
||||
#' @export
|
||||
plotPNG <- function(func, filename=tempfile(fileext='.png'),
|
||||
width=400, height=400, res=72, ...) {
|
||||
startPNG <- function(filename, width, height, res, ...) {
|
||||
# If quartz is available, use png() (which will default to quartz).
|
||||
# Otherwise, if the Cairo package is installed, use CairoPNG().
|
||||
# Finally, if neither quartz nor Cairo, use png().
|
||||
if (capabilities("aqua")) {
|
||||
pngfun <- grDevices::png
|
||||
} else if ((getOption('shiny.usecairo') %OR% TRUE) &&
|
||||
nchar(system.file(package = "Cairo"))) {
|
||||
nchar(system.file(package = "Cairo"))) {
|
||||
pngfun <- Cairo::CairoPNG
|
||||
} else {
|
||||
pngfun <- grDevices::png
|
||||
@@ -55,9 +25,77 @@ plotPNG <- function(func, filename=tempfile(fileext='.png'),
|
||||
finally = graphics::par(op)
|
||||
)
|
||||
|
||||
dv <- grDevices::dev.cur()
|
||||
grDevices::dev.cur()
|
||||
}
|
||||
|
||||
#' Run a plotting function and save the output as a PNG
|
||||
#'
|
||||
#' This function returns the name of the PNG file that it generates. In
|
||||
#' essence, it calls `png()`, then `func()`, then `dev.off()`.
|
||||
#' So `func` must be a function that will generate a plot when used this
|
||||
#' way.
|
||||
#'
|
||||
#' For output, it will try to use the following devices, in this order:
|
||||
#' quartz (via [grDevices::png()]), then [Cairo::CairoPNG()],
|
||||
#' and finally [grDevices::png()]. This is in order of quality of
|
||||
#' output. Notably, plain `png` output on Linux and Windows may not
|
||||
#' antialias some point shapes, resulting in poor quality output.
|
||||
#'
|
||||
#' In some cases, `Cairo()` provides output that looks worse than
|
||||
#' `png()`. To disable Cairo output for an app, use
|
||||
#' `options(shiny.usecairo=FALSE)`.
|
||||
#'
|
||||
#' @param func A function that generates a plot.
|
||||
#' @param filename The name of the output file. Defaults to a temp file with
|
||||
#' extension `.png`.
|
||||
#' @param width Width in pixels.
|
||||
#' @param height Height in pixels.
|
||||
#' @param res Resolution in pixels per inch. This value is passed to
|
||||
#' [grDevices::png()]. Note that this affects the resolution of PNG rendering in
|
||||
#' R; it won't change the actual ppi of the browser.
|
||||
#' @param ... Arguments to be passed through to [grDevices::png()].
|
||||
#' These can be used to set the width, height, background color, etc.
|
||||
#' @export
|
||||
plotPNG <- function(func, filename=tempfile(fileext='.png'),
|
||||
width=400, height=400, res=72, ...) {
|
||||
dv <- startPNG(filename, width, height, res, ...)
|
||||
on.exit(grDevices::dev.off(dv), add = TRUE)
|
||||
func()
|
||||
|
||||
filename
|
||||
}
|
||||
|
||||
#' @importFrom grDevices dev.set dev.cur
|
||||
createGraphicsDevicePromiseDomain <- function(which = dev.cur()) {
|
||||
force(which)
|
||||
|
||||
promises::new_promise_domain(
|
||||
wrapOnFulfilled = function(onFulfilled) {
|
||||
force(onFulfilled)
|
||||
function(...) {
|
||||
old <- dev.cur()
|
||||
dev.set(which)
|
||||
on.exit(dev.set(old))
|
||||
|
||||
onFulfilled(...)
|
||||
}
|
||||
},
|
||||
wrapOnRejected = function(onRejected) {
|
||||
force(onRejected)
|
||||
function(...) {
|
||||
old <- dev.cur()
|
||||
dev.set(which)
|
||||
on.exit(dev.set(old))
|
||||
|
||||
onRejected(...)
|
||||
}
|
||||
},
|
||||
wrapSync = function(expr) {
|
||||
old <- dev.cur()
|
||||
dev.set(which)
|
||||
on.exit(dev.set(old))
|
||||
|
||||
force(expr)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,35 +6,55 @@
|
||||
#' @inheritParams textInput
|
||||
#' @param label The contents of the button or link--usually a text label, but
|
||||
#' you could also use any other HTML, like an image.
|
||||
#' @param icon An optional \code{\link{icon}} to appear on the button.
|
||||
#' @param icon An optional [icon()] to appear on the button.
|
||||
#' @param ... Named attributes to be applied to the button or link.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' # In server.R
|
||||
#' output$distPlot <- renderPlot({
|
||||
#' # Take a dependency on input$goButton
|
||||
#' input$goButton
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # Use isolate() to avoid dependency on input$obs
|
||||
#' dist <- isolate(rnorm(input$obs))
|
||||
#' hist(dist)
|
||||
#' })
|
||||
#' ui <- fluidPage(
|
||||
#' sliderInput("obs", "Number of observations", 0, 1000, 500),
|
||||
#' actionButton("goButton", "Go!"),
|
||||
#' plotOutput("distPlot")
|
||||
#' )
|
||||
#'
|
||||
#' # In ui.R
|
||||
#' actionButton("goButton", "Go!")
|
||||
#' server <- function(input, output) {
|
||||
#' output$distPlot <- renderPlot({
|
||||
#' # Take a dependency on input$goButton. This will run once initially,
|
||||
#' # because the value changes from NULL to 0.
|
||||
#' input$goButton
|
||||
#'
|
||||
#' # Use isolate() to avoid dependency on input$obs
|
||||
#' dist <- isolate(rnorm(input$obs))
|
||||
#' hist(dist)
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' @seealso \code{\link{observeEvent}} and \code{\link{eventReactive}}
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' @seealso [observeEvent()] and [eventReactive()]
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' An integer of class `"shinyActionButtonValue"`. This class differs from
|
||||
#' ordinary integers in that a value of 0 is considered "falsy".
|
||||
#' This implies two things:
|
||||
#' * Event handlers (e.g., [observeEvent()], [eventReactive()]) won't execute on initial load.
|
||||
#' * Input validation (e.g., [req()], [need()]) will fail on initial load.
|
||||
#' @export
|
||||
actionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
|
||||
|
||||
value <- restoreInput(id = inputId, default = NULL)
|
||||
|
||||
tags$button(id=inputId,
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
type="button",
|
||||
class="btn btn-default action-button",
|
||||
list(icon, label),
|
||||
`data-val` = value,
|
||||
list(validateIcon(icon), label),
|
||||
...
|
||||
)
|
||||
}
|
||||
@@ -42,10 +62,32 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
|
||||
#' @rdname actionButton
|
||||
#' @export
|
||||
actionLink <- function(inputId, label, icon = NULL, ...) {
|
||||
value <- restoreInput(id = inputId, default = NULL)
|
||||
|
||||
tags$a(id=inputId,
|
||||
href="#",
|
||||
class="action-button",
|
||||
list(icon, label),
|
||||
`data-val` = value,
|
||||
list(validateIcon(icon), label),
|
||||
...
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# Check that the icon parameter is valid:
|
||||
# 1) Check if the user wants to actually add an icon:
|
||||
# -- if icon=NULL, it means leave the icon unchanged
|
||||
# -- if icon=character(0), it means don't add an icon or, more usefully,
|
||||
# remove the previous icon
|
||||
# 2) If so, check that the icon has the right format (this does not check whether
|
||||
# it is a *real* icon - currently that would require a massive cross reference
|
||||
# with the "font-awesome" and the "glyphicon" libraries)
|
||||
validateIcon <- function(icon) {
|
||||
if (is.null(icon) || identical(icon, character(0))) {
|
||||
return(icon)
|
||||
} else if (inherits(icon, "shiny.tag") && icon$name == "i") {
|
||||
return(icon)
|
||||
} else {
|
||||
stop("Invalid icon. Use Shiny's 'icon()' function to generate a valid icon")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,34 @@
|
||||
#' Create a checkbox that can be used to specify logical values.
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param value Initial value (\code{TRUE} or \code{FALSE}).
|
||||
#' @param value Initial value (`TRUE` or `FALSE`).
|
||||
#' @return A checkbox control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso \code{\link{checkboxGroupInput}}, \code{\link{updateCheckboxInput}}
|
||||
#' @seealso [checkboxGroupInput()], [updateCheckboxInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' checkboxInput("outliers", "Show outliers", FALSE)
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' checkboxInput("somevalue", "Some value", FALSE),
|
||||
#' verbatimTextOutput("value")
|
||||
#' )
|
||||
#' server <- function(input, output) {
|
||||
#' output$value <- renderText({ input$somevalue })
|
||||
#' }
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' `TRUE` if checked, `FALSE` otherwise.
|
||||
#'
|
||||
#' @export
|
||||
checkboxInput <- function(inputId, label, value = FALSE, width = NULL) {
|
||||
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
inputTag <- tags$input(id = inputId, type="checkbox")
|
||||
if (!is.null(value) && value)
|
||||
inputTag$attribs$checked <- "checked"
|
||||
|
||||
@@ -6,30 +6,88 @@
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param choices List of values to show checkboxes for. If elements of the list
|
||||
#' are named then that name rather than the value is displayed to the user.
|
||||
#' are named then that name rather than the value is displayed to the user. If
|
||||
#' this argument is provided, then `choiceNames` and `choiceValues`
|
||||
#' must not be provided, and vice-versa. The values should be strings; other
|
||||
#' types (such as logicals and numbers) will be coerced to strings.
|
||||
#' @param selected The values that should be initially selected, if any.
|
||||
#' @param inline If \code{TRUE}, render the choices inline (i.e. horizontally)
|
||||
#' @param inline If `TRUE`, render the choices inline (i.e. horizontally)
|
||||
#' @param choiceNames,choiceValues List of names and values, respectively,
|
||||
#' that are displayed to the user in the app and correspond to the each
|
||||
#' choice (for this reason, `choiceNames` and `choiceValues`
|
||||
#' must have the same length). If either of these arguments is
|
||||
#' provided, then the other *must* be provided and `choices`
|
||||
#' *must not* be provided. The advantage of using both of these over
|
||||
#' a named list for `choices` is that `choiceNames` allows any
|
||||
#' type of UI object to be passed through (tag objects, icons, HTML code,
|
||||
#' ...), instead of just simple text. See Examples.
|
||||
#'
|
||||
#' @return A list of HTML elements that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso \code{\link{checkboxInput}}, \code{\link{updateCheckboxGroupInput}}
|
||||
#' @seealso [checkboxInput()], [updateCheckboxGroupInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' checkboxGroupInput("variable", "Variable:",
|
||||
#' c("Cylinders" = "cyl",
|
||||
#' "Transmission" = "am",
|
||||
#' "Gears" = "gear"))
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' checkboxGroupInput("variable", "Variables to show:",
|
||||
#' c("Cylinders" = "cyl",
|
||||
#' "Transmission" = "am",
|
||||
#' "Gears" = "gear")),
|
||||
#' tableOutput("data")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' output$data <- renderTable({
|
||||
#' mtcars[, c("mpg", input$variable), drop = FALSE]
|
||||
#' }, rownames = TRUE)
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' checkboxGroupInput("icons", "Choose icons:",
|
||||
#' choiceNames =
|
||||
#' list(icon("calendar"), icon("bed"),
|
||||
#' icon("cog"), icon("bug")),
|
||||
#' choiceValues =
|
||||
#' list("calendar", "bed", "cog", "bug")
|
||||
#' ),
|
||||
#' textOutput("txt")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' output$txt <- renderText({
|
||||
#' icons <- paste(input$icons, collapse = ", ")
|
||||
#' paste("You chose", icons)
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @section Server value:
|
||||
#' Character vector of values corresponding to the boxes that are checked.
|
||||
#'
|
||||
#' @export
|
||||
checkboxGroupInput <- function(inputId, label, choices, selected = NULL,
|
||||
inline = FALSE, width = NULL) {
|
||||
checkboxGroupInput <- function(inputId, label, choices = NULL, selected = NULL,
|
||||
inline = FALSE, width = NULL, choiceNames = NULL, choiceValues = NULL) {
|
||||
|
||||
# resolve names
|
||||
choices <- choicesWithNames(choices)
|
||||
if (!is.null(selected))
|
||||
selected <- validateSelected(selected, choices, inputId)
|
||||
# keep backward compatibility with Shiny < 1.0.1 (see #1649)
|
||||
if (is.null(choices) && is.null(choiceNames) && is.null(choiceValues)) {
|
||||
choices <- character(0)
|
||||
}
|
||||
|
||||
options <- generateOptions(inputId, choices, selected, inline)
|
||||
args <- normalizeChoicesArgs(choices, choiceNames, choiceValues)
|
||||
|
||||
selected <- restoreInput(id = inputId, default = selected)
|
||||
|
||||
# default value if it's not specified
|
||||
if (!is.null(selected)) selected <- as.character(selected)
|
||||
|
||||
options <- generateOptions(inputId, selected, inline,
|
||||
'checkbox', args$choiceNames, args$choiceValues)
|
||||
|
||||
divClass <- "form-group shiny-input-checkboxgroup shiny-input-container"
|
||||
if (inline)
|
||||
@@ -39,7 +97,7 @@ checkboxGroupInput <- function(inputId, label, choices, selected = NULL,
|
||||
tags$div(id = inputId,
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
class = divClass,
|
||||
controlLabel(inputId, label),
|
||||
shinyInputLabel(inputId, label),
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
164
R/input-date.R
164
R/input-date.R
@@ -3,100 +3,140 @@
|
||||
#' Creates a text input which, when clicked on, brings up a calendar that
|
||||
#' the user can click on to select dates.
|
||||
#'
|
||||
#' The date \code{format} string specifies how the date will be displayed in
|
||||
#' The date `format` string specifies how the date will be displayed in
|
||||
#' the browser. It allows the following values:
|
||||
#'
|
||||
#' \itemize{
|
||||
#' \item \code{yy} Year without century (12)
|
||||
#' \item \code{yyyy} Year with century (2012)
|
||||
#' \item \code{mm} Month number, with leading zero (01-12)
|
||||
#' \item \code{m} Month number, without leading zero (01-12)
|
||||
#' \item \code{M} Abbreviated month name
|
||||
#' \item \code{MM} Full month name
|
||||
#' \item \code{dd} Day of month with leading zero
|
||||
#' \item \code{d} Day of month without leading zero
|
||||
#' \item \code{D} Abbreviated weekday name
|
||||
#' \item \code{DD} Full weekday name
|
||||
#' \item `yy` Year without century (12)
|
||||
#' \item `yyyy` Year with century (2012)
|
||||
#' \item `mm` Month number, with leading zero (01-12)
|
||||
#' \item `m` Month number, without leading zero (1-12)
|
||||
#' \item `M` Abbreviated month name
|
||||
#' \item `MM` Full month name
|
||||
#' \item `dd` Day of month with leading zero
|
||||
#' \item `d` Day of month without leading zero
|
||||
#' \item `D` Abbreviated weekday name
|
||||
#' \item `DD` Full weekday name
|
||||
#' }
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param value The starting date. Either a Date object, or a string in
|
||||
#' \code{yyyy-mm-dd} format. If NULL (the default), will use the current
|
||||
#' date in the client's time zone.
|
||||
#' `yyyy-mm-dd` format. If NULL (the default), will use the current date
|
||||
#' in the client's time zone.
|
||||
#' @param min The minimum allowed date. Either a Date object, or a string in
|
||||
#' \code{yyyy-mm-dd} format.
|
||||
#' `yyyy-mm-dd` format.
|
||||
#' @param max The maximum allowed date. Either a Date object, or a string in
|
||||
#' \code{yyyy-mm-dd} format.
|
||||
#' `yyyy-mm-dd` format.
|
||||
#' @param format The format of the date to display in the browser. Defaults to
|
||||
#' \code{"yyyy-mm-dd"}.
|
||||
#' @param startview The date range shown when the input object is first
|
||||
#' clicked. Can be "month" (the default), "year", or "decade".
|
||||
#' `"yyyy-mm-dd"`.
|
||||
#' @param startview The date range shown when the input object is first clicked.
|
||||
#' Can be "month" (the default), "year", or "decade".
|
||||
#' @param weekstart Which day is the start of the week. Should be an integer
|
||||
#' from 0 (Sunday) to 6 (Saturday).
|
||||
#' @param language The language used for month and day names. Default is "en".
|
||||
#' Other valid values include "bg", "ca", "cs", "da", "de", "el", "es", "fi",
|
||||
#' "fr", "he", "hr", "hu", "id", "is", "it", "ja", "kr", "lt", "lv", "ms",
|
||||
#' "nb", "nl", "pl", "pt", "pt-BR", "ro", "rs", "rs-latin", "ru", "sk", "sl",
|
||||
#' "sv", "sw", "th", "tr", "uk", "zh-CN", and "zh-TW".
|
||||
#' Other valid values include "ar", "az", "bg", "bs", "ca", "cs", "cy", "da",
|
||||
#' "de", "el", "en-AU", "en-GB", "eo", "es", "et", "eu", "fa", "fi", "fo",
|
||||
#' "fr-CH", "fr", "gl", "he", "hr", "hu", "hy", "id", "is", "it-CH", "it",
|
||||
#' "ja", "ka", "kh", "kk", "ko", "kr", "lt", "lv", "me", "mk", "mn", "ms",
|
||||
#' "nb", "nl-BE", "nl", "no", "pl", "pt-BR", "pt", "ro", "rs-latin", "rs",
|
||||
#' "ru", "sk", "sl", "sq", "sr-latin", "sr", "sv", "sw", "th", "tr", "uk",
|
||||
#' "vi", "zh-CN", and "zh-TW".
|
||||
#' @param autoclose Whether or not to close the datepicker immediately when a
|
||||
#' date is selected.
|
||||
#' @param datesdisabled Which dates should be disabled. Either a Date object,
|
||||
#' or a string in `yyyy-mm-dd` format.
|
||||
#' @param daysofweekdisabled Days of the week that should be disabled. Should be
|
||||
#' a integer vector with values from 0 (Sunday) to 6 (Saturday).
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso \code{\link{dateRangeInput}}, \code{\link{updateDateInput}}
|
||||
#' @seealso [dateRangeInput()], [updateDateInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' dateInput("date", "Date:", value = "2012-02-29")
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # Default value is the date in client's time zone
|
||||
#' dateInput("date", "Date:")
|
||||
#' ui <- fluidPage(
|
||||
#' dateInput("date1", "Date:", value = "2012-02-29"),
|
||||
#'
|
||||
#' # value is always yyyy-mm-dd, even if the display format is different
|
||||
#' dateInput("date", "Date:", value = "2012-02-29", format = "mm/dd/yy")
|
||||
#' # Default value is the date in client's time zone
|
||||
#' dateInput("date2", "Date:"),
|
||||
#'
|
||||
#' # Pass in a Date object
|
||||
#' dateInput("date", "Date:", value = Sys.Date()-10)
|
||||
#' # value is always yyyy-mm-dd, even if the display format is different
|
||||
#' dateInput("date3", "Date:", value = "2012-02-29", format = "mm/dd/yy"),
|
||||
#'
|
||||
#' # Use different language and different first day of week
|
||||
#' dateInput("date", "Date:",
|
||||
#' language = "de",
|
||||
#' weekstart = 1)
|
||||
#' # Pass in a Date object
|
||||
#' dateInput("date4", "Date:", value = Sys.Date()-10),
|
||||
#'
|
||||
#' # Start with decade view instead of default month view
|
||||
#' dateInput("date", "Date:",
|
||||
#' startview = "decade")
|
||||
#' # Use different language and different first day of week
|
||||
#' dateInput("date5", "Date:",
|
||||
#' language = "ru",
|
||||
#' weekstart = 1),
|
||||
#'
|
||||
#' # Start with decade view instead of default month view
|
||||
#' dateInput("date6", "Date:",
|
||||
#' startview = "decade"),
|
||||
#'
|
||||
#' # Disable Mondays and Tuesdays.
|
||||
#' dateInput("date7", "Date:", daysofweekdisabled = c(1,2)),
|
||||
#'
|
||||
#' # Disable specific dates.
|
||||
#' dateInput("date8", "Date:", value = "2012-02-29",
|
||||
#' datesdisabled = c("2012-03-01", "2012-03-02"))
|
||||
#' )
|
||||
#'
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A [Date] vector of length 1.
|
||||
#'
|
||||
#' @export
|
||||
dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
|
||||
format = "yyyy-mm-dd", startview = "month", weekstart = 0, language = "en",
|
||||
width = NULL) {
|
||||
format = "yyyy-mm-dd", startview = "month", weekstart = 0,
|
||||
language = "en", width = NULL, autoclose = TRUE,
|
||||
datesdisabled = NULL, daysofweekdisabled = NULL) {
|
||||
|
||||
# If value is a date object, convert it to a string with yyyy-mm-dd format
|
||||
# Same for min and max
|
||||
if (inherits(value, "Date")) value <- format(value, "%Y-%m-%d")
|
||||
if (inherits(min, "Date")) min <- format(min, "%Y-%m-%d")
|
||||
if (inherits(max, "Date")) max <- format(max, "%Y-%m-%d")
|
||||
value <- dateYMD(value, "value")
|
||||
min <- dateYMD(min, "min")
|
||||
max <- dateYMD(max, "max")
|
||||
datesdisabled <- dateYMD(datesdisabled, "datesdisabled")
|
||||
|
||||
attachDependencies(
|
||||
tags$div(id = inputId,
|
||||
class = "shiny-date-input form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
controlLabel(inputId, label),
|
||||
tags$input(type = "text",
|
||||
# datepicker class necessary for dropdown to display correctly
|
||||
class = "form-control datepicker",
|
||||
`data-date-language` = language,
|
||||
`data-date-weekstart` = weekstart,
|
||||
`data-date-format` = format,
|
||||
`data-date-start-view` = startview,
|
||||
`data-min-date` = min,
|
||||
`data-max-date` = max,
|
||||
`data-initial-date` = value
|
||||
)
|
||||
tags$div(id = inputId,
|
||||
class = "shiny-date-input form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
|
||||
shinyInputLabel(inputId, label),
|
||||
tags$input(type = "text",
|
||||
class = "form-control",
|
||||
`data-date-language` = language,
|
||||
`data-date-week-start` = weekstart,
|
||||
`data-date-format` = format,
|
||||
`data-date-start-view` = startview,
|
||||
`data-min-date` = min,
|
||||
`data-max-date` = max,
|
||||
`data-initial-date` = value,
|
||||
`data-date-autoclose` = if (autoclose) "true" else "false",
|
||||
`data-date-dates-disabled` =
|
||||
# Ensure NULL is not sent as `{}` but as 'null'
|
||||
jsonlite::toJSON(datesdisabled, null = 'null'),
|
||||
`data-date-days-of-week-disabled` =
|
||||
jsonlite::toJSON(daysofweekdisabled, null = 'null')
|
||||
),
|
||||
datePickerDependency
|
||||
)
|
||||
}
|
||||
|
||||
datePickerDependency <- htmlDependency(
|
||||
"bootstrap-datepicker", "1.0.2", c(href = "shared/datepicker"),
|
||||
"bootstrap-datepicker", "1.6.4", c(href = "shared/datepicker"),
|
||||
script = "js/bootstrap-datepicker.min.js",
|
||||
stylesheet = "css/datepicker.css")
|
||||
stylesheet = "css/bootstrap-datepicker3.min.css",
|
||||
# Need to enable noConflict mode. See #1346.
|
||||
head = "<script>
|
||||
(function() {
|
||||
var datepicker = $.fn.datepicker.noConflict();
|
||||
$.fn.bsDatepicker = datepicker;
|
||||
})();
|
||||
</script>"
|
||||
)
|
||||
|
||||
@@ -3,108 +3,129 @@
|
||||
#' Creates a pair of text inputs which, when clicked on, bring up calendars that
|
||||
#' the user can click on to select dates.
|
||||
#'
|
||||
#' The date \code{format} string specifies how the date will be displayed in
|
||||
#' The date `format` string specifies how the date will be displayed in
|
||||
#' the browser. It allows the following values:
|
||||
#'
|
||||
#' \itemize{
|
||||
#' \item \code{yy} Year without century (12)
|
||||
#' \item \code{yyyy} Year with century (2012)
|
||||
#' \item \code{mm} Month number, with leading zero (01-12)
|
||||
#' \item \code{m} Month number, without leading zero (01-12)
|
||||
#' \item \code{M} Abbreviated month name
|
||||
#' \item \code{MM} Full month name
|
||||
#' \item \code{dd} Day of month with leading zero
|
||||
#' \item \code{d} Day of month without leading zero
|
||||
#' \item \code{D} Abbreviated weekday name
|
||||
#' \item \code{DD} Full weekday name
|
||||
#' \item `yy` Year without century (12)
|
||||
#' \item `yyyy` Year with century (2012)
|
||||
#' \item `mm` Month number, with leading zero (01-12)
|
||||
#' \item `m` Month number, without leading zero (1-12)
|
||||
#' \item `M` Abbreviated month name
|
||||
#' \item `MM` Full month name
|
||||
#' \item `dd` Day of month with leading zero
|
||||
#' \item `d` Day of month without leading zero
|
||||
#' \item `D` Abbreviated weekday name
|
||||
#' \item `DD` Full weekday name
|
||||
#' }
|
||||
#'
|
||||
#' @inheritParams dateInput
|
||||
#' @param start The initial start date. Either a Date object, or a string in
|
||||
#' \code{yyyy-mm-dd} format. If NULL (the default), will use the current
|
||||
#' `yyyy-mm-dd` format. If NULL (the default), will use the current
|
||||
#' date in the client's time zone.
|
||||
#' @param end The initial end date. Either a Date object, or a string in
|
||||
#' \code{yyyy-mm-dd} format. If NULL (the default), will use the current
|
||||
#' `yyyy-mm-dd` format. If NULL (the default), will use the current
|
||||
#' date in the client's time zone.
|
||||
#' @param separator String to display between the start and end input boxes.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso \code{\link{dateInput}}, \code{\link{updateDateRangeInput}}
|
||||
#' @seealso [dateInput()], [updateDateRangeInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' dateRangeInput("daterange", "Date range:",
|
||||
#' start = "2001-01-01",
|
||||
#' end = "2010-12-31")
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # Default start and end is the current date in the client's time zone
|
||||
#' dateRangeInput("daterange", "Date range:")
|
||||
#' ui <- fluidPage(
|
||||
#' dateRangeInput("daterange1", "Date range:",
|
||||
#' start = "2001-01-01",
|
||||
#' end = "2010-12-31"),
|
||||
#'
|
||||
#' # start and end are always specified in yyyy-mm-dd, even if the display
|
||||
#' # format is different
|
||||
#' dateRangeInput("daterange", "Date range:",
|
||||
#' start = "2001-01-01",
|
||||
#' end = "2010-12-31",
|
||||
#' min = "2001-01-01",
|
||||
#' max = "2012-12-21",
|
||||
#' format = "mm/dd/yy",
|
||||
#' separator = " - ")
|
||||
#' # Default start and end is the current date in the client's time zone
|
||||
#' dateRangeInput("daterange2", "Date range:"),
|
||||
#'
|
||||
#' # Pass in Date objects
|
||||
#' dateRangeInput("daterange", "Date range:",
|
||||
#' start = Sys.Date()-10,
|
||||
#' end = Sys.Date()+10)
|
||||
#' # start and end are always specified in yyyy-mm-dd, even if the display
|
||||
#' # format is different
|
||||
#' dateRangeInput("daterange3", "Date range:",
|
||||
#' start = "2001-01-01",
|
||||
#' end = "2010-12-31",
|
||||
#' min = "2001-01-01",
|
||||
#' max = "2012-12-21",
|
||||
#' format = "mm/dd/yy",
|
||||
#' separator = " - "),
|
||||
#'
|
||||
#' # Use different language and different first day of week
|
||||
#' dateRangeInput("daterange", "Date range:",
|
||||
#' language = "de",
|
||||
#' weekstart = 1)
|
||||
#' # Pass in Date objects
|
||||
#' dateRangeInput("daterange4", "Date range:",
|
||||
#' start = Sys.Date()-10,
|
||||
#' end = Sys.Date()+10),
|
||||
#'
|
||||
#' # Start with decade view instead of default month view
|
||||
#' dateRangeInput("daterange", "Date range:",
|
||||
#' startview = "decade")
|
||||
#' # Use different language and different first day of week
|
||||
#' dateRangeInput("daterange5", "Date range:",
|
||||
#' language = "de",
|
||||
#' weekstart = 1),
|
||||
#'
|
||||
#' # Start with decade view instead of default month view
|
||||
#' dateRangeInput("daterange6", "Date range:",
|
||||
#' startview = "decade")
|
||||
#' )
|
||||
#'
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A [Date] vector of length 2.
|
||||
#'
|
||||
#' @export
|
||||
dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
|
||||
min = NULL, max = NULL, format = "yyyy-mm-dd", startview = "month",
|
||||
weekstart = 0, language = "en", separator = " to ", width = NULL) {
|
||||
weekstart = 0, language = "en", separator = " to ", width = NULL,
|
||||
autoclose = TRUE) {
|
||||
|
||||
# If start and end are date objects, convert to a string with yyyy-mm-dd format
|
||||
# Same for min and max
|
||||
if (inherits(start, "Date")) start <- format(start, "%Y-%m-%d")
|
||||
if (inherits(end, "Date")) end <- format(end, "%Y-%m-%d")
|
||||
if (inherits(min, "Date")) min <- format(min, "%Y-%m-%d")
|
||||
if (inherits(max, "Date")) max <- format(max, "%Y-%m-%d")
|
||||
start <- dateYMD(start, "start")
|
||||
end <- dateYMD(end, "end")
|
||||
min <- dateYMD(min, "min")
|
||||
max <- dateYMD(max, "max")
|
||||
|
||||
restored <- restoreInput(id = inputId, default = list(start, end))
|
||||
start <- restored[[1]]
|
||||
end <- restored[[2]]
|
||||
|
||||
attachDependencies(
|
||||
div(id = inputId,
|
||||
class = "shiny-date-range-input form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
|
||||
controlLabel(inputId, label),
|
||||
shinyInputLabel(inputId, label),
|
||||
# input-daterange class is needed for dropdown behavior
|
||||
div(class = "input-daterange input-group",
|
||||
div(class = "input-daterange input-group input-group-sm",
|
||||
tags$input(
|
||||
class = "input-sm form-control",
|
||||
class = "form-control",
|
||||
type = "text",
|
||||
`data-date-language` = language,
|
||||
`data-date-weekstart` = weekstart,
|
||||
`data-date-week-start` = weekstart,
|
||||
`data-date-format` = format,
|
||||
`data-date-start-view` = startview,
|
||||
`data-min-date` = min,
|
||||
`data-max-date` = max,
|
||||
`data-initial-date` = start
|
||||
`data-initial-date` = start,
|
||||
`data-date-autoclose` = if (autoclose) "true" else "false"
|
||||
),
|
||||
# input-group-prepend and input-group-append are for bootstrap 4 forward compat
|
||||
span(class = "input-group-addon input-group-prepend input-group-append",
|
||||
span(class = "input-group-text",
|
||||
separator
|
||||
)
|
||||
),
|
||||
span(class = "input-group-addon", separator),
|
||||
tags$input(
|
||||
class = "input-sm form-control",
|
||||
class = "form-control",
|
||||
type = "text",
|
||||
`data-date-language` = language,
|
||||
`data-date-weekstart` = weekstart,
|
||||
`data-date-week-start` = weekstart,
|
||||
`data-date-format` = format,
|
||||
`data-date-start-view` = startview,
|
||||
`data-min-date` = min,
|
||||
`data-max-date` = max,
|
||||
`data-initial-date` = end
|
||||
`data-initial-date` = end,
|
||||
`data-date-autoclose` = if (autoclose) "true" else "false"
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
124
R/input-file.R
124
R/input-file.R
@@ -3,48 +3,124 @@
|
||||
#' Create a file upload control that can be used to upload one or more files.
|
||||
#'
|
||||
#' Whenever a file upload completes, the corresponding input variable is set
|
||||
#' to a dataframe. This dataframe contains one row for each selected file, and
|
||||
#' the following columns:
|
||||
#' \describe{
|
||||
#' \item{\code{name}}{The filename provided by the web browser. This is
|
||||
#' \strong{not} the path to read to get at the actual data that was uploaded
|
||||
#' (see
|
||||
#' \code{datapath} column).}
|
||||
#' \item{\code{size}}{The size of the uploaded data, in
|
||||
#' bytes.}
|
||||
#' \item{\code{type}}{The MIME type reported by the browser (for example,
|
||||
#' \code{text/plain}), or empty string if the browser didn't know.}
|
||||
#' \item{\code{datapath}}{The path to a temp file that contains the data that was
|
||||
#' uploaded. This file may be deleted if the user performs another upload
|
||||
#' operation.}
|
||||
#' }
|
||||
#' to a dataframe. See the `Server value` section.
|
||||
#'
|
||||
#' @family input elements
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param multiple Whether the user should be allowed to select and upload
|
||||
#' multiple files at once. \bold{Does not work on older browsers, including
|
||||
#' Internet Explorer 9 and earlier.}
|
||||
#' @param accept A character vector of MIME types; gives the browser a hint of
|
||||
#' what kind of files the server is expecting.
|
||||
#' multiple files at once. **Does not work on older browsers, including
|
||||
#' Internet Explorer 9 and earlier.**
|
||||
#' @param accept A character vector of "unique file type specifiers" which
|
||||
#' gives the browser a hint as to the type of file the server expects.
|
||||
#' Many browsers use this prevent the user from selecting an invalid file.
|
||||
#'
|
||||
#' A unique file type specifier can be:
|
||||
#' * A case insensitive extension like `.csv` or `.rds`.
|
||||
#' * A valid MIME type, like `text/plain` or `application/pdf`
|
||||
#' * One of `audio/*`, `video/*`, or `image/*` meaning any audio, video,
|
||||
#' or image type, respectively.
|
||||
#' @param buttonLabel The label used on the button. Can be text or an HTML tag
|
||||
#' object.
|
||||
#' @param placeholder The text to show before a file has been uploaded.
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' fileInput("file1", "Choose CSV File", accept = ".csv"),
|
||||
#' checkboxInput("header", "Header", TRUE)
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' tableOutput("contents")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' output$contents <- renderTable({
|
||||
#' file <- input$file1
|
||||
#' ext <- tools::file_ext(file$datapath)
|
||||
#'
|
||||
#' req(file)
|
||||
#' validate(need(ext == "csv", "Please upload a csv file"))
|
||||
#'
|
||||
#' read.csv(file$datapath, header = input$header)
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A `data.frame` that contains one row for each selected file, and following columns:
|
||||
#' \describe{
|
||||
#' \item{`name`}{The filename provided by the web browser. This is
|
||||
#' **not** the path to read to get at the actual data that was uploaded
|
||||
#' (see
|
||||
#' `datapath` column).}
|
||||
#' \item{`size`}{The size of the uploaded data, in
|
||||
#' bytes.}
|
||||
#' \item{`type`}{The MIME type reported by the browser (for example,
|
||||
#' `text/plain`), or empty string if the browser didn't know.}
|
||||
#' \item{`datapath`}{The path to a temp file that contains the data that was
|
||||
#' uploaded. This file may be deleted if the user performs another upload
|
||||
#' operation.}
|
||||
#' }
|
||||
#'
|
||||
#' @export
|
||||
fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
|
||||
width = NULL) {
|
||||
width = NULL, buttonLabel = "Browse...", placeholder = "No file selected") {
|
||||
|
||||
restoredValue <- restoreInput(id = inputId, default = NULL)
|
||||
|
||||
# Catch potential edge case - ensure that it's either NULL or a data frame.
|
||||
if (!is.null(restoredValue) && !is.data.frame(restoredValue)) {
|
||||
warning("Restored value for ", inputId, " has incorrect format.")
|
||||
restoredValue <- NULL
|
||||
}
|
||||
|
||||
if (!is.null(restoredValue)) {
|
||||
restoredValue <- toJSON(restoredValue, strict_atomic = FALSE)
|
||||
}
|
||||
|
||||
inputTag <- tags$input(
|
||||
id = inputId,
|
||||
name = inputId,
|
||||
type = "file",
|
||||
style = "display: none;",
|
||||
`data-restore` = restoredValue
|
||||
)
|
||||
|
||||
inputTag <- tags$input(id = inputId, name = inputId, type = "file")
|
||||
if (multiple)
|
||||
inputTag$attribs$multiple <- "multiple"
|
||||
if (length(accept) > 0)
|
||||
inputTag$attribs$accept <- paste(accept, collapse=',')
|
||||
|
||||
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
label %AND% tags$label(label),
|
||||
inputTag,
|
||||
shinyInputLabel(inputId, label),
|
||||
|
||||
div(class = "input-group",
|
||||
# input-group-prepend is for bootstrap 4 compat
|
||||
tags$label(class = "input-group-btn input-group-prepend",
|
||||
span(class = "btn btn-default btn-file",
|
||||
buttonLabel,
|
||||
inputTag
|
||||
)
|
||||
),
|
||||
tags$input(type = "text", class = "form-control",
|
||||
placeholder = placeholder, readonly = "readonly"
|
||||
)
|
||||
),
|
||||
|
||||
tags$div(
|
||||
id=paste(inputId, "_progress", sep=""),
|
||||
class="progress progress-striped active shiny-file-input-progress",
|
||||
class="progress active shiny-file-input-progress",
|
||||
tags$div(class="progress-bar")
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
#' Create a numeric input control
|
||||
#'
|
||||
#' Create an input control for entry of numeric values
|
||||
@@ -10,15 +9,31 @@
|
||||
#' @return A numeric input control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso \code{\link{updateNumericInput}}
|
||||
#' @seealso [updateNumericInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' numericInput("obs", "Observations:", 10,
|
||||
#' min = 1, max = 100)
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' numericInput("obs", "Observations:", 10, min = 1, max = 100),
|
||||
#' verbatimTextOutput("value")
|
||||
#' )
|
||||
#' server <- function(input, output) {
|
||||
#' output$value <- renderText({ input$obs })
|
||||
#' }
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A numeric vector of length 1.
|
||||
#'
|
||||
#' @export
|
||||
numericInput <- function(inputId, label, value, min = NA, max = NA, step = NA,
|
||||
width = NULL) {
|
||||
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
# build input tag
|
||||
inputTag <- tags$input(id = inputId, type = "number", class="form-control",
|
||||
value = formatNoSci(value))
|
||||
@@ -31,7 +46,7 @@ numericInput <- function(inputId, label, value, min = NA, max = NA, step = NA,
|
||||
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
label %AND% tags$label(label, `for` = inputId),
|
||||
shinyInputLabel(inputId, label),
|
||||
inputTag
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,15 +6,36 @@
|
||||
#' @return A text input control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso \code{\link{updateTextInput}}
|
||||
#' @seealso [updateTextInput()]
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A character string of the password input. The default value is `""`
|
||||
#' unless `value` is provided.
|
||||
#'
|
||||
#' @examples
|
||||
#' passwordInput("password", "Password:")
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' passwordInput("password", "Password:"),
|
||||
#' actionButton("go", "Go"),
|
||||
#' verbatimTextOutput("value")
|
||||
#' )
|
||||
#' server <- function(input, output) {
|
||||
#' output$value <- renderText({
|
||||
#' req(input$go)
|
||||
#' isolate(input$password)
|
||||
#' })
|
||||
#' }
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
passwordInput <- function(inputId, label, value = "", width = NULL) {
|
||||
passwordInput <- function(inputId, label, value = "", width = NULL,
|
||||
placeholder = NULL) {
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
label %AND% tags$label(label, `for` = inputId),
|
||||
tags$input(id = inputId, type="password", class="form-control", value=value)
|
||||
shinyInputLabel(inputId, label),
|
||||
tags$input(id = inputId, type="password", class="form-control", value=value,
|
||||
placeholder = placeholder)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,52 +3,110 @@
|
||||
#' Create a set of radio buttons used to select an item from a list.
|
||||
#'
|
||||
#' If you need to represent a "None selected" state, it's possible to default
|
||||
#' the radio buttons to have no options selected by using
|
||||
#' \code{selected = character(0)}. However, this is not recommended, as it gives
|
||||
#' the user no way to return to that state once they've made a selection.
|
||||
#' Instead, consider having the first of your choices be \code{c("None selected"
|
||||
#' = "")}.
|
||||
#' the radio buttons to have no options selected by using `selected =
|
||||
#' character(0)`. However, this is not recommended, as it gives the user no way
|
||||
#' to return to that state once they've made a selection. Instead, consider
|
||||
#' having the first of your choices be `c("None selected" = "")`.
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param choices List of values to select from (if elements of the list are
|
||||
#' named then that name rather than the value is displayed to the user)
|
||||
#' @param selected The initially selected value (if not specified then
|
||||
#' defaults to the first value)
|
||||
#' @param inline If \code{TRUE}, render the choices inline (i.e. horizontally)
|
||||
#' named then that name rather than the value is displayed to the user). If
|
||||
#' this argument is provided, then `choiceNames` and `choiceValues`
|
||||
#' must not be provided, and vice-versa. The values should be strings; other
|
||||
#' types (such as logicals and numbers) will be coerced to strings.
|
||||
#' @param selected The initially selected value (if not specified then defaults
|
||||
#' to the first value)
|
||||
#' @param inline If `TRUE`, render the choices inline (i.e. horizontally)
|
||||
#' @return A set of radio buttons that can be added to a UI definition.
|
||||
#' @param choiceNames,choiceValues List of names and values, respectively, that
|
||||
#' are displayed to the user in the app and correspond to the each choice (for
|
||||
#' this reason, `choiceNames` and `choiceValues` must have the same
|
||||
#' length). If either of these arguments is provided, then the other
|
||||
#' *must* be provided and `choices` *must not* be provided. The
|
||||
#' advantage of using both of these over a named list for `choices` is
|
||||
#' that `choiceNames` allows any type of UI object to be passed through
|
||||
#' (tag objects, icons, HTML code, ...), instead of just simple text. See
|
||||
#' Examples.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso \code{\link{updateRadioButtons}}
|
||||
#' @seealso [updateRadioButtons()]
|
||||
#'
|
||||
#' @examples
|
||||
#' radioButtons("dist", "Distribution type:",
|
||||
#' c("Normal" = "norm",
|
||||
#' "Uniform" = "unif",
|
||||
#' "Log-normal" = "lnorm",
|
||||
#' "Exponential" = "exp"))
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' radioButtons("dist", "Distribution type:",
|
||||
#' c("Normal" = "norm",
|
||||
#' "Uniform" = "unif",
|
||||
#' "Log-normal" = "lnorm",
|
||||
#' "Exponential" = "exp")),
|
||||
#' plotOutput("distPlot")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' output$distPlot <- renderPlot({
|
||||
#' dist <- switch(input$dist,
|
||||
#' norm = rnorm,
|
||||
#' unif = runif,
|
||||
#' lnorm = rlnorm,
|
||||
#' exp = rexp,
|
||||
#' rnorm)
|
||||
#'
|
||||
#' hist(dist(500))
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' radioButtons("rb", "Choose one:",
|
||||
#' choiceNames = list(
|
||||
#' icon("calendar"),
|
||||
#' HTML("<p style='color:red;'>Red Text</p>"),
|
||||
#' "Normal text"
|
||||
#' ),
|
||||
#' choiceValues = list(
|
||||
#' "icon", "html", "text"
|
||||
#' )),
|
||||
#' textOutput("txt")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' output$txt <- renderText({
|
||||
#' paste("You chose", input$rb)
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A character string containing the value of the selected button.
|
||||
#'
|
||||
#' @export
|
||||
radioButtons <- function(inputId, label, choices, selected = NULL,
|
||||
inline = FALSE, width = NULL) {
|
||||
radioButtons <- function(inputId, label, choices = NULL, selected = NULL,
|
||||
inline = FALSE, width = NULL, choiceNames = NULL, choiceValues = NULL) {
|
||||
|
||||
# resolve names
|
||||
choices <- choicesWithNames(choices)
|
||||
args <- normalizeChoicesArgs(choices, choiceNames, choiceValues)
|
||||
|
||||
selected <- restoreInput(id = inputId, default = selected)
|
||||
|
||||
# default value if it's not specified
|
||||
selected <- if (is.null(selected)) choices[[1]] else {
|
||||
validateSelected(selected, choices, inputId)
|
||||
}
|
||||
selected <- if (is.null(selected)) args$choiceValues[[1]] else as.character(selected)
|
||||
|
||||
if (length(selected) > 1) stop("The 'selected' argument must be of length 1")
|
||||
|
||||
options <- generateOptions(inputId, choices, selected, inline, type = 'radio')
|
||||
options <- generateOptions(inputId, selected, inline,
|
||||
'radio', args$choiceNames, args$choiceValues)
|
||||
|
||||
divClass <- "form-group shiny-input-radiogroup shiny-input-container"
|
||||
if (inline)
|
||||
divClass <- paste(divClass, "shiny-input-container-inline")
|
||||
if (inline) divClass <- paste(divClass, "shiny-input-container-inline")
|
||||
|
||||
tags$div(id = inputId,
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
class = divClass,
|
||||
controlLabel(inputId, label),
|
||||
shinyInputLabel(inputId, label),
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
245
R/input-select.R
245
R/input-select.R
@@ -3,49 +3,93 @@
|
||||
#' Create a select list that can be used to choose a single or multiple items
|
||||
#' from a list of values.
|
||||
#'
|
||||
#' By default, \code{selectInput()} and \code{selectizeInput()} use the
|
||||
#' JavaScript library \pkg{selectize.js}
|
||||
#' (\url{https://github.com/brianreavis/selectize.js}) to instead of the basic
|
||||
#' select input element. To use the standard HTML select input element, use
|
||||
#' \code{selectInput()} with \code{selectize=FALSE}.
|
||||
#' By default, `selectInput()` and `selectizeInput()` use the JavaScript library
|
||||
#' \pkg{selectize.js} (<https://github.com/selectize/selectize.js>) instead of
|
||||
#' the basic select input element. To use the standard HTML select input
|
||||
#' element, use `selectInput()` with `selectize=FALSE`.
|
||||
#'
|
||||
#' In selectize mode, if the first element in \code{choices} has a value of
|
||||
#' \code{""}, its name will be treated as a placeholder prompt. For example:
|
||||
#' \code{selectInput("letter", "Letter", c("Choose one" = "", LETTERS))}
|
||||
#' In selectize mode, if the first element in `choices` has a value of `""`, its
|
||||
#' name will be treated as a placeholder prompt. For example:
|
||||
#' `selectInput("letter", "Letter", c("Choose one" = "", LETTERS))`
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param choices List of values to select from. If elements of the list are
|
||||
#' named then that name rather than the value is displayed to the user.
|
||||
#' @param selected The initially selected value (or multiple values if
|
||||
#' \code{multiple = TRUE}). If not specified then defaults to the first value
|
||||
#' for single-select lists and no values for multiple select lists.
|
||||
#' named, then that name --- rather than the value --- is displayed to the
|
||||
#' user. It's also possible to group related inputs by providing a named list
|
||||
#' whose elements are (either named or unnamed) lists, vectors, or factors. In
|
||||
#' this case, the outermost names will be used as the group labels (leveraging
|
||||
#' the `<optgroup>` HTML tag) for the elements in the respective sublist. See
|
||||
#' the example section for a small demo of this feature.
|
||||
#' @param selected The initially selected value (or multiple values if `multiple
|
||||
#' = TRUE`). If not specified then defaults to the first value for
|
||||
#' single-select lists and no values for multiple select lists.
|
||||
#' @param multiple Is selection of multiple items allowed?
|
||||
#' @param selectize Whether to use \pkg{selectize.js} or not.
|
||||
#' @param size Number of items to show in the selection box; a larger number
|
||||
#' will result in a taller box. Not compatible with \code{selectize=TRUE}.
|
||||
#' Normally, when \code{multiple=FALSE}, a select input will be a drop-down
|
||||
#' list, but when \code{size} is set, it will be a box instead.
|
||||
#' will result in a taller box. Not compatible with `selectize=TRUE`.
|
||||
#' Normally, when `multiple=FALSE`, a select input will be a drop-down list,
|
||||
#' but when `size` is set, it will be a box instead.
|
||||
#' @return A select list control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso \code{\link{updateSelectInput}}
|
||||
#' @seealso [updateSelectInput()] [varSelectInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' selectInput("variable", "Variable:",
|
||||
#' c("Cylinders" = "cyl",
|
||||
#' "Transmission" = "am",
|
||||
#' "Gears" = "gear"))
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # basic example
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' selectInput("variable", "Variable:",
|
||||
#' c("Cylinders" = "cyl",
|
||||
#' "Transmission" = "am",
|
||||
#' "Gears" = "gear")),
|
||||
#' tableOutput("data")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$data <- renderTable({
|
||||
#' mtcars[, c("mpg", input$variable), drop = FALSE]
|
||||
#' }, rownames = TRUE)
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' # demoing group support in the `choices` arg
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' selectInput("state", "Choose a state:",
|
||||
#' list(`East Coast` = list("NY", "NJ", "CT"),
|
||||
#' `West Coast` = list("WA", "OR", "CA"),
|
||||
#' `Midwest` = list("MN", "WI", "IA"))
|
||||
#' ),
|
||||
#' textOutput("result")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$result <- renderText({
|
||||
#' paste("You chose", input$state)
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value: A vector of character strings, usually of length
|
||||
#' 1, with the value of the selected items. When `multiple=TRUE` and
|
||||
#' nothing is selected, this value will be `NULL`.
|
||||
#'
|
||||
#' @export
|
||||
selectInput <- function(inputId, label, choices, selected = NULL,
|
||||
multiple = FALSE, selectize = TRUE, width = NULL,
|
||||
size = NULL) {
|
||||
multiple = FALSE, selectize = TRUE, width = NULL,
|
||||
size = NULL) {
|
||||
|
||||
selected <- restoreInput(id = inputId, default = selected)
|
||||
|
||||
# resolve names
|
||||
choices <- choicesWithNames(choices)
|
||||
|
||||
# default value if it's not specified
|
||||
if (is.null(selected)) {
|
||||
if (!multiple) selected <- firstChoice(choices)
|
||||
} else selected <- validateSelected(selected, choices, inputId)
|
||||
} else selected <- as.character(selected)
|
||||
|
||||
if (!is.null(size) && selectize) {
|
||||
stop("'size' argument is incompatible with 'selectize=TRUE'.")
|
||||
@@ -65,7 +109,7 @@ selectInput <- function(inputId, label, choices, selected = NULL,
|
||||
res <- div(
|
||||
class = "form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
controlLabel(inputId, label),
|
||||
shinyInputLabel(inputId, label),
|
||||
div(selectTag)
|
||||
)
|
||||
|
||||
@@ -113,21 +157,21 @@ needOptgroup <- function(choices) {
|
||||
}
|
||||
|
||||
#' @rdname selectInput
|
||||
#' @param ... Arguments passed to \code{selectInput()}.
|
||||
#' @param ... Arguments passed to `selectInput()`.
|
||||
#' @param options A list of options. See the documentation of \pkg{selectize.js}
|
||||
#' for possible options (character option values inside \code{\link{I}()} will
|
||||
#' be treated as literal JavaScript code; see \code{\link{renderDataTable}()}
|
||||
#' for possible options (character option values inside [base::I()] will
|
||||
#' be treated as literal JavaScript code; see [renderDataTable()]
|
||||
#' for details).
|
||||
#' @param width The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
|
||||
#' see \code{\link{validateCssUnit}}.
|
||||
#' @note The selectize input created from \code{selectizeInput()} allows
|
||||
#' @param width The width of the input, e.g. `'400px'`, or `'100%'`;
|
||||
#' see [validateCssUnit()].
|
||||
#' @note The selectize input created from `selectizeInput()` allows
|
||||
#' deletion of the selected option even in a single select input, which will
|
||||
#' return an empty string as its value. This is the default behavior of
|
||||
#' \pkg{selectize.js}. However, the selectize input created from
|
||||
#' \code{selectInput(..., selectize = TRUE)} will ignore the empty string
|
||||
#' `selectInput(..., selectize = TRUE)` will ignore the empty string
|
||||
#' value when it is a single choice input and the empty string is not in the
|
||||
#' \code{choices} argument. This is to keep compatibility with
|
||||
#' \code{selectInput(..., selectize = FALSE)}.
|
||||
#' `choices` argument. This is to keep compatibility with
|
||||
#' `selectInput(..., selectize = FALSE)`.
|
||||
#' @export
|
||||
selectizeInput <- function(inputId, ..., options = NULL, width = NULL) {
|
||||
selectizeIt(
|
||||
@@ -154,7 +198,7 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
|
||||
|
||||
if ('drag_drop' %in% options$plugins) {
|
||||
selectizeDep <- list(selectizeDep, htmlDependency(
|
||||
'jqueryui', '1.11.4', c(href = 'shared/jqueryui'),
|
||||
'jqueryui', '1.12.1', c(href = 'shared/jqueryui'),
|
||||
script = 'jquery-ui.min.js'
|
||||
))
|
||||
}
|
||||
@@ -172,3 +216,136 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
|
||||
|
||||
attachDependencies(select, selectizeDep)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#' Select variables from a data frame
|
||||
#'
|
||||
#' Create a select list that can be used to choose a single or multiple items
|
||||
#' from the column names of a data frame.
|
||||
#'
|
||||
#' By default, `varSelectInput()` and `selectizeInput()` use the
|
||||
#' JavaScript library \pkg{selectize.js}
|
||||
#' (<https://github.com/selectize/selectize.js>) to instead of the basic
|
||||
#' select input element. To use the standard HTML select input element, use
|
||||
#' `selectInput()` with `selectize=FALSE`.
|
||||
#'
|
||||
#' @inheritParams selectInput
|
||||
#' @param data A data frame. Used to retrieve the column names as choices for a [selectInput()]
|
||||
#' @return A variable select list control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [updateSelectInput()]
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' The resulting server `input` value will be returned as:
|
||||
#'
|
||||
#' * A symbol if `multiple = FALSE`. The `input` value should be
|
||||
#' used with rlang's [rlang::!!()]. For example,
|
||||
#' `ggplot2::aes(!!input$variable)`.
|
||||
#' * A list of symbols if `multiple = TRUE`. The `input` value
|
||||
#' should be used with rlang's [rlang::!!!()] to expand
|
||||
#' the symbol list as individual arguments. For example,
|
||||
#' `dplyr::select(mtcars, !!!input$variabls)` which is
|
||||
#' equivalent to `dplyr::select(mtcars, !!input$variabls[[1]], !!input$variabls[[2]], ..., !!input$variabls[[length(input$variabls)]])`.
|
||||
#'
|
||||
#' @examples
|
||||
#'
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' library(ggplot2)
|
||||
#'
|
||||
#' # single selection
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' varSelectInput("variable", "Variable:", mtcars),
|
||||
#' plotOutput("data")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$data <- renderPlot({
|
||||
#' ggplot(mtcars, aes(!!input$variable)) + geom_histogram()
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # multiple selections
|
||||
#' \dontrun{
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' varSelectInput("variables", "Variable:", mtcars, multiple = TRUE),
|
||||
#' tableOutput("data")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$data <- renderTable({
|
||||
#' if (length(input$variables) == 0) return(mtcars)
|
||||
#' mtcars %>% dplyr::select(!!!input$variables)
|
||||
#' }, rownames = TRUE)
|
||||
#' }
|
||||
#' )}
|
||||
#'
|
||||
#' }
|
||||
#' @export
|
||||
varSelectInput <- function(
|
||||
inputId, label, data, selected = NULL,
|
||||
multiple = FALSE, selectize = TRUE, width = NULL,
|
||||
size = NULL
|
||||
) {
|
||||
# no place holders
|
||||
choices <- colnames(data)
|
||||
|
||||
selectInputVal <- selectInput(
|
||||
inputId = inputId,
|
||||
label = label,
|
||||
choices = choices,
|
||||
selected = selected,
|
||||
multiple = multiple,
|
||||
selectize = selectize,
|
||||
width = width,
|
||||
size = size
|
||||
)
|
||||
|
||||
# set the select tag class to be "symbol"
|
||||
selectClass <- selectInputVal$children[[2]]$children[[1]]$attribs$class
|
||||
if (is.null(selectClass)) {
|
||||
newClass <- "symbol"
|
||||
} else {
|
||||
newClass <- paste(selectClass, "symbol", sep = " ")
|
||||
}
|
||||
selectInputVal$children[[2]]$children[[1]]$attribs$class <- newClass
|
||||
|
||||
selectInputVal
|
||||
}
|
||||
|
||||
|
||||
|
||||
#' @rdname varSelectInput
|
||||
#' @param ... Arguments passed to `varSelectInput()`.
|
||||
#' @param options A list of options. See the documentation of \pkg{selectize.js}
|
||||
#' for possible options (character option values inside [base::I()] will
|
||||
#' be treated as literal JavaScript code; see [renderDataTable()]
|
||||
#' for details).
|
||||
#' @param width The width of the input, e.g. `'400px'`, or `'100%'`;
|
||||
#' see [validateCssUnit()].
|
||||
#' @note The variable selectize input created from `varSelectizeInput()` allows
|
||||
#' deletion of the selected option even in a single select input, which will
|
||||
#' return an empty string as its value. This is the default behavior of
|
||||
#' \pkg{selectize.js}. However, the selectize input created from
|
||||
#' `selectInput(..., selectize = TRUE)` will ignore the empty string
|
||||
#' value when it is a single choice input and the empty string is not in the
|
||||
#' `choices` argument. This is to keep compatibility with
|
||||
#' `selectInput(..., selectize = FALSE)`.
|
||||
#' @export
|
||||
varSelectizeInput <- function(inputId, ..., options = NULL, width = NULL) {
|
||||
selectizeIt(
|
||||
inputId,
|
||||
varSelectInput(inputId, ..., selectize = FALSE, width = width),
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
154
R/input-slider.R
154
R/input-slider.R
@@ -8,45 +8,71 @@
|
||||
#' @param value The initial value of the slider. A numeric vector of length one
|
||||
#' will create a regular slider; a numeric vector of length two will create a
|
||||
#' double-ended range slider. A warning will be issued if the value doesn't
|
||||
#' fit between \code{min} and \code{max}.
|
||||
#' fit between `min` and `max`.
|
||||
#' @param step Specifies the interval between each selectable value on the
|
||||
#' slider (if \code{NULL}, a heuristic is used to determine the step size). If
|
||||
#' the values are dates, \code{step} is in days; if the values are times
|
||||
#' (POSIXt), \code{step} is in seconds.
|
||||
#' @param round \code{TRUE} to round all values to the nearest integer;
|
||||
#' \code{FALSE} if no rounding is desired; or an integer to round to that
|
||||
#' slider (if `NULL`, a heuristic is used to determine the step size). If
|
||||
#' the values are dates, `step` is in days; if the values are times
|
||||
#' (POSIXt), `step` is in seconds.
|
||||
#' @param round `TRUE` to round all values to the nearest integer;
|
||||
#' `FALSE` if no rounding is desired; or an integer to round to that
|
||||
#' number of digits (for example, 1 will round to the nearest 10, and -2 will
|
||||
#' round to the nearest .01). Any rounding will be applied after snapping to
|
||||
#' the nearest step.
|
||||
#' @param format Deprecated.
|
||||
#' @param locale Deprecated.
|
||||
#' @param ticks \code{FALSE} to hide tick marks, \code{TRUE} to show them
|
||||
#' @param ticks `FALSE` to hide tick marks, `TRUE` to show them
|
||||
#' according to some simple heuristics.
|
||||
#' @param animate \code{TRUE} to show simple animation controls with default
|
||||
#' settings; \code{FALSE} not to; or a custom settings list, such as those
|
||||
#' created using \code{\link{animationOptions}}.
|
||||
#' @param animate `TRUE` to show simple animation controls with default
|
||||
#' settings; `FALSE` not to; or a custom settings list, such as those
|
||||
#' created using [animationOptions()].
|
||||
#' @param sep Separator between thousands places in numbers.
|
||||
#' @param pre A prefix string to put in front of the value.
|
||||
#' @param post A suffix string to put after the value.
|
||||
#' @param dragRange This option is used only if it is a range slider (with two
|
||||
#' values). If \code{TRUE} (the default), the range can be dragged. In other
|
||||
#' words, the min and max can be dragged together. If \code{FALSE}, the range
|
||||
#' values). If `TRUE` (the default), the range can be dragged. In other
|
||||
#' words, the min and max can be dragged together. If `FALSE`, the range
|
||||
#' cannot be dragged.
|
||||
#' @param timeFormat Only used if the values are Date or POSIXt objects. A time
|
||||
#' format string, to be passed to the Javascript strftime library. See
|
||||
#' \url{https://github.com/samsonjs/strftime} for more details. The allowed
|
||||
#' <https://github.com/samsonjs/strftime> for more details. The allowed
|
||||
#' format specifications are very similar, but not identical, to those for R's
|
||||
#' \code{\link{strftime}} function. For Dates, the default is \code{"\%F"}
|
||||
#' (like \code{"2015-07-01"}), and for POSIXt, the default is \code{"\%F \%T"}
|
||||
#' (like \code{"2015-07-01 15:32:10"}).
|
||||
#' [base::strftime()] function. For Dates, the default is `"%F"`
|
||||
#' (like `"2015-07-01"`), and for POSIXt, the default is `"%F %T"`
|
||||
#' (like `"2015-07-01 15:32:10"`).
|
||||
#' @param timezone Only used if the values are POSIXt objects. A string
|
||||
#' specifying the time zone offset for the displayed times, in the format
|
||||
#' \code{"+HHMM"} or \code{"-HHMM"}. If \code{NULL} (the default), times will
|
||||
#' be displayed in the browser's time zone. The value \code{"+0000"} will
|
||||
#' `"+HHMM"` or `"-HHMM"`. If `NULL` (the default), times will
|
||||
#' be displayed in the browser's time zone. The value `"+0000"` will
|
||||
#' result in UTC time.
|
||||
#' @inheritParams selectizeInput
|
||||
#' @family input elements
|
||||
#' @seealso \code{\link{updateSliderInput}}
|
||||
#' @seealso [updateSliderInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sliderInput("obs", "Number of observations:",
|
||||
#' min = 0, max = 1000, value = 500
|
||||
#' ),
|
||||
#' plotOutput("distPlot")
|
||||
#' )
|
||||
#'
|
||||
#' # Server logic
|
||||
#' server <- function(input, output) {
|
||||
#' output$distPlot <- renderPlot({
|
||||
#' hist(rnorm(input$obs))
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' # Complete app with UI and server components
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A number, or in the case of slider range, a vector of two numbers.
|
||||
#'
|
||||
#' @export
|
||||
sliderInput <- function(inputId, label, min, max, value, step = NULL,
|
||||
@@ -64,38 +90,25 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
|
||||
version = "0.10.2.2")
|
||||
}
|
||||
|
||||
# If step is NULL, use heuristic to set the step size.
|
||||
findStepSize <- function(min, max, step) {
|
||||
if (!is.null(step)) return(step)
|
||||
dataType <- getSliderType(min, max, value)
|
||||
|
||||
range <- max - min
|
||||
# If short range or decimals, use continuous decimal with ~100 points
|
||||
if (range < 2 || hasDecimals(min) || hasDecimals(max)) {
|
||||
step <- pretty(c(min, max), n = 100)
|
||||
step[2] - step[1]
|
||||
} else {
|
||||
1
|
||||
}
|
||||
if (is.null(timeFormat)) {
|
||||
timeFormat <- switch(dataType, date = "%F", datetime = "%F %T", number = NULL)
|
||||
}
|
||||
|
||||
if (inherits(min, "Date")) {
|
||||
if (!inherits(max, "Date") || !inherits(value, "Date"))
|
||||
stop("`min`, `max`, and `value must all be Date or non-Date objects")
|
||||
dataType <- "date"
|
||||
# Restore bookmarked values here, after doing the type checking, because the
|
||||
# restored value will be a character vector instead of Date or POSIXct, and we can do
|
||||
# the conversion to correct type next.
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
if (is.null(timeFormat))
|
||||
timeFormat <- "%F"
|
||||
|
||||
} else if (inherits(min, "POSIXt")) {
|
||||
if (!inherits(max, "POSIXt") || !inherits(value, "POSIXt"))
|
||||
stop("`min`, `max`, and `value must all be POSIXt or non-POSIXt objects")
|
||||
dataType <- "datetime"
|
||||
|
||||
if (is.null(timeFormat))
|
||||
timeFormat <- "%F %T"
|
||||
|
||||
} else {
|
||||
dataType <- "number"
|
||||
if (is.character(value)) {
|
||||
# If we got here, the value was restored from a URL-encoded bookmark.
|
||||
if (dataType == "date") {
|
||||
value <- as.Date(value, format = "%Y-%m-%d")
|
||||
} else if (dataType == "datetime") {
|
||||
# Date-times will have a format like "2018-02-28T03:46:26Z"
|
||||
value <- as.POSIXct(value, format = "%Y-%m-%dT%H:%M:%SZ", tz = "UTC")
|
||||
}
|
||||
}
|
||||
|
||||
step <- findStepSize(min, max, step)
|
||||
@@ -141,11 +154,13 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
|
||||
`data-grid-num` = n_ticks,
|
||||
`data-grid-snap` = FALSE,
|
||||
`data-prettify-separator` = sep,
|
||||
`data-prettify-enabled` = (sep != ""),
|
||||
`data-prefix` = pre,
|
||||
`data-postfix` = post,
|
||||
`data-keyboard` = TRUE,
|
||||
`data-keyboard-step` = step / (max - min) * 100,
|
||||
`data-drag-interval` = dragRange,
|
||||
# This value is only relevant for range sliders; for non-range sliders it
|
||||
# causes problems since ion.RangeSlider 2.1.2 (issue #1605).
|
||||
`data-drag-interval` = if (length(value) > 1) dragRange,
|
||||
# The following are ignored by the ion.rangeSlider, but are used by Shiny.
|
||||
`data-data-type` = dataType,
|
||||
`data-time-format` = timeFormat,
|
||||
@@ -161,7 +176,7 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
|
||||
|
||||
sliderTag <- div(class = "form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
if (!is.null(label)) controlLabel(inputId, label),
|
||||
shinyInputLabel(inputId, label),
|
||||
do.call(tags$input, sliderProps)
|
||||
)
|
||||
|
||||
@@ -191,7 +206,7 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
|
||||
}
|
||||
|
||||
dep <- list(
|
||||
htmlDependency("ionrangeslider", "2.0.12", c(href="shared/ionrangeslider"),
|
||||
htmlDependency("ionrangeslider", "2.1.6", c(href="shared/ionrangeslider"),
|
||||
script = "js/ion.rangeSlider.min.js",
|
||||
# ion.rangeSlider also needs normalize.css, which is already included in
|
||||
# Bootstrap.
|
||||
@@ -211,17 +226,44 @@ hasDecimals <- function(value) {
|
||||
return (!identical(value, truncatedValue))
|
||||
}
|
||||
|
||||
|
||||
# If step is NULL, use heuristic to set the step size.
|
||||
findStepSize <- function(min, max, step) {
|
||||
if (!is.null(step)) return(step)
|
||||
|
||||
range <- max - min
|
||||
# If short range or decimals, use continuous decimal with ~100 points
|
||||
if (range < 2 || hasDecimals(min) || hasDecimals(max)) {
|
||||
# Workaround for rounding errors (#1006): the intervals between the items
|
||||
# returned by pretty() can have rounding errors. To avoid this, we'll use
|
||||
# pretty() to find the min, max, and number of steps, and then use those
|
||||
# values to calculate the step size.
|
||||
pretty_steps <- pretty(c(min, max), n = 100)
|
||||
n_steps <- length(pretty_steps) - 1
|
||||
|
||||
# Fix for #2061: Windows has low-significance digits (like 17 digits out)
|
||||
# even at the boundaries of pretty()'s output. Use signif(digits = 10),
|
||||
# which should be way way less significant than any data we'd want to keep.
|
||||
# It might make sense to use signif(steps[2] - steps[1], 10) instead, but
|
||||
# for now trying to make the minimal change.
|
||||
signif(digits = 10, (max(pretty_steps) - min(pretty_steps)) / n_steps)
|
||||
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#' @rdname sliderInput
|
||||
#'
|
||||
#' @param interval The interval, in milliseconds, between each animation step.
|
||||
#' @param loop \code{TRUE} to automatically restart the animation when it
|
||||
#' @param loop `TRUE` to automatically restart the animation when it
|
||||
#' reaches the end.
|
||||
#' @param playButton Specifies the appearance of the play button. Valid values
|
||||
#' are a one-element character vector (for a simple text label), an HTML tag
|
||||
#' or list of tags (using \code{\link{tag}} and friends), or raw HTML (using
|
||||
#' \code{\link{HTML}}).
|
||||
#' @param pauseButton Similar to \code{playButton}, but for the pause button.
|
||||
#'
|
||||
#' or list of tags (using [tag()] and friends), or raw HTML (using
|
||||
#' [HTML()]).
|
||||
#' @param pauseButton Similar to `playButton`, but for the pause button.
|
||||
#' @export
|
||||
animationOptions <- function(interval=1000,
|
||||
loop=FALSE,
|
||||
|
||||
@@ -1,20 +1,57 @@
|
||||
#' Create a submit button
|
||||
#'
|
||||
#' Create a submit button for an input form. Forms that include a submit
|
||||
#' Create a submit button for an app. Apps that include a submit
|
||||
#' button do not automatically update their outputs when inputs change,
|
||||
#' rather they wait until the user explicitly clicks the submit button.
|
||||
#' The use of `submitButton` is generally discouraged in favor of
|
||||
#' the more versatile [actionButton()] (see details below).
|
||||
#'
|
||||
#' Submit buttons are unusual Shiny inputs, and we recommend using
|
||||
#' [actionButton()] instead of `submitButton` when you
|
||||
#' want to delay a reaction.
|
||||
#' See [this
|
||||
#' article](http://shiny.rstudio.com/articles/action-buttons.html) for more information (including a demo of how to "translate"
|
||||
#' code using a `submitButton` to code using an `actionButton`).
|
||||
#'
|
||||
#' In essence, the presence of a submit button stops all inputs from
|
||||
#' sending their values automatically to the server. This means, for
|
||||
#' instance, that if there are *two* submit buttons in the same app,
|
||||
#' clicking either one will cause all inputs in the app to send their
|
||||
#' values to the server. This is probably not what you'd want, which is
|
||||
#' why submit button are unwieldy for all but the simplest apps. There
|
||||
#' are other problems with submit buttons: for example, dynamically
|
||||
#' created submit buttons (for example, with [renderUI()]
|
||||
#' or [insertUI()]) will not work.
|
||||
#'
|
||||
#' @param text Button caption
|
||||
#' @param icon Optional \code{\link{icon}} to appear on the button
|
||||
#' @param width The width of the button, e.g. \code{'400px'}, or \code{'100\%'};
|
||||
#' see \code{\link{validateCssUnit}}.
|
||||
#' @param icon Optional [icon()] to appear on the button
|
||||
#' @param width The width of the button, e.g. `'400px'`, or `'100%'`;
|
||||
#' see [validateCssUnit()].
|
||||
#' @return A submit button that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#'
|
||||
#' @examples
|
||||
#' submitButton("Update View")
|
||||
#' submitButton("Update View", icon("refresh"))
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' shinyApp(
|
||||
#' ui = basicPage(
|
||||
#' numericInput("num", label = "Make changes", value = 1),
|
||||
#' submitButton("Update View", icon("refresh")),
|
||||
#' helpText("When you click the button above, you should see",
|
||||
#' "the output below update to reflect the value you",
|
||||
#' "entered at the top:"),
|
||||
#' verbatimTextOutput("value")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#'
|
||||
#' # submit buttons do not have a value of their own,
|
||||
#' # they control when the app accesses values of other widgets.
|
||||
#' # input$num is the value of the number widget.
|
||||
#' output$value <- renderPrint({ input$num })
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#' @export
|
||||
submitButton <- function(text = "Apply Changes", icon = NULL, width = NULL) {
|
||||
div(
|
||||
|
||||
@@ -2,28 +2,46 @@
|
||||
#'
|
||||
#' Create an input control for entry of unstructured text values
|
||||
#'
|
||||
#' @param inputId The \code{input} slot that will be used to access the value.
|
||||
#' @param label Display label for the control, or \code{NULL} for no label.
|
||||
#' @param inputId The `input` slot that will be used to access the value.
|
||||
#' @param label Display label for the control, or `NULL` for no label.
|
||||
#' @param value Initial value.
|
||||
#' @param width The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
|
||||
#' see \code{\link{validateCssUnit}}.
|
||||
#' @param width The width of the input, e.g. `'400px'`, or `'100%'`;
|
||||
#' see [validateCssUnit()].
|
||||
#' @param placeholder A character string giving the user a hint as to what can
|
||||
#' be entered into the control. Internet Explorer 8 and 9 do not support this
|
||||
#' option.
|
||||
#' @return A text input control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso \code{\link{updateTextInput}}
|
||||
#' @seealso [updateTextInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' textInput("caption", "Caption:", "Data Summary")
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' textInput("caption", "Caption", "Data Summary"),
|
||||
#' verbatimTextOutput("value")
|
||||
#' )
|
||||
#' server <- function(input, output) {
|
||||
#' output$value <- renderText({ input$caption })
|
||||
#' }
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A character string of the text input. The default value is `""`
|
||||
#' unless `value` is provided.
|
||||
#'
|
||||
#' @export
|
||||
textInput <- function(inputId, label, value = "", width = NULL,
|
||||
placeholder = NULL) {
|
||||
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
label %AND% tags$label(label, `for` = inputId),
|
||||
shinyInputLabel(inputId, label),
|
||||
tags$input(id = inputId, type="text", class="form-control", value=value,
|
||||
placeholder = placeholder)
|
||||
)
|
||||
|
||||
75
R/input-textarea.R
Normal file
75
R/input-textarea.R
Normal file
@@ -0,0 +1,75 @@
|
||||
#' Create a textarea input control
|
||||
#'
|
||||
#' Create a textarea input control for entry of unstructured text values.
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param height The height of the input, e.g. `'400px'`, or `'100%'`; see
|
||||
#' [validateCssUnit()].
|
||||
#' @param cols Value of the visible character columns of the input, e.g. `80`.
|
||||
#' This argument will only take effect if there is not a CSS `width` rule
|
||||
#' defined for this element; such a rule could come from the `width` argument
|
||||
#' of this function or from a containing page layout such as
|
||||
#' [fluidPage()].
|
||||
#' @param rows The value of the visible character rows of the input, e.g. `6`.
|
||||
#' If the `height` argument is specified, `height` will take precedence in the
|
||||
#' browser's rendering.
|
||||
#' @param resize Which directions the textarea box can be resized. Can be one of
|
||||
#' `"both"`, `"none"`, `"vertical"`, and `"horizontal"`. The default, `NULL`,
|
||||
#' will use the client browser's default setting for resizing textareas.
|
||||
#' @return A textarea input control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [updateTextAreaInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' textAreaInput("caption", "Caption", "Data Summary", width = "1000px"),
|
||||
#' verbatimTextOutput("value")
|
||||
#' )
|
||||
#' server <- function(input, output) {
|
||||
#' output$value <- renderText({ input$caption })
|
||||
#' }
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A character string of the text input. The default value is `""`
|
||||
#' unless `value` is provided.
|
||||
#'
|
||||
#' @export
|
||||
textAreaInput <- function(inputId, label, value = "", width = NULL, height = NULL,
|
||||
cols = NULL, rows = NULL, placeholder = NULL, resize = NULL) {
|
||||
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
if (!is.null(resize)) {
|
||||
resize <- match.arg(resize, c("both", "none", "vertical", "horizontal"))
|
||||
}
|
||||
|
||||
style <- paste(
|
||||
if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
if (!is.null(height)) paste0("height: ", validateCssUnit(height), ";"),
|
||||
if (!is.null(resize)) paste0("resize: ", resize, ";")
|
||||
)
|
||||
|
||||
# Workaround for tag attribute=character(0) bug:
|
||||
# https://github.com/rstudio/htmltools/issues/65
|
||||
if (length(style) == 0) style <- NULL
|
||||
|
||||
div(class = "form-group shiny-input-container",
|
||||
shinyInputLabel(inputId, label),
|
||||
tags$textarea(
|
||||
id = inputId,
|
||||
class = "form-control",
|
||||
placeholder = placeholder,
|
||||
style = style,
|
||||
rows = rows,
|
||||
cols = cols,
|
||||
value
|
||||
)
|
||||
)
|
||||
}
|
||||
219
R/input-utils.R
219
R/input-utils.R
@@ -1,43 +1,68 @@
|
||||
controlLabel <- function(controlName, label) {
|
||||
label %AND% tags$label(class = "control-label", `for` = controlName, label)
|
||||
shinyInputLabel <- function(inputId, label = NULL) {
|
||||
tags$label(
|
||||
label,
|
||||
class = "control-label",
|
||||
class = if (is.null(label)) "shiny-label-null",
|
||||
`for` = inputId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# Before shiny 0.9, `selected` refers to names/labels of `choices`; now it
|
||||
# refers to values. Below is a function for backward compatibility.
|
||||
validateSelected <- function(selected, choices, inputId) {
|
||||
# drop names, otherwise toJSON() keeps them too
|
||||
selected <- unname(selected)
|
||||
# if you are using optgroups, you're using shiny > 0.10.0, and you should
|
||||
# already know that `selected` must be a value instead of a label
|
||||
if (needOptgroup(choices)) return(selected)
|
||||
|
||||
if (is.list(choices)) choices <- unlist(choices)
|
||||
|
||||
nms <- names(choices)
|
||||
# labels and values are identical, no need to validate
|
||||
if (identical(nms, unname(choices))) return(selected)
|
||||
# when selected labels instead of values
|
||||
i <- (selected %in% nms) & !(selected %in% choices)
|
||||
if (any(i)) {
|
||||
warnFun <- if (all(i)) {
|
||||
# replace names with values
|
||||
selected <- unname(choices[selected])
|
||||
warning
|
||||
} else stop # stop when it is ambiguous (some labels == values)
|
||||
warnFun("'selected' must be the values instead of names of 'choices' ",
|
||||
"for the input '", inputId, "'")
|
||||
# This function takes in either a list or vector for `choices` (and
|
||||
# `choiceNames` and `choiceValues` are passed in as NULL) OR it takes
|
||||
# in a list or vector for both `choiceNames` and `choiceValues` (and
|
||||
# `choices` is passed as NULL) and returns a list of two elements:
|
||||
# - `choiceNames` is a vector or list that holds the options names
|
||||
# (each element can be arbitrary UI, or simple text)
|
||||
# - `choiceValues` is a vector or list that holds the options values
|
||||
# (each element must be simple text)
|
||||
normalizeChoicesArgs <- function(choices, choiceNames, choiceValues,
|
||||
mustExist = TRUE) {
|
||||
# if-else to check that either choices OR (choiceNames + choiceValues)
|
||||
# were correctly provided
|
||||
if (is.null(choices)) {
|
||||
if (is.null(choiceNames) || is.null(choiceValues)) {
|
||||
if (mustExist) {
|
||||
stop("Please specify a non-empty vector for `choices` (or, ",
|
||||
"alternatively, for both `choiceNames` AND `choiceValues`).")
|
||||
} else {
|
||||
if (is.null(choiceNames) && is.null(choiceValues)) {
|
||||
# this is useful when we call this function from `updateInputOptions()`
|
||||
# in which case, all three `choices`, `choiceNames` and `choiceValues`
|
||||
# may legitimately be NULL
|
||||
return(list(choiceNames = NULL, choiceValues = NULL))
|
||||
} else {
|
||||
stop("One of `choiceNames` or `choiceValues` was set to ",
|
||||
"NULL, but either both or none should be NULL.")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (length(choiceNames) != length(choiceValues)) {
|
||||
stop("`choiceNames` and `choiceValues` must have the same length.")
|
||||
}
|
||||
if (anyNamed(choiceNames) || anyNamed(choiceValues)) {
|
||||
stop("`choiceNames` and `choiceValues` must not be named.")
|
||||
}
|
||||
} else {
|
||||
if (!is.null(choiceNames) || !is.null(choiceValues)) {
|
||||
warning("Using `choices` argument; ignoring `choiceNames` and `choiceValues`.")
|
||||
}
|
||||
choices <- choicesWithNames(choices) # resolve names if not specified
|
||||
choiceNames <- names(choices)
|
||||
choiceValues <- unname(choices)
|
||||
}
|
||||
selected
|
||||
}
|
||||
|
||||
return(list(choiceNames = as.list(choiceNames),
|
||||
choiceValues = as.list(as.character(choiceValues))))
|
||||
}
|
||||
|
||||
# generate options for radio buttons and checkbox groups (type = 'checkbox' or
|
||||
# 'radio')
|
||||
generateOptions <- function(inputId, choices, selected, inline, type = 'checkbox') {
|
||||
generateOptions <- function(inputId, selected, inline, type = 'checkbox',
|
||||
choiceNames, choiceValues,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
# generate a list of <input type=? [checked] />
|
||||
options <- mapply(
|
||||
choices, names(choices),
|
||||
choiceValues, choiceNames,
|
||||
FUN = function(value, name) {
|
||||
inputTag <- tags$input(
|
||||
type = type, name = inputId, value = value
|
||||
@@ -45,14 +70,18 @@ generateOptions <- function(inputId, choices, selected, inline, type = 'checkbox
|
||||
if (value %in% selected)
|
||||
inputTag$attribs$checked <- "checked"
|
||||
|
||||
# in case, the options include UI code other than text
|
||||
# (arbitrary HTML using the tags() function or equivalent)
|
||||
pd <- processDeps(name, session)
|
||||
|
||||
# If inline, there's no wrapper div, and the label needs a class like
|
||||
# checkbox-inline.
|
||||
if (inline) {
|
||||
tags$label(class = paste0(type, "-inline"), inputTag, tags$span(name))
|
||||
tags$label(class = paste0(type, "-inline"), inputTag,
|
||||
tags$span(pd$html, pd$deps))
|
||||
} else {
|
||||
tags$div(class = type,
|
||||
tags$label(inputTag, tags$span(name))
|
||||
)
|
||||
tags$div(class = type, tags$label(inputTag,
|
||||
tags$span(pd$html, pd$deps)))
|
||||
}
|
||||
},
|
||||
SIMPLIFY = FALSE, USE.NAMES = FALSE
|
||||
@@ -61,45 +90,83 @@ generateOptions <- function(inputId, choices, selected, inline, type = 'checkbox
|
||||
div(class = "shiny-options-group", options)
|
||||
}
|
||||
|
||||
|
||||
# Takes a vector or list, and adds names (same as the value) to any entries
|
||||
# without names.
|
||||
choicesWithNames <- function(choices) {
|
||||
# Take a vector or list, and convert to list. Also, if any children are
|
||||
# vectors with length > 1, convert those to list. If the list is unnamed,
|
||||
# convert it to a named list with blank names.
|
||||
listify <- function(obj) {
|
||||
# If a list/vector is unnamed, give it blank names
|
||||
makeNamed <- function(x) {
|
||||
if (is.null(names(x))) names(x) <- character(length(x))
|
||||
x
|
||||
}
|
||||
|
||||
res <- lapply(obj, function(val) {
|
||||
if (is.list(val))
|
||||
listify(val)
|
||||
else if (length(val) == 1 && is.null(names(val)))
|
||||
val
|
||||
else
|
||||
makeNamed(as.list(val))
|
||||
})
|
||||
|
||||
makeNamed(res)
|
||||
}
|
||||
|
||||
choices <- listify(choices)
|
||||
if (length(choices) == 0) return(choices)
|
||||
|
||||
# Recurse into any subgroups
|
||||
choices <- mapply(choices, names(choices), FUN = function(choice, name) {
|
||||
if (!is.list(choice)) return(choice)
|
||||
if (name == "") stop('All sub-lists in "choices" must be named.')
|
||||
choicesWithNames(choice)
|
||||
}, SIMPLIFY = FALSE)
|
||||
|
||||
# default missing names to choice values
|
||||
missing <- names(choices) == ""
|
||||
names(choices)[missing] <- as.character(choices)[missing]
|
||||
|
||||
choices
|
||||
# True when a choice list item represents a group of related inputs.
|
||||
isGroup <- function(choice) {
|
||||
is.list(choice) ||
|
||||
!is.null(names(choice)) ||
|
||||
length(choice) > 1 ||
|
||||
length(choice) == 0
|
||||
}
|
||||
|
||||
# True when choices is a list and contains at least one group of related inputs.
|
||||
hasGroups <- function(choices) {
|
||||
is.list(choices) && any(vapply(choices, isGroup, logical(1)))
|
||||
}
|
||||
|
||||
# Assigns empty names to x if it's unnamed, and then fills any empty names with
|
||||
# the corresponding value coerced to a character(1).
|
||||
setDefaultNames <- function(x) {
|
||||
x <- asNamed(x)
|
||||
emptyNames <- names(x) == ""
|
||||
names(x)[emptyNames] <- as.character(x)[emptyNames]
|
||||
x
|
||||
}
|
||||
|
||||
# Makes a character vector out of x in a way that preserves names.
|
||||
asCharacter <- function(x) {
|
||||
stats::setNames(as.character(x), names(x))
|
||||
}
|
||||
|
||||
# Processes a "flat" set of choices, or a collection of choices not containing
|
||||
# any named groups. choices should be a list without any list children, or an
|
||||
# atomic vector. choices may be named or unnamed. Any empty names are replaced
|
||||
# with the corresponding value coerced to a character.
|
||||
processFlatChoices <- function(choices) {
|
||||
choices <- setDefaultNames(asCharacter(choices))
|
||||
as.list(choices)
|
||||
}
|
||||
|
||||
# Processes a "nested" set of choices, or a collection of choices that contains
|
||||
# one or more named groups of related choices and zero or more "flat" choices.
|
||||
# choices should be a named list, and any choice group must have a non-empty
|
||||
# name. Empty names of remaining "flat" choices are replaced with that choice's
|
||||
# value coerced to a character.
|
||||
processGroupedChoices <- function(choices) {
|
||||
# We assert choices is a list, since only a list may contain a group.
|
||||
stopifnot(is.list(choices))
|
||||
# The list might be unnamed by this point. We add default names of "" so that
|
||||
# names(choices) is not zero-length and mapply can work. Within mapply, we
|
||||
# error if any group's name is ""
|
||||
choices <- asNamed(choices)
|
||||
choices <- mapply(function(name, choice) {
|
||||
choiceIsGroup <- isGroup(choice)
|
||||
if (choiceIsGroup && name == "") {
|
||||
# If the choice is a group, and if its name is empty, produce an error. We
|
||||
# error here because the composite nature of the choice prevents us from
|
||||
# meaningfully automatically naming it. Note that while not documented,
|
||||
# groups are not necessarily lists (aka generic vectors) but can also be
|
||||
# any named atomic vector, or any atomic vector of length > 1.
|
||||
stop('All sub-lists in "choices" must be named.')
|
||||
} else if (choiceIsGroup) {
|
||||
# The choice is a group, but it is named. Process it using the same
|
||||
# function we use for "top level" choices.
|
||||
processFlatChoices(choice)
|
||||
} else {
|
||||
# The choice was not named and is not a group; it is a "leaf".
|
||||
as.character(choice)
|
||||
}
|
||||
}, names(choices), choices, SIMPLIFY = FALSE)
|
||||
# By this point, any leaves in the choices list might still have empty names,
|
||||
# so we're sure to automatically name them.
|
||||
setDefaultNames(choices)
|
||||
}
|
||||
|
||||
# Takes a vector/list/factor, and adds names (same as the value) to any entries
|
||||
# without names. Coerces all leaf nodes to `character`.
|
||||
choicesWithNames <- function(choices) {
|
||||
if (hasGroups(choices)) {
|
||||
processGroupedChoices(choices)
|
||||
} else {
|
||||
processFlatChoices(choices)
|
||||
}
|
||||
}
|
||||
|
||||
325
R/insert-tab.R
Normal file
325
R/insert-tab.R
Normal file
@@ -0,0 +1,325 @@
|
||||
#' Dynamically insert/remove a tabPanel
|
||||
#'
|
||||
#' Dynamically insert or remove a [tabPanel()] (or a
|
||||
#' [navbarMenu()]) from an existing [tabsetPanel()],
|
||||
#' [navlistPanel()] or [navbarPage()].
|
||||
#'
|
||||
#' When you want to insert a new tab before or after an existing tab, you
|
||||
#' should use `insertTab`. When you want to prepend a tab (i.e. add a
|
||||
#' tab to the beginning of the `tabsetPanel`), use `prependTab`.
|
||||
#' When you want to append a tab (i.e. add a tab to the end of the
|
||||
#' `tabsetPanel`), use `appendTab`.
|
||||
#'
|
||||
#' For `navbarPage`, you can insert/remove conventional
|
||||
#' `tabPanel`s (whether at the top level or nested inside a
|
||||
#' `navbarMenu`), as well as an entire [navbarMenu()].
|
||||
#' For the latter case, `target` should be the `menuName` that
|
||||
#' you gave your `navbarMenu` when you first created it (by default,
|
||||
#' this is equal to the value of the `title` argument).
|
||||
#'
|
||||
#' @param inputId The `id` of the `tabsetPanel` (or
|
||||
#' `navlistPanel` or `navbarPage`) into which `tab` will
|
||||
#' be inserted/removed.
|
||||
#'
|
||||
#' @param tab The item to be added (must be created with `tabPanel`,
|
||||
#' or with `navbarMenu`).
|
||||
#'
|
||||
#' @param target If inserting: the `value` of an existing
|
||||
#' `tabPanel`, next to which `tab` will be added.
|
||||
#' If removing: the `value` of the `tabPanel` that
|
||||
#' you want to remove. See Details if you want to insert next to/remove
|
||||
#' an entire `navbarMenu` instead.
|
||||
#'
|
||||
#' @param position Should `tab` be added before or after the
|
||||
#' `target` tab?
|
||||
#'
|
||||
#' @param select Should `tab` be selected upon being inserted?
|
||||
#'
|
||||
#' @param session The shiny session within which to call this function.
|
||||
#'
|
||||
#' @seealso [showTab()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # example app for inserting/removing a tab
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' actionButton("add", "Add 'Dynamic' tab"),
|
||||
#' actionButton("remove", "Remove 'Foo' tab")
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' tabsetPanel(id = "tabs",
|
||||
#' tabPanel("Hello", "This is the hello tab"),
|
||||
#' tabPanel("Foo", "This is the foo tab"),
|
||||
#' tabPanel("Bar", "This is the bar tab")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$add, {
|
||||
#' insertTab(inputId = "tabs",
|
||||
#' tabPanel("Dynamic", "This a dynamically-added tab"),
|
||||
#' target = "Bar"
|
||||
#' )
|
||||
#' })
|
||||
#' observeEvent(input$remove, {
|
||||
#' removeTab(inputId = "tabs", target = "Foo")
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#'
|
||||
#' # example app for prepending/appending a navbarMenu
|
||||
#' ui <- navbarPage("Navbar page", id = "tabs",
|
||||
#' tabPanel("Home",
|
||||
#' actionButton("prepend", "Prepend a navbarMenu"),
|
||||
#' actionButton("append", "Append a navbarMenu")
|
||||
#' )
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$prepend, {
|
||||
#' id <- paste0("Dropdown", input$prepend, "p")
|
||||
#' prependTab(inputId = "tabs",
|
||||
#' navbarMenu(id,
|
||||
#' tabPanel("Drop1", paste("Drop1 page from", id)),
|
||||
#' tabPanel("Drop2", paste("Drop2 page from", id)),
|
||||
#' "------",
|
||||
#' "Header",
|
||||
#' tabPanel("Drop3", paste("Drop3 page from", id))
|
||||
#' )
|
||||
#' )
|
||||
#' })
|
||||
#' observeEvent(input$append, {
|
||||
#' id <- paste0("Dropdown", input$append, "a")
|
||||
#' appendTab(inputId = "tabs",
|
||||
#' navbarMenu(id,
|
||||
#' tabPanel("Drop1", paste("Drop1 page from", id)),
|
||||
#' tabPanel("Drop2", paste("Drop2 page from", id)),
|
||||
#' "------",
|
||||
#' "Header",
|
||||
#' tabPanel("Drop3", paste("Drop3 page from", id))
|
||||
#' )
|
||||
#' )
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' }
|
||||
#' @export
|
||||
insertTab <- function(inputId, tab, target,
|
||||
position = c("before", "after"), select = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
force(target)
|
||||
force(select)
|
||||
position <- match.arg(position)
|
||||
inputId <- session$ns(inputId)
|
||||
|
||||
# Barbara -- August 2017
|
||||
# Note: until now, the number of tabs in a tabsetPanel (or navbarPage
|
||||
# or navlistPanel) was always fixed. So, an easy way to give an id to
|
||||
# a tab was simply incrementing a counter. (Just like it was easy to
|
||||
# give a random 4-digit number to identify the tabsetPanel). Since we
|
||||
# can only know this in the client side, we'll just pass `id` and
|
||||
# `tsid` (TabSetID) as dummy values that will be fixed in the JS code.
|
||||
item <- buildTabItem("id", "tsid", TRUE, divTag = tab,
|
||||
textFilter = if (is.character(tab)) navbarMenuTextFilter else NULL)
|
||||
|
||||
callback <- function() {
|
||||
session$sendInsertTab(
|
||||
inputId = inputId,
|
||||
liTag = processDeps(item$liTag, session),
|
||||
divTag = processDeps(item$divTag, session),
|
||||
menuName = NULL,
|
||||
target = target,
|
||||
position = position,
|
||||
select = select)
|
||||
}
|
||||
session$onFlush(callback, once = TRUE)
|
||||
}
|
||||
|
||||
#' @param menuName This argument should only be used when you want to
|
||||
#' prepend (or append) `tab` to the beginning (or end) of an
|
||||
#' existing [navbarMenu()] (which must itself be part of
|
||||
#' an existing [navbarPage()]). In this case, this argument
|
||||
#' should be the `menuName` that you gave your `navbarMenu`
|
||||
#' when you first created it (by default, this is equal to the value
|
||||
#' of the `title` argument). Note that you still need to set the
|
||||
#' `inputId` argument to whatever the `id` of the parent
|
||||
#' `navbarPage` is. If `menuName` is left as `NULL`,
|
||||
#' `tab` will be prepended (or appended) to whatever
|
||||
#' `inputId` is.
|
||||
#'
|
||||
#' @rdname insertTab
|
||||
#' @export
|
||||
prependTab <- function(inputId, tab, select = FALSE, menuName = NULL,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
force(select)
|
||||
force(menuName)
|
||||
inputId <- session$ns(inputId)
|
||||
|
||||
item <- buildTabItem("id", "tsid", TRUE, divTag = tab,
|
||||
textFilter = if (is.character(tab)) navbarMenuTextFilter else NULL)
|
||||
|
||||
callback <- function() {
|
||||
session$sendInsertTab(
|
||||
inputId = inputId,
|
||||
liTag = processDeps(item$liTag, session),
|
||||
divTag = processDeps(item$divTag, session),
|
||||
menuName = menuName,
|
||||
target = NULL,
|
||||
position = "after",
|
||||
select = select)
|
||||
}
|
||||
session$onFlush(callback, once = TRUE)
|
||||
}
|
||||
|
||||
#' @rdname insertTab
|
||||
#' @export
|
||||
appendTab <- function(inputId, tab, select = FALSE, menuName = NULL,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
force(select)
|
||||
force(menuName)
|
||||
inputId <- session$ns(inputId)
|
||||
|
||||
item <- buildTabItem("id", "tsid", TRUE, divTag = tab,
|
||||
textFilter = if (is.character(tab)) navbarMenuTextFilter else NULL)
|
||||
|
||||
callback <- function() {
|
||||
session$sendInsertTab(
|
||||
inputId = inputId,
|
||||
liTag = processDeps(item$liTag, session),
|
||||
divTag = processDeps(item$divTag, session),
|
||||
menuName = menuName,
|
||||
target = NULL,
|
||||
position = "before",
|
||||
select = select)
|
||||
}
|
||||
session$onFlush(callback, once = TRUE)
|
||||
}
|
||||
|
||||
#' @rdname insertTab
|
||||
#' @export
|
||||
removeTab <- function(inputId, target,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
force(target)
|
||||
inputId <- session$ns(inputId)
|
||||
|
||||
callback <- function() {
|
||||
session$sendRemoveTab(
|
||||
inputId = inputId,
|
||||
target = target)
|
||||
}
|
||||
session$onFlush(callback, once = TRUE)
|
||||
}
|
||||
|
||||
|
||||
#' Dynamically hide/show a tabPanel
|
||||
#'
|
||||
#' Dynamically hide or show a [tabPanel()] (or a
|
||||
#' [navbarMenu()])from an existing [tabsetPanel()],
|
||||
#' [navlistPanel()] or [navbarPage()].
|
||||
#'
|
||||
#' For `navbarPage`, you can hide/show conventional
|
||||
#' `tabPanel`s (whether at the top level or nested inside a
|
||||
#' `navbarMenu`), as well as an entire [navbarMenu()].
|
||||
#' For the latter case, `target` should be the `menuName` that
|
||||
#' you gave your `navbarMenu` when you first created it (by default,
|
||||
#' this is equal to the value of the `title` argument).
|
||||
#'
|
||||
#' @param inputId The `id` of the `tabsetPanel` (or
|
||||
#' `navlistPanel` or `navbarPage`) in which to find
|
||||
#' `target`.
|
||||
#'
|
||||
#' @param target The `value` of the `tabPanel` to be
|
||||
#' hidden/shown. See Details if you want to hide/show an entire
|
||||
#' `navbarMenu` instead.
|
||||
#'
|
||||
#' @param select Should `target` be selected upon being shown?
|
||||
#'
|
||||
#' @param session The shiny session within which to call this function.
|
||||
#'
|
||||
#' @seealso [insertTab()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- navbarPage("Navbar page", id = "tabs",
|
||||
#' tabPanel("Home",
|
||||
#' actionButton("hideTab", "Hide 'Foo' tab"),
|
||||
#' actionButton("showTab", "Show 'Foo' tab"),
|
||||
#' actionButton("hideMenu", "Hide 'More' navbarMenu"),
|
||||
#' actionButton("showMenu", "Show 'More' navbarMenu")
|
||||
#' ),
|
||||
#' tabPanel("Foo", "This is the foo tab"),
|
||||
#' tabPanel("Bar", "This is the bar tab"),
|
||||
#' navbarMenu("More",
|
||||
#' tabPanel("Table", "Table page"),
|
||||
#' tabPanel("About", "About page"),
|
||||
#' "------",
|
||||
#' "Even more!",
|
||||
#' tabPanel("Email", "Email page")
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$hideTab, {
|
||||
#' hideTab(inputId = "tabs", target = "Foo")
|
||||
#' })
|
||||
#'
|
||||
#' observeEvent(input$showTab, {
|
||||
#' showTab(inputId = "tabs", target = "Foo")
|
||||
#' })
|
||||
#'
|
||||
#' observeEvent(input$hideMenu, {
|
||||
#' hideTab(inputId = "tabs", target = "More")
|
||||
#' })
|
||||
#'
|
||||
#' observeEvent(input$showMenu, {
|
||||
#' showTab(inputId = "tabs", target = "More")
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @export
|
||||
showTab <- function(inputId, target, select = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
force(target)
|
||||
|
||||
if (select) updateTabsetPanel(session, inputId, selected = target)
|
||||
inputId <- session$ns(inputId)
|
||||
|
||||
callback <- function() {
|
||||
session$sendChangeTabVisibility(
|
||||
inputId = inputId,
|
||||
target = target,
|
||||
type = "show"
|
||||
)
|
||||
}
|
||||
session$onFlush(callback, once = TRUE)
|
||||
}
|
||||
|
||||
#' @rdname showTab
|
||||
#' @export
|
||||
hideTab <- function(inputId, target,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
force(target)
|
||||
inputId <- session$ns(inputId)
|
||||
|
||||
callback <- function() {
|
||||
session$sendChangeTabVisibility(
|
||||
inputId = inputId,
|
||||
target = target,
|
||||
type = "hide"
|
||||
)
|
||||
}
|
||||
session$onFlush(callback, once = TRUE)
|
||||
}
|
||||
140
R/insert-ui.R
Normal file
140
R/insert-ui.R
Normal file
@@ -0,0 +1,140 @@
|
||||
#' Insert and remove UI objects
|
||||
#'
|
||||
#' These functions allow you to dynamically add and remove arbirary UI
|
||||
#' into your app, whenever you want, as many times as you want.
|
||||
#' Unlike [renderUI()], the UI generated with `insertUI()` is persistent:
|
||||
#' once it's created, it stays there until removed by `removeUI()`. Each
|
||||
#' new call to `insertUI()` creates more UI objects, in addition to
|
||||
#' the ones already there (all independent from one another). To
|
||||
#' update a part of the UI (ex: an input object), you must use the
|
||||
#' appropriate `render` function or a customized `reactive`
|
||||
#' function.
|
||||
#'
|
||||
#' It's particularly useful to pair `removeUI` with `insertUI()`, but there is
|
||||
#' no restriction on what you can use on. Any element that can be selected
|
||||
#' through a jQuery selector can be removed through this function.
|
||||
#'
|
||||
#' @param selector A string that is accepted by jQuery's selector
|
||||
#' (i.e. the string `s` to be placed in a `$(s)` jQuery call).
|
||||
#'
|
||||
#' For `insertUI()` this determines the element(s) relative to which you
|
||||
#' want to insert your UI object. For `removeUI()` this determine the
|
||||
#' element(s) to be removed. If you want to remove a Shiny input or output,
|
||||
#' note that many of these are wrapped in `<div>`s, so you may need to use a
|
||||
#' somewhat complex selector --- see the Examples below. (Alternatively, you
|
||||
#' could also wrap the inputs/outputs that you want to be able to remove
|
||||
#' easily in a `<div>` with an id.)
|
||||
#' @param where Where your UI object should go relative to the selector:
|
||||
#' \describe{
|
||||
#' \item{`beforeBegin`}{Before the selector element itself}
|
||||
#' \item{`afterBegin`}{Just inside the selector element, before its
|
||||
#' first child}
|
||||
#' \item{`beforeEnd`}{Just inside the selector element, after its
|
||||
#' last child (default)}
|
||||
#' \item{`afterEnd`}{After the selector element itself}
|
||||
#' }
|
||||
#' Adapted from <https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML>.
|
||||
#' @param ui The UI object you want to insert. This can be anything that
|
||||
#' you usually put inside your apps's `ui` function. If you're inserting
|
||||
#' multiple elements in one call, make sure to wrap them in either a
|
||||
#' `tagList()` or a `tags$div()` (the latter option has the
|
||||
#' advantage that you can give it an `id` to make it easier to
|
||||
#' reference or remove it later on). If you want to insert raw html, use
|
||||
#' `ui = HTML()`.
|
||||
#' @param multiple In case your selector matches more than one element,
|
||||
#' `multiple` determines whether Shiny should insert the UI object
|
||||
#' relative to all matched elements or just relative to the first
|
||||
#' matched element (default).
|
||||
#' @param immediate Whether the UI object should be immediately inserted
|
||||
#' or removed, or whether Shiny should wait until all outputs have been
|
||||
#' updated and all observers have been run (default).
|
||||
#' @param session The shiny session. Advanced use only.
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' # Define UI
|
||||
#' ui <- fluidPage(
|
||||
#' actionButton("add", "Add UI")
|
||||
#' )
|
||||
#'
|
||||
#' # Server logic
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$add, {
|
||||
#' insertUI(
|
||||
#' selector = "#add",
|
||||
#' where = "afterEnd",
|
||||
#' ui = textInput(paste0("txt", input$add),
|
||||
#' "Insert some text")
|
||||
#' )
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' # Complete app with UI and server components
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' if (interactive()) {
|
||||
#' # Define UI
|
||||
#' ui <- fluidPage(
|
||||
#' actionButton("rmv", "Remove UI"),
|
||||
#' textInput("txt", "This is no longer useful")
|
||||
#' )
|
||||
#'
|
||||
#' # Server logic
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$rmv, {
|
||||
#' removeUI(
|
||||
#' selector = "div:has(> #txt)"
|
||||
#' )
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' # Complete app with UI and server components
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
insertUI <- function(selector,
|
||||
where = c("beforeBegin", "afterBegin", "beforeEnd", "afterEnd"),
|
||||
ui,
|
||||
multiple = FALSE,
|
||||
immediate = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(selector)
|
||||
force(ui)
|
||||
force(session)
|
||||
force(multiple)
|
||||
if (missing(where)) where <- "beforeEnd"
|
||||
where <- match.arg(where)
|
||||
|
||||
callback <- function() {
|
||||
session$sendInsertUI(selector = selector,
|
||||
multiple = multiple,
|
||||
where = where,
|
||||
content = processDeps(ui, session))
|
||||
}
|
||||
|
||||
if (!immediate) session$onFlushed(callback, once = TRUE)
|
||||
else callback()
|
||||
}
|
||||
|
||||
|
||||
#' @rdname insertUI
|
||||
#' @export
|
||||
removeUI <- function(selector,
|
||||
multiple = FALSE,
|
||||
immediate = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(selector)
|
||||
force(multiple)
|
||||
force(session)
|
||||
|
||||
callback <- function() {
|
||||
session$sendRemoveUI(selector = selector,
|
||||
multiple = multiple)
|
||||
}
|
||||
|
||||
if (!immediate) session$onFlushed(callback, once = TRUE)
|
||||
else callback()
|
||||
}
|
||||
41
R/jqueryui.R
41
R/jqueryui.R
@@ -2,32 +2,32 @@
|
||||
#'
|
||||
#' Creates a panel whose contents are absolutely positioned.
|
||||
#'
|
||||
#' The \code{absolutePanel} function creates a \code{<div>} tag whose CSS
|
||||
#' position is set to \code{absolute} (or fixed if \code{fixed = TRUE}). The way
|
||||
#' The `absolutePanel` function creates a `<div>` tag whose CSS
|
||||
#' position is set to `absolute` (or fixed if `fixed = TRUE`). The way
|
||||
#' absolute positioning works in HTML is that absolute coordinates are specified
|
||||
#' relative to its nearest parent element whose position is not set to
|
||||
#' \code{static} (which is the default), and if no such parent is found, then
|
||||
#' `static` (which is the default), and if no such parent is found, then
|
||||
#' relative to the page borders. If you're not sure what that means, just keep
|
||||
#' in mind that you may get strange results if you use \code{absolutePanel} from
|
||||
#' in mind that you may get strange results if you use `absolutePanel` from
|
||||
#' inside of certain types of panels.
|
||||
#'
|
||||
#' The \code{fixedPanel} function is the same as \code{absolutePanel} with
|
||||
#' \code{fixed = TRUE}.
|
||||
#' The `fixedPanel` function is the same as `absolutePanel` with
|
||||
#' `fixed = TRUE`.
|
||||
#'
|
||||
#' The position (\code{top}, \code{left}, \code{right}, \code{bottom}) and size
|
||||
#' (\code{width}, \code{height}) parameters are all optional, but you should
|
||||
#' specify exactly two of \code{top}, \code{bottom}, and \code{height} and
|
||||
#' exactly two of \code{left}, \code{right}, and \code{width} for predictable
|
||||
#' The position (`top`, `left`, `right`, `bottom`) and size
|
||||
#' (`width`, `height`) parameters are all optional, but you should
|
||||
#' specify exactly two of `top`, `bottom`, and `height` and
|
||||
#' exactly two of `left`, `right`, and `width` for predictable
|
||||
#' results.
|
||||
#'
|
||||
#' Like most other distance parameters in Shiny, the position and size
|
||||
#' parameters take a number (interpreted as pixels) or a valid CSS size string,
|
||||
#' such as \code{"100px"} (100 pixels) or \code{"25\%"}.
|
||||
#' such as `"100px"` (100 pixels) or `"25%"`.
|
||||
#'
|
||||
#' For arcane HTML reasons, to have the panel fill the page or parent you should
|
||||
#' specify \code{0} for \code{top}, \code{left}, \code{right}, and \code{bottom}
|
||||
#' rather than the more obvious \code{width = "100\%"} and \code{height =
|
||||
#' "100\%"}.
|
||||
#' specify `0` for `top`, `left`, `right`, and `bottom`
|
||||
#' rather than the more obvious `width = "100%"` and `height =
|
||||
#' "100%"`.
|
||||
#'
|
||||
#' @param ... Attributes (named arguments) or children (unnamed arguments) that
|
||||
#' should be included in the panel.
|
||||
@@ -42,18 +42,17 @@
|
||||
#' page or parent container.
|
||||
#' @param width Width of the panel.
|
||||
#' @param height Height of the panel.
|
||||
#' @param draggable If \code{TRUE}, allows the user to move the panel by
|
||||
#' @param draggable If `TRUE`, allows the user to move the panel by
|
||||
#' clicking and dragging.
|
||||
#' @param fixed Positions the panel relative to the browser window and prevents
|
||||
#' it from being scrolled with the rest of the page.
|
||||
#' @param cursor The type of cursor that should appear when the user mouses over
|
||||
#' the panel. Use \code{"move"} for a north-east-south-west icon,
|
||||
#' \code{"default"} for the usual cursor arrow, or \code{"inherit"} for the
|
||||
#' the panel. Use `"move"` for a north-east-south-west icon,
|
||||
#' `"default"` for the usual cursor arrow, or `"inherit"` for the
|
||||
#' usual cursor behavior (including changing to an I-beam when the cursor is
|
||||
#' over text). The default is \code{"auto"}, which is equivalent to
|
||||
#' \code{ifelse(draggable, "move", "inherit")}.
|
||||
#' over text). The default is `"auto"`, which is equivalent to
|
||||
#' `ifelse(draggable, "move", "inherit")`.
|
||||
#' @return An HTML element or list of elements.
|
||||
#'
|
||||
#' @export
|
||||
absolutePanel <- function(...,
|
||||
top = NULL, left = NULL, right = NULL, bottom = NULL,
|
||||
@@ -80,8 +79,6 @@ absolutePanel <- function(...,
|
||||
if (isTRUE(draggable)) {
|
||||
divTag <- tagAppendAttributes(divTag, class='draggable')
|
||||
return(tagList(
|
||||
# IMPORTANT NOTE: If you update jqueryui, make sure you DON'T include the datepicker,
|
||||
# as it collides with our bootstrap datepicker!
|
||||
singleton(tags$head(tags$script(src='shared/jqueryui/jquery-ui.min.js'))),
|
||||
divTag,
|
||||
tags$script('$(".draggable").draggable();')
|
||||
|
||||
47
R/map.R
47
R/map.R
@@ -9,63 +9,58 @@
|
||||
# Remove of unknown key does nothing
|
||||
# Setting a key twice always results in last-one-wins
|
||||
# /TESTS
|
||||
|
||||
# Note that Map objects can't be saved in one R session and restored in
|
||||
# another, because they are based on fastmap, which uses an external pointer,
|
||||
# and external pointers can't be saved and restored in another session.
|
||||
#' @importFrom fastmap fastmap
|
||||
Map <- R6Class(
|
||||
'Map',
|
||||
portable = FALSE,
|
||||
public = list(
|
||||
initialize = function() {
|
||||
private$env <- new.env(parent=emptyenv())
|
||||
private$map <<- fastmap()
|
||||
},
|
||||
get = function(key) {
|
||||
env[[key]]
|
||||
map$get(key)
|
||||
},
|
||||
set = function(key, value) {
|
||||
env[[key]] <- value
|
||||
map$set(key, value)
|
||||
value
|
||||
},
|
||||
mget = function(keys) {
|
||||
base::mget(keys, env)
|
||||
map$mget(keys)
|
||||
},
|
||||
mset = function(...) {
|
||||
args <- list(...)
|
||||
if (length(args) == 0)
|
||||
return()
|
||||
|
||||
arg_names <- names(args)
|
||||
if (is.null(arg_names) || any(!nzchar(arg_names)))
|
||||
stop("All elements must be named")
|
||||
|
||||
list2env(args, envir = env)
|
||||
map$mset(...)
|
||||
},
|
||||
remove = function(key) {
|
||||
if (!self$containsKey(key))
|
||||
if (!map$has(key))
|
||||
return(NULL)
|
||||
|
||||
result <- env[[key]]
|
||||
rm(list=key, envir=env, inherits=FALSE)
|
||||
result <- map$get(key)
|
||||
map$remove(key)
|
||||
result
|
||||
},
|
||||
containsKey = function(key) {
|
||||
exists(key, envir=env, inherits=FALSE)
|
||||
map$has(key)
|
||||
},
|
||||
keys = function() {
|
||||
# Sadly, this is much faster than ls(), because it doesn't sort the keys.
|
||||
names(as.list(env, all.names=TRUE))
|
||||
keys = function(sort = FALSE) {
|
||||
map$keys(sort = sort)
|
||||
},
|
||||
values = function() {
|
||||
as.list(env, all.names=TRUE)
|
||||
values = function(sort = FALSE) {
|
||||
map$as_list(sort = sort)
|
||||
},
|
||||
clear = function() {
|
||||
private$env <- new.env(parent=emptyenv())
|
||||
invisible(NULL)
|
||||
map$reset()
|
||||
},
|
||||
size = function() {
|
||||
length(env)
|
||||
map$size()
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
env = 'environment'
|
||||
map = NULL
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -2,19 +2,64 @@
|
||||
NULL
|
||||
|
||||
reactLogHandler <- function(req) {
|
||||
if (!identical(req$PATH_INFO, '/reactlog'))
|
||||
return(NULL)
|
||||
if (! rLog$isLogging()) {
|
||||
if (
|
||||
identical(req$PATH_INFO, "/reactlog/mark") ||
|
||||
identical(req$PATH_INFO, "/reactlog")
|
||||
) {
|
||||
# is not logging, but is a reactlog path...
|
||||
|
||||
return(
|
||||
httpResponse(
|
||||
# Not Implemented
|
||||
# - The server either does not recognize the request method, or it lacks the ability to fulfil the request.
|
||||
status = 501,
|
||||
content_type = "text/plain; charset=utf-8",
|
||||
content = "To enable reactlog, set the following option before running the application: \n\noptions(shiny.reactlog = TRUE)"
|
||||
)
|
||||
)
|
||||
|
||||
} else {
|
||||
# continue on like normal
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
if (!isTRUE(getOption('shiny.reactlog'))) {
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
sessionToken <- parseQueryString(req$QUERY_STRING)$s
|
||||
if (identical(req$PATH_INFO, "/reactlog/mark")) {
|
||||
sessionToken <- parseQueryString(req$QUERY_STRING)$s
|
||||
shinysession <- appsByToken$get(sessionToken)
|
||||
|
||||
return(httpResponse(
|
||||
status=200,
|
||||
content=list(file=renderReactLog(sessionToken), owned=TRUE)
|
||||
))
|
||||
# log time
|
||||
withReactiveDomain(shinysession, {
|
||||
rLog$userMark(getDefaultReactiveDomain())
|
||||
})
|
||||
|
||||
return(httpResponse(
|
||||
status = 200,
|
||||
content = "marked",
|
||||
content_type = "text/plain"
|
||||
))
|
||||
|
||||
} else if (identical(req$PATH_INFO, "/reactlog")){
|
||||
|
||||
sessionToken <- parseQueryString(req$QUERY_STRING)$s
|
||||
|
||||
# `renderReactLog` will check/throw if reactlog doesn't exist
|
||||
reactlogFile <- renderReactlog(sessionToken)
|
||||
|
||||
return(httpResponse(
|
||||
status = 200,
|
||||
content = list(
|
||||
file = reactlogFile,
|
||||
owned = TRUE
|
||||
)
|
||||
))
|
||||
|
||||
} else {
|
||||
# continue on like normal
|
||||
return(NULL)
|
||||
}
|
||||
}
|
||||
|
||||
sessionHandler <- function(req) {
|
||||
|
||||
111
R/middleware.R
111
R/middleware.R
@@ -191,7 +191,7 @@ staticHandler <- function(root) {
|
||||
if (!identical(req$REQUEST_METHOD, 'GET'))
|
||||
return(NULL)
|
||||
|
||||
path <- req$PATH_INFO
|
||||
path <- URLdecode(req$PATH_INFO)
|
||||
|
||||
if (is.null(path))
|
||||
return(httpResponse(400, content="<h1>Bad Request</h1>"))
|
||||
@@ -199,6 +199,9 @@ staticHandler <- function(root) {
|
||||
if (path == '/')
|
||||
path <- '/index.html'
|
||||
|
||||
if (grepl('\\', path, fixed = TRUE))
|
||||
return(NULL)
|
||||
|
||||
abs.path <- resolve(root, path)
|
||||
if (is.null(abs.path))
|
||||
return(NULL)
|
||||
@@ -299,9 +302,7 @@ HandlerManager <- R6Class("HandlerManager",
|
||||
|
||||
if (reqSize > maxSize) {
|
||||
return(list(status = 413L,
|
||||
headers = list(
|
||||
'Content-Type' = 'text/plain'
|
||||
),
|
||||
headers = list('Content-Type' = 'text/plain'),
|
||||
body = 'Maximum upload size exceeded'))
|
||||
}
|
||||
else {
|
||||
@@ -310,30 +311,40 @@ HandlerManager <- R6Class("HandlerManager",
|
||||
},
|
||||
call = .httpServer(
|
||||
function (req) {
|
||||
withLogErrors(handlers$invoke(req))
|
||||
withCallingHandlers(withLogErrors(handlers$invoke(req)),
|
||||
error = function(cond) {
|
||||
sanitizeErrors <- getOption('shiny.sanitize.errors', FALSE)
|
||||
if (inherits(cond, 'shiny.custom.error') || !sanitizeErrors) {
|
||||
stop(cond$message, call. = FALSE)
|
||||
} else {
|
||||
stop(paste("An error has occurred. Check your logs or",
|
||||
"contact the app author for clarification."),
|
||||
call. = FALSE)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
getOption('shiny.sharedSecret')
|
||||
loadSharedSecret()
|
||||
),
|
||||
onWSOpen = function(ws) {
|
||||
return(wsHandlers$invoke(ws))
|
||||
}
|
||||
)
|
||||
},
|
||||
.httpServer = function(handler, sharedSecret) {
|
||||
.httpServer = function(handler, checkSharedSecret) {
|
||||
filter <- getOption('shiny.http.response.filter')
|
||||
if (is.null(filter))
|
||||
filter <- function(req, response) response
|
||||
|
||||
function(req) {
|
||||
if (!is.null(sharedSecret)
|
||||
&& !identical(sharedSecret, req$HTTP_SHINY_SHARED_SECRET)) {
|
||||
if (!checkSharedSecret(req$HTTP_SHINY_SHARED_SECRET)) {
|
||||
return(list(status=403,
|
||||
body='<h1>403 Forbidden</h1><p>Shared secret mismatch</p>',
|
||||
headers=list('Content-Type' = 'text/html')))
|
||||
}
|
||||
|
||||
# Catch HEAD requests. For the purposes of handler functions, they
|
||||
# should be treated like GET. The difference is that they shouldn't
|
||||
# should be treated like GET. The difference is that they shouldn't
|
||||
# return a body in the http response.
|
||||
head_request <- FALSE
|
||||
if (identical(req$REQUEST_METHOD, "HEAD")) {
|
||||
@@ -342,38 +353,72 @@ HandlerManager <- R6Class("HandlerManager",
|
||||
}
|
||||
|
||||
response <- handler(req)
|
||||
if (is.null(response))
|
||||
response <- httpResponse(404, content="<h1>Not Found</h1>")
|
||||
|
||||
if (inherits(response, "httpResponse")) {
|
||||
headers <- as.list(response$headers)
|
||||
headers$'Content-Type' <- response$content_type
|
||||
res <- hybrid_chain(response, function(response) {
|
||||
if (is.null(response))
|
||||
response <- httpResponse(404, content="<h1>Not Found</h1>")
|
||||
|
||||
if (inherits(response, "httpResponse")) {
|
||||
headers <- as.list(response$headers)
|
||||
headers$'Content-Type' <- response$content_type
|
||||
|
||||
response <- filter(req, response)
|
||||
if (head_request) {
|
||||
|
||||
headers$`Content-Length` <- getResponseContentLength(response, deleteOwnedContent = TRUE)
|
||||
|
||||
return(list(
|
||||
status = response$status,
|
||||
body = "",
|
||||
headers = headers
|
||||
))
|
||||
} else {
|
||||
return(list(
|
||||
status = response$status,
|
||||
body = response$content,
|
||||
headers = headers
|
||||
))
|
||||
}
|
||||
|
||||
response <- filter(req, response)
|
||||
if (head_request) {
|
||||
headers$`Content-Length` <- nchar(response$content, type = "bytes")
|
||||
return(list(
|
||||
status = response$status,
|
||||
body = "",
|
||||
headers = headers
|
||||
))
|
||||
} else {
|
||||
return(list(
|
||||
status = response$status,
|
||||
body = response$content,
|
||||
headers = headers
|
||||
))
|
||||
# Assume it's a Rook-compatible response
|
||||
return(response)
|
||||
}
|
||||
|
||||
} else {
|
||||
# Assume it's a Rook-compatible response
|
||||
return(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Safely get the Content-Length of a Rook response, or NULL if the length cannot
|
||||
# be determined for whatever reason (probably malformed response$content).
|
||||
# If deleteOwnedContent is TRUE, then the function should delete response
|
||||
# content that is of the form list(file=..., owned=TRUE).
|
||||
getResponseContentLength <- function(response, deleteOwnedContent) {
|
||||
force(deleteOwnedContent)
|
||||
|
||||
result <- if (is.character(response$content) && length(response$content) == 1) {
|
||||
nchar(response$content, type = "bytes")
|
||||
} else if (is.raw(response$content)) {
|
||||
length(response$content)
|
||||
} else if (is.list(response$content) && !is.null(response$content$file)) {
|
||||
if (deleteOwnedContent && isTRUE(response$content$owned)) {
|
||||
on.exit(unlink(response$content$file, recursive = FALSE, force = FALSE), add = TRUE)
|
||||
}
|
||||
file.info(response$content$file)$size
|
||||
} else {
|
||||
warning("HEAD request for unexpected content class ", class(response$content)[[1]])
|
||||
NULL
|
||||
}
|
||||
|
||||
if (is.na(result)) {
|
||||
# Mostly for missing file case
|
||||
return(NULL)
|
||||
} else {
|
||||
return(result)
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# ## Next steps
|
||||
#
|
||||
|
||||
435
R/mock-session.R
Normal file
435
R/mock-session.R
Normal file
@@ -0,0 +1,435 @@
|
||||
# Promise helpers taken from:
|
||||
# https://github.com/rstudio/promises/blob/master/tests/testthat/common.R
|
||||
# Block until all pending later tasks have executed
|
||||
wait_for_it <- function() {
|
||||
while (!later::loop_empty()) {
|
||||
later::run_now(0.1)
|
||||
}
|
||||
}
|
||||
|
||||
# Block until the promise is resolved/rejected. If resolved, return the value.
|
||||
# If rejected, throw (yes throw, not return) the error.
|
||||
#' @importFrom promises %...!%
|
||||
#' @importFrom promises %...>%
|
||||
extract <- function(promise) {
|
||||
promise_value <- NULL
|
||||
error <- NULL
|
||||
promise %...>%
|
||||
(function(value) promise_value <<- value) %...!%
|
||||
(function(reason) error <<- reason)
|
||||
|
||||
wait_for_it()
|
||||
if (!is.null(error))
|
||||
stop(error)
|
||||
else
|
||||
promise_value
|
||||
}
|
||||
|
||||
# TODO: is there a way to get this behavior without exporting these functions? R6?
|
||||
# TODO: clientData is documented as a reactiveValues, which this is not. Is it possible that
|
||||
# users are currently assigning into clientData? That would not work as expected here.
|
||||
#' @noRd
|
||||
#' @export
|
||||
`$.mockclientdata` <- function(x, name) {
|
||||
if (name == "allowDataUriScheme") { return(TRUE) }
|
||||
if (name == "pixelratio") { return(1) }
|
||||
if (name == "url_protocol") { return("http:") }
|
||||
if (name == "url_hostname") { return("mocksession") }
|
||||
if (name == "url_port") { return(1234) }
|
||||
if (name == "url_pathname") { return("/mockpath") }
|
||||
if (name == "url_hash") { return("#mockhash") }
|
||||
if (name == "url_hash_initial") { return("#mockhash") }
|
||||
if (name == "url_search") { return("?mocksearch=1") }
|
||||
|
||||
clientRE <- "^output_(.+)_([^_]+)$"
|
||||
if(grepl(clientRE, name)) {
|
||||
# TODO: use proper regex group matching here instead of redundantly parsing
|
||||
el <- sub(clientRE, "\\1", name)
|
||||
att <- sub(clientRE, "\\2", name)
|
||||
|
||||
if (att == "width") {
|
||||
return(600)
|
||||
} else if (att == "height") {
|
||||
return(400)
|
||||
} else if (att == "hidden") {
|
||||
return(FALSE)
|
||||
}
|
||||
}
|
||||
warning("Unexpected clientdata attribute accessed: ", name)
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
#' @noRd
|
||||
#' @export
|
||||
`[[.mockclientdata` <- `$.mockclientdata`
|
||||
|
||||
#' @noRd
|
||||
#' @export
|
||||
`[.mockclientdata` <- function(values, name) {
|
||||
stop("Single-bracket indexing of mockclientdata is not allowed.")
|
||||
}
|
||||
|
||||
#' Mock Shiny Session
|
||||
#'
|
||||
#' @description
|
||||
#' An R6 class suitable for testing that simulates the `session` parameter
|
||||
#' provided to Shiny server functions or modules.
|
||||
#'
|
||||
#' @include timer.R
|
||||
#' @export
|
||||
MockShinySession <- R6Class(
|
||||
'MockShinySession',
|
||||
portable = FALSE,
|
||||
public = list(
|
||||
#' @field env The environment associated with the session.
|
||||
env = NULL,
|
||||
#' @field singletons Hardcoded as empty. Needed for rendering HTML (i.e. renderUI)
|
||||
singletons = character(0),
|
||||
#' @field clientData Mock client data that always returns a size for plots
|
||||
clientData = structure(list(), class="mockclientdata"),
|
||||
#' @description No-op
|
||||
#' @param logEntry Not used
|
||||
reactlog = function(logEntry){},
|
||||
#' @description No-op
|
||||
incrementBusyCount = function(){},
|
||||
#' @field output The shinyoutputs associated with the session
|
||||
output = NULL,
|
||||
#' @field input The reactive inputs associated with the session
|
||||
input = NULL,
|
||||
#' @field userData An environment initialized as empty.
|
||||
userData = NULL,
|
||||
#' @field progressStack A stack of progress objects
|
||||
progressStack = 'Stack',
|
||||
|
||||
#' @description Create a new MockShinySession
|
||||
initialize = function() {
|
||||
private$.input <- ReactiveValues$new(dedupe = FALSE, label = "input")
|
||||
private$flushCBs <- Callbacks$new()
|
||||
private$flushedCBs <- Callbacks$new()
|
||||
private$endedCBs <- Callbacks$new()
|
||||
private$timer <- MockableTimerCallbacks$new()
|
||||
self$progressStack <- Stack$new()
|
||||
|
||||
self$userData <- new.env(parent=emptyenv())
|
||||
|
||||
# create output
|
||||
out <- .createOutputWriter(self)
|
||||
class(out) <- "shinyoutput"
|
||||
self$output <- out
|
||||
|
||||
# Create a read-only copy of the inputs reactive.
|
||||
self$input <- .createReactiveValues(private$.input, readonly = TRUE)
|
||||
},
|
||||
#' @description Define a callback to be invoked before a reactive flush
|
||||
#' @param fun The function to invoke
|
||||
#' @param once If `TRUE`, will only run once. Otherwise, will run every time reactives are flushed.
|
||||
onFlush = function(fun, once=TRUE) {
|
||||
if (!isTRUE(once)) {
|
||||
return(private$flushCBs$register(fun))
|
||||
} else {
|
||||
dereg <- private$flushCBs$register(function() {
|
||||
dereg()
|
||||
fun()
|
||||
})
|
||||
return(dereg)
|
||||
}
|
||||
},
|
||||
#' @description Define a callback to be invoked after a reactive flush
|
||||
#' @param fun The function to invoke
|
||||
#' @param once If `TRUE`, will only run once. Otherwise, will run every time reactives are flushed.
|
||||
onFlushed = function(fun, once=TRUE) {
|
||||
if (!isTRUE(once)) {
|
||||
return(private$flushedCBs$register(fun))
|
||||
} else {
|
||||
dereg <- private$flushedCBs$register(function() {
|
||||
dereg()
|
||||
fun()
|
||||
})
|
||||
return(dereg)
|
||||
}
|
||||
},
|
||||
#' @description Define a callback to be invoked when the session ends
|
||||
#' @param sessionEndedCallback The callback to invoke when the session has ended.
|
||||
onEnded = function(sessionEndedCallback) {
|
||||
private$endedCBs$register(sessionEndedCallback)
|
||||
},
|
||||
|
||||
#' @description Returns `FALSE` if the session has not yet been closed
|
||||
isEnded = function(){ private$closed },
|
||||
#' @description Returns `FALSE` if the session has not yet been closed
|
||||
isClosed = function(){ private$closed },
|
||||
#' @description Closes the session
|
||||
close = function(){ private$closed <- TRUE },
|
||||
|
||||
#FIXME: this is wrong. Will need to be more complex.
|
||||
#' @description Unsophisticated mock implementation that merely invokes
|
||||
#' the given callback immediately.
|
||||
#' @param callback The callback ato be invoked.
|
||||
cycleStartAction = function(callback){ callback() },
|
||||
|
||||
#' @description Base64-encode the given file. Needed for image rendering.
|
||||
#' @param name Not used
|
||||
#' @param file The file to be encoded
|
||||
#' @param contentType The content type of the base64-encoded string
|
||||
fileUrl = function(name, file, contentType='application/octet-stream') {
|
||||
bytes <- file.info(file)$size
|
||||
if (is.na(bytes))
|
||||
return(NULL)
|
||||
|
||||
fileData <- readBin(file, 'raw', n=bytes)
|
||||
b64 <- rawToBase64(fileData)
|
||||
return(paste('data:', contentType, ';base64,', b64, sep=''))
|
||||
},
|
||||
|
||||
#' @description Sets reactive values associated with the `session$inputs` object
|
||||
#' and flushes the reactives.
|
||||
#' @param ... The inputs to set.
|
||||
#' @examples
|
||||
#' s <- MockShinySession$new()
|
||||
#' s$setInputs(x=1, y=2)
|
||||
setInputs = function(...) {
|
||||
vals <- list(...)
|
||||
mapply(names(vals), vals, FUN = function(name, value) {
|
||||
private$.input$set(name, value)
|
||||
})
|
||||
private$flush()
|
||||
},
|
||||
|
||||
#' @description An internal method which shouldn't be used by others.
|
||||
#' @param millis The number of milliseconds on which to schedule a callback
|
||||
#' @param callback The function to schedule
|
||||
.scheduleTask = function(millis, callback) {
|
||||
id <- private$timer$schedule(millis, callback)
|
||||
|
||||
# Return a deregistration callback
|
||||
function() {
|
||||
invisible(private$timer$unschedule(id))
|
||||
}
|
||||
},
|
||||
|
||||
#' @description Simulate the passing of time by the given number of milliseconds.
|
||||
#' @param millis The number of milliseconds to advance time.
|
||||
elapse = function(millis) {
|
||||
msLeft <- millis
|
||||
|
||||
while (msLeft > 0){
|
||||
t <- private$timer$timeToNextEvent()
|
||||
|
||||
if (is.infinite(t) || t <= 0 || msLeft < t){
|
||||
# Either there's no good upcoming event or we can't make it to it in the allotted time.
|
||||
break
|
||||
}
|
||||
msLeft <- msLeft - t
|
||||
private$timer$elapse(t)
|
||||
|
||||
# timerCallbacks must run before flushReact.
|
||||
private$timer$executeElapsed()
|
||||
private$flush()
|
||||
}
|
||||
|
||||
private$timer$elapse(msLeft)
|
||||
|
||||
# Run again in case our callbacks resulted in a scheduled
|
||||
# function that needs executing.
|
||||
private$timer$executeElapsed()
|
||||
private$flush()
|
||||
},
|
||||
|
||||
#' @description An internal method which shouldn't be used by others.
|
||||
.now = function() {
|
||||
# Returns elapsed time in milliseconds
|
||||
private$timer$getElapsed()
|
||||
},
|
||||
|
||||
#' @description An internal method which shouldn't be used by others.
|
||||
#' @param name The name of the output
|
||||
#' @param func The render definition
|
||||
#' @param label Not used
|
||||
defineOutput = function(name, func, label) {
|
||||
force(name)
|
||||
|
||||
if (!is.null(private$outs[[name]]$obs)) {
|
||||
private$outs[[name]]$obs$destroy()
|
||||
}
|
||||
|
||||
if (is.null(func)) func <- missingOutput
|
||||
|
||||
if (!is.function(func))
|
||||
stop(paste("Unexpected", class(func), "output for", name))
|
||||
|
||||
obs <- observe({
|
||||
# We could just stash the promise, but we get an "unhandled promise error". This bypasses
|
||||
prom <- NULL
|
||||
tryCatch({
|
||||
v <- func(self, name)
|
||||
if (!promises::is.promise(v)){
|
||||
# Make our sync value into a promise
|
||||
prom <- promises::promise(function(resolve, reject){ resolve(v) })
|
||||
} else {
|
||||
prom <- v
|
||||
}
|
||||
}, error=function(e){
|
||||
# Error running value()
|
||||
prom <<- promises::promise(function(resolve, reject){ reject(e) })
|
||||
})
|
||||
|
||||
private$outs[[name]]$promise <- hybrid_chain(
|
||||
prom,
|
||||
function(v){
|
||||
list(val = v, err = NULL)
|
||||
}, catch=function(e){
|
||||
list(val = NULL, err = e)
|
||||
})
|
||||
})
|
||||
private$outs[[name]] <- list(obs = obs, func = func, promise = NULL)
|
||||
},
|
||||
|
||||
#' @description An internal method which shouldn't be used by others.
|
||||
#' @param name The name of the output
|
||||
getOutput = function(name) {
|
||||
# Unlike the real outputs, we're going to return the last value rather than the unevaluated function
|
||||
if (is.null(private$outs[[name]])) {
|
||||
stop("The test referenced an output that hasn't been defined yet: output$", name)
|
||||
}
|
||||
|
||||
if (is.null(private$outs[[name]]$promise)) {
|
||||
# Means the output was defined but the observer hasn't had a chance to run
|
||||
# yet. Run flushReact() now to force the observer to run.
|
||||
flushReact()
|
||||
|
||||
if (is.null(private$outs[[name]]$promise)) {
|
||||
stop("output$", name, " encountered an unexpected error resolving its promise")
|
||||
}
|
||||
}
|
||||
|
||||
# Make promise return
|
||||
v <- extract(private$outs[[name]]$promise)
|
||||
if (!is.null(v$err)){
|
||||
stop(v$err)
|
||||
} else {
|
||||
v$val
|
||||
}
|
||||
},
|
||||
|
||||
#' @description No-op
|
||||
#' @param name Not used
|
||||
#' @param data Not used
|
||||
#' @param filterFunc Not used
|
||||
registerDataObj = function(name, data, filterFunc) {},
|
||||
#' @description No-op
|
||||
#' @param value Not used
|
||||
allowReconnect = function(value) {},
|
||||
#' @description No-op
|
||||
reload = function() {},
|
||||
#' @description No-op
|
||||
#' @param brushId Not used
|
||||
resetBrush = function(brushId) {
|
||||
warning("session$brush isn't meaningfully mocked on the MockShinySession")
|
||||
},
|
||||
#' @description No-op
|
||||
#' @param type Not used
|
||||
#' @param message Not used
|
||||
sendCustomMessage = function(type, message) {},
|
||||
#' @description No-op
|
||||
#' @param type Not used
|
||||
#' @param message Not used
|
||||
sendBinaryMessage = function(type, message) {},
|
||||
#' @description No-op
|
||||
#' @param inputId Not used
|
||||
#' @param message Not used
|
||||
sendInputMessage = function(inputId, message) {},
|
||||
#' @description No-op
|
||||
#' @param names Not used
|
||||
setBookmarkExclude = function(names) {
|
||||
warning("Bookmarking isn't meaningfully mocked in MockShinySession")
|
||||
},
|
||||
#' @description No-op
|
||||
getBookmarkExclude = function() {
|
||||
warning("Bookmarking isn't meaningfully mocked in MockShinySession")
|
||||
},
|
||||
#' @description No-op
|
||||
#' @param fun Not used
|
||||
onBookmark = function(fun) {},
|
||||
#' @description No-op
|
||||
#' @param fun Not used
|
||||
onBookmarked = function(fun) {},
|
||||
#' @description No-op
|
||||
doBookmark = function() {
|
||||
warning("Bookmarking isn't meaningfully mocked in MockShinySession")
|
||||
},
|
||||
#' @description No-op
|
||||
#' @param fun Not used
|
||||
onRestore = function(fun) {},
|
||||
#' @description No-op
|
||||
#' @param fun Not used
|
||||
onRestored = function(fun) {},
|
||||
#' @description No-op
|
||||
exportTestValues = function() {},
|
||||
#' @description No-op
|
||||
#' @param input Not used
|
||||
#' @param output Not used
|
||||
#' @param export Not used
|
||||
#' @param format Not used
|
||||
getTestSnapshotUrl = function(input=TRUE, output=TRUE, export=TRUE, format="json") {},
|
||||
#' @description Returns the given id prefixed by `mock-session-`.
|
||||
#' @param id The id to modify.
|
||||
ns = function(id) {
|
||||
paste0("mock-session-", id) # TODO: does this need to be more complex/intelligent?
|
||||
},
|
||||
#' @description Trigger a reactive flush right now.
|
||||
flushReact = function(){
|
||||
private$flush()
|
||||
},
|
||||
#' @description Create and return a namespace-specific session proxy.
|
||||
#' @param namespace Character vector indicating a namespace.
|
||||
makeScope = function(namespace) {
|
||||
ns <- NS(namespace)
|
||||
createSessionProxy(
|
||||
self,
|
||||
input = .createReactiveValues(private$.input, readonly = TRUE, ns = ns),
|
||||
output = structure(.createOutputWriter(self, ns = ns), class = "shinyoutput"),
|
||||
makeScope = function(namespace) self$makeScope(ns(namespace))
|
||||
)
|
||||
}
|
||||
),
|
||||
private = list(
|
||||
.input = NULL,
|
||||
flushCBs = NULL,
|
||||
flushedCBs = NULL,
|
||||
endedCBs = NULL,
|
||||
timer = NULL,
|
||||
closed = FALSE,
|
||||
outs = list(),
|
||||
returnedVal = NULL,
|
||||
|
||||
flush = function(){
|
||||
isolate(private$flushCBs$invoke(..stacktraceon = TRUE))
|
||||
shiny:::flushReact() # namespace to avoid calling our own method
|
||||
isolate(private$flushedCBs$invoke(..stacktraceon = TRUE))
|
||||
later::run_now()
|
||||
}
|
||||
),
|
||||
active = list(
|
||||
# If assigning to `returned`, proactively flush
|
||||
#' @field returned The value returned from the module
|
||||
returned = function(value){
|
||||
if(missing(value)){
|
||||
return(private$returnedVal)
|
||||
}
|
||||
# When you assign to returned, that implies that you just ran
|
||||
# the module. So we should proactively flush. We have to do this
|
||||
# here since flush is private.
|
||||
private$returnedVal <- value
|
||||
private$flush()
|
||||
},
|
||||
#' @field request An empty environment where the request should be. The request isn't meaningfully mocked currently.
|
||||
request = function(value) {
|
||||
if (!missing(value)){
|
||||
stop("session$request can't be assigned to")
|
||||
}
|
||||
warning("session$request doesn't currently simulate a realistic request on MockShinySession")
|
||||
new.env(parent=emptyenv())
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
183
R/modal.R
Normal file
183
R/modal.R
Normal file
@@ -0,0 +1,183 @@
|
||||
#' Show or remove a modal dialog
|
||||
#'
|
||||
#' This causes a modal dialog to be displayed in the client browser, and is
|
||||
#' typically used with [modalDialog()].
|
||||
#'
|
||||
#' @param ui UI content to show in the modal.
|
||||
#' @param session The `session` object passed to function given to
|
||||
#' `shinyServer`.
|
||||
#'
|
||||
#' @seealso [modalDialog()] for examples.
|
||||
#' @export
|
||||
showModal <- function(ui, session = getDefaultReactiveDomain()) {
|
||||
res <- processDeps(ui, session)
|
||||
|
||||
session$sendModal("show",
|
||||
list(
|
||||
html = res$html,
|
||||
deps = res$deps
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#' @rdname showModal
|
||||
#' @export
|
||||
removeModal <- function(session = getDefaultReactiveDomain()) {
|
||||
session$sendModal("remove", NULL)
|
||||
}
|
||||
|
||||
|
||||
#' Create a modal dialog UI
|
||||
#'
|
||||
#' This creates the UI for a modal dialog, using Bootstrap's modal class. Modals
|
||||
#' are typically used for showing important messages, or for presenting UI that
|
||||
#' requires input from the user, such as a username and password input.
|
||||
#'
|
||||
#' @param ... UI elements for the body of the modal dialog box.
|
||||
#' @param title An optional title for the dialog.
|
||||
#' @param footer UI for footer. Use `NULL` for no footer.
|
||||
#' @param size One of `"s"` for small, `"m"` (the default) for medium,
|
||||
#' or `"l"` for large.
|
||||
#' @param easyClose If `TRUE`, the modal dialog can be dismissed by
|
||||
#' clicking outside the dialog box, or be pressing the Escape key. If
|
||||
#' `FALSE` (the default), the modal dialog can't be dismissed in those
|
||||
#' ways; instead it must be dismissed by clicking on the dismiss button, or
|
||||
#' from a call to [removeModal()] on the server.
|
||||
#' @param fade If `FALSE`, the modal dialog will have no fade-in animation
|
||||
#' (it will simply appear rather than fade in to view).
|
||||
#'
|
||||
#' @examples
|
||||
#' if (interactive()) {
|
||||
#' # Display an important message that can be dismissed only by clicking the
|
||||
#' # dismiss button.
|
||||
#' shinyApp(
|
||||
#' ui = basicPage(
|
||||
#' actionButton("show", "Show modal dialog")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' observeEvent(input$show, {
|
||||
#' showModal(modalDialog(
|
||||
#' title = "Important message",
|
||||
#' "This is an important message!"
|
||||
#' ))
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # Display a message that can be dismissed by clicking outside the modal dialog,
|
||||
#' # or by pressing Esc.
|
||||
#' shinyApp(
|
||||
#' ui = basicPage(
|
||||
#' actionButton("show", "Show modal dialog")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' observeEvent(input$show, {
|
||||
#' showModal(modalDialog(
|
||||
#' title = "Somewhat important message",
|
||||
#' "This is a somewhat important message.",
|
||||
#' easyClose = TRUE,
|
||||
#' footer = NULL
|
||||
#' ))
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # Display a modal that requires valid input before continuing.
|
||||
#' shinyApp(
|
||||
#' ui = basicPage(
|
||||
#' actionButton("show", "Show modal dialog"),
|
||||
#' verbatimTextOutput("dataInfo")
|
||||
#' ),
|
||||
#'
|
||||
#' server = function(input, output) {
|
||||
#' # reactiveValues object for storing current data set.
|
||||
#' vals <- reactiveValues(data = NULL)
|
||||
#'
|
||||
#' # Return the UI for a modal dialog with data selection input. If 'failed' is
|
||||
#' # TRUE, then display a message that the previous value was invalid.
|
||||
#' dataModal <- function(failed = FALSE) {
|
||||
#' modalDialog(
|
||||
#' textInput("dataset", "Choose data set",
|
||||
#' placeholder = 'Try "mtcars" or "abc"'
|
||||
#' ),
|
||||
#' span('(Try the name of a valid data object like "mtcars", ',
|
||||
#' 'then a name of a non-existent object like "abc")'),
|
||||
#' if (failed)
|
||||
#' div(tags$b("Invalid name of data object", style = "color: red;")),
|
||||
#'
|
||||
#' footer = tagList(
|
||||
#' modalButton("Cancel"),
|
||||
#' actionButton("ok", "OK")
|
||||
#' )
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' # Show modal when button is clicked.
|
||||
#' observeEvent(input$show, {
|
||||
#' showModal(dataModal())
|
||||
#' })
|
||||
#'
|
||||
#' # When OK button is pressed, attempt to load the data set. If successful,
|
||||
#' # remove the modal. If not show another modal, but this time with a failure
|
||||
#' # message.
|
||||
#' observeEvent(input$ok, {
|
||||
#' # Check that data object exists and is data frame.
|
||||
#' if (!is.null(input$dataset) && nzchar(input$dataset) &&
|
||||
#' exists(input$dataset) && is.data.frame(get(input$dataset))) {
|
||||
#' vals$data <- get(input$dataset)
|
||||
#' removeModal()
|
||||
#' } else {
|
||||
#' showModal(dataModal(failed = TRUE))
|
||||
#' }
|
||||
#' })
|
||||
#'
|
||||
#' # Display information about selected data
|
||||
#' output$dataInfo <- renderPrint({
|
||||
#' if (is.null(vals$data))
|
||||
#' "No data selected"
|
||||
#' else
|
||||
#' summary(vals$data)
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#' @export
|
||||
modalDialog <- function(..., title = NULL, footer = modalButton("Dismiss"),
|
||||
size = c("m", "s", "l"), easyClose = FALSE, fade = TRUE) {
|
||||
|
||||
size <- match.arg(size)
|
||||
|
||||
cls <- if (fade) "modal fade" else "modal"
|
||||
div(id = "shiny-modal", class = cls, tabindex = "-1",
|
||||
`data-backdrop` = if (!easyClose) "static",
|
||||
`data-keyboard` = if (!easyClose) "false",
|
||||
|
||||
div(
|
||||
class = "modal-dialog",
|
||||
class = switch(size, s = "modal-sm", m = NULL, l = "modal-lg"),
|
||||
div(class = "modal-content",
|
||||
if (!is.null(title)) div(class = "modal-header",
|
||||
tags$h4(class = "modal-title", title)
|
||||
),
|
||||
div(class = "modal-body", ...),
|
||||
if (!is.null(footer)) div(class = "modal-footer", footer)
|
||||
)
|
||||
),
|
||||
tags$script("$('#shiny-modal').modal().focus();")
|
||||
)
|
||||
}
|
||||
|
||||
#' Create a button for a modal dialog
|
||||
#'
|
||||
#' When clicked, a `modalButton` will dismiss the modal dialog.
|
||||
#'
|
||||
#' @inheritParams actionButton
|
||||
#' @seealso [modalDialog()] for examples.
|
||||
#' @export
|
||||
modalButton <- function(label, icon = NULL) {
|
||||
tags$button(type = "button", class = "btn btn-default",
|
||||
`data-dismiss` = "modal", validateIcon(icon), label
|
||||
)
|
||||
}
|
||||
116
R/modules.R
116
R/modules.R
@@ -1,4 +1,4 @@
|
||||
# Creates an object whose $ and $<- pass through to the parent
|
||||
# Creates an object whose $ and [[ pass through to the parent
|
||||
# session, unless the name is matched in ..., in which case
|
||||
# that value is returned instead. (See Decorator pattern.)
|
||||
createSessionProxy <- function(parentSession, ...) {
|
||||
@@ -14,37 +14,127 @@ createSessionProxy <- function(parentSession, ...) {
|
||||
|
||||
#' @export
|
||||
`$.session_proxy` <- function(x, name) {
|
||||
if (name %in% names(x[["overrides"]]))
|
||||
x[["overrides"]][[name]]
|
||||
if (name %in% names(.subset2(x, "overrides")))
|
||||
.subset2(x, "overrides")[[name]]
|
||||
else
|
||||
x[["parent"]][[name]]
|
||||
.subset2(x, "parent")[[name]]
|
||||
}
|
||||
|
||||
#' @export
|
||||
`[[.session_proxy` <- `$.session_proxy`
|
||||
|
||||
|
||||
#' @export
|
||||
`$<-.session_proxy` <- function(x, name, value) {
|
||||
x[["parent"]][[name]] <- value
|
||||
x
|
||||
# this line allows users to write into session$userData
|
||||
# (e.g. it allows something like `session$userData$x <- TRUE`,
|
||||
# but not `session$userData <- TRUE`) from within a module
|
||||
# without any hacks (see PR #1732)
|
||||
if (identical(x[[name]], value)) return(x)
|
||||
stop("Attempted to assign value on session proxy.")
|
||||
}
|
||||
|
||||
#' Invoke a Shiny module
|
||||
`[[<-.session_proxy` <- `$<-.session_proxy`
|
||||
|
||||
|
||||
#' Shiny modules
|
||||
#'
|
||||
#' Shiny's module feature lets you break complicated UI and server logic into
|
||||
#' smaller, self-contained pieces. Compared to large monolithic Shiny apps,
|
||||
#' modules are easier to reuse and easier to reason about. See the article at
|
||||
#' \url{http://shiny.rstudio.com/articles/modules.html} to learn more.
|
||||
#' <http://shiny.rstudio.com/articles/modules.html> to learn more.
|
||||
#'
|
||||
#' @param module A Shiny module server function
|
||||
#' Starting in Shiny 1.5.0, we recommend using `moduleFunction` instead of
|
||||
#' `callModule`, because syntax is a little easier to understand.
|
||||
#'
|
||||
#' @param module A Shiny module server function.
|
||||
#' @param id An ID string that corresponds with the ID used to call the module's
|
||||
#' UI function
|
||||
#' @param ... Additional parameters to pass to module server function
|
||||
#' UI function.
|
||||
#' @param ... For `callModule`, additional parameters to pass to module server
|
||||
#' function.
|
||||
#' @param session Session from which to make a child scope (the default should
|
||||
#' almost always be used)
|
||||
#' almost always be used).
|
||||
#'
|
||||
#' @return The return value, if any, from executing the module server function
|
||||
#' @seealso \url{http://shiny.rstudio.com/articles/modules.html}
|
||||
#' @seealso <http://shiny.rstudio.com/articles/modules.html>
|
||||
#'
|
||||
#' @examples
|
||||
#' # Define the UI for a module
|
||||
#' counterUI <- function(id, label = "Counter") {
|
||||
#' ns <- NS(id)
|
||||
#' tagList(
|
||||
#' actionButton(ns("button"), label = label),
|
||||
#' verbatimTextOutput(ns("out"))
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' # Define the server logic for a module
|
||||
#' counterServer <- function(id) {
|
||||
#' moduleServer(id, function(input, output, session) {
|
||||
#' count <- reactiveVal(0)
|
||||
#' observeEvent(input$button, {
|
||||
#' count(count() + 1)
|
||||
#' })
|
||||
#' output$out <- renderText({
|
||||
#' count()
|
||||
#' })
|
||||
#' count
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' # Use the module in an app
|
||||
#' ui <- fluidPage(
|
||||
#' counterUI("counter1", "Counter #1"),
|
||||
#' counterUI("counter2", "Counter #2")
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' counterServer("counter1")
|
||||
#' counterServer("counter2")
|
||||
#' }
|
||||
#' if (interactive()) {
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#'
|
||||
#'
|
||||
#' # If you want to pass extra parameters to the module's server logic, you can
|
||||
#' # add them to your function. In this case `prefix` is text that will be
|
||||
#' # printed before the count.
|
||||
#' counterServer2 <- function(id, prefix = NULL) {
|
||||
#' moduleServer(id, function(input, output, session) {
|
||||
#' count <- reactiveVal(0)
|
||||
#' observeEvent(input$button, {
|
||||
#' count(count() + 1)
|
||||
#' })
|
||||
#' output$out <- renderText({
|
||||
#' paste0(prefix, count())
|
||||
#' })
|
||||
#' count
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' counterUI("counter", "Counter"),
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' counterServer2("counter", "The current count is: ")
|
||||
#' }
|
||||
#' if (interactive()) {
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @export
|
||||
moduleServer <- function(id, module, session = getDefaultReactiveDomain()) {
|
||||
callModule(module, id, session = session)
|
||||
}
|
||||
|
||||
|
||||
#' @rdname moduleServer
|
||||
#' @export
|
||||
callModule <- function(module, id, ..., session = getDefaultReactiveDomain()) {
|
||||
if (!inherits(session, c("ShinySession", "session_proxy", "MockShinySession"))) {
|
||||
stop("session must be a ShinySession or session_proxy object.")
|
||||
}
|
||||
childScope <- session$makeScope(id)
|
||||
|
||||
withReactiveDomain(childScope, {
|
||||
|
||||
106
R/notifications.R
Normal file
106
R/notifications.R
Normal file
@@ -0,0 +1,106 @@
|
||||
#' Show or remove a notification
|
||||
#'
|
||||
#' These functions show and remove notifications in a Shiny application.
|
||||
#'
|
||||
#' @param ui Content of message.
|
||||
#' @param action Message content that represents an action. For example, this
|
||||
#' could be a link that the user can click on. This is separate from `ui`
|
||||
#' so customized layouts can handle the main notification content separately
|
||||
#' from action content.
|
||||
#' @param duration Number of seconds to display the message before it
|
||||
#' disappears. Use `NULL` to make the message not automatically
|
||||
#' disappear.
|
||||
#' @param closeButton If `TRUE`, display a button which will make the
|
||||
#' notification disappear when clicked. If `FALSE` do not display.
|
||||
#' @param id A unique identifier for the notification.
|
||||
#'
|
||||
#' `id` is optional for `showNotification()`: Shiny will automatically create
|
||||
#' one if needed. If you do supply it, Shiny will update an existing
|
||||
#' notification if it exists, otherwise it will create a new one.
|
||||
#'
|
||||
#' `id` is required for `removeNotification()`.
|
||||
#' @param type A string which controls the color of the notification. One of
|
||||
#' "default" (gray), "message" (blue), "warning" (yellow), or "error" (red).
|
||||
#' @param session Session object to send notification to.
|
||||
#'
|
||||
#' @return An ID for the notification.
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' # Show a message when button is clicked
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' actionButton("show", "Show")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' observeEvent(input$show, {
|
||||
#' showNotification("Message text",
|
||||
#' action = a(href = "javascript:location.reload();", "Reload page")
|
||||
#' )
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' # App with show and remove buttons
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' actionButton("show", "Show"),
|
||||
#' actionButton("remove", "Remove")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' # A queue of notification IDs
|
||||
#' ids <- character(0)
|
||||
#' # A counter
|
||||
#' n <- 0
|
||||
#'
|
||||
#' observeEvent(input$show, {
|
||||
#' # Save the ID for removal later
|
||||
#' id <- showNotification(paste("Message", n), duration = NULL)
|
||||
#' ids <<- c(ids, id)
|
||||
#' n <<- n + 1
|
||||
#' })
|
||||
#'
|
||||
#' observeEvent(input$remove, {
|
||||
#' if (length(ids) > 0)
|
||||
#' removeNotification(ids[1])
|
||||
#' ids <<- ids[-1]
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#' @export
|
||||
showNotification <- function(ui, action = NULL, duration = 5,
|
||||
closeButton = TRUE, id = NULL,
|
||||
type = c("default", "message", "warning", "error"),
|
||||
session = getDefaultReactiveDomain())
|
||||
{
|
||||
|
||||
if (is.null(id))
|
||||
id <- createUniqueId(8)
|
||||
|
||||
res <- processDeps(ui, session)
|
||||
actionRes <- processDeps(action, session)
|
||||
|
||||
session$sendNotification("show",
|
||||
list(
|
||||
html = res$html,
|
||||
action = actionRes$html,
|
||||
deps = c(res$deps, actionRes$deps),
|
||||
duration = if (!is.null(duration)) duration * 1000,
|
||||
closeButton = closeButton,
|
||||
id = id,
|
||||
type = match.arg(type)
|
||||
)
|
||||
)
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
#' @rdname showNotification
|
||||
#' @export
|
||||
removeNotification <- function(id, session = getDefaultReactiveDomain()) {
|
||||
force(id)
|
||||
session$sendNotification("remove", id)
|
||||
id
|
||||
}
|
||||
232
R/progress.R
232
R/progress.R
@@ -3,63 +3,41 @@
|
||||
#' Reports progress to the user during long-running operations.
|
||||
#'
|
||||
#' This package exposes two distinct programming APIs for working with
|
||||
#' progress. \code{\link{withProgress}} and \code{\link{setProgress}}
|
||||
#' progress. [withProgress()] and [setProgress()]
|
||||
#' together provide a simple function-based interface, while the
|
||||
#' \code{Progress} reference class provides an object-oriented API.
|
||||
#' `Progress` reference class provides an object-oriented API.
|
||||
#'
|
||||
#' Instantiating a \code{Progress} object causes a progress panel to be
|
||||
#' created, and it will be displayed the first time the \code{set}
|
||||
#' method is called. Calling \code{close} will cause the progress panel
|
||||
#' Instantiating a `Progress` object causes a progress panel to be
|
||||
#' created, and it will be displayed the first time the `set`
|
||||
#' method is called. Calling `close` will cause the progress panel
|
||||
#' to be removed.
|
||||
#'
|
||||
#' \strong{Methods}
|
||||
#' \describe{
|
||||
#' \item{\code{initialize(session, min = 0, max = 1)}}{
|
||||
#' Creates a new progress panel (but does not display it).
|
||||
#' }
|
||||
#' \item{\code{set(value = NULL, message = NULL, detail = NULL)}}{
|
||||
#' Updates the progress panel. When called the first time, the
|
||||
#' progress panel is displayed.
|
||||
#' }
|
||||
#' \item{\code{inc(amount = 0.1, message = NULL, detail = NULL)}}{
|
||||
#' Like \code{set}, this updates the progress panel. The difference is
|
||||
#' that \code{inc} increases the progress bar by \code{amount}, instead
|
||||
#' of setting it to a specific value.
|
||||
#' }
|
||||
#' \item{\code{close()}}{
|
||||
#' Removes the progress panel. Future calls to \code{set} and
|
||||
#' \code{close} will be ignored.
|
||||
#' }
|
||||
#' }
|
||||
#' As of version 0.14, the progress indicators use Shiny's new notification API.
|
||||
#' If you want to use the old styling (for example, you may have used customized
|
||||
#' CSS), you can use `style="old"` each time you call
|
||||
#' `Progress$new()`. If you don't want to set the style each time
|
||||
#' `Progress$new` is called, you can instead call
|
||||
#' [`shinyOptions(progress.style="old")`][shinyOptions] just once, inside the server
|
||||
#' function.
|
||||
#'
|
||||
#' @param session The Shiny session object, as provided by
|
||||
#' \code{shinyServer} to the server function.
|
||||
#' @param min The value that represents the starting point of the
|
||||
#' progress bar. Must be less tham \code{max}.
|
||||
#' @param max The value that represents the end of the progress bar.
|
||||
#' Must be greater than \code{min}.
|
||||
#' @param message A single-element character vector; the message to be
|
||||
#' displayed to the user, or \code{NULL} to hide the current message
|
||||
#' (if any).
|
||||
#' @param detail A single-element character vector; the detail message
|
||||
#' to be displayed to the user, or \code{NULL} to hide the current
|
||||
#' detail message (if any). The detail message will be shown with a
|
||||
#' de-emphasized appearance relative to \code{message}.
|
||||
#' @param value A numeric value at which to set
|
||||
#' the progress bar, relative to \code{min} and \code{max}.
|
||||
#' \code{NULL} hides the progress bar, if it is currently visible.
|
||||
#' @param amount Single-element numeric vector; the value at which to set
|
||||
#' the progress bar, relative to \code{min} and \code{max}.
|
||||
#' \code{NULL} hides the progress bar, if it is currently visible.
|
||||
#' @param amount For the \code{inc()} method, a numeric value to increment the
|
||||
#' progress bar.
|
||||
#' displayed to the user, or `NULL` to hide the current message (if any).
|
||||
#' @param detail A single-element character vector; the detail message to be
|
||||
#' displayed to the user, or `NULL` to hide the current detail message (if
|
||||
#' any). The detail message will be shown with a de-emphasized appearance
|
||||
#' relative to `message`.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' # server.R
|
||||
#' shinyServer(function(input, output, session) {
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' plotOutput("plot")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' output$plot <- renderPlot({
|
||||
#' progress <- shiny::Progress$new(session, min=1, max=15)
|
||||
#' progress <- Progress$new(session, min=1, max=15)
|
||||
#' on.exit(progress$close())
|
||||
#'
|
||||
#' progress$set(message = 'Calculation in progress',
|
||||
@@ -71,87 +49,122 @@
|
||||
#' }
|
||||
#' plot(cars)
|
||||
#' })
|
||||
#' })
|
||||
#' }
|
||||
#' @seealso \code{\link{withProgress}}
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @seealso [withProgress()]
|
||||
#' @format NULL
|
||||
#' @usage NULL
|
||||
#' @export
|
||||
Progress <- R6Class(
|
||||
'Progress',
|
||||
portable = TRUE,
|
||||
public = list(
|
||||
|
||||
initialize = function(session = getDefaultReactiveDomain(), min = 0, max = 1) {
|
||||
#' @description Creates a new progress panel (but does not display it).
|
||||
#' @param session The Shiny session object, as provided by `shinyServer` to
|
||||
#' the server function.
|
||||
#' @param min The value that represents the starting point of the progress
|
||||
#' bar. Must be less than `max`.
|
||||
#' @param max The value that represents the end of the progress bar. Must be
|
||||
#' greater than `min`.
|
||||
#' @param style Progress display style. If `"notification"` (the default),
|
||||
#' the progress indicator will show using Shiny's notification API. If
|
||||
#' `"old"`, use the same HTML and CSS used in Shiny 0.13.2 and below (this
|
||||
#' is for backward-compatibility).
|
||||
initialize = function(session = getDefaultReactiveDomain(),
|
||||
min = 0, max = 1,
|
||||
style = getShinyOption("progress.style", default = "notification"))
|
||||
{
|
||||
if (is.null(session$progressStack))
|
||||
stop("'session' is not a ShinySession object.")
|
||||
|
||||
private$session <- session
|
||||
private$id <- paste(as.character(as.raw(stats::runif(8, min=0, max=255))), collapse='')
|
||||
private$id <- createUniqueId(8)
|
||||
private$min <- min
|
||||
private$max <- max
|
||||
private$value <- NULL
|
||||
private$style <- match.arg(style, choices = c("notification", "old"))
|
||||
private$closed <- FALSE
|
||||
|
||||
session$sendProgress('open', list(id = private$id))
|
||||
session$sendProgress('open', list(id = private$id, style = private$style))
|
||||
},
|
||||
|
||||
#' @description Updates the progress panel. When called the first time, the
|
||||
#' progress panel is displayed.
|
||||
#' @param value Single-element numeric vector; the value at which to set the
|
||||
#' progress bar, relative to `min` and `max`. `NULL` hides the progress
|
||||
#' bar, if it is currently visible.
|
||||
set = function(value = NULL, message = NULL, detail = NULL) {
|
||||
if (private$closed) {
|
||||
warning("Attempting to set progress, but progress already closed.")
|
||||
return()
|
||||
}
|
||||
|
||||
if (is.null(value) || is.na(value)) {
|
||||
if (is.null(value) || is.na(value))
|
||||
value <- NULL
|
||||
} else {
|
||||
|
||||
if (!is.null(value)) {
|
||||
private$value <- value
|
||||
# Normalize value to number between 0 and 1
|
||||
value <- min(1, max(0, (value - private$min) / (private$max - private$min)))
|
||||
}
|
||||
|
||||
private$value <- value
|
||||
|
||||
data <- dropNulls(list(
|
||||
id = private$id,
|
||||
message = message,
|
||||
detail = detail,
|
||||
value = value
|
||||
value = value,
|
||||
style = private$style
|
||||
))
|
||||
|
||||
private$session$sendProgress('update', data)
|
||||
private$session$sendProgress('update', data)
|
||||
},
|
||||
|
||||
#' @description Like `set`, this updates the progress panel. The difference
|
||||
#' is that `inc` increases the progress bar by `amount`, instead of
|
||||
#' setting it to a specific value.
|
||||
#' @param amount For the `inc()` method, a numeric value to increment the
|
||||
#' progress bar.
|
||||
inc = function(amount = 0.1, message = NULL, detail = NULL) {
|
||||
value <- min(self$getValue() + amount, private$max)
|
||||
if (is.null(private$value))
|
||||
private$value <- private$min
|
||||
|
||||
value <- min(private$value + amount, private$max)
|
||||
self$set(value, message, detail)
|
||||
},
|
||||
|
||||
#' @description Returns the minimum value.
|
||||
getMin = function() private$min,
|
||||
|
||||
#' @description Returns the maximum value.
|
||||
getMax = function() private$max,
|
||||
|
||||
# Return value (not the normalized 0-1 value, but in the original range)
|
||||
getValue = function() {
|
||||
private$value * (private$max - private$min) + private$min
|
||||
},
|
||||
#' @description Returns the current value.
|
||||
getValue = function() private$value,
|
||||
|
||||
#' @description Removes the progress panel. Future calls to `set` and
|
||||
#' `close` will be ignored.
|
||||
close = function() {
|
||||
if (private$closed) {
|
||||
warning("Attempting to close progress, but progress already closed.")
|
||||
return()
|
||||
}
|
||||
|
||||
private$session$sendProgress('close', list(id = private$id))
|
||||
private$session$sendProgress('close',
|
||||
list(id = private$id, style = private$style)
|
||||
)
|
||||
private$closed <- TRUE
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
session = 'environment',
|
||||
session = 'ShinySession',
|
||||
id = character(0),
|
||||
min = numeric(0),
|
||||
max = numeric(0),
|
||||
value = NULL,
|
||||
style = character(0),
|
||||
value = numeric(0),
|
||||
closed = logical(0)
|
||||
)
|
||||
)
|
||||
@@ -161,52 +174,69 @@ Progress <- R6Class(
|
||||
#' Reports progress to the user during long-running operations.
|
||||
#'
|
||||
#' This package exposes two distinct programming APIs for working with progress.
|
||||
#' Using \code{withProgress} with \code{incProgress} or \code{setProgress}
|
||||
#' provide a simple function-based interface, while the \code{\link{Progress}}
|
||||
#' Using `withProgress` with `incProgress` or `setProgress`
|
||||
#' provide a simple function-based interface, while the [Progress()]
|
||||
#' reference class provides an object-oriented API.
|
||||
#'
|
||||
#' Use \code{withProgress} to wrap the scope of your work; doing so will cause a
|
||||
#' Use `withProgress` to wrap the scope of your work; doing so will cause a
|
||||
#' new progress panel to be created, and it will be displayed the first time
|
||||
#' \code{incProgress} or \code{setProgress} are called. When \code{withProgress}
|
||||
#' `incProgress` or `setProgress` are called. When `withProgress`
|
||||
#' exits, the corresponding progress panel will be removed.
|
||||
#'
|
||||
#' The \code{incProgress} function increments the status bar by a specified
|
||||
#' amount, whereas the \code{setProgress} function sets it to a specific value,
|
||||
#' The `incProgress` function increments the status bar by a specified
|
||||
#' amount, whereas the `setProgress` function sets it to a specific value,
|
||||
#' and can also set the text displayed.
|
||||
#'
|
||||
#' Generally, \code{withProgress}/\code{incProgress}/\code{setProgress} should
|
||||
#' Generally, `withProgress`/`incProgress`/`setProgress` should
|
||||
#' be sufficient; the exception is if the work to be done is asynchronous (this
|
||||
#' is not common) or otherwise cannot be encapsulated by a single scope. In that
|
||||
#' case, you can use the \code{Progress} reference class.
|
||||
#' case, you can use the `Progress` reference class.
|
||||
#'
|
||||
#' @param session The Shiny session object, as provided by \code{shinyServer} to
|
||||
#' As of version 0.14, the progress indicators use Shiny's new notification API.
|
||||
#' If you want to use the old styling (for example, you may have used customized
|
||||
#' CSS), you can use `style="old"` each time you call
|
||||
#' `withProgress()`. If you don't want to set the style each time
|
||||
#' `withProgress` is called, you can instead call
|
||||
#' [`shinyOptions(progress.style="old")`][shinyOptions] just once, inside the server
|
||||
#' function.
|
||||
#'
|
||||
#' @param session The Shiny session object, as provided by `shinyServer` to
|
||||
#' the server function. The default is to automatically find the session by
|
||||
#' using the current reactive domain.
|
||||
#' @param expr The work to be done. This expression should contain calls to
|
||||
#' \code{setProgress}.
|
||||
#' `setProgress`.
|
||||
#' @param min The value that represents the starting point of the progress bar.
|
||||
#' Must be less tham \code{max}. Default is 0.
|
||||
#' Must be less tham `max`. Default is 0.
|
||||
#' @param max The value that represents the end of the progress bar. Must be
|
||||
#' greater than \code{min}. Default is 1.
|
||||
#' @param amount For \code{incProgress}, the amount to increment the status bar.
|
||||
#' greater than `min`. Default is 1.
|
||||
#' @param amount For `incProgress`, the amount to increment the status bar.
|
||||
#' Default is 0.1.
|
||||
#' @param env The environment in which \code{expr} should be evaluated.
|
||||
#' @param quoted Whether \code{expr} is a quoted expression (this is not
|
||||
#' @param env The environment in which `expr` should be evaluated.
|
||||
#' @param quoted Whether `expr` is a quoted expression (this is not
|
||||
#' common).
|
||||
#' @param message A single-element character vector; the message to be displayed
|
||||
#' to the user, or \code{NULL} to hide the current message (if any).
|
||||
#' to the user, or `NULL` to hide the current message (if any).
|
||||
#' @param detail A single-element character vector; the detail message to be
|
||||
#' displayed to the user, or \code{NULL} to hide the current detail message
|
||||
#' displayed to the user, or `NULL` to hide the current detail message
|
||||
#' (if any). The detail message will be shown with a de-emphasized appearance
|
||||
#' relative to \code{message}.
|
||||
#' relative to `message`.
|
||||
#' @param style Progress display style. If `"notification"` (the default),
|
||||
#' the progress indicator will show using Shiny's notification API. If
|
||||
#' `"old"`, use the same HTML and CSS used in Shiny 0.13.2 and below
|
||||
#' (this is for backward-compatibility).
|
||||
#' @param value Single-element numeric vector; the value at which to set the
|
||||
#' progress bar, relative to \code{min} and \code{max}. \code{NULL} hides the
|
||||
#' progress bar, if it is currently visible.
|
||||
#' progress bar, relative to `min` and `max`.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' # server.R
|
||||
#' shinyServer(function(input, output) {
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' plotOutput("plot")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' output$plot <- renderPlot({
|
||||
#' withProgress(message = 'Calculation in progress',
|
||||
#' detail = 'This may take a while...', value = 0, {
|
||||
@@ -217,16 +247,20 @@ Progress <- R6Class(
|
||||
#' })
|
||||
#' plot(cars)
|
||||
#' })
|
||||
#' })
|
||||
#' }
|
||||
#' @seealso \code{\link{Progress}}
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @seealso [Progress()]
|
||||
#' @rdname withProgress
|
||||
#' @export
|
||||
withProgress <- function(expr, min = 0, max = 1,
|
||||
value = min + (max - min) * 0.1,
|
||||
message = NULL, detail = NULL,
|
||||
session = getDefaultReactiveDomain(),
|
||||
env = parent.frame(), quoted = FALSE) {
|
||||
value = min + (max - min) * 0.1,
|
||||
message = NULL, detail = NULL,
|
||||
style = getShinyOption("progress.style", default = "notification"),
|
||||
session = getDefaultReactiveDomain(),
|
||||
env = parent.frame(), quoted = FALSE)
|
||||
{
|
||||
|
||||
if (!quoted)
|
||||
expr <- substitute(expr)
|
||||
@@ -234,7 +268,9 @@ withProgress <- function(expr, min = 0, max = 1,
|
||||
if (is.null(session$progressStack))
|
||||
stop("'session' is not a ShinySession object.")
|
||||
|
||||
p <- Progress$new(session, min = min, max = max)
|
||||
style <- match.arg(style, c("notification", "old"))
|
||||
|
||||
p <- Progress$new(session, min = min, max = max, style = style)
|
||||
|
||||
session$progressStack$push(p)
|
||||
on.exit({
|
||||
|
||||
145
R/react.R
145
R/react.R
@@ -1,38 +1,80 @@
|
||||
processId <- local({
|
||||
# pid is not sufficient to uniquely identify a process, because
|
||||
# distributed futures span machines which could introduce pid
|
||||
# collisions.
|
||||
cached <- NULL
|
||||
function() {
|
||||
if (is.null(cached)) {
|
||||
cached <<- digest::digest(list(
|
||||
Sys.info(),
|
||||
Sys.time()
|
||||
))
|
||||
}
|
||||
# Sys.getpid() cannot be cached because forked children will
|
||||
# then have the same processId as their parents.
|
||||
paste(cached, Sys.getpid())
|
||||
}
|
||||
})
|
||||
|
||||
#' @include graph.R
|
||||
Context <- R6Class(
|
||||
'Context',
|
||||
portable = FALSE,
|
||||
class = FALSE,
|
||||
public = list(
|
||||
id = character(0),
|
||||
.reactId = character(0),
|
||||
.reactType = "other",
|
||||
.label = character(0), # For debug purposes
|
||||
.invalidated = FALSE,
|
||||
.invalidateCallbacks = list(),
|
||||
.flushCallbacks = list(),
|
||||
.domain = NULL,
|
||||
.pid = NULL,
|
||||
.weak = NULL,
|
||||
|
||||
initialize = function(domain, label='', type='other', prevId='') {
|
||||
id <<- .getReactiveEnvironment()$nextId()
|
||||
initialize = function(
|
||||
domain, label='', type='other', prevId='',
|
||||
reactId = rLog$noReactId,
|
||||
id = .getReactiveEnvironment()$nextId(), # For dummy context
|
||||
weak = FALSE
|
||||
) {
|
||||
id <<- id
|
||||
.label <<- label
|
||||
.domain <<- domain
|
||||
.graphCreateContext(id, label, type, prevId, domain)
|
||||
.pid <<- processId()
|
||||
.reactId <<- reactId
|
||||
.reactType <<- type
|
||||
.weak <<- weak
|
||||
rLog$createContext(id, label, type, prevId, domain)
|
||||
},
|
||||
run = function(func) {
|
||||
"Run the provided function under this context."
|
||||
withReactiveDomain(.domain, {
|
||||
env <- .getReactiveEnvironment()
|
||||
.graphEnterContext(id)
|
||||
on.exit(.graphExitContext(id), add = TRUE)
|
||||
env$runWith(self, func)
|
||||
|
||||
promises::with_promise_domain(reactivePromiseDomain(), {
|
||||
withReactiveDomain(.domain, {
|
||||
env <- .getReactiveEnvironment()
|
||||
rLog$enter(.reactId, id, .reactType, .domain)
|
||||
on.exit(rLog$exit(.reactId, id, .reactType, .domain), add = TRUE)
|
||||
env$runWith(self, func)
|
||||
})
|
||||
})
|
||||
},
|
||||
invalidate = function() {
|
||||
"Invalidate this context. It will immediately call the callbacks
|
||||
that have been registered with onInvalidate()."
|
||||
|
||||
if (!identical(.pid, processId())) {
|
||||
stop("Reactive context was created in one process and invalidated from another")
|
||||
}
|
||||
|
||||
if (.invalidated)
|
||||
return()
|
||||
.invalidated <<- TRUE
|
||||
|
||||
.graphInvalidate(id, .domain)
|
||||
rLog$invalidateStart(.reactId, id, .reactType, .domain)
|
||||
on.exit(rLog$invalidateEnd(.reactId, id, .reactType, .domain), add = TRUE)
|
||||
|
||||
lapply(.invalidateCallbacks, function(func) {
|
||||
func()
|
||||
})
|
||||
@@ -43,6 +85,11 @@ Context <- R6Class(
|
||||
"Register a function to be called when this context is invalidated.
|
||||
If this context is already invalidated, the function is called
|
||||
immediately."
|
||||
|
||||
if (!identical(.pid, processId())) {
|
||||
stop("Reactive context was created in one process and accessed from another")
|
||||
}
|
||||
|
||||
if (.invalidated)
|
||||
func()
|
||||
else
|
||||
@@ -52,9 +99,6 @@ Context <- R6Class(
|
||||
addPendingFlush = function(priority) {
|
||||
"Tell the reactive environment that this context should be flushed the
|
||||
next time flushReact() called."
|
||||
if (!is.null(.domain)) {
|
||||
.domain$incrementBusyCount()
|
||||
}
|
||||
.getReactiveEnvironment()$addPendingFlush(self, priority)
|
||||
},
|
||||
onFlush = function(func) {
|
||||
@@ -64,15 +108,12 @@ Context <- R6Class(
|
||||
executeFlushCallbacks = function() {
|
||||
"For internal use only."
|
||||
|
||||
on.exit({
|
||||
if (!is.null(.domain)) {
|
||||
.domain$decrementBusyCount()
|
||||
}
|
||||
}, add = TRUE)
|
||||
|
||||
lapply(.flushCallbacks, function(flushCallback) {
|
||||
flushCallback()
|
||||
})
|
||||
},
|
||||
isWeak = function() {
|
||||
.weak
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -115,16 +156,27 @@ ReactiveEnvironment <- R6Class(
|
||||
addPendingFlush = function(ctx, priority) {
|
||||
.pendingFlush$enqueue(ctx, priority)
|
||||
},
|
||||
hasPendingFlush = function() {
|
||||
return(!.pendingFlush$isEmpty())
|
||||
},
|
||||
# Returns TRUE if anything was actually called
|
||||
flush = function() {
|
||||
# If nothing to flush, exit early
|
||||
if (!hasPendingFlush()) return(invisible(FALSE))
|
||||
# If already in a flush, don't start another one
|
||||
if (.inFlush) return()
|
||||
if (.inFlush) return(invisible(FALSE))
|
||||
.inFlush <<- TRUE
|
||||
on.exit(.inFlush <<- FALSE)
|
||||
on.exit({
|
||||
.inFlush <<- FALSE
|
||||
rLog$idle(domain = NULL)
|
||||
})
|
||||
|
||||
while (!.pendingFlush$isEmpty()) {
|
||||
while (hasPendingFlush()) {
|
||||
ctx <- .pendingFlush$dequeue()
|
||||
ctx$executeFlushCallbacks()
|
||||
}
|
||||
|
||||
invisible(TRUE)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -138,9 +190,10 @@ ReactiveEnvironment <- R6Class(
|
||||
}
|
||||
})
|
||||
|
||||
# Causes any pending invalidations to run.
|
||||
# Causes any pending invalidations to run. Returns TRUE if any invalidations
|
||||
# were pending (i.e. if work was actually done).
|
||||
flushReact <- function() {
|
||||
.getReactiveEnvironment()$flush()
|
||||
return(.getReactiveEnvironment()$flush())
|
||||
}
|
||||
|
||||
# Retrieves the current reactive context, or errors if there is no reactive
|
||||
@@ -148,15 +201,41 @@ flushReact <- function() {
|
||||
getCurrentContext <- function() {
|
||||
.getReactiveEnvironment()$currentContext()
|
||||
}
|
||||
hasCurrentContext <- function() {
|
||||
!is.null(.getReactiveEnvironment()$.currentContext)
|
||||
}
|
||||
|
||||
getDummyContext <- function() {}
|
||||
local({
|
||||
dummyContext <- NULL
|
||||
getDummyContext <<- function() {
|
||||
if (is.null(dummyContext)) {
|
||||
dummyContext <<- Context$new(getDefaultReactiveDomain(), '[none]',
|
||||
type='isolate')
|
||||
}
|
||||
return(dummyContext)
|
||||
getDummyContext <- function() {
|
||||
Context$new(
|
||||
getDefaultReactiveDomain(), '[none]', type = 'isolate',
|
||||
id = "Dummy", reactId = rLog$dummyReactId
|
||||
)
|
||||
}
|
||||
|
||||
wrapForContext <- function(func, ctx) {
|
||||
force(func)
|
||||
force(ctx)
|
||||
|
||||
function(...) {
|
||||
ctx$run(function() {
|
||||
captureStackTraces(
|
||||
func(...)
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
reactivePromiseDomain <- function() {
|
||||
promises::new_promise_domain(
|
||||
wrapOnFulfilled = function(onFulfilled) {
|
||||
force(onFulfilled)
|
||||
ctx <- getCurrentContext()
|
||||
wrapForContext(onFulfilled, ctx)
|
||||
},
|
||||
wrapOnRejected = function(onRejected) {
|
||||
force(onRejected)
|
||||
ctx <- getCurrentContext()
|
||||
wrapForContext(onRejected, ctx)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,11 +42,11 @@ NULL
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
createMockDomain <- function() {
|
||||
callbacks <- list()
|
||||
callbacks <- Callbacks$new()
|
||||
ended <- FALSE
|
||||
domain <- new.env(parent = emptyenv())
|
||||
domain$onEnded <- function(callback) {
|
||||
callbacks <<- c(callbacks, callback)
|
||||
return(callbacks$register(callback))
|
||||
}
|
||||
domain$isEnded <- function() {
|
||||
ended
|
||||
@@ -55,7 +55,7 @@ createMockDomain <- function() {
|
||||
domain$end <- function() {
|
||||
if (!ended) {
|
||||
ended <<- TRUE
|
||||
lapply(callbacks, do.call, list())
|
||||
callbacks$invoke()
|
||||
}
|
||||
invisible()
|
||||
}
|
||||
@@ -95,11 +95,7 @@ getDefaultReactiveDomain <- function() {
|
||||
#' @rdname domains
|
||||
#' @export
|
||||
withReactiveDomain <- function(domain, expr) {
|
||||
oldValue <- .globals$domain
|
||||
.globals$domain <- domain
|
||||
on.exit(.globals$domain <- oldValue)
|
||||
|
||||
expr
|
||||
promises::with_promise_domain(createVarPromiseDomain(.globals, "domain", domain), expr)
|
||||
}
|
||||
|
||||
#
|
||||
@@ -172,31 +168,31 @@ onReactiveDomainEnded <- function(domain, callback, failIfNull = FALSE) {
|
||||
#' them ends) and error handling.
|
||||
#'
|
||||
#' At any given time, there can be either a single "default" reactive domain
|
||||
#' object, or none (i.e. the reactive domain object is \code{NULL}). You can
|
||||
#' object, or none (i.e. the reactive domain object is `NULL`). You can
|
||||
#' access the current default reactive domain by calling
|
||||
#' \code{getDefaultReactiveDomain}.
|
||||
#' `getDefaultReactiveDomain`.
|
||||
#'
|
||||
#' Unless you specify otherwise, newly created observers and reactive
|
||||
#' expressions will be assigned to the current default domain (if any). You can
|
||||
#' override this assignment by providing an explicit \code{domain} argument to
|
||||
#' \code{\link{reactive}} or \code{\link{observe}}.
|
||||
#' override this assignment by providing an explicit `domain` argument to
|
||||
#' [reactive()] or [observe()].
|
||||
#'
|
||||
#' For advanced usage, it's possible to override the default domain using
|
||||
#' \code{withReactiveDomain}. The \code{domain} argument will be made the
|
||||
#' default domain while \code{expr} is evaluated.
|
||||
#' `withReactiveDomain`. The `domain` argument will be made the
|
||||
#' default domain while `expr` is evaluated.
|
||||
#'
|
||||
#' Implementers of new reactive primitives can use \code{onReactiveDomainEnded}
|
||||
#' Implementers of new reactive primitives can use `onReactiveDomainEnded`
|
||||
#' as a convenience function for registering callbacks. If the reactive domain
|
||||
#' is \code{NULL} and \code{failIfNull} is \code{FALSE}, then the callback will
|
||||
#' is `NULL` and `failIfNull` is `FALSE`, then the callback will
|
||||
#' never be invoked.
|
||||
#'
|
||||
#' @name domains
|
||||
#' @param domain A valid domain object (for example, a Shiny session), or
|
||||
#' \code{NULL}
|
||||
#' @param expr An expression to evaluate under \code{domain}
|
||||
#' `NULL`
|
||||
#' @param expr An expression to evaluate under `domain`
|
||||
#' @param callback A callback function to be invoked
|
||||
#' @param failIfNull If \code{TRUE} then an error is given if the \code{domain}
|
||||
#' is \code{NULL}
|
||||
#' @param failIfNull If `TRUE` then an error is given if the `domain`
|
||||
#' is `NULL`
|
||||
NULL
|
||||
|
||||
#
|
||||
|
||||
1670
R/reactives.R
1670
R/reactives.R
File diff suppressed because it is too large
Load Diff
590
R/render-cached-plot.R
Normal file
590
R/render-cached-plot.R
Normal file
@@ -0,0 +1,590 @@
|
||||
#' Plot output with cached images
|
||||
#'
|
||||
#' Renders a reactive plot, with plot images cached to disk.
|
||||
#'
|
||||
#' `expr` is an expression that generates a plot, similar to that in
|
||||
#' `renderPlot`. Unlike with `renderPlot`, this expression does not
|
||||
#' take reactive dependencies. It is re-executed only when the cache key
|
||||
#' changes.
|
||||
#'
|
||||
#' `cacheKeyExpr` is an expression which, when evaluated, returns an object
|
||||
#' which will be serialized and hashed using the [digest::digest()]
|
||||
#' function to generate a string that will be used as a cache key. This key is
|
||||
#' used to identify the contents of the plot: if the cache key is the same as a
|
||||
#' previous time, it assumes that the plot is the same and can be retrieved from
|
||||
#' the cache.
|
||||
#'
|
||||
#' This `cacheKeyExpr` is reactive, and so it will be re-evaluated when any
|
||||
#' upstream reactives are invalidated. This will also trigger re-execution of
|
||||
#' the plotting expression, `expr`.
|
||||
#'
|
||||
#' The key should consist of "normal" R objects, like vectors and lists. Lists
|
||||
#' should in turn contain other normal R objects. If the key contains
|
||||
#' environments, external pointers, or reference objects --- or even if it has
|
||||
#' such objects attached as attributes --- then it is possible that it will
|
||||
#' change unpredictably even when you do not expect it to. Additionally, because
|
||||
#' the entire key is serialized and hashed, if it contains a very large object
|
||||
#' --- a large data set, for example --- there may be a noticeable performance
|
||||
#' penalty.
|
||||
#'
|
||||
#' If you face these issues with the cache key, you can work around them by
|
||||
#' extracting out the important parts of the objects, and/or by converting them
|
||||
#' to normal R objects before returning them. Your expression could even
|
||||
#' serialize and hash that information in an efficient way and return a string,
|
||||
#' which will in turn be hashed (very quickly) by the
|
||||
#' [digest::digest()] function.
|
||||
#'
|
||||
#' Internally, the result from `cacheKeyExpr` is combined with the name of
|
||||
#' the output (if you assign it to `output$plot1`, it will be combined
|
||||
#' with `"plot1"`) to form the actual key that is used. As a result, even
|
||||
#' if there are multiple plots that have the same `cacheKeyExpr`, they
|
||||
#' will not have cache key collisions.
|
||||
#'
|
||||
#' @section Cache scoping:
|
||||
#'
|
||||
#' There are a number of different ways you may want to scope the cache. For
|
||||
#' example, you may want each user session to have their own plot cache, or
|
||||
#' you may want each run of the application to have a cache (shared among
|
||||
#' possibly multiple simultaneous user sessions), or you may want to have a
|
||||
#' cache that persists even after the application is shut down and started
|
||||
#' again.
|
||||
#'
|
||||
#' To control the scope of the cache, use the `cache` parameter. There
|
||||
#' are two ways of having Shiny automatically create and clean up the disk
|
||||
#' cache.
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{1}{To scope the cache to one run of a Shiny application (shared
|
||||
#' among possibly multiple user sessions), use `cache="app"`. This
|
||||
#' is the default. The cache will be shared across multiple sessions, so
|
||||
#' there is potentially a large performance benefit if there are many users
|
||||
#' of the application. When the application stops running, the cache will
|
||||
#' be deleted. If plots cannot be safely shared across users, this should
|
||||
#' not be used.}
|
||||
#' \item{2}{To scope the cache to one session, use `cache="session"`.
|
||||
#' When a new user session starts --- in other words, when a web browser
|
||||
#' visits the Shiny application --- a new cache will be created on disk
|
||||
#' for that session. When the session ends, the cache will be deleted.
|
||||
#' The cache will not be shared across multiple sessions.}
|
||||
#' }
|
||||
#'
|
||||
#' If either `"app"` or `"session"` is used, the cache will be 10 MB
|
||||
#' in size, and will be stored stored in memory, using a
|
||||
#' [memoryCache()] object. Note that the cache space will be shared
|
||||
#' among all cached plots within a single application or session.
|
||||
#'
|
||||
#' In some cases, you may want more control over the caching behavior. For
|
||||
#' example, you may want to use a larger or smaller cache, share a cache
|
||||
#' among multiple R processes, or you may want the cache to persist across
|
||||
#' multiple runs of an application, or even across multiple R processes.
|
||||
#'
|
||||
#' To use different settings for an application-scoped cache, you can call
|
||||
#' [shinyOptions()] at the top of your app.R, server.R, or
|
||||
#' global.R. For example, this will create a cache with 20 MB of space
|
||||
#' instead of the default 10 MB:
|
||||
#' \preformatted{
|
||||
#' shinyOptions(cache = memoryCache(size = 20e6))
|
||||
#' }
|
||||
#'
|
||||
#' To use different settings for a session-scoped cache, you can call
|
||||
#' [shinyOptions()] at the top of your server function. To use
|
||||
#' the session-scoped cache, you must also call `renderCachedPlot` with
|
||||
#' `cache="session"`. This will create a 20 MB cache for the session:
|
||||
#' \preformatted{
|
||||
#' function(input, output, session) {
|
||||
#' shinyOptions(cache = memoryCache(size = 20e6))
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' ...,
|
||||
#' cache = "session"
|
||||
#' )
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' If you want to create a cache that is shared across multiple concurrent
|
||||
#' R processes, you can use a [diskCache()]. You can create an
|
||||
#' application-level shared cache by putting this at the top of your app.R,
|
||||
#' server.R, or global.R:
|
||||
#' \preformatted{
|
||||
#' shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
#' }
|
||||
#'
|
||||
#' This will create a subdirectory in your system temp directory named
|
||||
#' `myapp-cache` (replace `myapp-cache` with a unique name of
|
||||
#' your choosing). On most platforms, this directory will be removed when
|
||||
#' your system reboots. This cache will persist across multiple starts and
|
||||
#' stops of the R process, as long as you do not reboot.
|
||||
#'
|
||||
#' To have the cache persist even across multiple reboots, you can create the
|
||||
#' cache in a location outside of the temp directory. For example, it could
|
||||
#' be a subdirectory of the application:
|
||||
#' \preformatted{
|
||||
#' shinyOptions(cache = diskCache("./myapp-cache"))
|
||||
#' }
|
||||
#'
|
||||
#' In this case, resetting the cache will have to be done manually, by deleting
|
||||
#' the directory.
|
||||
#'
|
||||
#' You can also scope a cache to just one plot, or selected plots. To do that,
|
||||
#' create a [memoryCache()] or [diskCache()], and pass it
|
||||
#' as the `cache` argument of `renderCachedPlot`.
|
||||
#'
|
||||
#' @section Interactive plots:
|
||||
#'
|
||||
#' `renderCachedPlot` can be used to create interactive plots. See
|
||||
#' [plotOutput()] for more information and examples.
|
||||
#'
|
||||
#'
|
||||
#' @inheritParams renderPlot
|
||||
#' @param cacheKeyExpr An expression that returns a cache key. This key should
|
||||
#' be a unique identifier for a plot: the assumption is that if the cache key
|
||||
#' is the same, then the plot will be the same.
|
||||
#' @param sizePolicy A function that takes two arguments, `width` and
|
||||
#' `height`, and returns a list with `width` and `height`. The
|
||||
#' purpose is to round the actual pixel dimensions from the browser to some
|
||||
#' other dimensions, so that this will not generate and cache images of every
|
||||
#' possible pixel dimension. See [sizeGrowthRatio()] for more
|
||||
#' information on the default sizing policy.
|
||||
#' @param res The resolution of the PNG, in pixels per inch.
|
||||
#' @param cache The scope of the cache, or a cache object. This can be
|
||||
#' `"app"` (the default), `"session"`, or a cache object like
|
||||
#' a [diskCache()]. See the Cache Scoping section for more
|
||||
#' information.
|
||||
#'
|
||||
#' @seealso See [renderPlot()] for the regular, non-cached version of
|
||||
#' this function. For more about configuring caches, see
|
||||
#' [memoryCache()] and [diskCache()].
|
||||
#'
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # A basic example that uses the default app-scoped memory cache.
|
||||
#' # The cache will be shared among all simultaneous users of the application.
|
||||
#' shinyApp(
|
||||
#' fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
|
||||
#' ),
|
||||
#' mainPanel(plotOutput("plot"))
|
||||
#' )
|
||||
#' ),
|
||||
#' function(input, output, session) {
|
||||
#' output$plot <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) }
|
||||
#' )
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#'
|
||||
#' # An example uses a data object shared across sessions. mydata() is part of
|
||||
#' # the cache key, so when its value changes, plots that were previously
|
||||
#' # stored in the cache will no longer be used (unless mydata() changes back
|
||||
#' # to its previous value).
|
||||
#' mydata <- reactiveVal(data.frame(x = rnorm(400), y = rnorm(400)))
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("n", "Number of points", 50, 400, 100, step = 50),
|
||||
#' actionButton("newdata", "New data")
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' plotOutput("plot")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$newdata, {
|
||||
#' mydata(data.frame(x = rnorm(400), y = rnorm(400)))
|
||||
#' })
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' {
|
||||
#' Sys.sleep(2)
|
||||
#' d <- mydata()
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(d$x[seqn], d$y[seqn], xlim = range(d$x), ylim = range(d$y))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n, mydata()) },
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#'
|
||||
#' # A basic application with two plots, where each plot in each session has
|
||||
#' # a separate cache.
|
||||
#' shinyApp(
|
||||
#' fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' plotOutput("plot1"),
|
||||
#' plotOutput("plot2")
|
||||
#' )
|
||||
#' )
|
||||
#' ),
|
||||
#' function(input, output, session) {
|
||||
#' output$plot1 <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) },
|
||||
#' cache = memoryCache()
|
||||
#' )
|
||||
#' output$plot2 <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) },
|
||||
#' cache = memoryCache()
|
||||
#' )
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' \dontrun{
|
||||
#' # At the top of app.R, this set the application-scoped cache to be a memory
|
||||
#' # cache that is 20 MB in size, and where cached objects expire after one
|
||||
#' # hour.
|
||||
#' shinyOptions(cache = memoryCache(max_size = 20e6, max_age = 3600))
|
||||
#'
|
||||
#' # At the top of app.R, this set the application-scoped cache to be a disk
|
||||
#' # cache that can be shared among multiple concurrent R processes, and is
|
||||
#' # deleted when the system reboots.
|
||||
#' shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
#'
|
||||
#' # At the top of app.R, this set the application-scoped cache to be a disk
|
||||
#' # cache that can be shared among multiple concurrent R processes, and
|
||||
#' # persists on disk across reboots.
|
||||
#' shinyOptions(cache = diskCache("./myapp-cache"))
|
||||
#'
|
||||
#' # At the top of the server function, this set the session-scoped cache to be
|
||||
#' # a memory cache that is 5 MB in size.
|
||||
#' server <- function(input, output, session) {
|
||||
#' shinyOptions(cache = memoryCache(max_size = 5e6))
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' ...,
|
||||
#' cache = "session"
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' }
|
||||
#' @export
|
||||
renderCachedPlot <- function(expr,
|
||||
cacheKeyExpr,
|
||||
sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2),
|
||||
res = 72,
|
||||
cache = "app",
|
||||
...,
|
||||
outputArgs = list()
|
||||
) {
|
||||
|
||||
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
|
||||
# is called
|
||||
installExprFunction(expr, "func", parent.frame(), quoted = FALSE, ..stacktraceon = TRUE)
|
||||
# This is so that the expr doesn't re-execute by itself; it needs to be
|
||||
# triggered by the cache key (or width/height) changing.
|
||||
isolatedFunc <- function() isolate(func())
|
||||
|
||||
args <- list(...)
|
||||
|
||||
cacheKeyExpr <- substitute(cacheKeyExpr)
|
||||
# The real cache key we'll use also includes width, height, res, pixelratio.
|
||||
# This is just the part supplied by the user.
|
||||
userCacheKey <- reactive(cacheKeyExpr, env = parent.frame(), quoted = TRUE, label = "userCacheKey")
|
||||
|
||||
ensureCacheSetup <- function() {
|
||||
# For our purposes, cache objects must support these methods.
|
||||
isCacheObject <- function(x) {
|
||||
# Use tryCatch in case the object does not support `$`.
|
||||
tryCatch(
|
||||
is.function(x$get) && is.function(x$set),
|
||||
error = function(e) FALSE
|
||||
)
|
||||
}
|
||||
|
||||
if (isCacheObject(cache)) {
|
||||
# If `cache` is already a cache object, do nothing
|
||||
return()
|
||||
|
||||
} else if (identical(cache, "app")) {
|
||||
cache <<- getShinyOption("cache")
|
||||
|
||||
} else if (identical(cache, "session")) {
|
||||
cache <<- session$cache
|
||||
|
||||
} else {
|
||||
stop('`cache` must either be "app", "session", or a cache object with methods, `$get`, and `$set`.')
|
||||
}
|
||||
}
|
||||
|
||||
# The width and height of the plot to draw, given from sizePolicy. These
|
||||
# values get filled by an observer below.
|
||||
fitDims <- reactiveValues(width = NULL, height = NULL)
|
||||
|
||||
resizeObserver <- NULL
|
||||
ensureResizeObserver <- function() {
|
||||
if (!is.null(resizeObserver))
|
||||
return()
|
||||
|
||||
# Given the actual width/height of the image in the browser, this gets the
|
||||
# width/height from sizePolicy() and pushes those values into `fitDims`.
|
||||
# It's done this way so that the `fitDims` only change (and cause
|
||||
# invalidations) when the rendered image size changes, and not every time
|
||||
# the browser's <img> tag changes size.
|
||||
doResizeCheck <- function() {
|
||||
width <- session$clientData[[paste0('output_', outputName, '_width')]]
|
||||
height <- session$clientData[[paste0('output_', outputName, '_height')]]
|
||||
|
||||
if (is.null(width)) width <- 0
|
||||
if (is.null(height)) height <- 0
|
||||
|
||||
rect <- sizePolicy(c(width, height))
|
||||
fitDims$width <- rect[1]
|
||||
fitDims$height <- rect[2]
|
||||
}
|
||||
|
||||
# Run it once immediately, then set up the observer
|
||||
isolate(doResizeCheck())
|
||||
|
||||
resizeObserver <<- observe(doResizeCheck())
|
||||
}
|
||||
|
||||
# Vars to store session and output, so that they can be accessed from
|
||||
# the plotObj() reactive.
|
||||
session <- NULL
|
||||
outputName <- NULL
|
||||
|
||||
|
||||
drawReactive <- reactive(label = "plotObj", {
|
||||
hybrid_chain(
|
||||
# Depend on the user cache key, even though we don't use the value. When
|
||||
# it changes, it can cause the drawReactive to re-execute. (Though
|
||||
# drawReactive will not necessarily re-execute --- it must be called from
|
||||
# renderFunc, which happens only if there's a cache miss.)
|
||||
userCacheKey(),
|
||||
function(userCacheKeyValue) {
|
||||
# Get width/height, but don't depend on them.
|
||||
isolate({
|
||||
width <- fitDims$width
|
||||
height <- fitDims$height
|
||||
})
|
||||
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
|
||||
do.call("drawPlot", c(
|
||||
list(
|
||||
name = outputName,
|
||||
session = session,
|
||||
func = isolatedFunc,
|
||||
width = width,
|
||||
height = height,
|
||||
pixelratio = pixelratio,
|
||||
res = res
|
||||
),
|
||||
args
|
||||
))
|
||||
},
|
||||
catch = function(reason) {
|
||||
# Non-isolating read. A common reason for errors in plotting is because
|
||||
# the dimensions are too small. By taking a dependency on width/height,
|
||||
# we can try again if the plot output element changes size.
|
||||
fitDims$width
|
||||
fitDims$height
|
||||
|
||||
# Propagate the error
|
||||
stop(reason)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
# This function is the one that's returned from renderPlot(), and gets
|
||||
# wrapped in an observer when the output value is assigned.
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
outputName <<- name
|
||||
session <<- shinysession
|
||||
ensureCacheSetup()
|
||||
ensureResizeObserver()
|
||||
|
||||
hybrid_chain(
|
||||
# This use of the userCacheKey() sets up the reactive dependency that
|
||||
# causes plot re-draw events. These may involve pulling from the cache,
|
||||
# replaying a display list, or re-executing user code.
|
||||
userCacheKey(),
|
||||
function(userCacheKeyResult) {
|
||||
width <- fitDims$width
|
||||
height <- fitDims$height
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
|
||||
key <- digest::digest(list(outputName, userCacheKeyResult, width, height, res, pixelratio), "xxhash64")
|
||||
|
||||
plotObj <- cache$get(key)
|
||||
|
||||
# First look in cache.
|
||||
# Case 1. cache hit.
|
||||
if (!is.key_missing(plotObj)) {
|
||||
return(list(
|
||||
cacheHit = TRUE,
|
||||
key = key,
|
||||
plotObj = plotObj,
|
||||
width = width,
|
||||
height = height,
|
||||
pixelratio = pixelratio
|
||||
))
|
||||
}
|
||||
|
||||
# If not in cache, hybrid_chain call to drawReactive
|
||||
#
|
||||
# Two more possible cases:
|
||||
# 2. drawReactive will re-execute and return a plot that's the
|
||||
# correct size.
|
||||
# 3. It will not re-execute, but it will return the previous value,
|
||||
# which is the wrong size. It will include a valid display list
|
||||
# which can be used by resizeSavedPlot.
|
||||
hybrid_chain(
|
||||
drawReactive(),
|
||||
function(drawReactiveResult) {
|
||||
# Pass along the key for caching in the next stage
|
||||
list(
|
||||
cacheHit = FALSE,
|
||||
key = key,
|
||||
plotObj = drawReactiveResult,
|
||||
width = width,
|
||||
height = height,
|
||||
pixelratio = pixelratio
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
function(possiblyAsyncResult) {
|
||||
hybrid_chain(possiblyAsyncResult, function(result) {
|
||||
width <- result$width
|
||||
height <- result$height
|
||||
pixelratio <- result$pixelratio
|
||||
|
||||
# Three possibilities when we get here:
|
||||
# 1. There was a cache hit. No need to set a value in the cache.
|
||||
# 2. There was a cache miss, and the plotObj is already the correct
|
||||
# size (because drawReactive re-executed). In this case, we need
|
||||
# to cache it.
|
||||
# 3. There was a cache miss, and the plotObj was not the corect size.
|
||||
# In this case, we need to replay the display list, and then cache
|
||||
# the result.
|
||||
if (!result$cacheHit) {
|
||||
# If the image is already the correct size, this just returns the
|
||||
# object unchanged.
|
||||
result$plotObj <- do.call("resizeSavedPlot", c(
|
||||
list(
|
||||
name,
|
||||
shinysession,
|
||||
result$plotObj,
|
||||
width,
|
||||
height,
|
||||
pixelratio,
|
||||
res
|
||||
),
|
||||
args
|
||||
))
|
||||
|
||||
# Save a cached copy of the plotObj. The recorded displaylist for
|
||||
# the plot can't be serialized and restored properly within the same
|
||||
# R session, so we NULL it out before saving. (The image data and
|
||||
# other metadata be saved and restored just fine.) Displaylists can
|
||||
# also be very large (~1.5MB for a basic ggplot), and they would not
|
||||
# be commonly used. Note that displaylist serialization was fixed in
|
||||
# revision 74506 (2e6c669), and should be in R 3.6. A MemoryCache
|
||||
# doesn't need to serialize objects, so it could actually save a
|
||||
# display list, but for the reasons listed previously, it's
|
||||
# generally not worth it.
|
||||
# The plotResult is not the same as the recordedPlot (it is used to
|
||||
# retrieve coordmap information for ggplot2 objects) but it is only
|
||||
# used in conjunction with the recordedPlot, and we'll remove it
|
||||
# because it can be quite large.
|
||||
result$plotObj$plotResult <- NULL
|
||||
result$plotObj$recordedPlot <- NULL
|
||||
cache$set(result$key, result$plotObj)
|
||||
}
|
||||
|
||||
img <- result$plotObj$img
|
||||
# Replace exact pixel dimensions; instead, the max-height and
|
||||
# max-width will be set to 100% from CSS.
|
||||
img$class <- "shiny-scalable"
|
||||
img$width <- NULL
|
||||
img$height <- NULL
|
||||
|
||||
img
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# If renderPlot isn't going to adapt to the height of the div, then the
|
||||
# div needs to adapt to the height of renderPlot. By default, plotOutput
|
||||
# sets the height to 400px, so to make it adapt we need to override it
|
||||
# with NULL.
|
||||
outputFunc <- plotOutput
|
||||
formals(outputFunc)['height'] <- list(NULL)
|
||||
|
||||
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
|
||||
}
|
||||
|
||||
|
||||
#' Create a sizing function that grows at a given ratio
|
||||
#'
|
||||
#' Returns a function which takes a two-element vector representing an input
|
||||
#' width and height, and returns a two-element vector of width and height. The
|
||||
#' possible widths are the base width times the growthRate to any integer power.
|
||||
#' For example, with a base width of 500 and growth rate of 1.25, the possible
|
||||
#' widths include 320, 400, 500, 625, 782, and so on, both smaller and larger.
|
||||
#' Sizes are rounded up to the next pixel. Heights are computed the same way as
|
||||
#' widths.
|
||||
#'
|
||||
#' @param width,height Base width and height.
|
||||
#' @param growthRate Growth rate multiplier.
|
||||
#'
|
||||
#' @seealso This is to be used with [renderCachedPlot()].
|
||||
#'
|
||||
#' @examples
|
||||
#' f <- sizeGrowthRatio(500, 500, 1.25)
|
||||
#' f(c(400, 400))
|
||||
#' f(c(500, 500))
|
||||
#' f(c(530, 550))
|
||||
#' f(c(625, 700))
|
||||
#'
|
||||
#' @export
|
||||
sizeGrowthRatio <- function(width = 400, height = 400, growthRate = 1.2) {
|
||||
round_dim_up <- function(x, base, rate) {
|
||||
power <- ceiling(log(x / base, rate))
|
||||
ceiling(base * rate^power)
|
||||
}
|
||||
|
||||
function(dims) {
|
||||
if (length(dims) != 2) {
|
||||
stop("dims must be a vector with two numbers, for width and height.")
|
||||
}
|
||||
c(
|
||||
round_dim_up(dims[1], width, growthRate),
|
||||
round_dim_up(dims[2], height, growthRate)
|
||||
)
|
||||
}
|
||||
}
|
||||
1260
R/render-plot.R
1260
R/render-plot.R
File diff suppressed because it is too large
Load Diff
257
R/render-table.R
257
R/render-table.R
@@ -1,59 +1,228 @@
|
||||
#' Table Output
|
||||
#'
|
||||
#' Creates a reactive table that is suitable for assigning to an \code{output}
|
||||
#' Creates a reactive table that is suitable for assigning to an `output`
|
||||
#' slot.
|
||||
#'
|
||||
#' The corresponding HTML output tag should be \code{div} and have the CSS class
|
||||
#' name \code{shiny-html-output}.
|
||||
#' The corresponding HTML output tag should be `div` and have the CSS
|
||||
#' class name `shiny-html-output`.
|
||||
#'
|
||||
#' @param expr An expression that returns an R object that can be used with
|
||||
#' \code{\link[xtable]{xtable}}.
|
||||
#' @param ... Arguments to be passed through to \code{\link[xtable]{xtable}} and
|
||||
#' \code{\link[xtable]{print.xtable}}.
|
||||
#' @param env The environment in which to evaluate \code{expr}.
|
||||
#' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This
|
||||
#' is useful if you want to save an expression in a variable.
|
||||
#' @param func A function that returns an R object that can be used with
|
||||
#' \code{\link[xtable]{xtable}} (deprecated; use \code{expr} instead).
|
||||
#'
|
||||
#' [xtable::xtable()].
|
||||
#' @param striped,hover,bordered Logicals: if `TRUE`, apply the
|
||||
#' corresponding Bootstrap table format to the output table.
|
||||
#' @param spacing The spacing between the rows of the table (`xs`
|
||||
#' stands for "extra small", `s` for "small", `m` for "medium"
|
||||
#' and `l` for "large").
|
||||
#' @param width Table width. Must be a valid CSS unit (like "100%", "400px",
|
||||
#' "auto") or a number, which will be coerced to a string and
|
||||
#' have "px" appended.
|
||||
#' @param align A string that specifies the column alignment. If equal to
|
||||
#' `'l'`, `'c'` or `'r'`, then all columns will be,
|
||||
#' respectively, left-, center- or right-aligned. Otherwise, `align`
|
||||
#' must have the same number of characters as the resulting table (if
|
||||
#' `rownames = TRUE`, this will be equal to `ncol()+1`), with
|
||||
#' the *i*-th character specifying the alignment for the
|
||||
#' *i*-th column (besides `'l'`, `'c'` and
|
||||
#' `'r'`, `'?'` is also permitted - `'?'` is a placeholder
|
||||
#' for that particular column, indicating that it should keep its default
|
||||
#' alignment). If `NULL`, then all numeric/integer columns (including
|
||||
#' the row names, if they are numbers) will be right-aligned and
|
||||
#' everything else will be left-aligned (`align = '?'` produces the
|
||||
#' same result).
|
||||
#' @param rownames,colnames Logicals: include rownames? include colnames
|
||||
#' (column headers)?
|
||||
#' @param digits An integer specifying the number of decimal places for
|
||||
#' the numeric columns (this will not apply to columns with an integer
|
||||
#' class). If `digits` is set to a negative value, then the numeric
|
||||
#' columns will be displayed in scientific format with a precision of
|
||||
#' `abs(digits)` digits.
|
||||
#' @param na The string to use in the table cells whose values are missing
|
||||
#' (i.e. they either evaluate to `NA` or `NaN`).
|
||||
#' @param ... Arguments to be passed through to [xtable::xtable()]
|
||||
#' and [xtable::print.xtable()].
|
||||
#' @param env The environment in which to evaluate `expr`.
|
||||
#' @param quoted Is `expr` a quoted expression (with `quote()`)?
|
||||
#' This is useful if you want to save an expression in a variable.
|
||||
#' @param outputArgs A list of arguments to be passed through to the
|
||||
#' implicit call to [tableOutput()] when `renderTable` is
|
||||
#' used in an interactive R Markdown document.
|
||||
#' @export
|
||||
renderTable <- function(expr, ..., env=parent.frame(), quoted=FALSE, func=NULL) {
|
||||
if (!is.null(func)) {
|
||||
shinyDeprecated(msg="renderTable: argument 'func' is deprecated. Please use 'expr' instead.")
|
||||
} else {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
renderTable <- function(expr, striped = FALSE, hover = FALSE,
|
||||
bordered = FALSE, spacing = c("s", "xs", "m", "l"),
|
||||
width = "auto", align = NULL,
|
||||
rownames = FALSE, colnames = TRUE,
|
||||
digits = NULL, na = "NA", ...,
|
||||
env = parent.frame(), quoted = FALSE,
|
||||
outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
if (!is.function(spacing)) spacing <- match.arg(spacing)
|
||||
|
||||
# A small helper function to create a wrapper for an argument that was
|
||||
# passed to renderTable()
|
||||
createWrapper <- function(arg) {
|
||||
if (is.function(arg)) wrapper <- arg
|
||||
else wrapper <- function() arg
|
||||
return(wrapper)
|
||||
}
|
||||
|
||||
markRenderFunction(tableOutput, function() {
|
||||
classNames <- getOption('shiny.table.class') %OR% 'data table table-bordered table-condensed'
|
||||
data <- func()
|
||||
# Create wrappers for most arguments so that functions can also be passed
|
||||
# in, rather than only literals (useful for shiny apps)
|
||||
stripedWrapper <- createWrapper(striped)
|
||||
hoverWrapper <- createWrapper(hover)
|
||||
borderedWrapper <- createWrapper(bordered)
|
||||
spacingWrapper <- createWrapper(spacing)
|
||||
widthWrapper <- createWrapper(width)
|
||||
alignWrapper <- createWrapper(align)
|
||||
rownamesWrapper <- createWrapper(rownames)
|
||||
colnamesWrapper <- createWrapper(colnames)
|
||||
digitsWrapper <- createWrapper(digits)
|
||||
naWrapper <- createWrapper(na)
|
||||
|
||||
if (is.null(data) || identical(data, data.frame()))
|
||||
return("")
|
||||
dots <- list(...) ## used later (but defined here because of scoping)
|
||||
|
||||
# Separate the ... args to pass to xtable() vs print.xtable()
|
||||
dots <- list(...)
|
||||
xtable_argnames <- setdiff(names(formals(xtable)), c("x", "..."))
|
||||
xtable_args <- dots[intersect(names(dots), xtable_argnames)]
|
||||
non_xtable_args <- dots[setdiff(names(dots), xtable_argnames)]
|
||||
createRenderFunction(
|
||||
func,
|
||||
function(data, session, name, ...) {
|
||||
striped <- stripedWrapper()
|
||||
hover <- hoverWrapper()
|
||||
bordered <- borderedWrapper()
|
||||
format <- c(striped = striped, hover = hover, bordered = bordered)
|
||||
spacing <- spacingWrapper()
|
||||
width <- widthWrapper()
|
||||
align <- alignWrapper()
|
||||
rownames <- rownamesWrapper()
|
||||
colnames <- colnamesWrapper()
|
||||
digits <- digitsWrapper()
|
||||
na <- naWrapper()
|
||||
|
||||
# Call xtable with its args
|
||||
xtable_res <- do.call(xtable, c(list(data), xtable_args))
|
||||
spacing_choices <- c("s", "xs", "m", "l")
|
||||
if (!(spacing %in% spacing_choices)) {
|
||||
stop(paste("`spacing` must be one of",
|
||||
paste0("'", spacing_choices, "'", collapse=", ")))
|
||||
}
|
||||
|
||||
# Set up print args
|
||||
print_args <- list(
|
||||
xtable_res,
|
||||
type = 'html',
|
||||
html.table.attributes = paste('class="', htmlEscape(classNames, TRUE),
|
||||
'"', sep='')
|
||||
)
|
||||
print_args <- c(print_args, non_xtable_args)
|
||||
# For css styling
|
||||
classNames <- paste0("table shiny-table",
|
||||
paste0(" table-", names(format)[format], collapse = "" ),
|
||||
paste0(" spacing-", spacing))
|
||||
|
||||
return(paste(
|
||||
utils::capture.output(
|
||||
do.call(print, print_args)
|
||||
),
|
||||
collapse="\n"
|
||||
))
|
||||
})
|
||||
data <- as.data.frame(data)
|
||||
|
||||
# Return NULL if no data is provided
|
||||
if (is.null(data) ||
|
||||
(is.data.frame(data) && nrow(data) == 0 && ncol(data) == 0))
|
||||
return(NULL)
|
||||
|
||||
# Separate the ... args to pass to xtable() vs print.xtable()
|
||||
xtable_argnames <- setdiff(names(formals(xtable)), c("x", "..."))
|
||||
xtable_args <- dots[intersect(names(dots), xtable_argnames)]
|
||||
non_xtable_args <- dots[setdiff(names(dots), xtable_argnames)]
|
||||
|
||||
# By default, numbers are right-aligned and everything else is left-aligned.
|
||||
defaultAlignment <- function(col) {
|
||||
if (is.numeric(col)) "r" else "l"
|
||||
}
|
||||
|
||||
# Figure out column alignment
|
||||
## Case 1: default alignment
|
||||
if (is.null(align) || align == "?") {
|
||||
names <- defaultAlignment(attr(data, "row.names"))
|
||||
cols <- paste(vapply(data, defaultAlignment, character(1)), collapse = "")
|
||||
cols <- paste0(names, cols)
|
||||
} else {
|
||||
## Case 2: user-specified alignment
|
||||
num_cols <- if (rownames) nchar(align) else nchar(align)+1
|
||||
valid <- !grepl("[^lcr\\?]", align)
|
||||
if (num_cols == ncol(data)+1 && valid) {
|
||||
cols <- if (rownames) align else paste0("r", align)
|
||||
defaults <- grep("\\?", strsplit(cols,"")[[1]])
|
||||
if (length(defaults) != 0) {
|
||||
vals <- vapply(data[,defaults-1], defaultAlignment, character(1))
|
||||
for (i in seq_len(length(defaults))) {
|
||||
substr(cols, defaults[i], defaults[i]) <- vals[i]
|
||||
}
|
||||
}
|
||||
} else if (nchar(align) == 1 && valid) {
|
||||
cols <- paste0(rep(align, ncol(data)+1), collapse="")
|
||||
} else {
|
||||
stop("`align` must contain only the characters `l`, `c`, `r` and/or `?` and",
|
||||
"have length either equal to 1 or to the total number of columns")
|
||||
}
|
||||
}
|
||||
|
||||
# Call xtable with its (updated) args
|
||||
xtable_args <- c(xtable_args, align = cols, digits = digits)
|
||||
xtable_res <- do.call(xtable, c(list(data), xtable_args))
|
||||
|
||||
# Set up print args
|
||||
print_args <- list(
|
||||
x = xtable_res,
|
||||
type = 'html',
|
||||
include.rownames = {
|
||||
if ("include.rownames" %in% names(dots)) dots$include.rownames
|
||||
else rownames
|
||||
},
|
||||
include.colnames = {
|
||||
if ("include.colnames" %in% names(dots)) dots$include.colnames
|
||||
else colnames
|
||||
},
|
||||
NA.string = {
|
||||
if ("NA.string" %in% names(dots)) dots$NA.string
|
||||
else na
|
||||
},
|
||||
html.table.attributes =
|
||||
paste0({
|
||||
if ("html.table.attributes" %in% names(dots)) dots$html.table.attributes
|
||||
else ""
|
||||
}, " ",
|
||||
"class = '", htmlEscape(classNames, TRUE), "' ",
|
||||
"style = 'width:", validateCssUnit(width), ";'"),
|
||||
comment = {
|
||||
if ("comment" %in% names(dots)) dots$comment
|
||||
else FALSE
|
||||
}
|
||||
)
|
||||
|
||||
print_args <- c(print_args, non_xtable_args)
|
||||
print_args <- print_args[unique(names(print_args))]
|
||||
|
||||
# Capture the raw html table returned by print.xtable(), and store it in
|
||||
# a variable for further processing
|
||||
tab <- paste(utils::capture.output(do.call(print, print_args)),collapse = "\n")
|
||||
|
||||
# Add extra class to cells with NA value, to be able to style them separately
|
||||
tab <- gsub(paste(">", na, "<"), paste(" class='NA'>", na, "<"), tab)
|
||||
|
||||
# All further processing concerns the table headers, so we don't need to run
|
||||
# any of this if colnames=FALSE
|
||||
if (colnames) {
|
||||
# Make sure that the final html table has a proper header (not included
|
||||
# in the print.xtable() default)
|
||||
tab <- sub("<tr>", "<thead> <tr>", tab)
|
||||
tab <- sub("</tr>", "</tr> </thead> <tbody>", tab)
|
||||
tab <- sub("</table>$", "</tbody> </table>", tab)
|
||||
|
||||
# Update the `cols` string (which stores the alignment of each column) so
|
||||
# that it only includes the alignment for the table variables (and not
|
||||
# for the row.names)
|
||||
cols <- if (rownames) cols else substr(cols, 2, nchar(cols))
|
||||
|
||||
# Create a vector whose i-th entry corresponds to the i-th table variable
|
||||
# alignment (substituting "l" by "left", "c" by "center" and "r" by "right")
|
||||
cols <- strsplit(cols, "")[[1]]
|
||||
cols[cols == "l"] <- "left"
|
||||
cols[cols == "r"] <- "right"
|
||||
cols[cols == "c"] <- "center"
|
||||
|
||||
# Align each header accordingly (this guarantees that each header and its
|
||||
# corresponding column have the same alignment)
|
||||
for (i in seq_len(length(cols))) {
|
||||
tab <- sub("<th>", paste0("<th style='text-align: ", cols[i], ";'>"), tab)
|
||||
}
|
||||
}
|
||||
return(tab)
|
||||
},
|
||||
tableOutput, outputArgs
|
||||
)
|
||||
}
|
||||
|
||||
32
R/run-url.R
32
R/run-url.R
@@ -1,28 +1,28 @@
|
||||
#' Run a Shiny application from a URL
|
||||
#'
|
||||
#' \code{runUrl()} downloads and launches a Shiny application that is hosted at
|
||||
#' `runUrl()` downloads and launches a Shiny application that is hosted at
|
||||
#' a downloadable URL. The Shiny application must be saved in a .zip, .tar, or
|
||||
#' .tar.gz file. The Shiny application files must be contained in the root
|
||||
#' directory or a subdirectory in the archive. For example, the files might be
|
||||
#' \code{myapp/server.r} and \code{myapp/ui.r}. The functions \code{runGitHub()}
|
||||
#' and \code{runGist()} are based on \code{runUrl()}, using URL's from GitHub
|
||||
#' (\url{https://github.com}) and GitHub gists (\url{https://gist.github.com}),
|
||||
#' `myapp/server.r` and `myapp/ui.r`. The functions `runGitHub()`
|
||||
#' and `runGist()` are based on `runUrl()`, using URL's from GitHub
|
||||
#' (<https://github.com>) and GitHub gists (<https://gist.github.com>),
|
||||
#' respectively.
|
||||
#' @param url URL of the application.
|
||||
#' @param filetype The file type (\code{".zip"}, \code{".tar"}, or
|
||||
#' \code{".tar.gz"}. Defaults to the file extension taken from the url.
|
||||
#' @param filetype The file type (`".zip"`, `".tar"`, or
|
||||
#' `".tar.gz"`. Defaults to the file extension taken from the url.
|
||||
#' @param subdir A subdirectory in the repository that contains the app. By
|
||||
#' default, this function will run an app from the top level of the repo, but
|
||||
#' you can use a path such as `\code{"inst/shinyapp"}.
|
||||
#' @param destdir Directory to store the downloaded application files. If \code{NULL}
|
||||
#' you can use a path such as `"inst/shinyapp"`.
|
||||
#' @param destdir Directory to store the downloaded application files. If `NULL`
|
||||
#' (the default), the application files will be stored in a temporary directory
|
||||
#' and removed when the app exits
|
||||
#' @param ... Other arguments to be passed to \code{\link{runApp}()}, such as
|
||||
#' \code{port} and \code{launch.browser}.
|
||||
#' @param ... Other arguments to be passed to [runApp()], such as
|
||||
#' `port` and `launch.browser`.
|
||||
#' @export
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' if (interactive()) {
|
||||
#' runUrl('https://github.com/rstudio/shiny_example/archive/master.tar.gz')
|
||||
#'
|
||||
#' # Can run an app from a subdirectory in the archive
|
||||
@@ -88,8 +88,8 @@ runUrl <- function(url, filetype = NULL, subdir = NULL, destdir = NULL, ...) {
|
||||
|
||||
#' @rdname runUrl
|
||||
#' @param gist The identifier of the gist. For example, if the gist is
|
||||
#' https://gist.github.com/jcheng5/3239667, then \code{3239667},
|
||||
#' \code{'3239667'}, and \code{'https://gist.github.com/jcheng5/3239667'} are
|
||||
#' https://gist.github.com/jcheng5/3239667, then `3239667`,
|
||||
#' `'3239667'`, and `'https://gist.github.com/jcheng5/3239667'` are
|
||||
#' all valid values.
|
||||
#' @export
|
||||
#' @examples
|
||||
@@ -118,10 +118,10 @@ runGist <- function(gist, destdir = NULL, ...) {
|
||||
|
||||
#' @rdname runUrl
|
||||
#' @param repo Name of the repository.
|
||||
#' @param username GitHub username. If \code{repo} is of the form
|
||||
#' \code{"username/repo"}, \code{username} will be taken from \code{repo}.
|
||||
#' @param username GitHub username. If `repo` is of the form
|
||||
#' `"username/repo"`, `username` will be taken from `repo`.
|
||||
#' @param ref Desired git reference. Could be a commit, tag, or branch name.
|
||||
#' Defaults to \code{"master"}.
|
||||
#' Defaults to `"master"`.
|
||||
#' @export
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
|
||||
91
R/serializers.R
Normal file
91
R/serializers.R
Normal file
@@ -0,0 +1,91 @@
|
||||
#' Add a function for serializing an input before bookmarking application state
|
||||
#'
|
||||
#' @param inputId Name of the input value.
|
||||
#' @param fun A function that takes the input value and returns a modified
|
||||
#' value. The returned value will be used for the test snapshot.
|
||||
#' @param session A Shiny session object.
|
||||
#'
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
setSerializer <- function(inputId, fun, session = getDefaultReactiveDomain()) {
|
||||
if (is.null(session)) {
|
||||
stop("setSerializer() needs a session object.")
|
||||
}
|
||||
|
||||
input_impl <- .subset2(session$input, "impl")
|
||||
input_impl$setMeta(inputId, "shiny.serializer", fun)
|
||||
}
|
||||
|
||||
|
||||
# For most types of values, simply return the value unchanged.
|
||||
serializerDefault <- function(value, stateDir) {
|
||||
value
|
||||
}
|
||||
|
||||
|
||||
serializerFileInput <- function(value, stateDir = NULL) {
|
||||
# File inputs can be serialized only if there's a stateDir
|
||||
if (is.null(stateDir)) {
|
||||
return(serializerUnserializable())
|
||||
}
|
||||
|
||||
# value is a data frame. When persisting files, we need to copy the file to
|
||||
# the persistent dir and then strip the original path before saving.
|
||||
newpaths <- file.path(stateDir, basename(value$datapath))
|
||||
file.copy(value$datapath, newpaths, overwrite = TRUE)
|
||||
value$datapath <- basename(newpaths)
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
|
||||
# Return a sentinel value that represents "unserializable". This is applied to
|
||||
# for example, passwords and actionButtons.
|
||||
serializerUnserializable <- function(value, stateDir) {
|
||||
structure(
|
||||
list(),
|
||||
serializable = FALSE
|
||||
)
|
||||
}
|
||||
|
||||
# Is this an "unserializable" sentinel value?
|
||||
isUnserializable <- function(x) {
|
||||
identical(
|
||||
attr(x, "serializable", exact = TRUE),
|
||||
FALSE
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# Given a reactiveValues object and optional directory for saving state, apply
|
||||
# serializer function to each of the values, and return a list of the returned
|
||||
# values. This function passes stateDir to the serializer functions, so if
|
||||
# stateDir is non-NULL, it can have a side effect of writing values to disk (in
|
||||
# stateDir).
|
||||
serializeReactiveValues <- function(values, exclude, stateDir = NULL) {
|
||||
impl <- .subset2(values, "impl")
|
||||
|
||||
# Get named list where keys and values are the names of inputs; we'll retrieve
|
||||
# actual values later.
|
||||
vals <- isolate(impl$names())
|
||||
vals <- setdiff(vals, exclude)
|
||||
names(vals) <- vals
|
||||
|
||||
# Get values and apply serializer functions
|
||||
vals <- lapply(vals, function(name) {
|
||||
val <- impl$get(name)
|
||||
|
||||
# Get the serializer function for this input value. If none specified, use
|
||||
# the default.
|
||||
serializer_fun <- impl$getMeta(name, "shiny.serializer")
|
||||
if (is.null(serializer_fun))
|
||||
serializer_fun <- serializerDefault
|
||||
|
||||
# Apply serializer function.
|
||||
serializer_fun(val, stateDir)
|
||||
})
|
||||
|
||||
# Filter out any values that were marked as unserializable.
|
||||
vals <- Filter(Negate(isUnserializable), vals)
|
||||
vals
|
||||
}
|
||||
@@ -5,34 +5,34 @@ inputHandlers <- Map$new()
|
||||
#'
|
||||
#' Adds an input handler for data of this type. When called, Shiny will use the
|
||||
#' function provided to refine the data passed back from the client (after being
|
||||
#' deserialized by jsonlite) before making it available in the \code{input}
|
||||
#' variable of the \code{server.R} file.
|
||||
#' deserialized by jsonlite) before making it available in the `input`
|
||||
#' variable of the `server.R` file.
|
||||
#'
|
||||
#' This function will register the handler for the duration of the R process
|
||||
#' (unless Shiny is explicitly reloaded). For that reason, the \code{type} used
|
||||
#' (unless Shiny is explicitly reloaded). For that reason, the `type` used
|
||||
#' should be very specific to this package to minimize the risk of colliding
|
||||
#' with another Shiny package which might use this data type name. We recommend
|
||||
#' the format of "packageName.widgetName".
|
||||
#'
|
||||
#' Currently Shiny registers the following handlers: \code{shiny.matrix},
|
||||
#' \code{shiny.number}, and \code{shiny.date}.
|
||||
#' Currently Shiny registers the following handlers: `shiny.matrix`,
|
||||
#' `shiny.number`, and `shiny.date`.
|
||||
#'
|
||||
#' The \code{type} of a custom Shiny Input widget will be deduced using the
|
||||
#' \code{getType()} JavaScript function on the registered Shiny inputBinding.
|
||||
#' @param type The type for which the handler should be added -- should be a
|
||||
#' The `type` of a custom Shiny Input widget will be deduced using the
|
||||
#' `getType()` JavaScript function on the registered Shiny inputBinding.
|
||||
#' @param type The type for which the handler should be added --- should be a
|
||||
#' single-element character vector.
|
||||
#' @param fun The handler function. This is the function that will be used to
|
||||
#' parse the data delivered from the client before it is available in the
|
||||
#' \code{input} variable. The function will be called with the following three
|
||||
#' `input` variable. The function will be called with the following three
|
||||
#' parameters:
|
||||
#' \enumerate{
|
||||
#' \item{The value of this input as provided by the client, deserialized
|
||||
#' using jsonlite.}
|
||||
#' \item{The \code{shinysession} in which the input exists.}
|
||||
#' \item{The `shinysession` in which the input exists.}
|
||||
#' \item{The name of the input.}
|
||||
#' }
|
||||
#' @param force If \code{TRUE}, will overwrite any existing handler without
|
||||
#' warning. If \code{FALSE}, will throw an error if this class already has
|
||||
#' @param force If `TRUE`, will overwrite any existing handler without
|
||||
#' warning. If `FALSE`, will throw an error if this class already has
|
||||
#' a handler defined.
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
@@ -48,7 +48,7 @@ inputHandlers <- Map$new()
|
||||
#' }
|
||||
#'
|
||||
#' }
|
||||
#' @seealso \code{\link{removeInputHandler}}
|
||||
#' @seealso [removeInputHandler()]
|
||||
#' @export
|
||||
registerInputHandler <- function(type, fun, force=FALSE){
|
||||
if (inputHandlers$containsKey(type) && !force){
|
||||
@@ -63,14 +63,71 @@ registerInputHandler <- function(type, fun, force=FALSE){
|
||||
#' for data of this type, the default jsonlite serialization will be used.
|
||||
#'
|
||||
#' @param type The type for which handlers should be removed.
|
||||
#' @return The handler previously associated with this \code{type}, if one
|
||||
#' existed. Otherwise, \code{NULL}.
|
||||
#' @seealso \code{\link{registerInputHandler}}
|
||||
#' @return The handler previously associated with this `type`, if one
|
||||
#' existed. Otherwise, `NULL`.
|
||||
#' @seealso [registerInputHandler()]
|
||||
#' @export
|
||||
removeInputHandler <- function(type){
|
||||
inputHandlers$remove(type)
|
||||
}
|
||||
|
||||
|
||||
# Apply input handler to a single input value
|
||||
applyInputHandler <- function(name, val, shinysession) {
|
||||
splitName <- strsplit(name, ':')[[1]]
|
||||
if (length(splitName) > 1) {
|
||||
if (!inputHandlers$containsKey(splitName[[2]])) {
|
||||
# No input handler registered for this type
|
||||
stop("No handler registered for type ", name)
|
||||
}
|
||||
|
||||
inputName <- splitName[[1]]
|
||||
|
||||
# Get the function for processing this type of input
|
||||
inputHandler <- inputHandlers$get(splitName[[2]])
|
||||
|
||||
return(inputHandler(val, shinysession, inputName))
|
||||
|
||||
} else if (is.list(val) && is.null(names(val))) {
|
||||
return(unlist(val, recursive = TRUE))
|
||||
} else {
|
||||
return(val)
|
||||
}
|
||||
}
|
||||
|
||||
#' Apply input handlers to raw input values
|
||||
#'
|
||||
#' The purpose of this function is to make it possible for external packages to
|
||||
#' test Shiny inputs. It takes a named list of raw input values, applies input
|
||||
#' handlers to those values, and then returns a named list of the processed
|
||||
#' values.
|
||||
#'
|
||||
#' The raw input values should be in a named list. Some values may have names
|
||||
#' like `"x:shiny.date"`. This function would apply the `"shiny.date"`
|
||||
#' input handler to the value, and then rename the result to `"x"`, in the
|
||||
#' output.
|
||||
#'
|
||||
#' @param inputs A named list of input values.
|
||||
#' @param shinysession A Shiny session object.
|
||||
#'
|
||||
#' @seealso registerInputHandler
|
||||
#' @keywords internal
|
||||
applyInputHandlers <- function(inputs, shinysession = getDefaultReactiveDomain()) {
|
||||
inputs <- mapply(applyInputHandler, names(inputs), inputs,
|
||||
MoreArgs = list(shinysession = shinysession),
|
||||
SIMPLIFY = FALSE)
|
||||
|
||||
# Convert names like "button1:shiny.action" to "button1"
|
||||
names(inputs) <- vapply(
|
||||
names(inputs),
|
||||
function(name) { strsplit(name, ":")[[1]][1] },
|
||||
FUN.VALUE = character(1)
|
||||
)
|
||||
|
||||
inputs
|
||||
}
|
||||
|
||||
|
||||
# Takes a list-of-lists and returns a matrix. The lists
|
||||
# must all be the same length. NULL is replaced by NA.
|
||||
registerInputHandler("shiny.matrix", function(data, ...) {
|
||||
@@ -85,14 +142,34 @@ registerInputHandler("shiny.matrix", function(data, ...) {
|
||||
return(m)
|
||||
})
|
||||
|
||||
|
||||
registerInputHandler("shiny.number", function(val, ...){
|
||||
ifelse(is.null(val), NA, val)
|
||||
})
|
||||
|
||||
registerInputHandler("shiny.password", function(val, shinysession, name) {
|
||||
# Mark passwords as not serializable
|
||||
setSerializer(name, serializerUnserializable)
|
||||
val
|
||||
})
|
||||
|
||||
registerInputHandler("shiny.date", function(val, ...){
|
||||
# First replace NULLs with NA, then convert to Date vector
|
||||
datelist <- ifelse(lapply(val, is.null), NA, val)
|
||||
as.Date(unlist(datelist))
|
||||
|
||||
res <- NULL
|
||||
tryCatch({
|
||||
res <- as.Date(unlist(datelist))
|
||||
},
|
||||
error = function(e) {
|
||||
# It's possible for client to send a string like "99999-01-01", which
|
||||
# as.Date can't handle.
|
||||
warning(e$message)
|
||||
res <<- as.Date(rep(NA, length(datelist)))
|
||||
}
|
||||
)
|
||||
|
||||
res
|
||||
})
|
||||
|
||||
registerInputHandler("shiny.datetime", function(val, ...){
|
||||
@@ -104,8 +181,61 @@ registerInputHandler("shiny.datetime", function(val, ...){
|
||||
as.POSIXct(unlist(times), origin = "1970-01-01", tz = "UTC")
|
||||
})
|
||||
|
||||
registerInputHandler("shiny.action", function(val, ...) {
|
||||
registerInputHandler("shiny.action", function(val, shinysession, name) {
|
||||
# mark up the action button value with a special class so we can recognize it later
|
||||
class(val) <- c(class(val), "shinyActionButtonValue")
|
||||
val
|
||||
})
|
||||
|
||||
registerInputHandler("shiny.file", function(val, shinysession, name) {
|
||||
# This function is only used when restoring a Shiny fileInput. When a file is
|
||||
# uploaded the usual way, it takes a different code path and won't hit this
|
||||
# function.
|
||||
if (is.null(val))
|
||||
return(NULL)
|
||||
|
||||
# The data will be a named list of lists; convert to a data frame.
|
||||
val <- as.data.frame(lapply(val, unlist), stringsAsFactors = FALSE)
|
||||
|
||||
# `val$datapath` should be a filename without a path, for security reasons.
|
||||
if (basename(val$datapath) != val$datapath) {
|
||||
stop("Invalid '/' found in file input path.")
|
||||
}
|
||||
|
||||
# Prepend the persistent dir
|
||||
oldfile <- file.path(getCurrentRestoreContext()$dir, val$datapath)
|
||||
|
||||
# Copy the original file to a new temp dir, so that a restored session can't
|
||||
# modify the original.
|
||||
newdir <- file.path(tempdir(), createUniqueId(12))
|
||||
dir.create(newdir)
|
||||
val$datapath <- file.path(newdir, val$datapath)
|
||||
file.copy(oldfile, val$datapath)
|
||||
|
||||
# Need to mark this input value with the correct serializer. When a file is
|
||||
# uploaded the usual way (instead of being restored), this occurs in
|
||||
# session$`@uploadEnd`.
|
||||
setSerializer(name, serializerFileInput)
|
||||
|
||||
snapshotPreprocessInput(name, snapshotPreprocessorFileInput)
|
||||
|
||||
val
|
||||
})
|
||||
|
||||
|
||||
# to be used with !!!answer
|
||||
registerInputHandler("shiny.symbolList", function(val, ...) {
|
||||
if (is.null(val)) {
|
||||
list()
|
||||
} else {
|
||||
lapply(val, as.symbol)
|
||||
}
|
||||
})
|
||||
# to be used with !!answer
|
||||
registerInputHandler("shiny.symbol", function(val, ...) {
|
||||
if (is.null(val) || identical(val, "")) {
|
||||
NULL
|
||||
} else {
|
||||
as.symbol(val)
|
||||
}
|
||||
})
|
||||
|
||||
637
R/server.R
637
R/server.R
@@ -1,6 +1,7 @@
|
||||
#' @include server-input-handlers.R
|
||||
|
||||
appsByToken <- Map$new()
|
||||
appsNeedingFlush <- Map$new()
|
||||
|
||||
# Provide a character representation of the WS that can be used
|
||||
# as a key in a Map.
|
||||
@@ -21,6 +22,7 @@ registerClient <- function(client) {
|
||||
}
|
||||
|
||||
|
||||
.globals$resourcePaths <- list()
|
||||
.globals$resources <- list()
|
||||
|
||||
.globals$showcaseDefault <- 0
|
||||
@@ -29,65 +31,165 @@ registerClient <- function(client) {
|
||||
|
||||
#' Resource Publishing
|
||||
#'
|
||||
#' Adds a directory of static resources to Shiny's web server, with the given
|
||||
#' path prefix. Primarily intended for package authors to make supporting
|
||||
#' JavaScript/CSS files available to their components.
|
||||
#' Add, remove, or list directory of static resources to Shiny's web server,
|
||||
#' with the given path prefix. Primarily intended for package authors to make
|
||||
#' supporting JavaScript/CSS files available to their components.
|
||||
#'
|
||||
#' Shiny provides two ways of serving static files (i.e., resources):
|
||||
#'
|
||||
#' 1. Static files under the `www/` directory are automatically made available
|
||||
#' under a request path that begins with `/`.
|
||||
#' 2. `addResourcePath()` makes static files in a `directoryPath` available
|
||||
#' under a request path that begins with `prefix`.
|
||||
#'
|
||||
#' The second approach is primarily intended for package authors to make
|
||||
#' supporting JavaScript/CSS files available to their components.
|
||||
#'
|
||||
#' Tools for managing static resources published by Shiny's web server:
|
||||
#' * `addResourcePath()` adds a directory of static resources.
|
||||
#' * `resourcePaths()` lists the currently active resource mappings.
|
||||
#' * `removeResourcePath()` removes a directory of static resources.
|
||||
#'
|
||||
#' @param prefix The URL prefix (without slashes). Valid characters are a-z,
|
||||
#' A-Z, 0-9, hyphen, period, and underscore; and must begin with a-z or A-Z.
|
||||
#' For example, a value of 'foo' means that any request paths that begin with
|
||||
#' '/foo' will be mapped to the given directory.
|
||||
#' A-Z, 0-9, hyphen, period, and underscore. For example, a value of 'foo'
|
||||
#' means that any request paths that begin with '/foo' will be mapped to the
|
||||
#' given directory.
|
||||
#' @param directoryPath The directory that contains the static resources to be
|
||||
#' served.
|
||||
#'
|
||||
#' @details You can call \code{addResourcePath} multiple times for a given
|
||||
#' \code{prefix}; only the most recent value will be retained. If the
|
||||
#' normalized \code{directoryPath} is different than the directory that's
|
||||
#' currently mapped to the \code{prefix}, a warning will be issued.
|
||||
#'
|
||||
#' @seealso \code{\link{singleton}}
|
||||
#' @rdname resourcePaths
|
||||
#' @seealso [singleton()]
|
||||
#'
|
||||
#' @examples
|
||||
#' addResourcePath('datasets', system.file('data', package='datasets'))
|
||||
#' resourcePaths()
|
||||
#' removeResourcePath('datasets')
|
||||
#' resourcePaths()
|
||||
#'
|
||||
#' # make sure all resources are removed
|
||||
#' lapply(names(resourcePaths()), removeResourcePath)
|
||||
#' @export
|
||||
addResourcePath <- function(prefix, directoryPath) {
|
||||
prefix <- prefix[1]
|
||||
if (!grepl('^[a-z][a-z0-9\\-_.]*$', prefix, ignore.case=TRUE, perl=TRUE)) {
|
||||
if (length(prefix) != 1) stop("prefix must be of length 1")
|
||||
if (!grepl('^[a-z0-9\\-_][a-z0-9\\-_.]*$', prefix, ignore.case = TRUE, perl = TRUE)) {
|
||||
stop("addResourcePath called with invalid prefix; please see documentation")
|
||||
}
|
||||
|
||||
if (prefix %in% c('shared')) {
|
||||
stop("addResourcePath called with the reserved prefix '", prefix, "'; ",
|
||||
"please use a different prefix")
|
||||
}
|
||||
normalizedPath <- tryCatch(normalizePath(directoryPath, mustWork = TRUE),
|
||||
error = function(e) {
|
||||
stop("Couldn't normalize path in `addResourcePath`, with arguments: ",
|
||||
"`prefix` = '", prefix, "'; `directoryPath` = '" , directoryPath, "'")
|
||||
}
|
||||
)
|
||||
|
||||
directoryPath <- normalizePath(directoryPath, mustWork=TRUE)
|
||||
# # Often times overwriting a resource path is "what you want",
|
||||
# # but sometimes it can lead to difficult to diagnose issues
|
||||
# # (e.g. an implict dependency might set a resource path that
|
||||
# # conflicts with what you, the app author, are trying to register)
|
||||
# # Note that previous versions of shiny used to warn about this case,
|
||||
# # but it was eventually removed since it caused confusion (#567).
|
||||
# # It seems a good compromise is to throw a more information message.
|
||||
# if (getOption("shiny.resourcePathChanges", FALSE) &&
|
||||
# prefix %in% names(.globals$resourcePaths)) {
|
||||
# existingPath <- .globals$resourcePaths[[prefix]]$path
|
||||
# if (normalizedPath != existingPath) {
|
||||
# message(
|
||||
# "The resource path '", prefix, "' used to point to ",
|
||||
# existingPath, ", but it now points to ", normalizedPath, ". ",
|
||||
# "If your app doesn't work as expected, you may want to ",
|
||||
# "choose a different prefix name."
|
||||
# )
|
||||
# }
|
||||
# }
|
||||
|
||||
existing <- .globals$resources[[prefix]]
|
||||
# If a shiny app is currently running, dynamically register this path with
|
||||
# the corresponding httpuv server object.
|
||||
if (!is.null(getShinyOption("server")))
|
||||
{
|
||||
getShinyOption("server")$setStaticPath(.list = stats::setNames(normalizedPath, prefix))
|
||||
}
|
||||
|
||||
.globals$resources[[prefix]] <- list(directoryPath=directoryPath,
|
||||
func=staticHandler(directoryPath))
|
||||
# .globals$resourcePaths and .globals$resources persist across runs of applications.
|
||||
.globals$resourcePaths[[prefix]] <- staticPath(normalizedPath)
|
||||
# This is necessary because resourcePaths is only for serving assets out of C++;
|
||||
# to support subapps, we also need assets to be served out of R, because those
|
||||
# URLs are rewritten by R code (i.e. routeHandler) before they can be matched to
|
||||
# a resource path.
|
||||
.globals$resources[[prefix]] <- list(
|
||||
directoryPath = normalizedPath,
|
||||
func = staticHandler(normalizedPath)
|
||||
)
|
||||
}
|
||||
|
||||
#' @rdname resourcePaths
|
||||
#' @export
|
||||
resourcePaths <- function() {
|
||||
urls <- names(.globals$resourcePaths)
|
||||
paths <- vapply(.globals$resourcePaths, function(x) x$path, character(1))
|
||||
stats::setNames(paths, urls)
|
||||
}
|
||||
|
||||
hasResourcePath <- function(prefix) {
|
||||
prefix %in% names(resourcePaths())
|
||||
}
|
||||
|
||||
#' @rdname resourcePaths
|
||||
#' @export
|
||||
removeResourcePath <- function(prefix) {
|
||||
if (length(prefix) > 1) stop("`prefix` must be of length 1.")
|
||||
if (!hasResourcePath(prefix)) {
|
||||
warning("Resource ", prefix, " not found.")
|
||||
return(invisible(FALSE))
|
||||
}
|
||||
.globals$resourcePaths[[prefix]] <- NULL
|
||||
.globals$resources[[prefix]] <- NULL
|
||||
invisible(TRUE)
|
||||
}
|
||||
|
||||
|
||||
|
||||
# This function handles any GET request with two or more path elements where the
|
||||
# first path element matches a prefix that was previously added using
|
||||
# addResourcePath().
|
||||
#
|
||||
# For example, if `addResourcePath("foo", "~/bar")` was called, then a GET
|
||||
# request for /foo/one/two.html would rewrite the PATH_INFO as /one/two.html and
|
||||
# send it to the resource path function for "foo". As of this writing, that
|
||||
# function will always be a staticHandler, which serves up a file if it exists
|
||||
# and NULL if it does not.
|
||||
#
|
||||
# Since Shiny 1.3.x, assets registered via addResourcePath should mostly be
|
||||
# served out of httpuv's native static file serving features. However, in the
|
||||
# specific case of subapps, the R code path must be used, because subapps insert
|
||||
# a giant random ID into the beginning of the URL that must be stripped off by
|
||||
# an R route handler (see addSubApp()).
|
||||
resourcePathHandler <- function(req) {
|
||||
if (!identical(req$REQUEST_METHOD, 'GET'))
|
||||
return(NULL)
|
||||
|
||||
# e.g. "/foo/one/two.html"
|
||||
path <- req$PATH_INFO
|
||||
|
||||
match <- regexpr('^/([^/]+)/', path, perl=TRUE)
|
||||
if (match == -1)
|
||||
return(NULL)
|
||||
len <- attr(match, 'capture.length')
|
||||
# e.g. "foo"
|
||||
prefix <- substr(path, 2, 2 + len - 1)
|
||||
|
||||
resInfo <- .globals$resources[[prefix]]
|
||||
if (is.null(resInfo))
|
||||
return(NULL)
|
||||
|
||||
# e.g. "/one/two.html"
|
||||
suffix <- substr(path, 2 + len, nchar(path))
|
||||
|
||||
# Create a new request that's a clone of the current request, but adjust
|
||||
# PATH_INFO and SCRIPT_NAME to reflect that we have already matched the first
|
||||
# path element (e.g. "/foo"). See routeHandler() for more info.
|
||||
subreq <- as.environment(as.list(req, all.names=TRUE))
|
||||
subreq$PATH_INFO <- suffix
|
||||
subreq$SCRIPT_NAME <- paste(subreq$SCRIPT_NAME, substr(path, 1, 2 + len), sep='')
|
||||
@@ -99,23 +201,23 @@ resourcePathHandler <- function(req) {
|
||||
#'
|
||||
#' Defines the server-side logic of the Shiny application. This generally
|
||||
#' involves creating functions that map user inputs to various kinds of output.
|
||||
#' In older versions of Shiny, it was necessary to call \code{shinyServer()} in
|
||||
#' the \code{server.R} file, but this is no longer required as of Shiny 0.10.
|
||||
#' Now the \code{server.R} file may simply return the appropriate server
|
||||
#' In older versions of Shiny, it was necessary to call `shinyServer()` in
|
||||
#' the `server.R` file, but this is no longer required as of Shiny 0.10.
|
||||
#' Now the `server.R` file may simply return the appropriate server
|
||||
#' function (as the last expression in the code), without calling
|
||||
#' \code{shinyServer()}.
|
||||
#' `shinyServer()`.
|
||||
#'
|
||||
#' Call \code{shinyServer} from your application's \code{server.R}
|
||||
#' Call `shinyServer` from your application's `server.R`
|
||||
#' file, passing in a "server function" that provides the server-side logic of
|
||||
#' your application.
|
||||
#'
|
||||
#' The server function will be called when each client (web browser) first loads
|
||||
#' the Shiny application's page. It must take an \code{input} and an
|
||||
#' \code{output} parameter. Any return value will be ignored. It also takes an
|
||||
#' optional \code{session} parameter, which is used when greater control is
|
||||
#' the Shiny application's page. It must take an `input` and an
|
||||
#' `output` parameter. Any return value will be ignored. It also takes an
|
||||
#' optional `session` parameter, which is used when greater control is
|
||||
#' needed.
|
||||
#'
|
||||
#' See the \href{http://rstudio.github.com/shiny/tutorial/}{tutorial} for more
|
||||
#' See the [tutorial](http://rstudio.github.com/shiny/tutorial/) for more
|
||||
#' on how to write a server function.
|
||||
#'
|
||||
#' @param func The server function for this application. See the details section
|
||||
@@ -141,8 +243,8 @@ resourcePathHandler <- function(req) {
|
||||
#' })
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @export
|
||||
#' @keywords internal
|
||||
shinyServer <- function(func) {
|
||||
.globals$server <- list(func)
|
||||
invisible(func)
|
||||
@@ -157,7 +259,7 @@ decodeMessage <- function(data) {
|
||||
# Treat message as UTF-8
|
||||
charData <- rawToChar(data)
|
||||
Encoding(charData) <- 'UTF-8'
|
||||
return(jsonlite::fromJSON(charData, simplifyVector=FALSE))
|
||||
return(safeFromJSON(charData, simplifyVector=FALSE))
|
||||
}
|
||||
|
||||
i <- 5
|
||||
@@ -186,7 +288,7 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
|
||||
# This value, if non-NULL, must be present on all HTTP and WebSocket
|
||||
# requests as the Shiny-Shared-Secret header or else access will be
|
||||
# denied (403 response for HTTP, and instant close for websocket).
|
||||
sharedSecret <- getOption('shiny.sharedSecret')
|
||||
checkSharedSecret <- loadSharedSecret()
|
||||
|
||||
appHandlers <- list(
|
||||
http = joinHandlers(c(
|
||||
@@ -194,10 +296,10 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
|
||||
httpHandlers,
|
||||
sys.www.root,
|
||||
resourcePathHandler,
|
||||
reactLogHandler)),
|
||||
reactLogHandler
|
||||
)),
|
||||
ws = function(ws) {
|
||||
if (!is.null(sharedSecret)
|
||||
&& !identical(sharedSecret, ws$request$HTTP_SHINY_SHARED_SECRET)) {
|
||||
if (!checkSharedSecret(ws$request$HTTP_SHINY_SHARED_SECRET)) {
|
||||
ws$close()
|
||||
return(TRUE)
|
||||
}
|
||||
@@ -220,52 +322,38 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
|
||||
if (is.character(msg))
|
||||
msg <- charToRaw(msg)
|
||||
|
||||
if (isTRUE(getOption('shiny.trace'))) {
|
||||
traceOption <- getOption('shiny.trace', FALSE)
|
||||
if (isTRUE(traceOption) || traceOption == "recv") {
|
||||
if (binary)
|
||||
message("RECV ", '$$binary data$$')
|
||||
else
|
||||
message("RECV ", rawToChar(msg))
|
||||
}
|
||||
|
||||
if (identical(charToRaw("\003\xe9"), msg))
|
||||
if (isEmptyMessage(msg))
|
||||
return()
|
||||
|
||||
msg <- decodeMessage(msg)
|
||||
|
||||
# Do our own list simplifying here. sapply/simplify2array give names to
|
||||
# character vectors, which is rarely what we want.
|
||||
if (!is.null(msg$data)) {
|
||||
for (name in names(msg$data)) {
|
||||
val <- msg$data[[name]]
|
||||
|
||||
splitName <- strsplit(name, ':')[[1]]
|
||||
if (length(splitName) > 1) {
|
||||
msg$data[[name]] <- NULL
|
||||
|
||||
if (!inputHandlers$containsKey(splitName[[2]])){
|
||||
# No input handler registered for this type
|
||||
stop("No handler registered for for type ", name)
|
||||
}
|
||||
|
||||
msg$data[[ splitName[[1]] ]] <-
|
||||
inputHandlers$get(splitName[[2]])(
|
||||
val,
|
||||
shinysession,
|
||||
splitName[[1]] )
|
||||
}
|
||||
else if (is.list(val) && is.null(names(val))) {
|
||||
val_flat <- unlist(val, recursive = TRUE)
|
||||
|
||||
if (is.null(val_flat)) {
|
||||
# This is to assign NULL instead of deleting the item
|
||||
msg$data[name] <- list(NULL)
|
||||
} else {
|
||||
msg$data[[name]] <- val_flat
|
||||
}
|
||||
}
|
||||
# Set up a restore context from .clientdata_url_search before
|
||||
# handling all the input values, because the restore context may be
|
||||
# used by an input handler (like the one for "shiny.file"). This
|
||||
# should only happen once, when the app starts.
|
||||
if (is.null(shinysession$restoreContext)) {
|
||||
bookmarkStore <- getShinyOption("bookmarkStore", default = "disable")
|
||||
if (bookmarkStore == "disable") {
|
||||
# If bookmarking is disabled, use empty context
|
||||
shinysession$restoreContext <- RestoreContext$new()
|
||||
} else {
|
||||
# If there's bookmarked state, save it on the session object
|
||||
shinysession$restoreContext <- RestoreContext$new(msg$data$.clientdata_url_search)
|
||||
shinysession$createBookmarkObservers()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
msg$data <- applyInputHandlers(msg$data)
|
||||
|
||||
switch(
|
||||
msg$method,
|
||||
init = {
|
||||
@@ -292,7 +380,19 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
|
||||
shinysession$setShowcase(mode)
|
||||
}
|
||||
|
||||
shinysession$manageInputs(msg$data)
|
||||
# In shinysession$createBookmarkObservers() above, observers may be
|
||||
# created, which puts the shiny session in busyCount > 0 state. That
|
||||
# prevents the manageInputs here from taking immediate effect, by
|
||||
# default. The manageInputs here needs to take effect though, because
|
||||
# otherwise the bookmark observers won't find the clientData they are
|
||||
# looking for. So use `now = TRUE` to force the changes to be
|
||||
# immediate.
|
||||
#
|
||||
# FIXME: break createBookmarkObservers into two separate steps, one
|
||||
# before and one after manageInputs, and put the observer creation
|
||||
# in the latter. Then add an assertion that busyCount == 0L when
|
||||
# this manageInputs is called.
|
||||
shinysession$manageInputs(msg$data, now = TRUE)
|
||||
|
||||
# The client tells us what singletons were rendered into
|
||||
# the initial page
|
||||
@@ -321,33 +421,13 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
|
||||
},
|
||||
shinysession$dispatch(msg)
|
||||
)
|
||||
shinysession$manageHiddenOutputs()
|
||||
# The HTTP_GUID, if it exists, is for Shiny Server reporting purposes
|
||||
shinysession$startTiming(ws$request$HTTP_GUID)
|
||||
shinysession$requestFlush()
|
||||
|
||||
if (exists(".shiny__stdout", globalenv()) &&
|
||||
exists("HTTP_GUID", ws$request)) {
|
||||
# safe to assume we're in shiny-server
|
||||
shiny_stdout <- get(".shiny__stdout", globalenv())
|
||||
|
||||
# eNter a flushReact
|
||||
writeLines(paste("_n_flushReact ", get("HTTP_GUID", ws$request),
|
||||
" @ ", sprintf("%.3f", as.numeric(Sys.time())),
|
||||
sep=""), con=shiny_stdout)
|
||||
flush(shiny_stdout)
|
||||
|
||||
flushReact()
|
||||
|
||||
# eXit a flushReact
|
||||
writeLines(paste("_x_flushReact ", get("HTTP_GUID", ws$request),
|
||||
" @ ", sprintf("%.3f", as.numeric(Sys.time())),
|
||||
sep=""), con=shiny_stdout)
|
||||
flush(shiny_stdout)
|
||||
} else {
|
||||
flushReact()
|
||||
}
|
||||
lapply(appsByToken$values(), function(shinysession) {
|
||||
shinysession$flushOutput()
|
||||
NULL
|
||||
})
|
||||
# Make httpuv return control to Shiny quickly, instead of waiting
|
||||
# for the usual timeout
|
||||
httpuv::interrupt()
|
||||
})
|
||||
}
|
||||
ws$onMessage(function(binary, msg) {
|
||||
@@ -358,6 +438,7 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
|
||||
ws$onClose(function() {
|
||||
shinysession$wsClosed()
|
||||
appsByToken$remove(shinysession$token)
|
||||
appsNeedingFlush$remove(shinysession$token)
|
||||
})
|
||||
|
||||
return(TRUE)
|
||||
@@ -387,9 +468,9 @@ argsForServerFunc <- function(serverFunc, session) {
|
||||
}
|
||||
|
||||
getEffectiveBody <- function(func) {
|
||||
# Note: NULL values are OK. isS4(NULL) returns FALSE, body(NULL)
|
||||
# returns NULL.
|
||||
if (isS4(func) && class(func) == "functionWithTrace")
|
||||
if (is.null(func))
|
||||
NULL
|
||||
else if (isS4(func) && class(func) == "functionWithTrace")
|
||||
body(func@original)
|
||||
else
|
||||
body(func)
|
||||
@@ -437,86 +518,172 @@ startApp <- function(appObj, port, host, quiet) {
|
||||
handlerManager$addHandler(appHandlers$http, "/", tail = TRUE)
|
||||
handlerManager$addWSHandler(appHandlers$ws, "/", tail = TRUE)
|
||||
|
||||
httpuvApp <- handlerManager$createHttpuvApp()
|
||||
httpuvApp$staticPaths <- c(
|
||||
appObj$staticPaths,
|
||||
list(
|
||||
# Always handle /session URLs dynamically, even if / is a static path.
|
||||
"session" = excludeStaticPath(),
|
||||
"shared" = system.file(package = "shiny", "www", "shared")
|
||||
),
|
||||
.globals$resourcePaths
|
||||
)
|
||||
|
||||
# throw an informative warning if a subdirectory of the
|
||||
# app's www dir conflicts with another resource prefix
|
||||
wwwDir <- httpuvApp$staticPaths[["/"]]$path
|
||||
if (length(wwwDir)) {
|
||||
# although httpuv allows for resource prefixes like 'foo/bar',
|
||||
# we won't worry about conflicts in sub-sub directories since
|
||||
# addResourcePath() currently doesn't allow it
|
||||
wwwSubDirs <- list.dirs(wwwDir, recursive = FALSE, full.names = FALSE)
|
||||
resourceConflicts <- intersect(wwwSubDirs, names(httpuvApp$staticPaths))
|
||||
if (length(resourceConflicts)) {
|
||||
warning(
|
||||
"Found subdirectories of your app's www/ directory that ",
|
||||
"conflict with other resource URL prefixes. ",
|
||||
"Consider renaming these directories: '",
|
||||
paste0("www/", resourceConflicts, collapse = "', '"), "'",
|
||||
call. = FALSE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
# check for conflicts in each pairwise combinations of resource mappings
|
||||
checkResourceConflict <- function(paths) {
|
||||
if (length(paths) < 2) return(NULL)
|
||||
# ensure paths is a named character vector: c(resource_path = local_path)
|
||||
paths <- vapply(paths, function(x) if (inherits(x, "staticPath")) x$path else x, character(1))
|
||||
# get all possible pairwise combinations of paths
|
||||
pair_indices <- utils::combn(length(paths), 2, simplify = FALSE)
|
||||
lapply(pair_indices, function(x) {
|
||||
p1 <- paths[x[1]]
|
||||
p2 <- paths[x[2]]
|
||||
if (identical(names(p1), names(p2)) && (p1 != p2)) {
|
||||
warning(
|
||||
"Found multiple local file paths pointing the same resource prefix: ", names(p1), ". ",
|
||||
"If you run into resource-related issues (e.g. 404 requests), consider ",
|
||||
"using `addResourcePath()` and/or `removeResourcePath()` to manage resource mappings.",
|
||||
call. = FALSE
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
checkResourceConflict(httpuvApp$staticPaths)
|
||||
|
||||
httpuvApp$staticPathOptions <- httpuv::staticPathOptions(
|
||||
html_charset = "utf-8",
|
||||
headers = list("X-UA-Compatible" = "IE=edge,chrome=1"),
|
||||
validation =
|
||||
if (!is.null(getOption("shiny.sharedSecret"))) {
|
||||
sprintf('"Shiny-Shared-Secret" == "%s"', getOption("shiny.sharedSecret"))
|
||||
} else {
|
||||
character(0)
|
||||
}
|
||||
)
|
||||
|
||||
if (is.numeric(port) || is.integer(port)) {
|
||||
if (!quiet) {
|
||||
message('\n', 'Listening on http://', host, ':', port)
|
||||
hostString <- host
|
||||
if (httpuv::ipFamily(host) == 6L)
|
||||
hostString <- paste0("[", hostString, "]")
|
||||
message('\n', 'Listening on http://', hostString, ':', port)
|
||||
}
|
||||
return(startServer(host, port, handlerManager$createHttpuvApp()))
|
||||
return(startServer(host, port, httpuvApp))
|
||||
} else if (is.character(port)) {
|
||||
if (!quiet) {
|
||||
message('\n', 'Listening on domain socket ', port)
|
||||
}
|
||||
mask <- attr(port, 'mask')
|
||||
return(startPipeServer(port, mask, handlerManager$createHttpuvApp()))
|
||||
if (is.null(mask)) {
|
||||
stop("`port` is not a valid domain socket (missing `mask` attribute). ",
|
||||
"Note that if you're using the default `host` + `port` ",
|
||||
"configuration (and not domain sockets), then `port` must ",
|
||||
"be numeric, not a string.")
|
||||
}
|
||||
return(startPipeServer(port, mask, httpuvApp))
|
||||
}
|
||||
}
|
||||
|
||||
# Run an application that was created by \code{\link{startApp}}. This
|
||||
# function should normally be called in a \code{while(TRUE)} loop.
|
||||
serviceApp <- function() {
|
||||
if (timerCallbacks$executeElapsed()) {
|
||||
for (shinysession in appsByToken$values()) {
|
||||
shinysession$manageHiddenOutputs()
|
||||
}
|
||||
timerCallbacks$executeElapsed()
|
||||
|
||||
flushReact()
|
||||
|
||||
for (shinysession in appsByToken$values()) {
|
||||
shinysession$flushOutput()
|
||||
}
|
||||
}
|
||||
flushReact()
|
||||
flushPendingSessions()
|
||||
|
||||
# If this R session is interactive, then call service() with a short timeout
|
||||
# to keep the session responsive to user input
|
||||
maxTimeout <- ifelse(interactive(), 100, 1000)
|
||||
|
||||
timeout <- max(1, min(maxTimeout, timerCallbacks$timeToNextEvent()))
|
||||
timeout <- max(1, min(maxTimeout, timerCallbacks$timeToNextEvent(), later::next_op_secs()))
|
||||
service(timeout)
|
||||
|
||||
flushReact()
|
||||
flushPendingSessions()
|
||||
}
|
||||
|
||||
.shinyServerMinVersion <- '0.3.4'
|
||||
|
||||
# Global flag that's TRUE whenever we're inside of the scope of a call to runApp
|
||||
.globals$running <- FALSE
|
||||
|
||||
#' Check whether a Shiny application is running
|
||||
#'
|
||||
#' This function tests whether a Shiny application is currently running.
|
||||
#'
|
||||
#' @return `TRUE` if a Shiny application is currently running. Otherwise,
|
||||
#' `FALSE`.
|
||||
#' @export
|
||||
isRunning <- function() {
|
||||
.globals$running
|
||||
}
|
||||
|
||||
#' Run Shiny Application
|
||||
#'
|
||||
#' Runs a Shiny application. This function normally does not return; interrupt R
|
||||
#' to stop the application (usually by pressing Ctrl+C or Esc).
|
||||
#'
|
||||
#' The host parameter was introduced in Shiny 0.9.0. Its default value of
|
||||
#' \code{"127.0.0.1"} means that, contrary to previous versions of Shiny, only
|
||||
#' `"127.0.0.1"` means that, contrary to previous versions of Shiny, only
|
||||
#' the current machine can access locally hosted Shiny apps. To allow other
|
||||
#' clients to connect, use the value \code{"0.0.0.0"} instead (which was the
|
||||
#' clients to connect, use the value `"0.0.0.0"` instead (which was the
|
||||
#' value that was hard-coded into Shiny in 0.8.0 and earlier).
|
||||
#'
|
||||
#' @param appDir The application to run. Should be one of the following:
|
||||
#' \itemize{
|
||||
#' \item A directory containing \code{server.R}, plus, either \code{ui.R} or
|
||||
#' a \code{www} directory that contains the file \code{index.html}.
|
||||
#' \item A directory containing \code{app.R}.
|
||||
#' \item An \code{.R} file containing a Shiny application, ending with an
|
||||
#' \item A directory containing `server.R`, plus, either `ui.R` or
|
||||
#' a `www` directory that contains the file `index.html`.
|
||||
#' \item A directory containing `app.R`.
|
||||
#' \item An `.R` file containing a Shiny application, ending with an
|
||||
#' expression that produces a Shiny app object.
|
||||
#' \item A list with \code{ui} and \code{server} components.
|
||||
#' \item A Shiny app object created by \code{\link{shinyApp}}.
|
||||
#' \item A list with `ui` and `server` components.
|
||||
#' \item A Shiny app object created by [shinyApp()].
|
||||
#' }
|
||||
#' @param port The TCP port that the application should listen on. If the
|
||||
#' \code{port} is not specified, and the \code{shiny.port} option is set (with
|
||||
#' \code{options(shiny.port = XX)}), then that port will be used. Otherwise,
|
||||
#' `port` is not specified, and the `shiny.port` option is set (with
|
||||
#' `options(shiny.port = XX)`), then that port will be used. Otherwise,
|
||||
#' use a random port.
|
||||
#' @param launch.browser If true, the system's default web browser will be
|
||||
#' launched automatically after the app is started. Defaults to true in
|
||||
#' interactive sessions only. This value of this parameter can also be a
|
||||
#' function to call with the application's URL.
|
||||
#' @param host The IPv4 address that the application should listen on. Defaults
|
||||
#' to the \code{shiny.host} option, if set, or \code{"127.0.0.1"} if not. See
|
||||
#' to the `shiny.host` option, if set, or `"127.0.0.1"` if not. See
|
||||
#' Details.
|
||||
#' @param workerId Can generally be ignored. Exists to help some editions of
|
||||
#' Shiny Server Pro route requests to the correct process.
|
||||
#' @param quiet Should Shiny status messages be shown? Defaults to FALSE.
|
||||
#' @param display.mode The mode in which to display the application. If set to
|
||||
#' the value \code{"showcase"}, shows application code and metadata from a
|
||||
#' \code{DESCRIPTION} file in the application directory alongside the
|
||||
#' application. If set to \code{"normal"}, displays the application normally.
|
||||
#' Defaults to \code{"auto"}, which displays the application in the mode given
|
||||
#' in its \code{DESCRIPTION} file, if any.
|
||||
#' the value `"showcase"`, shows application code and metadata from a
|
||||
#' `DESCRIPTION` file in the application directory alongside the
|
||||
#' application. If set to `"normal"`, displays the application normally.
|
||||
#' Defaults to `"auto"`, which displays the application in the mode given
|
||||
#' in its `DESCRIPTION` file, if any.
|
||||
#' @param test.mode Should the application be launched in test mode? This is
|
||||
#' only used for recording or running automated tests. Defaults to the
|
||||
#' `shiny.testmode` option, or FALSE if the option is not set.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
@@ -529,6 +696,8 @@ serviceApp <- function() {
|
||||
#'
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' # Apps can be run without a server.r and ui.r file
|
||||
#' runApp(list(
|
||||
#' ui = bootstrapPage(
|
||||
@@ -560,22 +729,89 @@ runApp <- function(appDir=getwd(),
|
||||
interactive()),
|
||||
host=getOption('shiny.host', '127.0.0.1'),
|
||||
workerId="", quiet=FALSE,
|
||||
display.mode=c("auto", "normal", "showcase")) {
|
||||
display.mode=c("auto", "normal", "showcase"),
|
||||
test.mode=getOption('shiny.testmode', FALSE)) {
|
||||
on.exit({
|
||||
handlerManager$clear()
|
||||
}, add = TRUE)
|
||||
|
||||
if (.globals$running) {
|
||||
stop("Can't call `runApp()` from within `runApp()`. If your ",
|
||||
"application code contains `runApp()`, please remove it.")
|
||||
}
|
||||
.globals$running <- TRUE
|
||||
on.exit({
|
||||
.globals$running <- FALSE
|
||||
}, add = TRUE)
|
||||
|
||||
if (is.null(host) || is.na(host))
|
||||
host <- '0.0.0.0'
|
||||
# Enable per-app Shiny options, for shinyOptions() and getShinyOption().
|
||||
oldOptionSet <- .globals$options
|
||||
on.exit({
|
||||
.globals$options <- oldOptionSet
|
||||
},add = TRUE)
|
||||
|
||||
# A unique identifier associated with this run of this application. It is
|
||||
# shared across sessions.
|
||||
shinyOptions(appToken = createUniqueId(8))
|
||||
|
||||
# Make warnings print immediately
|
||||
ops <- options(warn = 1)
|
||||
# Set pool.scheduler to support pool package
|
||||
ops <- options(
|
||||
# Raise warn level to 1, but don't lower it
|
||||
warn = max(1, getOption("warn", default = 1)),
|
||||
pool.scheduler = scheduleTask
|
||||
)
|
||||
on.exit(options(ops), add = TRUE)
|
||||
|
||||
# Set up default cache for app.
|
||||
if (is.null(getShinyOption("cache"))) {
|
||||
shinyOptions(cache = MemoryCache$new())
|
||||
}
|
||||
|
||||
appParts <- as.shiny.appobj(appDir)
|
||||
|
||||
# The lines below set some of the app's running options, which
|
||||
# can be:
|
||||
# - left unspeficied (in which case the arguments' default
|
||||
# values from `runApp` kick in);
|
||||
# - passed through `shinyApp`
|
||||
# - passed through `runApp` (this function)
|
||||
# - passed through both `shinyApp` and `runApp` (the latter
|
||||
# takes precedence)
|
||||
#
|
||||
# Matrix of possibilities:
|
||||
# | IN shinyApp | IN runApp | result | check |
|
||||
# |-------------|-----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------|
|
||||
# | no | no | use defaults | exhaust all possibilities: if it's missing (runApp does not specify); THEN if it's not in shinyApp appParts$options; THEN use defaults |
|
||||
# | yes | no | use shinyApp | if it's missing (runApp does not specify); THEN if it's in shinyApp appParts$options; THEN use shinyApp |
|
||||
# | no | yes | use runApp | if it's not missing (runApp specifies), use those |
|
||||
# | yes | yes | use runApp | if it's not missing (runApp specifies), use those |
|
||||
#
|
||||
# I tried to make this as compact and intuitive as possible,
|
||||
# given that there are four distinct possibilities to check
|
||||
appOps <- appParts$options
|
||||
findVal <- function(arg, default) {
|
||||
if (arg %in% names(appOps)) appOps[[arg]] else default
|
||||
}
|
||||
|
||||
if (missing(port))
|
||||
port <- findVal("port", port)
|
||||
if (missing(launch.browser))
|
||||
launch.browser <- findVal("launch.browser", launch.browser)
|
||||
if (missing(host))
|
||||
host <- findVal("host", host)
|
||||
if (missing(quiet))
|
||||
quiet <- findVal("quiet", quiet)
|
||||
if (missing(display.mode))
|
||||
display.mode <- findVal("display.mode", display.mode)
|
||||
if (missing(test.mode))
|
||||
test.mode <- findVal("test.mode", test.mode)
|
||||
|
||||
if (is.null(host) || is.na(host)) host <- '0.0.0.0'
|
||||
|
||||
workerId(workerId)
|
||||
|
||||
if (nzchar(Sys.getenv('SHINY_PORT'))) {
|
||||
if (inShinyServer()) {
|
||||
# If SHINY_PORT is set, we're running under Shiny Server. Check the version
|
||||
# to make sure it is compatible. Older versions of Shiny Server don't set
|
||||
# SHINY_SERVER_VERSION, those will return "" which is considered less than
|
||||
@@ -592,6 +828,11 @@ runApp <- function(appDir=getwd(),
|
||||
# the display.mode parameter. The latter takes precedence.
|
||||
setShowcaseDefault(0)
|
||||
|
||||
.globals$testMode <- test.mode
|
||||
if (test.mode) {
|
||||
message("Running application in test mode.")
|
||||
}
|
||||
|
||||
# If appDir specifies a path, and display mode is specified in the
|
||||
# DESCRIPTION file at that path, apply it here.
|
||||
if (is.character(appDir)) {
|
||||
@@ -607,21 +848,38 @@ runApp <- function(appDir=getwd(),
|
||||
on.exit(close(con), add = TRUE)
|
||||
settings <- read.dcf(con)
|
||||
if ("DisplayMode" %in% colnames(settings)) {
|
||||
mode <- settings[1,"DisplayMode"]
|
||||
mode <- settings[1, "DisplayMode"]
|
||||
if (mode == "Showcase") {
|
||||
setShowcaseDefault(1)
|
||||
if ("IncludeWWW" %in% colnames(settings)) {
|
||||
.globals$IncludeWWW <- as.logical(settings[1, "IncludeWWW"])
|
||||
if (is.na(.globals$IncludeWWW)) {
|
||||
stop("In your Description file, `IncludeWWW` ",
|
||||
"must be set to `True` (default) or `False`")
|
||||
}
|
||||
} else {
|
||||
.globals$IncludeWWW <- TRUE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## default is to show the .js, .css and .html files in the www directory
|
||||
## (if not in showcase mode, this variable will simply be ignored)
|
||||
if (is.null(.globals$IncludeWWW) || is.na(.globals$IncludeWWW)) {
|
||||
.globals$IncludeWWW <- TRUE
|
||||
}
|
||||
|
||||
# If display mode is specified as an argument, apply it (overriding the
|
||||
# value specified in DESCRIPTION, if any).
|
||||
display.mode <- match.arg(display.mode)
|
||||
if (display.mode == "normal")
|
||||
if (display.mode == "normal") {
|
||||
setShowcaseDefault(0)
|
||||
else if (display.mode == "showcase")
|
||||
}
|
||||
else if (display.mode == "showcase") {
|
||||
setShowcaseDefault(1)
|
||||
}
|
||||
|
||||
require(shiny)
|
||||
|
||||
@@ -642,7 +900,15 @@ runApp <- function(appDir=getwd(),
|
||||
}
|
||||
else {
|
||||
# Try up to 20 random ports
|
||||
port <- p_randomInt(3000, 8000)
|
||||
while (TRUE) {
|
||||
port <- p_randomInt(3000, 8000)
|
||||
# Reject ports in this range that are considered unsafe by Chrome
|
||||
# http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome
|
||||
# https://github.com/rstudio/shiny/issues/1784
|
||||
if (!port %in% c(3659, 4045, 6000, 6665:6669, 6697)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Test port to see if we can use it
|
||||
@@ -655,23 +921,47 @@ runApp <- function(appDir=getwd(),
|
||||
}
|
||||
}
|
||||
|
||||
appParts <- as.shiny.appobj(appDir)
|
||||
# Set up the onEnd before we call onStart, so that it gets called even if an
|
||||
# Invoke user-defined onStop callbacks, before the application's internal
|
||||
# onStop callbacks.
|
||||
on.exit({
|
||||
.globals$onStopCallbacks$invoke()
|
||||
.globals$onStopCallbacks <- Callbacks$new()
|
||||
}, add = TRUE)
|
||||
|
||||
# Extract appOptions (which is a list) and store them as shinyOptions, for
|
||||
# this app. (This is the only place we have to store settings that are
|
||||
# accessible both the UI and server portion of the app.)
|
||||
unconsumeAppOptions(appParts$appOptions)
|
||||
|
||||
# Set up the onStop before we call onStart, so that it gets called even if an
|
||||
# error happens in onStart.
|
||||
if (!is.null(appParts$onEnd))
|
||||
on.exit(appParts$onEnd(), add = TRUE)
|
||||
if (!is.null(appParts$onStop))
|
||||
on.exit(appParts$onStop(), add = TRUE)
|
||||
if (!is.null(appParts$onStart))
|
||||
appParts$onStart()
|
||||
|
||||
server <- startApp(appParts, port, host, quiet)
|
||||
|
||||
# Make the httpuv server object accessible. Needed for calling
|
||||
# addResourcePath while app is running.
|
||||
shinyOptions(server = server)
|
||||
|
||||
on.exit({
|
||||
stopServer(server)
|
||||
}, add = TRUE)
|
||||
|
||||
if (!is.character(port)) {
|
||||
# http://0.0.0.0/ doesn't work on QtWebKit (i.e. RStudio viewer)
|
||||
browseHost <- if (identical(host, "0.0.0.0")) "127.0.0.1" else host
|
||||
browseHost <- host
|
||||
if (identical(host, "0.0.0.0")) {
|
||||
# http://0.0.0.0/ doesn't work on QtWebKit (i.e. RStudio viewer)
|
||||
browseHost <- "127.0.0.1"
|
||||
} else if (identical(host, "::")) {
|
||||
browseHost <- "::1"
|
||||
}
|
||||
|
||||
if (httpuv::ipFamily(browseHost) == 6L) {
|
||||
browseHost <- paste0("[", browseHost, "]")
|
||||
}
|
||||
|
||||
appUrl <- paste("http://", browseHost, ":", port, sep="")
|
||||
if (is.function(launch.browser))
|
||||
@@ -694,12 +984,11 @@ runApp <- function(appDir=getwd(),
|
||||
# Top-level ..stacktraceoff..; matches with ..stacktraceon in observe(),
|
||||
# reactive(), Callbacks$invoke(), and others
|
||||
..stacktraceoff..(
|
||||
captureStackTraces(
|
||||
captureStackTraces({
|
||||
while (!.globals$stopped) {
|
||||
serviceApp()
|
||||
Sys.sleep(0.001)
|
||||
..stacktracefloor..(serviceApp())
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
if (isTRUE(.globals$reterror)) {
|
||||
@@ -714,11 +1003,10 @@ runApp <- function(appDir=getwd(),
|
||||
#' Stop the currently running Shiny app
|
||||
#'
|
||||
#' Stops the currently running Shiny app, returning control to the caller of
|
||||
#' \code{\link{runApp}}.
|
||||
#' [runApp()].
|
||||
#'
|
||||
#' @param returnValue The value that should be returned from
|
||||
#' \code{\link{runApp}}.
|
||||
#'
|
||||
#' [runApp()].
|
||||
#' @export
|
||||
stopApp <- function(returnValue = invisible()) {
|
||||
# reterror will indicate whether retval is an error (i.e. it should be passed
|
||||
@@ -747,7 +1035,7 @@ stopApp <- function(returnValue = invisible()) {
|
||||
#'
|
||||
#' Launch Shiny example applications, and optionally, your system's web browser.
|
||||
#'
|
||||
#' @param example The name of the example to run, or \code{NA} (the default) to
|
||||
#' @param example The name of the example to run, or `NA` (the default) to
|
||||
#' list the available examples.
|
||||
#' @param port The TCP port that the application should listen on. Defaults to
|
||||
#' choosing a random port.
|
||||
@@ -755,9 +1043,9 @@ stopApp <- function(returnValue = invisible()) {
|
||||
#' launched automatically after the app is started. Defaults to true in
|
||||
#' interactive sessions only.
|
||||
#' @param host The IPv4 address that the application should listen on. Defaults
|
||||
#' to the \code{shiny.host} option, if set, or \code{"127.0.0.1"} if not.
|
||||
#' to the `shiny.host` option, if set, or `"127.0.0.1"` if not.
|
||||
#' @param display.mode The mode in which to display the example. Defaults to
|
||||
#' \code{showcase}, but may be set to \code{normal} to see the example without
|
||||
#' `showcase`, but may be set to `normal` to see the example without
|
||||
#' code or commentary.
|
||||
#'
|
||||
#' @examples
|
||||
@@ -804,21 +1092,21 @@ runExample <- function(example=NA,
|
||||
|
||||
#' Run a gadget
|
||||
#'
|
||||
#' Similar to \code{runApp}, but handles \code{input$cancel} automatically, and
|
||||
#' Similar to `runApp`, but handles `input$cancel` automatically, and
|
||||
#' if running in RStudio, defaults to viewing the app in the Viewer pane.
|
||||
#'
|
||||
#' @param app Either a Shiny app object as created by
|
||||
#' \code{\link[=shiny]{shinyApp}} et al, or, a UI object.
|
||||
#' @param server Ignored if \code{app} is a Shiny app object; otherwise, passed
|
||||
#' along to \code{shinyApp} (i.e. \code{shinyApp(ui = app, server = server)}).
|
||||
#' @param port See \code{\link[=shiny]{runApp}}.
|
||||
#' [`shinyApp()`][shiny] et al, or, a UI object.
|
||||
#' @param server Ignored if `app` is a Shiny app object; otherwise, passed
|
||||
#' along to `shinyApp` (i.e. `shinyApp(ui = app, server = server)`).
|
||||
#' @param port See [`runApp()`][shiny].
|
||||
#' @param viewer Specify where the gadget should be displayed--viewer pane,
|
||||
#' dialog window, or external browser--by passing in a call to one of the
|
||||
#' \code{\link{viewer}} functions.
|
||||
#' @param stopOnCancel If \code{TRUE} (the default), then an \code{observeEvent}
|
||||
#' is automatically created that handles \code{input$cancel} by calling
|
||||
#' \code{stopApp()} with an error. Pass \code{FALSE} if you want to handle
|
||||
#' \code{input$cancel} yourself.
|
||||
#' [viewer()] functions.
|
||||
#' @param stopOnCancel If `TRUE` (the default), then an `observeEvent`
|
||||
#' is automatically created that handles `input$cancel` by calling
|
||||
#' `stopApp()` with an error. Pass `FALSE` if you want to handle
|
||||
#' `input$cancel` yourself.
|
||||
#' @return The value returned by the gadget.
|
||||
#'
|
||||
#' @examples
|
||||
@@ -837,7 +1125,6 @@ runExample <- function(example=NA,
|
||||
#' # ...or as a single app object
|
||||
#' runGadget(shinyApp(ui, server))
|
||||
#' }
|
||||
#'
|
||||
#' @export
|
||||
runGadget <- function(app, server = NULL, port = getOption("shiny.port"),
|
||||
viewer = paneViewer(), stopOnCancel = TRUE) {
|
||||
@@ -884,10 +1171,10 @@ decorateServerFunc <- function(appobj, serverFunc) {
|
||||
#' other R environments that emulate RStudio's viewer pane/dialog APIs). If
|
||||
#' viewer APIs are not available in the current R environment, then the gadget
|
||||
#' will be displayed in the system's default web browser (see
|
||||
#' \code{\link[utils]{browseURL}}).
|
||||
#' [utils::browseURL()]).
|
||||
#'
|
||||
#' @return A function that takes a single \code{url} parameter, suitable for
|
||||
#' passing as the \code{viewer} argument of \code{\link{runGadget}}.
|
||||
#' @return A function that takes a single `url` parameter, suitable for
|
||||
#' passing as the `viewer` argument of [runGadget()].
|
||||
#'
|
||||
#' @rdname viewer
|
||||
#' @name viewer
|
||||
@@ -895,8 +1182,8 @@ NULL
|
||||
|
||||
#' @param minHeight The minimum height (in pixels) desired to show the gadget in
|
||||
#' the viewer pane. If a positive number, resize the pane if necessary to show
|
||||
#' at least that many pixels. If \code{NULL}, use the existing viewer pane
|
||||
#' size. If \code{"maximize"}, use the maximum available vertical space.
|
||||
#' at least that many pixels. If `NULL`, use the existing viewer pane
|
||||
#' size. If `"maximize"`, use the maximum available vertical space.
|
||||
#' @rdname viewer
|
||||
#' @export
|
||||
paneViewer <- function(minHeight = NULL) {
|
||||
@@ -925,7 +1212,7 @@ dialogViewer <- function(dialogName, width = 600, height = 600) {
|
||||
}
|
||||
}
|
||||
|
||||
#' @param browser See \code{\link[utils]{browseURL}}.
|
||||
#' @param browser See [utils::browseURL()].
|
||||
#' @rdname viewer
|
||||
#' @export
|
||||
browserViewer <- function(browser = getOption("browser")) {
|
||||
@@ -933,3 +1220,15 @@ browserViewer <- function(browser = getOption("browser")) {
|
||||
utils::browseURL(url, browser = browser)
|
||||
}
|
||||
}
|
||||
|
||||
# Returns TRUE if we're running in Shiny Server or other hosting environment,
|
||||
# otherwise returns FALSE.
|
||||
inShinyServer <- function() {
|
||||
nzchar(Sys.getenv('SHINY_PORT'))
|
||||
}
|
||||
|
||||
# This check was moved out of the main function body because of an issue with
|
||||
# the RStudio debugger. (#1474)
|
||||
isEmptyMessage <- function(msg) {
|
||||
identical(as.raw(c(0x03, 0xe9)), msg)
|
||||
}
|
||||
|
||||
166
R/shiny-options.R
Normal file
166
R/shiny-options.R
Normal file
@@ -0,0 +1,166 @@
|
||||
.globals$options <- list()
|
||||
|
||||
#' @param name Name of an option to get.
|
||||
#' @param default Value to be returned if the option is not currently set.
|
||||
#' @rdname shinyOptions
|
||||
#' @export
|
||||
getShinyOption <- function(name, default = NULL) {
|
||||
# Make sure to use named (not numeric) indexing
|
||||
name <- as.character(name)
|
||||
|
||||
if (name %in% names(.globals$options))
|
||||
.globals$options[[name]]
|
||||
else
|
||||
default
|
||||
}
|
||||
|
||||
#' Get or set Shiny options
|
||||
#'
|
||||
#' `getShinyOption()` retrieves the value of a Shiny option. `shinyOptions()`
|
||||
#' sets the value of Shiny options; it can also be used to return a list of all
|
||||
#' currently-set Shiny options.
|
||||
#'
|
||||
#' @section Scope:
|
||||
#' There is a global option set which is available by default. When a Shiny
|
||||
#' application is run with [runApp()], that option set is duplicated and the
|
||||
#' new option set is available for getting or setting values. If options
|
||||
#' are set from `global.R`, `app.R`, `ui.R`, or `server.R`, or if they are set
|
||||
#' from inside the server function, then the options will be scoped to the
|
||||
#' application. When the application exits, the new option set is discarded and
|
||||
#' the global option set is restored.
|
||||
#'
|
||||
#' @section Options:
|
||||
#' There are a number of global options that affect Shiny's behavior. These can
|
||||
#' be set globally with `options()` or locally (for a single app) with
|
||||
#' `shinyOptions()`.
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{shiny.autoreload (defaults to `FALSE`)}{If `TRUE` when a Shiny app is launched, the
|
||||
#' app directory will be continually monitored for changes to files that
|
||||
#' have the extensions: r, htm, html, js, css, png, jpg, jpeg, gif. If any
|
||||
#' changes are detected, all connected Shiny sessions are reloaded. This
|
||||
#' allows for fast feedback loops when tweaking Shiny UI.
|
||||
#'
|
||||
#' Since monitoring for changes is expensive (we simply poll for last
|
||||
#' modified times), this feature is intended only for development.
|
||||
#'
|
||||
#' You can customize the file patterns Shiny will monitor by setting the
|
||||
#' shiny.autoreload.pattern option. For example, to monitor only ui.R:
|
||||
#' `options(shiny.autoreload.pattern = glob2rx("ui.R"))`
|
||||
#'
|
||||
#' The default polling interval is 500 milliseconds. You can change this
|
||||
#' by setting e.g. `options(shiny.autoreload.interval = 2000)` (every
|
||||
#' two seconds).}
|
||||
#' \item{shiny.deprecation.messages (defaults to `TRUE`)}{This controls whether messages for
|
||||
#' deprecated functions in Shiny will be printed. See
|
||||
#' [shinyDeprecated()] for more information.}
|
||||
#' \item{shiny.error (defaults to `NULL`)}{This can be a function which is called when an error
|
||||
#' occurs. For example, `options(shiny.error=recover)` will result a
|
||||
#' the debugger prompt when an error occurs.}
|
||||
#' \item{shiny.fullstacktrace (defaults to `FALSE`)}{Controls whether "pretty" (`FALSE`) or full
|
||||
#' stack traces (`TRUE`) are dumped to the console when errors occur during Shiny app execution.
|
||||
#' Pretty stack traces attempt to only show user-supplied code, but this pruning can't always
|
||||
#' be done 100% correctly.}
|
||||
#' \item{shiny.host (defaults to `"127.0.0.1"`)}{The IP address that Shiny should listen on. See
|
||||
#' [runApp()] for more information.}
|
||||
#' \item{shiny.jquery.version (defaults to `3`)}{The major version of jQuery to use.
|
||||
#' Currently only values of `3` or `1` are supported. If `1`, then jQuery 1.12.4 is used. If `3`,
|
||||
#' then jQuery 3.4.1 is used.}
|
||||
#' \item{shiny.json.digits (defaults to `16`)}{The number of digits to use when converting
|
||||
#' numbers to JSON format to send to the client web browser.}
|
||||
#' \item{shiny.launch.browser (defaults to `interactive()`)}{A boolean which controls the default behavior
|
||||
#' when an app is run. See [runApp()] for more information.}
|
||||
#' \item{shiny.maxRequestSize (defaults to 5MB)}{This is a number which specifies the maximum
|
||||
#' web request size, which serves as a size limit for file uploads.}
|
||||
#' \item{shiny.minified (defaults to `TRUE`)}{By default
|
||||
#' Whether or not to include Shiny's JavaScript as a minified (`shiny.min.js`)
|
||||
#' or un-minified (`shiny.js`) file. The un-minified version is larger,
|
||||
#' but can be helpful for development and debugging.}
|
||||
#' \item{shiny.port (defaults to a random open port)}{A port number that Shiny will listen on. See
|
||||
#' [runApp()] for more information.}
|
||||
#' \item{shiny.reactlog (defaults to `FALSE`)}{If `TRUE`, enable logging of reactive events,
|
||||
#' which can be viewed later with the [reactlogShow()] function.
|
||||
#' This incurs a substantial performance penalty and should not be used in
|
||||
#' production.}
|
||||
#' \item{shiny.sanitize.errors (defaults to `FALSE`)}{If `TRUE`, then normal errors (i.e.
|
||||
#' errors not wrapped in `safeError`) won't show up in the app; a simple
|
||||
#' generic error message is printed instead (the error and strack trace printed
|
||||
#' to the console remain unchanged). If you want to sanitize errors in general, but you DO want a
|
||||
#' particular error `e` to get displayed to the user, then set this option
|
||||
#' to `TRUE` and use `stop(safeError(e))` for errors you want the
|
||||
#' user to see.}
|
||||
#' \item{shiny.stacktraceoffset (defaults to `TRUE`)}{If `TRUE`, then Shiny's printed stack
|
||||
#' traces will display srcrefs one line above their usual location. This is
|
||||
#' an arguably more intuitive arrangement for casual R users, as the name
|
||||
#' of a function appears next to the srcref where it is defined, rather than
|
||||
#' where it is currently being called from.}
|
||||
#' \item{shiny.suppressMissingContextError (defaults to `FALSE`)}{Normally, invoking a reactive
|
||||
#' outside of a reactive context (or [isolate()]) results in
|
||||
#' an error. If this is `TRUE`, don't error in these cases. This
|
||||
#' should only be used for debugging or demonstrations of reactivity at the
|
||||
#' console.}
|
||||
#' \item{shiny.testmode (defaults to `FALSE`)}{If `TRUE`, then various features for testing Shiny
|
||||
#' applications are enabled.}
|
||||
#' \item{shiny.trace (defaults to `FALSE`)}{Print messages sent between the R server and the web
|
||||
#' browser client to the R console. This is useful for debugging. Possible
|
||||
#' values are `"send"` (only print messages sent to the client),
|
||||
#' `"recv"` (only print messages received by the server), `TRUE`
|
||||
#' (print all messages), or `FALSE` (default; don't print any of these
|
||||
#' messages).}
|
||||
#' \item{shiny.usecairo (defaults to `TRUE`)}{This is used to disable graphical rendering by the
|
||||
#' Cairo package, if it is installed. See [plotPNG()] for more
|
||||
#' information.}
|
||||
#' }
|
||||
#' @param ... Options to set, with the form `name = value`.
|
||||
#' @aliases shiny-options
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' shinyOptions(myOption = 10)
|
||||
#' getShinyOption("myOption")
|
||||
#' }
|
||||
#' @export
|
||||
shinyOptions <- function(...) {
|
||||
newOpts <- list(...)
|
||||
|
||||
if (length(newOpts) > 0) {
|
||||
.globals$options <- dropNulls(mergeVectors(.globals$options, newOpts))
|
||||
invisible(.globals$options)
|
||||
} else {
|
||||
.globals$options
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Eval an expression with a new option set
|
||||
withLocalOptions <- function(expr) {
|
||||
oldOptionSet <- .globals$options
|
||||
on.exit(.globals$options <- oldOptionSet)
|
||||
|
||||
expr
|
||||
}
|
||||
|
||||
|
||||
# Get specific shiny options and put them in a list, reset those shiny options,
|
||||
# and then return the options list. This should be during the creation of a
|
||||
# shiny app object, which happens before another option frame is added to the
|
||||
# options stack (the new option frame is added when the app is run). This
|
||||
# function "consumes" the options when the shinyApp object is created, so the
|
||||
# options won't affect another app that is created later.
|
||||
consumeAppOptions <- function() {
|
||||
options <- list(
|
||||
appDir = getwd(),
|
||||
bookmarkStore = getShinyOption("bookmarkStore")
|
||||
)
|
||||
|
||||
shinyOptions(appDir = NULL, bookmarkStore = NULL)
|
||||
|
||||
options
|
||||
}
|
||||
|
||||
# Do the inverse of consumeAppOptions. This should be called once the app is
|
||||
# started.
|
||||
unconsumeAppOptions <- function(options) {
|
||||
if (!is.null(options)) {
|
||||
do.call(shinyOptions, options)
|
||||
}
|
||||
}
|
||||
82
R/shinyui.R
82
R/shinyui.R
@@ -4,9 +4,9 @@ NULL
|
||||
#' Load the MathJax library and typeset math expressions
|
||||
#'
|
||||
#' This function adds MathJax to the page and typeset the math expressions (if
|
||||
#' found) in the content \code{...}. It only needs to be called once in an app
|
||||
#' unless the content is rendered \emph{after} the page is loaded, e.g. via
|
||||
#' \code{\link{renderUI}}, in which case we have to call it explicitly every
|
||||
#' found) in the content `...`. It only needs to be called once in an app
|
||||
#' unless the content is rendered *after* the page is loaded, e.g. via
|
||||
#' [renderUI()], in which case we have to call it explicitly every
|
||||
#' time we write math expressions to the output.
|
||||
#' @param ... any HTML elements to apply MathJax to
|
||||
#' @export
|
||||
@@ -14,17 +14,17 @@ NULL
|
||||
#' # now we can just write "static" content without withMathJax()
|
||||
#' div("more math here $$\\sqrt{2}$$")
|
||||
withMathJax <- function(...) {
|
||||
path <- 'https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'
|
||||
path <- 'https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'
|
||||
tagList(
|
||||
tags$head(
|
||||
singleton(tags$script(src = path, type = 'text/javascript'))
|
||||
),
|
||||
...,
|
||||
tags$script(HTML('MathJax.Hub.Queue(["Typeset", MathJax.Hub]);'))
|
||||
tags$script(HTML('if (window.MathJax) MathJax.Hub.Queue(["Typeset", MathJax.Hub]);'))
|
||||
)
|
||||
}
|
||||
|
||||
renderPage <- function(ui, connection, showcase=0) {
|
||||
renderPage <- function(ui, connection, showcase=0, testMode=FALSE) {
|
||||
# If the ui is a NOT complete document (created by htmlTemplate()), then do some
|
||||
# preprocessing and make sure it's a complete document.
|
||||
if (!inherits(ui, "html_document")) {
|
||||
@@ -42,13 +42,40 @@ renderPage <- function(ui, connection, showcase=0) {
|
||||
)
|
||||
}
|
||||
|
||||
jquery <- function() {
|
||||
version <- getOption("shiny.jquery.version", 3)
|
||||
if (version == 3) {
|
||||
return(htmlDependency(
|
||||
"jquery", "3.4.1",
|
||||
c(href = "shared"),
|
||||
script = "jquery.min.js"
|
||||
))
|
||||
}
|
||||
if (version == 1) {
|
||||
return(htmlDependency(
|
||||
"jquery", "1.12.4",
|
||||
c(href = "shared/legacy"),
|
||||
script = "jquery.min.js"
|
||||
))
|
||||
}
|
||||
stop("Unsupported version of jQuery: ", version)
|
||||
}
|
||||
|
||||
shiny_deps <- list(
|
||||
htmlDependency("json2", "2014.02.04", c(href="shared"), script = "json2-min.js"),
|
||||
htmlDependency("jquery", "1.11.3", c(href="shared"), script = "jquery.min.js"),
|
||||
jquery(),
|
||||
htmlDependency("shiny", utils::packageVersion("shiny"), c(href="shared"),
|
||||
script = if (getOption("shiny.minified", TRUE)) "shiny.min.js" else "shiny.js",
|
||||
stylesheet = "shiny.css")
|
||||
)
|
||||
|
||||
if (testMode) {
|
||||
# Add code injection listener if in test mode
|
||||
shiny_deps[[length(shiny_deps) + 1]] <-
|
||||
htmlDependency("shiny-testmode", utils::packageVersion("shiny"),
|
||||
c(href="shared"), script = "shiny-testmode.js")
|
||||
}
|
||||
|
||||
html <- renderDocument(ui, shiny_deps, processDep = createWebDependency)
|
||||
writeUTF8(html, con = connection)
|
||||
}
|
||||
@@ -63,7 +90,7 @@ renderPage <- function(ui, connection, showcase=0) {
|
||||
#'
|
||||
#' @param ui A user interace definition
|
||||
#' @return The user interface definition, without modifications or side effects.
|
||||
#'
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
shinyUI <- function(ui) {
|
||||
.globals$ui <- list(ui)
|
||||
@@ -90,21 +117,40 @@ uiHttpHandler <- function(ui, uiPattern = "^/$") {
|
||||
if (!is.null(mode))
|
||||
showcaseMode <- mode
|
||||
}
|
||||
uiValue <- if (is.function(ui)) {
|
||||
if (length(formals(ui)) > 0) {
|
||||
# No corresponding ..stacktraceoff.., this is pure user code
|
||||
..stacktraceon..(ui(req))
|
||||
} else {
|
||||
# No corresponding ..stacktraceoff.., this is pure user code
|
||||
..stacktraceon..(ui())
|
||||
}
|
||||
|
||||
testMode <- .globals$testMode %OR% FALSE
|
||||
|
||||
# Create a restore context using query string
|
||||
bookmarkStore <- getShinyOption("bookmarkStore", default = "disable")
|
||||
if (bookmarkStore == "disable") {
|
||||
# If bookmarking is disabled, use empty context
|
||||
restoreContext <- RestoreContext$new()
|
||||
} else {
|
||||
ui
|
||||
restoreContext <- RestoreContext$new(req$QUERY_STRING)
|
||||
}
|
||||
|
||||
withRestoreContext(restoreContext, {
|
||||
uiValue <- NULL
|
||||
|
||||
if (is.function(ui)) {
|
||||
if (length(formals(ui)) > 0) {
|
||||
# No corresponding ..stacktraceoff.., this is pure user code
|
||||
uiValue <- ..stacktraceon..(ui(req))
|
||||
} else {
|
||||
# No corresponding ..stacktraceoff.., this is pure user code
|
||||
uiValue <- ..stacktraceon..(ui())
|
||||
}
|
||||
} else {
|
||||
if (getCurrentRestoreContext()$active) {
|
||||
warning("Trying to restore saved app state, but UI code must be a function for this to work! See ?enableBookmarking")
|
||||
}
|
||||
uiValue <- ui
|
||||
}
|
||||
})
|
||||
if (is.null(uiValue))
|
||||
return(NULL)
|
||||
|
||||
renderPage(uiValue, textConn, showcaseMode)
|
||||
renderPage(uiValue, textConn, showcaseMode, testMode)
|
||||
html <- paste(readLines(textConn, encoding = 'UTF-8'), collapse='\n')
|
||||
return(httpResponse(200, content=enc2utf8(html)))
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
globalVariables('func')
|
||||
utils::globalVariables('func')
|
||||
|
||||
#' Mark a function as a render function
|
||||
#'
|
||||
#' Should be called by implementers of \code{renderXXX} functions in order to
|
||||
#' Should be called by implementers of `renderXXX` functions in order to
|
||||
#' mark their return values as Shiny render functions, and to provide a hint to
|
||||
#' Shiny regarding what UI function is most commonly used with this type of
|
||||
#' render function. This can be used in R Markdown documents to create complete
|
||||
@@ -12,24 +12,126 @@ globalVariables('func')
|
||||
#' an output ID.
|
||||
#' @param renderFunc A function that is suitable for assigning to a Shiny output
|
||||
#' slot.
|
||||
#' @return The \code{renderFunc} function, with annotations.
|
||||
#' @param outputArgs A list of arguments to pass to the `uiFunc`. Render
|
||||
#' functions should include `outputArgs = list()` in their own parameter
|
||||
#' list, and pass through the value to `markRenderFunction`, to allow
|
||||
#' app authors to customize outputs. (Currently, this is only supported for
|
||||
#' dynamically generated UIs, such as those created by Shiny code snippets
|
||||
#' embedded in R Markdown documents).
|
||||
#' @return The `renderFunc` function, with annotations.
|
||||
#' @export
|
||||
markRenderFunction <- function(uiFunc, renderFunc, outputArgs = list()) {
|
||||
# a mutable object that keeps track of whether `useRenderFunction` has been
|
||||
# executed (this usually only happens when rendering Shiny code snippets in
|
||||
# an interactive R Markdown document); its initial value is FALSE
|
||||
hasExecuted <- Mutable$new()
|
||||
hasExecuted$set(FALSE)
|
||||
|
||||
origRenderFunc <- renderFunc
|
||||
renderFunc <- function(...) {
|
||||
# if the user provided something through `outputArgs` BUT the
|
||||
# `useRenderFunction` was not executed, then outputArgs will be ignored,
|
||||
# so throw a warning to let user know the correct usage
|
||||
if (length(outputArgs) != 0 && !hasExecuted$get()) {
|
||||
warning("Unused argument: outputArgs. The argument outputArgs is only ",
|
||||
"meant to be used when embedding snippets of Shiny code in an ",
|
||||
"R Markdown code chunk (using runtime: shiny). When running a ",
|
||||
"full Shiny app, please set the output arguments directly in ",
|
||||
"the corresponding output function of your UI code.")
|
||||
# stop warning from happening again for the same object
|
||||
hasExecuted$set(TRUE)
|
||||
}
|
||||
if (is.null(formals(origRenderFunc))) origRenderFunc()
|
||||
else origRenderFunc(...)
|
||||
}
|
||||
|
||||
structure(renderFunc,
|
||||
class = c("shiny.render.function", "function"),
|
||||
outputFunc = uiFunc,
|
||||
outputArgs = outputArgs,
|
||||
hasExecuted = hasExecuted)
|
||||
}
|
||||
|
||||
#' @export
|
||||
print.shiny.render.function <- function(x, ...) {
|
||||
cat_line("<shiny.render.function>")
|
||||
}
|
||||
|
||||
#' Implement render functions
|
||||
#'
|
||||
#' @param func A function without parameters, that returns user data. If the
|
||||
#' returned value is a promise, then the render function will proceed in async
|
||||
#' mode.
|
||||
#' @param transform A function that takes four arguments: `value`,
|
||||
#' `session`, `name`, and `...` (for future-proofing). This
|
||||
#' function will be invoked each time a value is returned from `func`,
|
||||
#' and is responsible for changing the value into a JSON-ready value to be
|
||||
#' JSON-encoded and sent to the browser.
|
||||
#' @param outputFunc The UI function that is used (or most commonly used) with
|
||||
#' this render function. This can be used in R Markdown documents to create
|
||||
#' complete output widgets out of just the render function.
|
||||
#' @param outputArgs A list of arguments to pass to the `outputFunc`.
|
||||
#' Render functions should include `outputArgs = list()` in their own
|
||||
#' parameter list, and pass through the value as this argument, to allow app
|
||||
#' authors to customize outputs. (Currently, this is only supported for
|
||||
#' dynamically generated UIs, such as those created by Shiny code snippets
|
||||
#' embedded in R Markdown documents).
|
||||
#' @return An annotated render function, ready to be assigned to an
|
||||
#' `output` slot.
|
||||
#'
|
||||
#' @export
|
||||
markRenderFunction <- function(uiFunc, renderFunc) {
|
||||
structure(renderFunc,
|
||||
class = c("shiny.render.function", "function"),
|
||||
outputFunc = uiFunc)
|
||||
createRenderFunction <- function(
|
||||
func, transform = function(value, session, name, ...) value,
|
||||
outputFunc = NULL, outputArgs = NULL
|
||||
) {
|
||||
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
hybrid_chain(
|
||||
func(),
|
||||
function(value, .visible) {
|
||||
transform(setVisible(value, .visible), shinysession, name, ...)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!is.null(outputFunc))
|
||||
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
|
||||
else
|
||||
renderFunc
|
||||
}
|
||||
|
||||
useRenderFunction <- function(renderFunc, inline = FALSE) {
|
||||
outputFunction <- attr(renderFunc, "outputFunc")
|
||||
outputArgs <- attr(renderFunc, "outputArgs")
|
||||
hasExecuted <- attr(renderFunc, "hasExecuted")
|
||||
hasExecuted$set(TRUE)
|
||||
|
||||
for (arg in names(outputArgs)) {
|
||||
if (!arg %in% names(formals(outputFunction))) {
|
||||
stop(paste0("Unused argument: in 'outputArgs', '",
|
||||
arg, "' is not an valid argument for ",
|
||||
"the output function"))
|
||||
outputArgs[[arg]] <- NULL
|
||||
}
|
||||
}
|
||||
|
||||
id <- createUniqueId(8, "out")
|
||||
|
||||
o <- getDefaultReactiveDomain()$output
|
||||
if (!is.null(o))
|
||||
if (!is.null(o)) {
|
||||
o[[id]] <- renderFunc
|
||||
if (is.logical(formals(outputFunction)[["inline"]])) {
|
||||
outputFunction(id, inline = inline)
|
||||
} else outputFunction(id)
|
||||
# If there's a namespace, we must respect it
|
||||
id <- getDefaultReactiveDomain()$ns(id)
|
||||
}
|
||||
|
||||
# Make the id the first positional argument
|
||||
outputArgs <- c(list(id), outputArgs)
|
||||
|
||||
if (is.logical(formals(outputFunction)[["inline"]]) && !("inline" %in% names(outputArgs))) {
|
||||
outputArgs[["inline"]] <- inline
|
||||
}
|
||||
|
||||
do.call(outputFunction, outputArgs)
|
||||
}
|
||||
|
||||
#' @export
|
||||
@@ -38,44 +140,83 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
|
||||
useRenderFunction(x, inline = inline)
|
||||
}
|
||||
|
||||
|
||||
#' Mark a render function with attributes that will be used by the output
|
||||
#'
|
||||
#' @inheritParams markRenderFunction
|
||||
#' @param snapshotExclude If TRUE, exclude the output from test snapshots.
|
||||
#' @param snapshotPreprocess A function for preprocessing the value before
|
||||
#' taking a test snapshot.
|
||||
#'
|
||||
#' @keywords internal
|
||||
markOutputAttrs <- function(renderFunc, snapshotExclude = NULL,
|
||||
snapshotPreprocess = NULL)
|
||||
{
|
||||
# Add the outputAttrs attribute if necessary
|
||||
if (is.null(attr(renderFunc, "outputAttrs", TRUE))) {
|
||||
attr(renderFunc, "outputAttrs") <- list()
|
||||
}
|
||||
|
||||
if (!is.null(snapshotExclude)) {
|
||||
attr(renderFunc, "outputAttrs")$snapshotExclude <- snapshotExclude
|
||||
}
|
||||
|
||||
if (!is.null(snapshotPreprocess)) {
|
||||
attr(renderFunc, "outputAttrs")$snapshotPreprocess <- snapshotPreprocess
|
||||
}
|
||||
|
||||
renderFunc
|
||||
}
|
||||
|
||||
#' Image file output
|
||||
#'
|
||||
#' Renders a reactive image that is suitable for assigning to an \code{output}
|
||||
#' Renders a reactive image that is suitable for assigning to an `output`
|
||||
#' slot.
|
||||
#'
|
||||
#' The expression \code{expr} must return a list containing the attributes for
|
||||
#' the \code{img} object on the client web page. For the image to display,
|
||||
#' properly, the list must have at least one entry, \code{src}, which is the
|
||||
#' path to the image file. It may also useful to have a \code{contentType}
|
||||
#' The expression `expr` must return a list containing the attributes for
|
||||
#' the `img` object on the client web page. For the image to display,
|
||||
#' properly, the list must have at least one entry, `src`, which is the
|
||||
#' path to the image file. It may also useful to have a `contentType`
|
||||
#' entry specifying the MIME type of the image. If one is not provided,
|
||||
#' \code{renderImage} will try to autodetect the type, based on the file
|
||||
#' `renderImage` will try to autodetect the type, based on the file
|
||||
#' extension.
|
||||
#'
|
||||
#' Other elements such as \code{width}, \code{height}, \code{class}, and
|
||||
#' \code{alt}, can also be added to the list, and they will be used as
|
||||
#' attributes in the \code{img} object.
|
||||
#' Other elements such as `width`, `height`, `class`, and
|
||||
#' `alt`, can also be added to the list, and they will be used as
|
||||
#' attributes in the `img` object.
|
||||
#'
|
||||
#' The corresponding HTML output tag should be \code{div} or \code{img} and have
|
||||
#' the CSS class name \code{shiny-image-output}.
|
||||
#' The corresponding HTML output tag should be `div` or `img` and have
|
||||
#' the CSS class name `shiny-image-output`.
|
||||
#'
|
||||
#' @seealso For more details on how the images are generated, and how to control
|
||||
#' the output, see \code{\link{plotPNG}}.
|
||||
#' the output, see [plotPNG()].
|
||||
#'
|
||||
#' @param expr An expression that returns a list.
|
||||
#' @param env The environment in which to evaluate \code{expr}.
|
||||
#' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This
|
||||
#' @param env The environment in which to evaluate `expr`.
|
||||
#' @param quoted Is `expr` a quoted expression (with `quote()`)? This
|
||||
#' is useful if you want to save an expression in a variable.
|
||||
#' @param deleteFile Should the file in \code{func()$src} be deleted after
|
||||
#' @param deleteFile Should the file in `func()$src` be deleted after
|
||||
#' it is sent to the client browser? Generally speaking, if the image is a
|
||||
#' temp file generated within \code{func}, then this should be \code{TRUE};
|
||||
#' if the image is not a temp file, this should be \code{FALSE}.
|
||||
#'
|
||||
#' temp file generated within `func`, then this should be `TRUE`;
|
||||
#' if the image is not a temp file, this should be `FALSE`.
|
||||
#' @param outputArgs A list of arguments to be passed through to the implicit
|
||||
#' call to [imageOutput()] when `renderImage` is used in an
|
||||
#' interactive R Markdown document.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' shinyServer(function(input, output, clientData) {
|
||||
#' ui <- fluidPage(
|
||||
#' sliderInput("n", "Number of observations", 2, 1000, 500),
|
||||
#' plotOutput("plot1"),
|
||||
#' plotOutput("plot2"),
|
||||
#' plotOutput("plot3")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#'
|
||||
#' # A plot of fixed size
|
||||
#' output$plot1 <- renderImage({
|
||||
@@ -97,14 +238,14 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
|
||||
#' output$plot2 <- renderImage({
|
||||
#' # Read plot2's width and height. These are reactive values, so this
|
||||
#' # expression will re-run whenever these values change.
|
||||
#' width <- clientData$output_plot2_width
|
||||
#' height <- clientData$output_plot2_height
|
||||
#' width <- session$clientData$output_plot2_width
|
||||
#' height <- session$clientData$output_plot2_height
|
||||
#'
|
||||
#' # A temp file to save the output.
|
||||
#' outfile <- tempfile(fileext='.png')
|
||||
#'
|
||||
#' png(outfile, width=width, height=height)
|
||||
#' hist(rnorm(input$obs))
|
||||
#' hist(rnorm(input$n))
|
||||
#' dev.off()
|
||||
#'
|
||||
#' # Return a list containing the filename
|
||||
@@ -115,6 +256,8 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
|
||||
#' }, deleteFile = TRUE)
|
||||
#'
|
||||
#' # Send a pre-rendered image, and don't delete the image after sending it
|
||||
#' # NOTE: For this example to work, it would require files in a subdirectory
|
||||
#' # named images/
|
||||
#' output$plot3 <- renderImage({
|
||||
#' # When input$n is 1, filename is ./images/image1.jpeg
|
||||
#' filename <- normalizePath(file.path('./images',
|
||||
@@ -123,31 +266,33 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
|
||||
#' # Return a list containing the filename
|
||||
#' list(src = filename)
|
||||
#' }, deleteFile = FALSE)
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
deleteFile=TRUE) {
|
||||
deleteFile=TRUE, outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
return(markRenderFunction(imageOutput, function(shinysession, name, ...) {
|
||||
imageinfo <- func()
|
||||
# Should the file be deleted after being sent? If .deleteFile not set or if
|
||||
# TRUE, then delete; otherwise don't delete.
|
||||
if (deleteFile) {
|
||||
on.exit(unlink(imageinfo$src))
|
||||
}
|
||||
createRenderFunction(func,
|
||||
transform = function(imageinfo, session, name, ...) {
|
||||
# Should the file be deleted after being sent? If .deleteFile not set or if
|
||||
# TRUE, then delete; otherwise don't delete.
|
||||
if (deleteFile) {
|
||||
on.exit(unlink(imageinfo$src))
|
||||
}
|
||||
|
||||
# If contentType not specified, autodetect based on extension
|
||||
contentType <- imageinfo$contentType %OR% getContentType(imageinfo$src)
|
||||
# If contentType not specified, autodetect based on extension
|
||||
contentType <- imageinfo$contentType %OR% getContentType(imageinfo$src)
|
||||
|
||||
# Extra values are everything in imageinfo except 'src' and 'contentType'
|
||||
extra_attr <- imageinfo[!names(imageinfo) %in% c('src', 'contentType')]
|
||||
# Extra values are everything in imageinfo except 'src' and 'contentType'
|
||||
extra_attr <- imageinfo[!names(imageinfo) %in% c('src', 'contentType')]
|
||||
|
||||
# Return a list with src, and other img attributes
|
||||
c(src = shinysession$fileUrl(name, file=imageinfo$src, contentType=contentType),
|
||||
extra_attr)
|
||||
}))
|
||||
# Return a list with src, and other img attributes
|
||||
c(src = session$fileUrl(name, file=imageinfo$src, contentType=contentType),
|
||||
extra_attr)
|
||||
},
|
||||
imageOutput, outputArgs)
|
||||
}
|
||||
|
||||
|
||||
@@ -155,141 +300,199 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
#'
|
||||
#' Makes a reactive version of the given function that captures any printed
|
||||
#' output, and also captures its printable result (unless
|
||||
#' \code{\link{invisible}}), into a string. The resulting function is suitable
|
||||
#' for assigning to an \code{output} slot.
|
||||
#' [base::invisible()]), into a string. The resulting function is suitable
|
||||
#' for assigning to an `output` slot.
|
||||
#'
|
||||
#' The corresponding HTML output tag can be anything (though \code{pre} is
|
||||
#' The corresponding HTML output tag can be anything (though `pre` is
|
||||
#' recommended if you need a monospace font and whitespace preserved) and should
|
||||
#' have the CSS class name \code{shiny-text-output}.
|
||||
#' have the CSS class name `shiny-text-output`.
|
||||
#'
|
||||
#' The result of executing \code{func} will be printed inside a
|
||||
#' \code{\link[utils]{capture.output}} call.
|
||||
#' The result of executing `func` will be printed inside a
|
||||
#' [utils::capture.output()] call.
|
||||
#'
|
||||
#' Note that unlike most other Shiny output functions, if the given function
|
||||
#' returns \code{NULL} then \code{NULL} will actually be visible in the output.
|
||||
#' To display nothing, make your function return \code{\link{invisible}()}.
|
||||
#' returns `NULL` then `NULL` will actually be visible in the output.
|
||||
#' To display nothing, make your function return [base::invisible()].
|
||||
#'
|
||||
#' @param expr An expression that may print output and/or return a printable R
|
||||
#' object.
|
||||
#' @param env The environment in which to evaluate \code{expr}.
|
||||
#' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This
|
||||
#' @param func A function that may print output and/or return a printable R
|
||||
#' object (deprecated; use \code{expr} instead).
|
||||
#' @param width The value for \code{\link{options}('width')}.
|
||||
#' @seealso \code{\link{renderText}} for displaying the value returned from a
|
||||
#' @param env The environment in which to evaluate `expr`.
|
||||
#' @param quoted Is `expr` a quoted expression (with `quote()`)? This
|
||||
#' is useful if you want to save an expression in a variable.
|
||||
#' @param width The value for `[options][base::options]('width')`.
|
||||
#' @param outputArgs A list of arguments to be passed through to the implicit
|
||||
#' call to [verbatimTextOutput()] when `renderPrint` is used
|
||||
#' in an interactive R Markdown document.
|
||||
#' @seealso [renderText()] for displaying the value returned from a
|
||||
#' function, instead of the printed output.
|
||||
#'
|
||||
#' @example res/text-example.R
|
||||
#'
|
||||
#' @export
|
||||
renderPrint <- function(expr, env = parent.frame(), quoted = FALSE, func = NULL,
|
||||
width = getOption('width')) {
|
||||
if (!is.null(func)) {
|
||||
shinyDeprecated(msg="renderPrint: argument 'func' is deprecated. Please use 'expr' instead.")
|
||||
} else {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
width = getOption('width'), outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
# Set a promise domain that sets the console width
|
||||
# and captures output
|
||||
# op <- options(width = width)
|
||||
# on.exit(options(op), add = TRUE)
|
||||
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
domain <- createRenderPrintPromiseDomain(width)
|
||||
hybrid_chain(
|
||||
{
|
||||
promises::with_promise_domain(domain, func())
|
||||
},
|
||||
function(value, .visible) {
|
||||
if (.visible) {
|
||||
cat(file = domain$conn, paste(utils::capture.output(value, append = TRUE), collapse = "\n"))
|
||||
}
|
||||
res <- paste(readLines(domain$conn, warn = FALSE), collapse = "\n")
|
||||
res
|
||||
},
|
||||
finally = function() {
|
||||
close(domain$conn)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
markRenderFunction(verbatimTextOutput, function() {
|
||||
op <- options(width = width)
|
||||
on.exit(options(op), add = TRUE)
|
||||
paste(utils::capture.output(func()), collapse = "\n")
|
||||
})
|
||||
markRenderFunction(verbatimTextOutput, renderFunc, outputArgs = outputArgs)
|
||||
}
|
||||
|
||||
createRenderPrintPromiseDomain <- function(width) {
|
||||
f <- file()
|
||||
|
||||
promises::new_promise_domain(
|
||||
wrapOnFulfilled = function(onFulfilled) {
|
||||
force(onFulfilled)
|
||||
function(...) {
|
||||
op <- options(width = width)
|
||||
on.exit(options(op), add = TRUE)
|
||||
|
||||
sink(f, append = TRUE)
|
||||
on.exit(sink(NULL), add = TRUE)
|
||||
|
||||
onFulfilled(...)
|
||||
}
|
||||
},
|
||||
wrapOnRejected = function(onRejected) {
|
||||
force(onRejected)
|
||||
function(...) {
|
||||
op <- options(width = width)
|
||||
on.exit(options(op), add = TRUE)
|
||||
|
||||
sink(f, append = TRUE)
|
||||
on.exit(sink(NULL), add = TRUE)
|
||||
|
||||
onRejected(...)
|
||||
}
|
||||
},
|
||||
wrapSync = function(expr) {
|
||||
op <- options(width = width)
|
||||
on.exit(options(op), add = TRUE)
|
||||
|
||||
sink(f, append = TRUE)
|
||||
on.exit(sink(NULL), add = TRUE)
|
||||
|
||||
force(expr)
|
||||
},
|
||||
conn = f
|
||||
)
|
||||
}
|
||||
|
||||
#' Text Output
|
||||
#'
|
||||
#' Makes a reactive version of the given function that also uses
|
||||
#' \code{\link[base]{cat}} to turn its result into a single-element character
|
||||
#' [base::cat()] to turn its result into a single-element character
|
||||
#' vector.
|
||||
#'
|
||||
#' The corresponding HTML output tag can be anything (though \code{pre} is
|
||||
#' The corresponding HTML output tag can be anything (though `pre` is
|
||||
#' recommended if you need a monospace font and whitespace preserved) and should
|
||||
#' have the CSS class name \code{shiny-text-output}.
|
||||
#' have the CSS class name `shiny-text-output`.
|
||||
#'
|
||||
#' The result of executing \code{func} will passed to \code{cat}, inside a
|
||||
#' \code{\link[utils]{capture.output}} call.
|
||||
#' The result of executing `func` will passed to `cat`, inside a
|
||||
#' [utils::capture.output()] call.
|
||||
#'
|
||||
#' @param expr An expression that returns an R object that can be used as an
|
||||
#' argument to \code{cat}.
|
||||
#' @param env The environment in which to evaluate \code{expr}.
|
||||
#' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This
|
||||
#' argument to `cat`.
|
||||
#' @param env The environment in which to evaluate `expr`.
|
||||
#' @param quoted Is `expr` a quoted expression (with `quote()`)? This
|
||||
#' is useful if you want to save an expression in a variable.
|
||||
#' @param func A function that returns an R object that can be used as an
|
||||
#' argument to \code{cat}.(deprecated; use \code{expr} instead).
|
||||
#' @param outputArgs A list of arguments to be passed through to the implicit
|
||||
#' call to [textOutput()] when `renderText` is used in an
|
||||
#' interactive R Markdown document.
|
||||
#' @param sep A separator passed to `cat` to be appended after each
|
||||
#' element.
|
||||
#'
|
||||
#' @seealso \code{\link{renderPrint}} for capturing the print output of a
|
||||
#' @seealso [renderPrint()] for capturing the print output of a
|
||||
#' function, rather than the returned text value.
|
||||
#'
|
||||
#' @example res/text-example.R
|
||||
#'
|
||||
#' @export
|
||||
renderText <- function(expr, env=parent.frame(), quoted=FALSE, func=NULL) {
|
||||
if (!is.null(func)) {
|
||||
shinyDeprecated(msg="renderText: argument 'func' is deprecated. Please use 'expr' instead.")
|
||||
} else {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
}
|
||||
renderText <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
outputArgs=list(), sep=" ") {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
markRenderFunction(textOutput, function() {
|
||||
value <- func()
|
||||
return(paste(utils::capture.output(cat(value)), collapse="\n"))
|
||||
})
|
||||
createRenderFunction(
|
||||
func,
|
||||
function(value, session, name, ...) {
|
||||
paste(utils::capture.output(cat(value, sep=sep)), collapse="\n")
|
||||
},
|
||||
textOutput, outputArgs
|
||||
)
|
||||
}
|
||||
|
||||
#' UI Output
|
||||
#'
|
||||
#' \bold{Experimental feature.} Makes a reactive version of a function that
|
||||
#' generates HTML using the Shiny UI library.
|
||||
#' Renders reactive HTML using the Shiny UI library.
|
||||
#'
|
||||
#' The corresponding HTML output tag should be \code{div} and have the CSS class
|
||||
#' name \code{shiny-html-output} (or use \code{\link{uiOutput}}).
|
||||
#' The corresponding HTML output tag should be `div` and have the CSS class
|
||||
#' name `shiny-html-output` (or use [uiOutput()]).
|
||||
#'
|
||||
#' @param expr An expression that returns a Shiny tag object, \code{\link{HTML}},
|
||||
#' @param expr An expression that returns a Shiny tag object, [HTML()],
|
||||
#' or a list of such objects.
|
||||
#' @param env The environment in which to evaluate \code{expr}.
|
||||
#' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This
|
||||
#' @param env The environment in which to evaluate `expr`.
|
||||
#' @param quoted Is `expr` a quoted expression (with `quote()`)? This
|
||||
#' is useful if you want to save an expression in a variable.
|
||||
#' @param func A function that returns a Shiny tag object, \code{\link{HTML}},
|
||||
#' or a list of such objects (deprecated; use \code{expr} instead).
|
||||
#'
|
||||
#' @seealso conditionalPanel
|
||||
#' @param outputArgs A list of arguments to be passed through to the implicit
|
||||
#' call to [uiOutput()] when `renderUI` is used in an
|
||||
#' interactive R Markdown document.
|
||||
#'
|
||||
#' @seealso [uiOutput()]
|
||||
#' @export
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' output$moreControls <- renderUI({
|
||||
#' list(
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' uiOutput("moreControls")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' output$moreControls <- renderUI({
|
||||
#' tagList(
|
||||
#' sliderInput("n", "N", 1, 1000, 500),
|
||||
#' textInput("label", "Label")
|
||||
#' )
|
||||
#' })
|
||||
#' }
|
||||
renderUI <- function(expr, env=parent.frame(), quoted=FALSE, func=NULL) {
|
||||
if (!is.null(func)) {
|
||||
shinyDeprecated(msg="renderUI: argument 'func' is deprecated. Please use 'expr' instead.")
|
||||
} else {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
}
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
markRenderFunction(uiOutput, function(shinysession, name, ...) {
|
||||
result <- func()
|
||||
if (is.null(result) || length(result) == 0)
|
||||
return(NULL)
|
||||
createRenderFunction(
|
||||
func,
|
||||
function(result, shinysession, name, ...) {
|
||||
if (is.null(result) || length(result) == 0)
|
||||
return(NULL)
|
||||
|
||||
result <- takeSingletons(result, shinysession$singletons, desingleton=FALSE)$ui
|
||||
result <- surroundSingletons(result)
|
||||
dependencies <- lapply(resolveDependencies(findDependencies(result)),
|
||||
createWebDependency)
|
||||
names(dependencies) <- NULL
|
||||
|
||||
# renderTags returns a list with head, singletons, and html
|
||||
output <- list(
|
||||
html = doRenderTags(result),
|
||||
deps = dependencies
|
||||
)
|
||||
|
||||
return(output)
|
||||
})
|
||||
processDeps(result, shinysession)
|
||||
},
|
||||
uiOutput, outputArgs
|
||||
)
|
||||
}
|
||||
|
||||
#' File Downloads
|
||||
@@ -298,45 +501,59 @@ renderUI <- function(expr, env=parent.frame(), quoted=FALSE, func=NULL) {
|
||||
#' file downloads (for example, downloading the currently visible data as a CSV
|
||||
#' file). Both filename and contents can be calculated dynamically at the time
|
||||
#' the user initiates the download. Assign the return value to a slot on
|
||||
#' \code{output} in your server function, and in the UI use
|
||||
#' \code{\link{downloadButton}} or \code{\link{downloadLink}} to make the
|
||||
#' `output` in your server function, and in the UI use
|
||||
#' [downloadButton()] or [downloadLink()] to make the
|
||||
#' download available.
|
||||
#'
|
||||
#' @param filename A string of the filename, including extension, that the
|
||||
#' user's web browser should default to when downloading the file; or a
|
||||
#' function that returns such a string. (Reactive values and functions may be
|
||||
#' used from this function.)
|
||||
#' @param content A function that takes a single argument \code{file} that is a
|
||||
#' @param content A function that takes a single argument `file` that is a
|
||||
#' file path (string) of a nonexistent temp file, and writes the content to
|
||||
#' that file path. (Reactive values and functions may be used from this
|
||||
#' function.)
|
||||
#' @param contentType A string of the download's
|
||||
#' \href{http://en.wikipedia.org/wiki/Internet_media_type}{content type}, for
|
||||
#' example \code{"text/csv"} or \code{"image/png"}. If \code{NULL} or
|
||||
#' \code{NA}, the content type will be guessed based on the filename
|
||||
#' extension, or \code{application/octet-stream} if the extension is unknown.
|
||||
#' [content type](http://en.wikipedia.org/wiki/Internet_media_type), for
|
||||
#' example `"text/csv"` or `"image/png"`. If `NULL` or
|
||||
#' `NA`, the content type will be guessed based on the filename
|
||||
#' extension, or `application/octet-stream` if the extension is unknown.
|
||||
#' @param outputArgs A list of arguments to be passed through to the implicit
|
||||
#' call to [downloadButton()] when `downloadHandler` is used
|
||||
#' in an interactive R Markdown document.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' # In server.R:
|
||||
#' output$downloadData <- downloadHandler(
|
||||
#' filename = function() {
|
||||
#' paste('data-', Sys.Date(), '.csv', sep='')
|
||||
#' },
|
||||
#' content = function(file) {
|
||||
#' write.csv(data, file)
|
||||
#' }
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' downloadLink("downloadData", "Download")
|
||||
#' )
|
||||
#'
|
||||
#' # In ui.R:
|
||||
#' downloadLink('downloadData', 'Download')
|
||||
#' server <- function(input, output) {
|
||||
#' # Our dataset
|
||||
#' data <- mtcars
|
||||
#'
|
||||
#' output$downloadData <- downloadHandler(
|
||||
#' filename = function() {
|
||||
#' paste("data-", Sys.Date(), ".csv", sep="")
|
||||
#' },
|
||||
#' content = function(file) {
|
||||
#' write.csv(data, file)
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
downloadHandler <- function(filename, content, contentType=NA) {
|
||||
return(markRenderFunction(downloadButton, function(shinysession, name, ...) {
|
||||
downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()) {
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
shinysession$registerDownload(name, filename, contentType, content)
|
||||
}))
|
||||
}
|
||||
snapshotExclude(
|
||||
markRenderFunction(downloadButton, renderFunc, outputArgs = outputArgs)
|
||||
)
|
||||
}
|
||||
|
||||
#' Table output with the JavaScript library DataTables
|
||||
@@ -346,12 +563,12 @@ downloadHandler <- function(filename, content, contentType=NA) {
|
||||
#' searching, filtering, and sorting can be done on the R side using Shiny as
|
||||
#' the server infrastructure.
|
||||
#'
|
||||
#' For the \code{options} argument, the character elements that have the class
|
||||
#' \code{"AsIs"} (usually returned from \code{\link{I}()}) will be evaluated in
|
||||
#' For the `options` argument, the character elements that have the class
|
||||
#' `"AsIs"` (usually returned from [base::I()]) will be evaluated in
|
||||
#' JavaScript. This is useful when the type of the option value is not supported
|
||||
#' in JSON, e.g., a JavaScript function, which can be obtained by evaluating a
|
||||
#' character string. Note this only applies to the root-level elements of the
|
||||
#' options list, and the \code{I()} notation does not work for lower-level
|
||||
#' options list, and the `I()` notation does not work for lower-level
|
||||
#' elements in the list.
|
||||
#' @param expr An expression that returns a data frame or a matrix.
|
||||
#' @param options A list of initialization options to be passed to DataTables,
|
||||
@@ -360,21 +577,25 @@ downloadHandler <- function(filename, content, contentType=NA) {
|
||||
#' frequent search requests).
|
||||
#' @param callback A JavaScript function to be applied to the DataTable object.
|
||||
#' This is useful for DataTables plug-ins, which often require the DataTable
|
||||
#' instance to be available (\url{http://datatables.net/extensions/}).
|
||||
#' @param escape Whether to escape HTML entities in the table: \code{TRUE} means
|
||||
#' to escape the whole table, and \code{FALSE} means not to escape it.
|
||||
#' instance to be available (<http://datatables.net/extensions/>).
|
||||
#' @param escape Whether to escape HTML entities in the table: `TRUE` means
|
||||
#' to escape the whole table, and `FALSE` means not to escape it.
|
||||
#' Alternatively, you can specify numeric column indices or column names to
|
||||
#' indicate which columns to escape, e.g. \code{1:5} (the first 5 columns),
|
||||
#' \code{c(1, 3, 4)}, or \code{c(-1, -3)} (all columns except the first and
|
||||
#' third), or \code{c('Species', 'Sepal.Length')}.
|
||||
#' @references \url{http://datatables.net}
|
||||
#' indicate which columns to escape, e.g. `1:5` (the first 5 columns),
|
||||
#' `c(1, 3, 4)`, or `c(-1, -3)` (all columns except the first and
|
||||
#' third), or `c('Species', 'Sepal.Length')`.
|
||||
#' @param outputArgs A list of arguments to be passed through to the implicit
|
||||
#' call to [dataTableOutput()] when `renderDataTable` is used
|
||||
#' in an interactive R Markdown document.
|
||||
#'
|
||||
#' @references <http://datatables.net>
|
||||
#' @note This function only provides the server-side version of DataTables
|
||||
#' (using R to process the data object on the server side). There is a
|
||||
#' separate package \pkg{DT} (\url{https://github.com/rstudio/DT}) that allows
|
||||
#' separate package \pkg{DT} (<https://github.com/rstudio/DT>) that allows
|
||||
#' you to create both server-side and client-side DataTables, and supports
|
||||
#' additional DataTables features. Consider using \code{DT::renderDataTable()}
|
||||
#' and \code{DT::dataTableOutput()} (see
|
||||
#' \url{http://rstudio.github.io/DT/shiny.html} for more information).
|
||||
#' additional DataTables features. Consider using `DT::renderDataTable()`
|
||||
#' and `DT::dataTableOutput()` (see
|
||||
#' <http://rstudio.github.io/DT/shiny.html> for more information).
|
||||
#' @export
|
||||
#' @inheritParams renderPlot
|
||||
#' @examples
|
||||
@@ -401,36 +622,54 @@ downloadHandler <- function(filename, content, contentType=NA) {
|
||||
#' }
|
||||
renderDataTable <- function(expr, options = NULL, searchDelay = 500,
|
||||
callback = 'function(oTable) {}', escape = TRUE,
|
||||
env = parent.frame(), quoted = FALSE) {
|
||||
env = parent.frame(), quoted = FALSE,
|
||||
outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
markRenderFunction(dataTableOutput, function(shinysession, name, ...) {
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
if (is.function(options)) options <- options()
|
||||
options <- checkDT9(options)
|
||||
res <- checkAsIs(options)
|
||||
data <- func()
|
||||
if (length(dim(data)) != 2) return() # expects a rectangular data object
|
||||
if (is.data.frame(data)) data <- as.data.frame(data)
|
||||
action <- shinysession$registerDataObj(name, data, dataTablesJSON)
|
||||
colnames <- colnames(data)
|
||||
# if escape is column names, turn names to numeric indices
|
||||
if (is.character(escape)) {
|
||||
escape <- stats::setNames(seq_len(ncol(data)), colnames)[escape]
|
||||
if (any(is.na(escape)))
|
||||
stop("Some column names in the 'escape' argument not found in data")
|
||||
}
|
||||
colnames[escape] <- htmlEscape(colnames[escape])
|
||||
if (!is.logical(escape)) {
|
||||
if (!is.numeric(escape))
|
||||
stop("'escape' must be TRUE, FALSE, or a numeric vector, or column names")
|
||||
escape <- paste(escape, collapse = ',')
|
||||
}
|
||||
list(
|
||||
colnames = colnames, action = action, options = res$options,
|
||||
evalOptions = if (length(res$eval)) I(res$eval), searchDelay = searchDelay,
|
||||
callback = paste(callback, collapse = '\n'), escape = escape
|
||||
hybrid_chain(
|
||||
func(),
|
||||
function(data) {
|
||||
if (length(dim(data)) != 2) return() # expects a rectangular data object
|
||||
if (is.data.frame(data)) data <- as.data.frame(data)
|
||||
action <- shinysession$registerDataObj(name, data, dataTablesJSON)
|
||||
colnames <- colnames(data)
|
||||
# if escape is column names, turn names to numeric indices
|
||||
if (is.character(escape)) {
|
||||
escape <- stats::setNames(seq_len(ncol(data)), colnames)[escape]
|
||||
if (any(is.na(escape)))
|
||||
stop("Some column names in the 'escape' argument not found in data")
|
||||
}
|
||||
colnames[escape] <- htmlEscape(colnames[escape])
|
||||
if (!is.logical(escape)) {
|
||||
if (!is.numeric(escape))
|
||||
stop("'escape' must be TRUE, FALSE, or a numeric vector, or column names")
|
||||
escape <- paste(escape, collapse = ',')
|
||||
}
|
||||
list(
|
||||
colnames = colnames, action = action, options = res$options,
|
||||
evalOptions = if (length(res$eval)) I(res$eval), searchDelay = searchDelay,
|
||||
callback = paste(callback, collapse = '\n'), escape = escape
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
renderFunc <- markRenderFunction(dataTableOutput, renderFunc, outputArgs = outputArgs)
|
||||
|
||||
renderFunc <- snapshotPreprocessOutput(renderFunc, function(value) {
|
||||
# Remove the action field so that it's not saved in test snapshots. It
|
||||
# contains a value that changes every time an app is run, and shouldn't be
|
||||
# stored for test snapshots. It will be something like:
|
||||
# "session/e0d14d3fe97f672f9655a127f2a1e079/dataobj/table?w=&nonce=7f5d6d54e22450a3"
|
||||
value$action <- NULL
|
||||
value
|
||||
})
|
||||
|
||||
renderFunc
|
||||
}
|
||||
|
||||
# a data frame containing the DataTables 1.9 and 1.10 names
|
||||
@@ -475,13 +714,19 @@ checkDT9 <- function(options) {
|
||||
|
||||
# Deprecated functions ------------------------------------------------------
|
||||
|
||||
#' Deprecated reactive functions
|
||||
#' @name deprecatedReactives
|
||||
#' @keywords internal
|
||||
NULL
|
||||
|
||||
#' Plot output (deprecated)
|
||||
#'
|
||||
#' See \code{\link{renderPlot}}.
|
||||
#' `reactivePlot` has been replaced by [renderPlot()].
|
||||
#' @param func A function.
|
||||
#' @param width Width.
|
||||
#' @param height Height.
|
||||
#' @param ... Other arguments to pass on.
|
||||
#' @rdname deprecatedReactives
|
||||
#' @export
|
||||
reactivePlot <- function(func, width='auto', height='auto', ...) {
|
||||
shinyDeprecated(new="renderPlot")
|
||||
@@ -490,9 +735,8 @@ reactivePlot <- function(func, width='auto', height='auto', ...) {
|
||||
|
||||
#' Table output (deprecated)
|
||||
#'
|
||||
#' See \code{\link{renderTable}}.
|
||||
#' @param func A function.
|
||||
#' @param ... Other arguments to pass on.
|
||||
#' `reactiveTable` has been replaced by [renderTable()].
|
||||
#' @rdname deprecatedReactives
|
||||
#' @export
|
||||
reactiveTable <- function(func, ...) {
|
||||
shinyDeprecated(new="renderTable")
|
||||
@@ -501,8 +745,8 @@ reactiveTable <- function(func, ...) {
|
||||
|
||||
#' Print output (deprecated)
|
||||
#'
|
||||
#' See \code{\link{renderPrint}}.
|
||||
#' @param func A function.
|
||||
#' `reactivePrint` has been replaced by [renderPrint()].
|
||||
#' @rdname deprecatedReactives
|
||||
#' @export
|
||||
reactivePrint <- function(func) {
|
||||
shinyDeprecated(new="renderPrint")
|
||||
@@ -511,8 +755,8 @@ reactivePrint <- function(func) {
|
||||
|
||||
#' UI output (deprecated)
|
||||
#'
|
||||
#' See \code{\link{renderUI}}.
|
||||
#' @param func A function.
|
||||
#' `reactiveUI` has been replaced by [renderUI()].
|
||||
#' @rdname deprecatedReactives
|
||||
#' @export
|
||||
reactiveUI <- function(func) {
|
||||
shinyDeprecated(new="renderUI")
|
||||
@@ -521,8 +765,8 @@ reactiveUI <- function(func) {
|
||||
|
||||
#' Text output (deprecated)
|
||||
#'
|
||||
#' See \code{\link{renderText}}.
|
||||
#' @param func A function.
|
||||
#' `reactiveText` has been replaced by [renderText()].
|
||||
#' @rdname deprecatedReactives
|
||||
#' @export
|
||||
reactiveText <- function(func) {
|
||||
shinyDeprecated(new="renderText")
|
||||
|
||||
90
R/showcase.R
90
R/showcase.R
@@ -18,7 +18,8 @@ licenseLink <- function(licenseName) {
|
||||
"Artistic-2.0" = "http://www.r-project.org/Licenses/Artistic-2.0",
|
||||
"BSD_2_clause" = "http://www.r-project.org/Licenses/BSD_2_clause",
|
||||
"BSD_3_clause" = "http://www.r-project.org/Licenses/BSD_3_clause",
|
||||
"MIT" = "http://www.r-project.org/Licenses/MIT")
|
||||
"MIT" = "http://www.r-project.org/Licenses/MIT",
|
||||
"CC-BY-SA-4.0" = "https://www.r-project.org/Licenses/CC-BY-SA-4.0")
|
||||
if (exists(licenseName, where = licenses)) {
|
||||
tags$a(href=licenses[[licenseName]], licenseName)
|
||||
} else {
|
||||
@@ -31,7 +32,7 @@ licenseLink <- function(licenseName) {
|
||||
showcaseHead <- function() {
|
||||
|
||||
deps <- list(
|
||||
htmlDependency("jqueryui", "1.11.4", c(href="shared/jqueryui"),
|
||||
htmlDependency("jqueryui", "1.12.1", c(href="shared/jqueryui"),
|
||||
script = "jquery-ui.min.js"),
|
||||
htmlDependency("showdown", "0.3.1", c(href="shared/showdown/compressed"),
|
||||
script = "showdown.js"),
|
||||
@@ -77,10 +78,60 @@ appMetadata <- function(desc) {
|
||||
else ""
|
||||
}
|
||||
|
||||
navTabsHelper <- function(files, prefix = "") {
|
||||
lapply(files, function(file) {
|
||||
with(tags,
|
||||
li(class=if (tolower(file) %in% c("app.r", "server.r")) "active" else "",
|
||||
a(href=paste("#", gsub(".", "_", file, fixed=TRUE), "_code", sep=""),
|
||||
"data-toggle"="tab", paste0(prefix, file)))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
navTabsDropdown <- function(files) {
|
||||
if (length(files) > 0) {
|
||||
with(tags,
|
||||
li(role="presentation", class="dropdown",
|
||||
a(class="dropdown-toggle", `data-toggle`="dropdown", href="#",
|
||||
role="button", `aria-haspopup`="true", `aria-expanded`="false",
|
||||
"www", span(class="caret")
|
||||
),
|
||||
ul(class="dropdown-menu", navTabsHelper(files))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tabContentHelper <- function(files, path, language) {
|
||||
lapply(files, function(file) {
|
||||
with(tags,
|
||||
div(class=paste("tab-pane",
|
||||
if (tolower(file) %in% c("app.r", "server.r")) " active"
|
||||
else "",
|
||||
sep=""),
|
||||
id=paste(gsub(".", "_", file, fixed=TRUE),
|
||||
"_code", sep=""),
|
||||
pre(class="shiny-code",
|
||||
# we need to prevent the indentation of <code> ... </code>
|
||||
HTML(format(tags$code(
|
||||
class=paste0("language-", language),
|
||||
paste(readUTF8(file.path.ci(path, file)), collapse="\n")
|
||||
), indent = FALSE))))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
# Returns tags containing the application's code in Bootstrap-style tabs in
|
||||
# showcase mode.
|
||||
showcaseCodeTabs <- function(codeLicense) {
|
||||
rFiles <- list.files(pattern = "\\.[rR]$")
|
||||
wwwFiles <- list()
|
||||
if (isTRUE(.globals$IncludeWWW)) {
|
||||
path <- file.path(getwd(), "www")
|
||||
wwwFiles$jsFiles <- list.files(path, pattern = "\\.js$")
|
||||
wwwFiles$cssFiles <- list.files(path, pattern = "\\.css$")
|
||||
wwwFiles$htmlFiles <- list.files(path, pattern = "\\.html$")
|
||||
}
|
||||
with(tags, div(id="showcase-code-tabs",
|
||||
a(id="showcase-code-position-toggle",
|
||||
class="btn btn-default btn-sm",
|
||||
@@ -88,27 +139,21 @@ showcaseCodeTabs <- function(codeLicense) {
|
||||
icon("level-up"),
|
||||
"show with app"),
|
||||
ul(class="nav nav-tabs",
|
||||
lapply(rFiles, function(rFile) {
|
||||
li(class=if (tolower(rFile) %in% c("app.r", "server.r")) "active" else "",
|
||||
a(href=paste("#", gsub(".", "_", rFile, fixed=TRUE),
|
||||
"_code", sep=""),
|
||||
"data-toggle"="tab", rFile))
|
||||
})),
|
||||
navTabsHelper(rFiles),
|
||||
navTabsDropdown(unlist(wwwFiles))
|
||||
),
|
||||
div(class="tab-content", id="showcase-code-content",
|
||||
lapply(rFiles, function(rFile) {
|
||||
div(class=paste("tab-pane",
|
||||
if (tolower(rFile) %in% c("app.r", "server.r")) " active"
|
||||
else "",
|
||||
sep=""),
|
||||
id=paste(gsub(".", "_", rFile, fixed=TRUE),
|
||||
"_code", sep=""),
|
||||
pre(class="shiny-code",
|
||||
# we need to prevent the indentation of <code> ... </code>
|
||||
HTML(format(tags$code(
|
||||
class="language-r",
|
||||
paste(readUTF8(file.path.ci(getwd(), rFile)), collapse="\n")
|
||||
), indent = FALSE))))
|
||||
})),
|
||||
tabContentHelper(rFiles, path = getwd(), language = "r"),
|
||||
tabContentHelper(wwwFiles$jsFiles,
|
||||
path = paste0(getwd(), "/www"),
|
||||
language = "javascript"),
|
||||
tabContentHelper(wwwFiles$cssFiles,
|
||||
path = paste0(getwd(), "/www"),
|
||||
language = "css"),
|
||||
tabContentHelper(wwwFiles$htmlFiles,
|
||||
path = paste0(getwd(), "/www"),
|
||||
language = "xml")
|
||||
),
|
||||
codeLicense))
|
||||
}
|
||||
|
||||
@@ -177,3 +222,4 @@ showcaseUI <- function(ui) {
|
||||
showcaseBody(ui)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
44
R/snapshot.R
Normal file
44
R/snapshot.R
Normal file
@@ -0,0 +1,44 @@
|
||||
#' Mark an output to be excluded from test snapshots
|
||||
#'
|
||||
#' @param x A reactive which will be assigned to an output.
|
||||
#'
|
||||
#' @export
|
||||
snapshotExclude <- function(x) {
|
||||
markOutputAttrs(x, snapshotExclude = TRUE)
|
||||
}
|
||||
|
||||
#' Add a function for preprocessing an output before taking a test snapshot
|
||||
#'
|
||||
#' @param x A reactive which will be assigned to an output.
|
||||
#' @param fun A function that takes the output value as an input and returns a
|
||||
#' modified value. The returned value will be used for the test snapshot.
|
||||
#'
|
||||
#' @export
|
||||
snapshotPreprocessOutput <- function(x, fun) {
|
||||
markOutputAttrs(x, snapshotPreprocess = fun)
|
||||
}
|
||||
|
||||
|
||||
#' Add a function for preprocessing an input before taking a test snapshot
|
||||
#'
|
||||
#' @param inputId Name of the input value.
|
||||
#' @param fun A function that takes the input value and returns a modified
|
||||
#' value. The returned value will be used for the test snapshot.
|
||||
#' @param session A Shiny session object.
|
||||
#'
|
||||
#' @export
|
||||
snapshotPreprocessInput <- function(inputId, fun, session = getDefaultReactiveDomain()) {
|
||||
if (is.null(session)) {
|
||||
stop("snapshotPreprocessInput() needs a session object.")
|
||||
}
|
||||
|
||||
input_impl <- .subset2(session$input, "impl")
|
||||
input_impl$setMeta(inputId, "shiny.snapshot.preprocess", fun)
|
||||
}
|
||||
|
||||
|
||||
# Strip out file path from fileInput value
|
||||
snapshotPreprocessorFileInput <- function(value) {
|
||||
value$datapath <- basename(value$datapath)
|
||||
value
|
||||
}
|
||||
61
R/test-export.R
Normal file
61
R/test-export.R
Normal file
@@ -0,0 +1,61 @@
|
||||
#' Register expressions for export in test mode
|
||||
#'
|
||||
#' This function registers expressions that will be evaluated when a test export
|
||||
#' event occurs. These events are triggered by accessing a snapshot URL.
|
||||
#'
|
||||
#' This function only has an effect if the app is launched in test mode. This is
|
||||
#' done by calling `runApp()` with `test.mode=TRUE`, or by setting the
|
||||
#' global option `shiny.testmode` to `TRUE`.
|
||||
#'
|
||||
#' @param quoted_ Are the expression quoted? Default is `FALSE`.
|
||||
#' @param env_ The environment in which the expression should be evaluated.
|
||||
#' @param session_ A Shiny session object.
|
||||
#' @param ... Named arguments that are quoted or unquoted expressions that will
|
||||
#' be captured and evaluated when snapshot URL is visited.
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' options(shiny.testmode = TRUE)
|
||||
#'
|
||||
#' # This application shows the test snapshot URL; clicking on it will
|
||||
#' # fetch the input, output, and exported values in JSON format.
|
||||
#' shinyApp(
|
||||
#' ui = basicPage(
|
||||
#' h4("Snapshot URL: "),
|
||||
#' uiOutput("url"),
|
||||
#' h4("Current values:"),
|
||||
#' verbatimTextOutput("values"),
|
||||
#' actionButton("inc", "Increment x")
|
||||
#' ),
|
||||
#'
|
||||
#' server = function(input, output, session) {
|
||||
#' vals <- reactiveValues(x = 1)
|
||||
#' y <- reactive({ vals$x + 1 })
|
||||
#'
|
||||
#' observeEvent(input$inc, {
|
||||
#' vals$x <<- vals$x + 1
|
||||
#' })
|
||||
#'
|
||||
#' exportTestValues(
|
||||
#' x = vals$x,
|
||||
#' y = y()
|
||||
#' )
|
||||
#'
|
||||
#' output$url <- renderUI({
|
||||
#' url <- session$getTestSnapshotUrl(format="json")
|
||||
#' a(href = url, url)
|
||||
#' })
|
||||
#'
|
||||
#' output$values <- renderText({
|
||||
#' paste0("vals$x: ", vals$x, "\ny: ", y())
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#' @export
|
||||
exportTestValues <- function(..., quoted_ = FALSE, env_ = parent.frame(),
|
||||
session_ = getDefaultReactiveDomain())
|
||||
{
|
||||
session_$exportTestValues(..., quoted_ = quoted_, env_ = env_)
|
||||
}
|
||||
169
R/test-module.R
Normal file
169
R/test-module.R
Normal file
@@ -0,0 +1,169 @@
|
||||
|
||||
|
||||
#' Integration testing for Shiny modules or server functions
|
||||
#'
|
||||
#' Offer a way to test the reactive interactions in Shiny --- either in Shiny
|
||||
#' modules or in the server portion of a Shiny application. For more
|
||||
#' information, visit [the Shiny Dev Center article on integration
|
||||
#' testing](https://shiny.rstudio.com/articles/integration-testing.html).
|
||||
#' @param module The module to test
|
||||
#' @param expr Test code containing expectations. The test expression will run
|
||||
#' in the module's environment, meaning that the module's parameters (e.g.
|
||||
#' `input`, `output`, and `session`) will be available along with any other
|
||||
#' values created inside of the module.
|
||||
#' @param ... Additional arguments to pass to the module function. These
|
||||
#' arguments are processed with [rlang::list2()] and so are
|
||||
#' _[dynamic][rlang::dyn-dots]_.
|
||||
#' @return The result of evaluating `expr`.
|
||||
#' @include mock-session.R
|
||||
#' @rdname testModule
|
||||
#' @examples
|
||||
#' module <- function(input, output, session, multiplier = 2, prefix = "I am ") {
|
||||
#' myreactive <- reactive({
|
||||
#' input$x * multiplier
|
||||
#' })
|
||||
#' output$txt <- renderText({
|
||||
#' paste0(prefix, myreactive())
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' # Basic Usage
|
||||
#' # -----------
|
||||
#' testModule(module, {
|
||||
#' session$setInputs(x = 1)
|
||||
#' # You're also free to use third-party
|
||||
#' # testing packages like testthat:
|
||||
#' # expect_equal(myreactive(), 2)
|
||||
#' stopifnot(myreactive() == 2)
|
||||
#' stopifnot(output$txt == "I am 2")
|
||||
#'
|
||||
#' session$setInputs(x = 2)
|
||||
#' stopifnot(myreactive() == 4)
|
||||
#' stopifnot(output$txt == "I am 4")
|
||||
#' # Any additional arguments, below, are passed along to the module.
|
||||
#' }, multiplier = 2)
|
||||
#'
|
||||
#' # Advanced Usage
|
||||
#' # --------------
|
||||
#' multiplier_arg_name = "multiplier"
|
||||
#' more_args <- list(prefix = "I am ")
|
||||
#' testModule(module, {
|
||||
#' session$setInputs(x = 1)
|
||||
#' stopifnot(myreactive() == 2)
|
||||
#' stopifnot(output$txt == "I am 2")
|
||||
#' # !!/:= and !!! from rlang are used below to splice computed arguments
|
||||
#' # into the testModule() argument list.
|
||||
#' }, !!multiplier_arg_name := 2, !!!more_args)
|
||||
#' @export
|
||||
testModule <- function(module, expr, ...) {
|
||||
.testModule(
|
||||
module,
|
||||
quosure = rlang::enquo(expr),
|
||||
dots = rlang::list2(...),
|
||||
env = rlang::caller_env()
|
||||
)
|
||||
}
|
||||
|
||||
#' @noRd
|
||||
#' @importFrom withr with_options
|
||||
.testModule <- function(module, quosure, dots, env) {
|
||||
# Modify the module function locally by inserting `session$env <-
|
||||
# environment()` at the beginning of its body. The dynamic environment of the
|
||||
# module function is saved so that it may be referenced after the module
|
||||
# function has returned. The saved dynamic environment is the basis for the
|
||||
# `data` argument of tidy_eval() when used below to evaluate `quosure`, the
|
||||
# test code expression.
|
||||
body(module) <- rlang::expr({
|
||||
session$env <- base::environment()
|
||||
!!!body(module)
|
||||
})
|
||||
|
||||
session <- MockShinySession$new()
|
||||
on.exit(if (!session$isClosed()) session$close())
|
||||
args <- append(dots, list(input = session$input, output = session$output, session = session))
|
||||
|
||||
isolate(
|
||||
withReactiveDomain(
|
||||
session,
|
||||
withr::with_options(list(`shiny.allowoutputreads`=TRUE), {
|
||||
# Assigning to `$returned` causes a flush to happen automatically.
|
||||
session$returned <- do.call(module, args)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
# Evaluate `quosure` in a reactive context, and in the provided `env`, but
|
||||
# with `env` masked by a shallow view of `session$env`, the environment that
|
||||
# was saved when the module function was invoked. flush is not needed before
|
||||
# entering the loop because the first expr executed is `{`.
|
||||
isolate({
|
||||
withReactiveDomain(
|
||||
session,
|
||||
withr::with_options(list(`shiny.allowoutputreads`=TRUE), {
|
||||
rlang::eval_tidy(
|
||||
quosure,
|
||||
data = rlang::as_data_mask(as.list(session$env)),
|
||||
env = env
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#' Test an app's server-side logic
|
||||
#' @param appDir The directory root of the Shiny application. If `NULL`, this function
|
||||
#' will work up the directory hierarchy --- starting with the current directory ---
|
||||
#' looking for a directory that contains an `app.R` or `server.R` file.
|
||||
#' @rdname testModule
|
||||
#' @export
|
||||
testServer <- function(expr, appDir=NULL) {
|
||||
if (is.null(appDir)){
|
||||
appDir <- findApp()
|
||||
}
|
||||
|
||||
app <- shinyAppDir(appDir)
|
||||
message("Testing application found in: ", appDir)
|
||||
server <- app$serverFuncSource()
|
||||
|
||||
origwd <- getwd()
|
||||
setwd(appDir)
|
||||
on.exit({ setwd(origwd) }, add=TRUE)
|
||||
|
||||
# Add `session` argument if not present
|
||||
fn_formals <- formals(server)
|
||||
if (! "session" %in% names(fn_formals)) {
|
||||
fn_formals$session <- bquote()
|
||||
formals(server) <- fn_formals
|
||||
}
|
||||
|
||||
# Test the server function almost as if it were a module. `dots` is empty
|
||||
# because server functions never take additional arguments.
|
||||
.testModule(
|
||||
server,
|
||||
quosure = rlang::enquo(expr),
|
||||
dots = list(),
|
||||
env = rlang::caller_env()
|
||||
)
|
||||
}
|
||||
|
||||
findApp <- function(startDir="."){
|
||||
dir <- normalizePath(startDir)
|
||||
|
||||
# The loop will either return or stop() itself.
|
||||
while (TRUE){
|
||||
if(file.exists.ci(file.path(dir, "app.R")) || file.exists.ci(file.path(dir, "server.R"))){
|
||||
return(dir)
|
||||
}
|
||||
|
||||
# Move up a directory
|
||||
origDir <- dir
|
||||
dir <- dirname(dir)
|
||||
|
||||
# Testing for "root" path can be tricky. OSs differ and on Windows, network shares
|
||||
# might have a \\ prefix. Easier to just see if we got stuck and abort.
|
||||
if (dir == origDir){
|
||||
# We can go no further.
|
||||
stop("No shiny app was found in ", startDir, " or any of its parent directories")
|
||||
}
|
||||
}
|
||||
}
|
||||
107
R/test.R
Normal file
107
R/test.R
Normal file
@@ -0,0 +1,107 @@
|
||||
|
||||
#' Check to see if the given text is a shinytest
|
||||
#' Scans for the magic string of `app <- ShinyDriver$new(` as an indicator that this is a shinytest.
|
||||
#' Brought in from shinytest to avoid having to export this function.
|
||||
#' @noRd
|
||||
isShinyTest <- function(text){
|
||||
lines <- grepl("app\\s*<-\\s*ShinyDriver\\$new\\(", text, perl=TRUE)
|
||||
any(lines)
|
||||
}
|
||||
|
||||
#' Runs the tests associated with this Shiny app
|
||||
#'
|
||||
#' Sources the `.R` files in the top-level of `tests/` much like `R CMD check`.
|
||||
#' These files are typically simple runners for tests nested in other
|
||||
#' directories under `tests/`.
|
||||
#'
|
||||
#' @param appDir The base directory for the application.
|
||||
#' @param filter If not `NULL`, only tests with file names matching this regular
|
||||
#' expression will be executed. Matching is performed on the file name
|
||||
#' including the extension.
|
||||
#'
|
||||
#' @details Historically, [shinytest](https://rstudio.github.io/shinytest/)
|
||||
#' recommended placing tests at the top-level of the `tests/` directory. In
|
||||
#' order to support that model, `testApp` first checks to see if the `.R`
|
||||
#' files in the `tests/` directory are all shinytests; if so, just calls out
|
||||
#' to [shinytest::testApp()].
|
||||
#' @export
|
||||
runTests <- function(appDir=".", filter=NULL){
|
||||
require(shiny)
|
||||
|
||||
testsDir <- file.path(appDir, "tests")
|
||||
if (!dirExists(testsDir)){
|
||||
stop("No tests directory found: ", testsDir)
|
||||
}
|
||||
runners <- list.files(testsDir, pattern="\\.r$", ignore.case = TRUE)
|
||||
|
||||
if (length(runners) == 0){
|
||||
message("No test runners found in ", testsDir)
|
||||
return(structure(list(result=NA, files=list()), class="shinytestrun"))
|
||||
}
|
||||
|
||||
if (!is.null(filter)){
|
||||
runners <- runners[grepl(filter, runners)]
|
||||
}
|
||||
if (length(runners) == 0){
|
||||
stop("No test runners matched the given filter: '", filter, "'")
|
||||
}
|
||||
|
||||
# Inspect each runner to see if it appears to be a shinytest
|
||||
isST <- vapply(runners, function(r){
|
||||
text <- readLines(file.path(testsDir, r), warn = FALSE)
|
||||
isShinyTest(text)
|
||||
}, logical(1))
|
||||
|
||||
if (all(isST)){
|
||||
# just call out to shinytest
|
||||
# We don't need to message/warn here since shinytest already does it.
|
||||
if (!requireNamespace("shinytest", quietly=TRUE) ){
|
||||
stop("It appears that the .R files in ", testsDir,
|
||||
" are all shinytests, but shinytest is not installed.")
|
||||
}
|
||||
|
||||
if (!getOption("shiny.autoload.r", TRUE)) {
|
||||
warning("You've disabled `shiny.autoload.r` via an option but this is not passed through to shinytest. Consider using a _disable_autoload.R file as described at https://rstd.io/shiny-autoload")
|
||||
}
|
||||
|
||||
sares <- shinytest::testApp(appDir)
|
||||
res <- list()
|
||||
lapply(sares$results, function(r){
|
||||
e <- NA_character_
|
||||
if (!r$pass){
|
||||
e <- simpleError("Unknown shinytest error")
|
||||
}
|
||||
res[[r$name]] <<- e
|
||||
})
|
||||
return(structure(list(result=all(is.na(res)), files=res), class="shinytestrun"))
|
||||
}
|
||||
|
||||
testenv <- new.env(parent=globalenv())
|
||||
renv <- new.env(parent=testenv)
|
||||
if (getOption("shiny.autoload.r", TRUE)) {
|
||||
loadSupport(appDir, renv=renv, globalrenv=testenv)
|
||||
} else if (file.exists.ci(file.path(appDir, "server.R"))){
|
||||
# then check for global.R to load
|
||||
if (file.exists(file.path.ci(appDir, "global.R"))){
|
||||
sourceUTF8(file.path.ci(appDir, "global.R"))
|
||||
}
|
||||
}
|
||||
|
||||
oldwd <- getwd()
|
||||
on.exit({
|
||||
setwd(oldwd)
|
||||
}, add=TRUE)
|
||||
|
||||
setwd(testsDir)
|
||||
|
||||
# Otherwise source all the runners -- each in their own environment.
|
||||
fileResults <- list()
|
||||
lapply(runners, function(r){
|
||||
env <- new.env(parent=renv)
|
||||
tryCatch({sourceUTF8(r, envir=env); fileResults[[r]] <<- NA_character_}, error=function(e){
|
||||
fileResults[[r]] <<- e
|
||||
})
|
||||
})
|
||||
|
||||
return(structure(list(result=all(is.na(fileResults)), files=fileResults), class="shinytestrun"))
|
||||
}
|
||||
94
R/timer.R
94
R/timer.R
@@ -1,6 +1,5 @@
|
||||
# Return the current time, in milliseconds from epoch, with
|
||||
# unspecified time zone.
|
||||
now <- function() {
|
||||
# Return the current time, in milliseconds from epoch.
|
||||
getTimeMs <- function() {
|
||||
as.numeric(Sys.time()) * 1000
|
||||
}
|
||||
|
||||
@@ -12,9 +11,11 @@ TimerCallbacks <- R6Class(
|
||||
.nextId = 0L,
|
||||
.funcs = 'Map',
|
||||
.times = data.frame(),
|
||||
.now = 'Function',
|
||||
|
||||
initialize = function() {
|
||||
initialize = function(nowFn = getTimeMs) {
|
||||
.funcs <<- Map$new()
|
||||
.now <<- nowFn
|
||||
},
|
||||
clear = function() {
|
||||
.nextId <<- 0L
|
||||
@@ -22,10 +23,15 @@ TimerCallbacks <- R6Class(
|
||||
.times <<- data.frame()
|
||||
},
|
||||
schedule = function(millis, func) {
|
||||
# If args could fail to evaluate, let's make them do that before
|
||||
# we change any state
|
||||
force(millis)
|
||||
force(func)
|
||||
|
||||
id <- .nextId
|
||||
.nextId <<- .nextId + 1L
|
||||
|
||||
t <- now()
|
||||
t <- .now()
|
||||
|
||||
# TODO: Horribly inefficient, use a heap instead
|
||||
.times <<- rbind(.times, data.frame(time=t+millis,
|
||||
@@ -37,26 +43,37 @@ TimerCallbacks <- R6Class(
|
||||
|
||||
return(id)
|
||||
},
|
||||
unschedule = function(id) {
|
||||
toRemoveIndices <- .times$id %in% id
|
||||
toRemoveIds <- .times[toRemoveIndices, "id", drop = TRUE]
|
||||
if (length(toRemoveIds) > 0) {
|
||||
.times <<- .times[!toRemoveIndices,]
|
||||
for (toRemoveId in as.character(toRemoveIds)) {
|
||||
.funcs$remove(toRemoveId)
|
||||
}
|
||||
}
|
||||
return(id %in% toRemoveIds)
|
||||
},
|
||||
timeToNextEvent = function() {
|
||||
if (dim(.times)[1] == 0)
|
||||
return(Inf)
|
||||
return(.times[1, 'time'] - now())
|
||||
return(.times[1, 'time'] - .now())
|
||||
},
|
||||
takeElapsed = function() {
|
||||
t <- now()
|
||||
elapsed <- .times$time < now()
|
||||
t <- .now()
|
||||
elapsed <- .times$time <= .now()
|
||||
result <- .times[elapsed,]
|
||||
.times <<- .times[!elapsed,]
|
||||
|
||||
# TODO: Examine scheduled column to check if any funny business
|
||||
# has occurred with the system clock (e.g. if scheduled
|
||||
# is later than now())
|
||||
# is later than .now())
|
||||
|
||||
return(result)
|
||||
},
|
||||
executeElapsed = function() {
|
||||
elapsed <- takeElapsed()
|
||||
if (length(elapsed) == 0)
|
||||
if (nrow(elapsed) == 0)
|
||||
return(FALSE)
|
||||
|
||||
for (id in elapsed$id) {
|
||||
@@ -70,4 +87,61 @@ TimerCallbacks <- R6Class(
|
||||
)
|
||||
)
|
||||
|
||||
MockableTimerCallbacks <- R6Class(
|
||||
'MockableTimerCallbacks',
|
||||
inherit = TimerCallbacks,
|
||||
portable = FALSE,
|
||||
class = FALSE,
|
||||
public = list(
|
||||
# Empty constructor defaults to the getNow implementation
|
||||
initialize = function() {
|
||||
super$initialize(self$mockNow)
|
||||
},
|
||||
mockNow = function() {
|
||||
return(private$time)
|
||||
},
|
||||
elapse = function(millis) {
|
||||
private$time <- private$time + millis
|
||||
},
|
||||
getElapsed = function() {
|
||||
private$time
|
||||
}
|
||||
), private = list(
|
||||
time = 0L
|
||||
)
|
||||
)
|
||||
|
||||
timerCallbacks <- TimerCallbacks$new()
|
||||
|
||||
scheduleTask <- function(millis, callback) {
|
||||
cancelled <- FALSE
|
||||
id <- timerCallbacks$schedule(millis, callback)
|
||||
|
||||
function() {
|
||||
invisible(timerCallbacks$unschedule(id))
|
||||
}
|
||||
}
|
||||
|
||||
#' Get a scheduler function for scheduling tasks. Give priority to the
|
||||
#' session scheduler, but if it doesn't exist, use the global one.
|
||||
#' @noRd
|
||||
defineScheduler <- function(session){
|
||||
if (!is.null(session) && !is.null(session$.scheduleTask)){
|
||||
return(session$.scheduleTask)
|
||||
}
|
||||
scheduleTask
|
||||
}
|
||||
|
||||
|
||||
#' Get the current time using the current reactive domain. This will try to use
|
||||
#' the session's .now() method, but if that's not available, it will just return
|
||||
#' the real time (from getTimeMs()). The purpose of this function is to allow
|
||||
#' MockableTimerCallbacks to work.
|
||||
#' @noRd
|
||||
getDomainTimeMs <- function(session){
|
||||
if (!is.null(session) && !is.null(session$.now)){
|
||||
return(session$.now())
|
||||
} else {
|
||||
getTimeMs()
|
||||
}
|
||||
}
|
||||
|
||||
663
R/update-input.R
663
R/update-input.R
@@ -2,13 +2,21 @@
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param value The value to set for the input object.
|
||||
#' @param placeholder The placeholder to set for the input object.
|
||||
#'
|
||||
#' @seealso \code{\link{textInput}}
|
||||
#' @seealso [textInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' shinyServer(function(input, output, session) {
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sliderInput("controller", "Controller", 0, 20, 10),
|
||||
#' textInput("inText", "Input text"),
|
||||
#' textInput("inText2", "Input text 2")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observe({
|
||||
#' # We'll use the input$controller variable multiple times, so save it as x
|
||||
#' # for convenience.
|
||||
@@ -22,77 +30,184 @@
|
||||
#' label = paste("New label", x),
|
||||
#' value = paste("New text", x))
|
||||
#' })
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateTextInput <- function(session, inputId, label = NULL, value = NULL) {
|
||||
message <- dropNulls(list(label=label, value=value))
|
||||
updateTextInput <- function(session, inputId, label = NULL, value = NULL, placeholder = NULL) {
|
||||
message <- dropNulls(list(label=label, value=value, placeholder=placeholder))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
|
||||
#' Change the value of a textarea input on the client
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @inheritParams updateTextInput
|
||||
#'
|
||||
#' @seealso [textAreaInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sliderInput("controller", "Controller", 0, 20, 10),
|
||||
#' textAreaInput("inText", "Input textarea"),
|
||||
#' textAreaInput("inText2", "Input textarea 2")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observe({
|
||||
#' # We'll use the input$controller variable multiple times, so save it as x
|
||||
#' # for convenience.
|
||||
#' x <- input$controller
|
||||
#'
|
||||
#' # This will change the value of input$inText, based on x
|
||||
#' updateTextAreaInput(session, "inText", value = paste("New text", x))
|
||||
#'
|
||||
#' # Can also set the label, this time for input$inText2
|
||||
#' updateTextAreaInput(session, "inText2",
|
||||
#' label = paste("New label", x),
|
||||
#' value = paste("New text", x))
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateTextAreaInput <- updateTextInput
|
||||
|
||||
|
||||
#' Change the value of a checkbox input on the client
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param value The value to set for the input object.
|
||||
#'
|
||||
#' @seealso \code{\link{checkboxInput}}
|
||||
#' @seealso [checkboxInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' shinyServer(function(input, output, session) {
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sliderInput("controller", "Controller", 0, 1, 0, step = 1),
|
||||
#' checkboxInput("inCheckbox", "Input checkbox")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observe({
|
||||
#' # TRUE if input$controller is even, FALSE otherwise.
|
||||
#' x_even <- input$controller %% 2 == 0
|
||||
#' # TRUE if input$controller is odd, FALSE if even.
|
||||
#' x_even <- input$controller %% 2 == 1
|
||||
#'
|
||||
#' updateCheckboxInput(session, "inCheckbox", value = x_even)
|
||||
#' })
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateCheckboxInput <- updateTextInput
|
||||
updateCheckboxInput <- function(session, inputId, label = NULL, value = NULL) {
|
||||
message <- dropNulls(list(label=label, value=value))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
|
||||
|
||||
#' Change the label or icon of an action button on the client
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param icon The icon to set for the input object. To remove the
|
||||
#' current icon, use `icon=character(0)`.
|
||||
#'
|
||||
#' @seealso [actionButton()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' actionButton("update", "Update other buttons"),
|
||||
#' br(),
|
||||
#' actionButton("goButton", "Go"),
|
||||
#' br(),
|
||||
#' actionButton("goButton2", "Go 2", icon = icon("area-chart")),
|
||||
#' br(),
|
||||
#' actionButton("goButton3", "Go 3")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observe({
|
||||
#' req(input$update)
|
||||
#'
|
||||
#' # Updates goButton's label and icon
|
||||
#' updateActionButton(session, "goButton",
|
||||
#' label = "New label",
|
||||
#' icon = icon("calendar"))
|
||||
#'
|
||||
#' # Leaves goButton2's label unchaged and
|
||||
#' # removes its icon
|
||||
#' updateActionButton(session, "goButton2",
|
||||
#' icon = character(0))
|
||||
#'
|
||||
#' # Leaves goButton3's icon, if it exists,
|
||||
#' # unchaged and changes its label
|
||||
#' updateActionButton(session, "goButton3",
|
||||
#' label = "New label 3")
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateActionButton <- function(session, inputId, label = NULL, icon = NULL) {
|
||||
if (!is.null(icon)) icon <- as.character(validateIcon(icon))
|
||||
message <- dropNulls(list(label=label, icon=icon))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
|
||||
|
||||
#' Change the value of a date input on the client
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param value The desired date value. Either a Date object, or a string in
|
||||
#' \code{yyyy-mm-dd} format.
|
||||
#' `yyyy-mm-dd` format. Supply `NA` to clear the date.
|
||||
#' @param min The minimum allowed date. Either a Date object, or a string in
|
||||
#' \code{yyyy-mm-dd} format.
|
||||
#' `yyyy-mm-dd` format.
|
||||
#' @param max The maximum allowed date. Either a Date object, or a string in
|
||||
#' \code{yyyy-mm-dd} format.
|
||||
#' `yyyy-mm-dd` format.
|
||||
#'
|
||||
#' @seealso \code{\link{dateInput}}
|
||||
#' @seealso [dateInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' shinyServer(function(input, output, session) {
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sliderInput("n", "Day of month", 1, 30, 10),
|
||||
#' dateInput("inDate", "Input date")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observe({
|
||||
#' # We'll use the input$controller variable multiple times, so save it as x
|
||||
#' # for convenience.
|
||||
#' x <- input$controller
|
||||
#'
|
||||
#' date <- as.Date(paste0("2013-04-", input$n))
|
||||
#' updateDateInput(session, "inDate",
|
||||
#' label = paste("Date label", x),
|
||||
#' value = paste("2013-04-", x, sep=""),
|
||||
#' min = paste("2013-04-", x-1, sep=""),
|
||||
#' max = paste("2013-04-", x+1, sep="")
|
||||
#' label = paste("Date label", input$n),
|
||||
#' value = date,
|
||||
#' min = date - 3,
|
||||
#' max = date + 3
|
||||
#' )
|
||||
#' })
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateDateInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
min = NULL, max = NULL) {
|
||||
|
||||
# If value is a date object, convert it to a string with yyyy-mm-dd format
|
||||
# Same for min and max
|
||||
if (inherits(value, "Date")) value <- format(value, "%Y-%m-%d")
|
||||
if (inherits(min, "Date")) min <- format(min, "%Y-%m-%d")
|
||||
if (inherits(max, "Date")) max <- format(max, "%Y-%m-%d")
|
||||
value <- dateYMD(value, "value")
|
||||
min <- dateYMD(min, "min")
|
||||
max <- dateYMD(max, "max")
|
||||
|
||||
message <- dropNulls(list(label=label, value=value, min=min, max=max))
|
||||
session$sendInputMessage(inputId, message)
|
||||
@@ -103,46 +218,54 @@ updateDateInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param start The start date. Either a Date object, or a string in
|
||||
#' \code{yyyy-mm-dd} format.
|
||||
#' `yyyy-mm-dd` format. Supplying `NA` clears the start date.
|
||||
#' @param end The end date. Either a Date object, or a string in
|
||||
#' \code{yyyy-mm-dd} format.
|
||||
#' `yyyy-mm-dd` format. Supplying `NA` clears the end date.
|
||||
#' @param min The minimum allowed date. Either a Date object, or a string in
|
||||
#' \code{yyyy-mm-dd} format.
|
||||
#' `yyyy-mm-dd` format.
|
||||
#' @param max The maximum allowed date. Either a Date object, or a string in
|
||||
#' \code{yyyy-mm-dd} format.
|
||||
#' `yyyy-mm-dd` format.
|
||||
#'
|
||||
#' @seealso \code{\link{dateRangeInput}}
|
||||
#' @seealso [dateRangeInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' shinyServer(function(input, output, session) {
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sliderInput("n", "Day of month", 1, 30, 10),
|
||||
#' dateRangeInput("inDateRange", "Input date range")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observe({
|
||||
#' # We'll use the input$controller variable multiple times, so save it as x
|
||||
#' # for convenience.
|
||||
#' x <- input$controller
|
||||
#' date <- as.Date(paste0("2013-04-", input$n))
|
||||
#'
|
||||
#' updateDateRangeInput(session, "inDateRange",
|
||||
#' label = paste("Date range label", x),
|
||||
#' start = paste("2013-01-", x, sep=""))
|
||||
#' end = paste("2013-12-", x, sep=""))
|
||||
#' label = paste("Date range label", input$n),
|
||||
#' start = date - 1,
|
||||
#' end = date + 1,
|
||||
#' min = date - 5,
|
||||
#' max = date + 5
|
||||
#' )
|
||||
#' })
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateDateRangeInput <- function(session, inputId, label = NULL,
|
||||
start = NULL, end = NULL, min = NULL,
|
||||
max = NULL) {
|
||||
# Make sure start and end are strings, not date objects. This is for
|
||||
# consistency across different locales.
|
||||
if (inherits(start, "Date")) start <- format(start, '%Y-%m-%d')
|
||||
if (inherits(end, "Date")) end <- format(end, '%Y-%m-%d')
|
||||
if (inherits(min, "Date")) min <- format(min, '%Y-%m-%d')
|
||||
if (inherits(max, "Date")) max <- format(max, '%Y-%m-%d')
|
||||
|
||||
start <- dateYMD(start, "start")
|
||||
end <- dateYMD(end, "end")
|
||||
min <- dateYMD(min, "min")
|
||||
max <- dateYMD(max, "max")
|
||||
|
||||
message <- dropNulls(list(
|
||||
label = label,
|
||||
value = c(start, end),
|
||||
value = dropNulls(list(start = start, end = end)),
|
||||
min = min,
|
||||
max = max
|
||||
))
|
||||
@@ -152,32 +275,41 @@ updateDateRangeInput <- function(session, inputId, label = NULL,
|
||||
|
||||
#' Change the selected tab on the client
|
||||
#'
|
||||
#' @param session The \code{session} object passed to function given to
|
||||
#' \code{shinyServer}.
|
||||
#' @param inputId The id of the \code{tabsetPanel}, \code{navlistPanel},
|
||||
#' or \code{navbarPage} object.
|
||||
#' @param session The `session` object passed to function given to
|
||||
#' `shinyServer`.
|
||||
#' @param inputId The id of the `tabsetPanel`, `navlistPanel`,
|
||||
#' or `navbarPage` object.
|
||||
#' @param selected The name of the tab to make active.
|
||||
#'
|
||||
#' @seealso \code{\link{tabsetPanel}}, \code{\link{navlistPanel}},
|
||||
#' \code{\link{navbarPage}}
|
||||
#' @seealso [tabsetPanel()], [navlistPanel()],
|
||||
#' [navbarPage()]
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' shinyServer(function(input, output, session) {
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' observe({
|
||||
#' # TRUE if input$controller is even, FALSE otherwise.
|
||||
#' x_even <- input$controller %% 2 == 0
|
||||
#' ui <- fluidPage(sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("controller", "Controller", 1, 3, 1)
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' tabsetPanel(id = "inTabset",
|
||||
#' tabPanel(title = "Panel 1", value = "panel1", "Panel 1 content"),
|
||||
#' tabPanel(title = "Panel 2", value = "panel2", "Panel 2 content"),
|
||||
#' tabPanel(title = "Panel 3", value = "panel3", "Panel 3 content")
|
||||
#' )
|
||||
#' )
|
||||
#' ))
|
||||
#'
|
||||
#' # Change the selected tab.
|
||||
#' # Note that the tabset container must have been created with an 'id' argument
|
||||
#' if (x_even) {
|
||||
#' updateTabsetPanel(session, "inTabset", selected = "panel2")
|
||||
#' } else {
|
||||
#' updateTabsetPanel(session, "inTabset", selected = "panel1")
|
||||
#' }
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$controller, {
|
||||
#' updateTabsetPanel(session, "inTabset",
|
||||
#' selected = paste0("panel", input$controller)
|
||||
#' )
|
||||
#' })
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateTabsetPanel <- function(session, inputId, selected = NULL) {
|
||||
@@ -201,13 +333,21 @@ updateNavlistPanel <- updateTabsetPanel
|
||||
#' @param max Maximum value.
|
||||
#' @param step Step size.
|
||||
#'
|
||||
#' @seealso \code{\link{numericInput}}
|
||||
#' @seealso [numericInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' shinyServer(function(input, output, session) {
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' observe({
|
||||
#' ui <- fluidPage(
|
||||
#' sliderInput("controller", "Controller", 0, 20, 10),
|
||||
#' numericInput("inNumber", "Input number", 0),
|
||||
#' numericInput("inNumber2", "Input number 2", 0)
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#'
|
||||
#' observeEvent(input$controller, {
|
||||
#' # We'll use the input$controller variable multiple times, so save it as x
|
||||
#' # for convenience.
|
||||
#' x <- input$controller
|
||||
@@ -218,7 +358,9 @@ updateNavlistPanel <- updateTabsetPanel
|
||||
#' label = paste("Number label ", x),
|
||||
#' value = x, min = x-10, max = x+10, step = 5)
|
||||
#' })
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
@@ -231,15 +373,19 @@ updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
|
||||
#' Change the value of a slider input on the client
|
||||
#' Update Slider Input Widget
|
||||
#'
|
||||
#' Change the value of a slider input on the client.
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param value The value to set for the input object.
|
||||
#' @param min Minimum value.
|
||||
#' @param max Maximum value.
|
||||
#' @param step Step size.
|
||||
#' @param timeFormat Date and POSIXt formatting.
|
||||
#' @param timezone The timezone offset for POSIXt objects.
|
||||
#'
|
||||
#' @seealso \code{\link{sliderInput}}
|
||||
#' @seealso [sliderInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
@@ -270,22 +416,17 @@ updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
#' }
|
||||
#' @export
|
||||
updateSliderInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
min = NULL, max = NULL, step = NULL)
|
||||
min = NULL, max = NULL, step = NULL, timeFormat = NULL, timezone = NULL)
|
||||
{
|
||||
# Make sure that value, min, max all have the same type, because we need
|
||||
# special handling for dates and datetimes.
|
||||
vals <- dropNulls(list(value, min, max))
|
||||
# If no min/max/value is provided, we won't know the
|
||||
# type, and this will return an empty string
|
||||
dataType <- getSliderType(min, max, value)
|
||||
|
||||
type <- unique(lapply(vals, function(x) {
|
||||
if (inherits(x, "Date")) "date"
|
||||
else if (inherits(x, "POSIXt")) "datetime"
|
||||
else "number"
|
||||
}))
|
||||
if (length(type) > 1) {
|
||||
stop("Type mismatch for value, min, and max")
|
||||
if (is.null(timeFormat)) {
|
||||
timeFormat <- switch(dataType, date = "%F", datetime = "%F %T", number = NULL)
|
||||
}
|
||||
|
||||
if ((length(type) == 1) && (type == "date" || type == "datetime")) {
|
||||
if (isTRUE(dataType %in% c("date", "datetime"))) {
|
||||
to_ms <- function(x) 1000 * as.numeric(as.POSIXct(x))
|
||||
if (!is.null(min)) min <- to_ms(min)
|
||||
if (!is.null(max)) max <- to_ms(max)
|
||||
@@ -297,23 +438,28 @@ updateSliderInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
value = formatNoSci(value),
|
||||
min = formatNoSci(min),
|
||||
max = formatNoSci(max),
|
||||
step = formatNoSci(step)
|
||||
step = formatNoSci(step),
|
||||
`data-type` = dataType,
|
||||
`time-format` = timeFormat,
|
||||
timezone = timezone
|
||||
))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
|
||||
|
||||
updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
|
||||
selected = NULL, inline = FALSE,
|
||||
type = 'checkbox') {
|
||||
if (!is.null(choices))
|
||||
choices <- choicesWithNames(choices)
|
||||
if (!is.null(selected))
|
||||
selected <- validateSelected(selected, choices, inputId)
|
||||
selected = NULL, inline = FALSE, type = NULL,
|
||||
choiceNames = NULL, choiceValues = NULL) {
|
||||
if (is.null(type)) stop("Please specify the type ('checkbox' or 'radio')")
|
||||
|
||||
options <- if (!is.null(choices)) {
|
||||
args <- normalizeChoicesArgs(choices, choiceNames, choiceValues, mustExist = FALSE)
|
||||
|
||||
if (!is.null(selected)) selected <- as.character(selected)
|
||||
|
||||
options <- if (!is.null(args$choiceValues)) {
|
||||
format(tagList(
|
||||
generateOptions(inputId, choices, selected, inline, type = type)
|
||||
generateOptions(session$ns(inputId), selected, inline, type,
|
||||
args$choiceNames, args$choiceValues)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -327,40 +473,45 @@ updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
|
||||
#' @template update-input
|
||||
#' @inheritParams checkboxGroupInput
|
||||
#'
|
||||
#' @seealso \code{\link{checkboxGroupInput}}
|
||||
#' @seealso [checkboxGroupInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' shinyServer(function(input, output, session) {
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' p("The first checkbox group controls the second"),
|
||||
#' checkboxGroupInput("inCheckboxGroup", "Input checkbox",
|
||||
#' c("Item A", "Item B", "Item C")),
|
||||
#' checkboxGroupInput("inCheckboxGroup2", "Input checkbox 2",
|
||||
#' c("Item A", "Item B", "Item C"))
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observe({
|
||||
#' # We'll use the input$controller variable multiple times, so save it as x
|
||||
#' # for convenience.
|
||||
#' x <- input$controller
|
||||
#' x <- input$inCheckboxGroup
|
||||
#'
|
||||
#' # Create a list of new options, where the name of the items is something
|
||||
#' # like 'option label x 1', and the values are 'option-x-1'.
|
||||
#' cb_options <- list()
|
||||
#' cb_options[[sprintf("option label %d 1", x)]] <- sprintf("option-%d-1", x)
|
||||
#' cb_options[[sprintf("option label %d 2", x)]] <- sprintf("option-%d-2", x)
|
||||
#'
|
||||
#' # Change values for input$inCheckboxGroup
|
||||
#' updateCheckboxGroupInput(session, "inCheckboxGroup", choices = cb_options)
|
||||
#' # Can use character(0) to remove all choices
|
||||
#' if (is.null(x))
|
||||
#' x <- character(0)
|
||||
#'
|
||||
#' # Can also set the label and select items
|
||||
#' updateCheckboxGroupInput(session, "inCheckboxGroup2",
|
||||
#' label = paste("checkboxgroup label", x),
|
||||
#' choices = cb_options,
|
||||
#' selected = sprintf("option-%d-2", x)
|
||||
#' label = paste("Checkboxgroup label", length(x)),
|
||||
#' choices = x,
|
||||
#' selected = x
|
||||
#' )
|
||||
#' })
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateCheckboxGroupInput <- function(session, inputId, label = NULL,
|
||||
choices = NULL, selected = NULL,
|
||||
inline = FALSE) {
|
||||
updateInputOptions(session, inputId, label, choices, selected, inline)
|
||||
choices = NULL, selected = NULL, inline = FALSE,
|
||||
choiceNames = NULL, choiceValues = NULL) {
|
||||
updateInputOptions(session, inputId, label, choices, selected,
|
||||
inline, "checkbox", choiceNames, choiceValues)
|
||||
}
|
||||
|
||||
|
||||
@@ -369,39 +520,46 @@ updateCheckboxGroupInput <- function(session, inputId, label = NULL,
|
||||
#' @template update-input
|
||||
#' @inheritParams radioButtons
|
||||
#'
|
||||
#' @seealso \code{\link{radioButtons}}
|
||||
#' @seealso [radioButtons()]
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' shinyServer(function(input, output, session) {
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' p("The first radio button group controls the second"),
|
||||
#' radioButtons("inRadioButtons", "Input radio buttons",
|
||||
#' c("Item A", "Item B", "Item C")),
|
||||
#' radioButtons("inRadioButtons2", "Input radio buttons 2",
|
||||
#' c("Item A", "Item B", "Item C"))
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observe({
|
||||
#' # We'll use the input$controller variable multiple times, so save it as x
|
||||
#' # for convenience.
|
||||
#' x <- input$controller
|
||||
#' x <- input$inRadioButtons
|
||||
#'
|
||||
#' r_options <- list()
|
||||
#' r_options[[sprintf("option label %d 1", x)]] <- sprintf("option-%d-1", x)
|
||||
#' r_options[[sprintf("option label %d 2", x)]] <- sprintf("option-%d-2", x)
|
||||
#'
|
||||
#' # Change values for input$inRadio
|
||||
#' updateRadioButtons(session, "inRadio", choices = r_options)
|
||||
#'
|
||||
#' # Can also set the label and select an item
|
||||
#' updateRadioButtons(session, "inRadio2",
|
||||
#' label = paste("Radio label", x),
|
||||
#' choices = r_options,
|
||||
#' selected = sprintf("option-%d-2", x)
|
||||
#' # Can also set the label and select items
|
||||
#' updateRadioButtons(session, "inRadioButtons2",
|
||||
#' label = paste("radioButtons label", x),
|
||||
#' choices = x,
|
||||
#' selected = x
|
||||
#' )
|
||||
#' })
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateRadioButtons <- function(session, inputId, label = NULL, choices = NULL,
|
||||
selected = NULL, inline = FALSE) {
|
||||
selected = NULL, inline = FALSE,
|
||||
choiceNames = NULL, choiceValues = NULL) {
|
||||
# you must select at least one radio button
|
||||
if (is.null(selected) && !is.null(choices)) selected <- choices[[1]]
|
||||
updateInputOptions(session, inputId, label, choices, selected, inline, type = 'radio')
|
||||
if (is.null(selected)) {
|
||||
if (!is.null(choices)) selected <- choices[[1]]
|
||||
else if (!is.null(choiceValues)) selected <- choiceValues[[1]]
|
||||
}
|
||||
updateInputOptions(session, inputId, label, choices, selected,
|
||||
inline, 'radio', choiceNames, choiceValues)
|
||||
}
|
||||
|
||||
|
||||
@@ -410,42 +568,44 @@ updateRadioButtons <- function(session, inputId, label = NULL, choices = NULL,
|
||||
#' @template update-input
|
||||
#' @inheritParams selectInput
|
||||
#'
|
||||
#' @seealso \code{\link{selectInput}}
|
||||
#' @seealso [selectInput()] [varSelectInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' shinyServer(function(input, output, session) {
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' p("The checkbox group controls the select input"),
|
||||
#' checkboxGroupInput("inCheckboxGroup", "Input checkbox",
|
||||
#' c("Item A", "Item B", "Item C")),
|
||||
#' selectInput("inSelect", "Select input",
|
||||
#' c("Item A", "Item B", "Item C"))
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observe({
|
||||
#' # We'll use the input$controller variable multiple times, so save it as x
|
||||
#' # for convenience.
|
||||
#' x <- input$controller
|
||||
#' x <- input$inCheckboxGroup
|
||||
#'
|
||||
#' # Create a list of new options, where the name of the items is something
|
||||
#' # like 'option label x 1', and the values are 'option-x-1'.
|
||||
#' s_options <- list()
|
||||
#' s_options[[sprintf("option label %d 1", x)]] <- sprintf("option-%d-1", x)
|
||||
#' s_options[[sprintf("option label %d 2", x)]] <- sprintf("option-%d-2", x)
|
||||
#' # Can use character(0) to remove all choices
|
||||
#' if (is.null(x))
|
||||
#' x <- character(0)
|
||||
#'
|
||||
#' # Change values for input$inSelect
|
||||
#' updateSelectInput(session, "inSelect", choices = s_options)
|
||||
#'
|
||||
#' # Can also set the label and select an item (or more than one if it's a
|
||||
#' # multi-select)
|
||||
#' updateSelectInput(session, "inSelect2",
|
||||
#' label = paste("Select label", x),
|
||||
#' choices = s_options,
|
||||
#' selected = sprintf("option-%d-2", x)
|
||||
#' # Can also set the label and select items
|
||||
#' updateSelectInput(session, "inSelect",
|
||||
#' label = paste("Select input label", length(x)),
|
||||
#' choices = x,
|
||||
#' selected = tail(x, 1)
|
||||
#' )
|
||||
#' })
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateSelectInput <- function(session, inputId, label = NULL, choices = NULL,
|
||||
selected = NULL) {
|
||||
choices <- if (!is.null(choices)) choicesWithNames(choices)
|
||||
if (!is.null(selected))
|
||||
selected <- validateSelected(selected, choices, inputId)
|
||||
if (!is.null(selected)) selected <- as.character(selected)
|
||||
options <- if (!is.null(choices)) selectOptions(choices, selected)
|
||||
message <- dropNulls(list(label = label, options = options, value = selected))
|
||||
session$sendInputMessage(inputId, message)
|
||||
@@ -453,9 +613,9 @@ updateSelectInput <- function(session, inputId, label = NULL, choices = NULL,
|
||||
|
||||
#' @rdname updateSelectInput
|
||||
#' @inheritParams selectizeInput
|
||||
#' @param server whether to store \code{choices} on the server side, and load
|
||||
#' @param server whether to store `choices` on the server side, and load
|
||||
#' the select options dynamically on searching, instead of writing all
|
||||
#' \code{choices} into the page at once (i.e., only use the client-side
|
||||
#' `choices` into the page at once (i.e., only use the client-side
|
||||
#' version of \pkg{selectize.js})
|
||||
#' @export
|
||||
updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
|
||||
@@ -465,7 +625,7 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
|
||||
res <- checkAsIs(options)
|
||||
cfg <- tags$script(
|
||||
type = 'application/json',
|
||||
`data-for` = inputId,
|
||||
`data-for` = session$ns(inputId),
|
||||
`data-eval` = if (length(res$eval)) HTML(toJSON(res$eval)),
|
||||
HTML(toJSON(res$options))
|
||||
)
|
||||
@@ -474,8 +634,95 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
|
||||
if (!server) {
|
||||
return(updateSelectInput(session, inputId, label, choices, selected))
|
||||
}
|
||||
|
||||
noOptGroup <- TRUE
|
||||
if (is.list(choices)) {
|
||||
# check if list is nested
|
||||
for (i in seq_along(choices)) {
|
||||
if (is.list(choices[[i]]) || length(choices[[i]]) > 1) {
|
||||
noOptGroup <- FALSE
|
||||
break()
|
||||
}
|
||||
}
|
||||
}
|
||||
# convert choices to a data frame so it returns [{label: , value: , optgroup: },...]
|
||||
choices <- if (is.data.frame(choices)) {
|
||||
# jcheng 2018/09/25: I don't think we ever said data frames were OK to pass
|
||||
# to updateSelectInput, but one of the example apps does this and at least
|
||||
# one user noticed when we broke it.
|
||||
# https://github.com/rstudio/shiny/issues/2172
|
||||
# https://github.com/rstudio/shiny/issues/2192
|
||||
as.data.frame(choices, stringsAsFactors = FALSE)
|
||||
} else if (is.atomic(choices) || noOptGroup) {
|
||||
# fast path for vectors and flat lists
|
||||
if (is.list(choices)) {
|
||||
choices <- unlist(choices)
|
||||
}
|
||||
if (is.null(names(choices))) {
|
||||
lab <- as.character(choices)
|
||||
} else {
|
||||
lab <- names(choices)
|
||||
# replace empty names like: choices = c(a = 1, 2)
|
||||
# in this case: names(choices) = c("a", "")
|
||||
# with replacement below choices will be: lab = c("a", "2")
|
||||
empty_names_indices <- lab == ""
|
||||
lab[empty_names_indices] <- as.character(choices[empty_names_indices])
|
||||
}
|
||||
data.frame(label = lab, value = choices, stringsAsFactors = FALSE)
|
||||
} else {
|
||||
# slow path for nested lists/optgroups
|
||||
list_names <- names(choices)
|
||||
if (is.null(list_names)) {
|
||||
list_names <- rep("", length(choices))
|
||||
}
|
||||
|
||||
choice_list <- mapply(choices, list_names, FUN = function (choice, name) {
|
||||
group <- ""
|
||||
lab <- name
|
||||
if (lab == "") lab <- as.character(choice)
|
||||
|
||||
if (is.list(choice) || length(choice) > 1) {
|
||||
group <- rep(name, length(choice))
|
||||
choice <- unlist(choice)
|
||||
|
||||
if (is.null(names(choice))) {
|
||||
lab <- as.character(choice)
|
||||
} else {
|
||||
lab <- names(choice)
|
||||
# replace empty names like: choices = c(a = 1, 2)
|
||||
# in this case: names(choices) = c("a", "")
|
||||
# with replacement below choices will be: lab = c("a", "2")
|
||||
empty_names_indices <- lab == ""
|
||||
lab[empty_names_indices] <- as.character(choice[empty_names_indices])
|
||||
}
|
||||
}
|
||||
|
||||
list(
|
||||
label = lab,
|
||||
value = as.character(choice),
|
||||
# The name "optgroup" is because this is the default field where
|
||||
# selectize will look for group IDs
|
||||
optgroup = group
|
||||
)
|
||||
}, SIMPLIFY = FALSE)
|
||||
|
||||
|
||||
extract_vector <- function(x, name) {
|
||||
vecs <- lapply(x, `[[`, name)
|
||||
do.call(c, vecs)
|
||||
}
|
||||
|
||||
data.frame(
|
||||
label = extract_vector(choice_list, "label"),
|
||||
value = extract_vector(choice_list, "value"),
|
||||
optgroup = extract_vector(choice_list, "optgroup"),
|
||||
stringsAsFactors = FALSE, row.names = NULL
|
||||
)
|
||||
}
|
||||
|
||||
value <- unname(selected)
|
||||
attr(choices, 'selected_value') <- value
|
||||
|
||||
message <- dropNulls(list(
|
||||
label = label,
|
||||
value = value,
|
||||
@@ -483,38 +730,76 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
|
||||
))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
#' @rdname updateSelectInput
|
||||
#' @inheritParams varSelectInput
|
||||
#' @export
|
||||
updateVarSelectInput <- function(session, inputId, label = NULL, data = NULL, selected = NULL) {
|
||||
if (is.null(data)) {
|
||||
choices <- NULL
|
||||
} else {
|
||||
choices <- colnames(data)
|
||||
}
|
||||
updateSelectInput(
|
||||
session = session,
|
||||
inputId = inputId,
|
||||
label = label,
|
||||
choices = choices,
|
||||
selected = selected
|
||||
)
|
||||
}
|
||||
#' @rdname updateSelectInput
|
||||
#' @export
|
||||
updateVarSelectizeInput <- function(session, inputId, label = NULL, data = NULL, selected = NULL, options = list(), server = FALSE) {
|
||||
if (is.null(data)) {
|
||||
choices <- NULL
|
||||
} else {
|
||||
choices <- colnames(data)
|
||||
}
|
||||
updateSelectizeInput(
|
||||
session = session,
|
||||
inputId = inputId,
|
||||
label = label,
|
||||
choices = choices,
|
||||
selected = selected,
|
||||
options = options,
|
||||
server = server
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
selectizeJSON <- function(data, req) {
|
||||
query <- parseQueryString(req$QUERY_STRING)
|
||||
|
||||
# extract the query variables, conjunction (and/or), search string, maximum options
|
||||
var <- c(jsonlite::fromJSON(query$field))
|
||||
cjn <- if (query$conju == 'and') all else any
|
||||
var <- c(safeFromJSON(query$field))
|
||||
|
||||
# all keywords in lower-case, for case-insensitive matching
|
||||
key <- unique(strsplit(tolower(query$query), '\\s+')[[1]])
|
||||
|
||||
if (identical(key, '')) key <- character(0)
|
||||
mop <- as.numeric(query$maxop)
|
||||
vfd <- query$value # the value field name
|
||||
sel <- attr(data, 'selected_value', exact = TRUE)
|
||||
|
||||
# convert a single vector to a data frame so it returns {label: , value: }
|
||||
# later in JSON; other objects return arbitrary JSON {x: , y: , foo: , ...}
|
||||
data <- if (is.atomic(data)) {
|
||||
data.frame(label = names(choicesWithNames(data)), value = data,
|
||||
stringsAsFactors = FALSE)
|
||||
} else as.data.frame(data, stringsAsFactors = FALSE)
|
||||
|
||||
# start searching for keywords in all specified columns
|
||||
idx <- logical(nrow(data))
|
||||
if (length(key)) for (v in var) {
|
||||
matches <- do.call(
|
||||
cbind,
|
||||
lapply(key, function(k) {
|
||||
grepl(k, tolower(as.character(data[[v]])), fixed = TRUE)
|
||||
})
|
||||
)
|
||||
# merge column matches using OR, and match multiple keywords in one column
|
||||
# using the conjunction setting (AND or OR)
|
||||
idx <- idx | apply(matches, 1, cjn)
|
||||
if (length(key)) {
|
||||
for (v in var) {
|
||||
matches <- do.call(
|
||||
cbind,
|
||||
lapply(key, function(k) {
|
||||
grepl(k, tolower(as.character(data[[v]])), fixed = TRUE)
|
||||
})
|
||||
)
|
||||
# merge column matches using OR, and match multiple keywords in one column
|
||||
# using the conjunction setting (AND or OR)
|
||||
matches <- rowSums(matches)
|
||||
if (query$conju == 'and')
|
||||
idx <- idx | (matches == length(key))
|
||||
else
|
||||
idx <- idx | matches
|
||||
}
|
||||
}
|
||||
# only return the first n rows (n = maximum options in configuration)
|
||||
idx <- utils::head(if (length(key)) which(idx) else seq_along(idx), mop)
|
||||
|
||||
21
README.md
21
README.md
@@ -1,20 +1,24 @@
|
||||
# Shiny
|
||||
|
||||
[](https://travis-ci.org/rstudio/shiny)
|
||||
*Travis:* [](https://travis-ci.org/rstudio/shiny)
|
||||
|
||||
*AppVeyor:* [](https://ci.appveyor.com/project/rstudio/shiny)
|
||||
|
||||
Shiny is a new package from RStudio that makes it incredibly easy to build interactive web applications with R.
|
||||
|
||||
For an introduction and examples, visit the [Shiny Dev Center](http://shiny.rstudio.com/).
|
||||
|
||||
If you have general questions about using Shiny, please use the [RStudio Community website](https://community.rstudio.com). For bug reports, please use the [issue tracker](https://github.com/rstudio/shiny/issues).
|
||||
|
||||
## Features
|
||||
|
||||
* Build useful web applications with only a few lines of code—no JavaScript required.
|
||||
* Shiny applications are automatically "live" in the same way that spreadsheets are live. Outputs change instantly as users modify inputs, without requiring a reload of the browser.
|
||||
* Shiny user interfaces can be built entirely using R, or can be written directly in HTML, CSS, and JavaScript for more flexibility.
|
||||
* Works in any R environment (Console R, Rgui for Windows or Mac, ESS, StatET, RStudio, etc.).
|
||||
* Attractive default UI theme based on [Bootstrap](http://getbootstrap.com/2.3.2/).
|
||||
* Attractive default UI theme based on [Bootstrap](http://getbootstrap.com/).
|
||||
* A highly customizable slider widget with built-in support for animation.
|
||||
* Pre-built output widgets for displaying plots, tables, and printed output of R objects.
|
||||
* Prebuilt output widgets for displaying plots, tables, and printed output of R objects.
|
||||
* Fast bidirectional communication between the web browser and R using the [httpuv](https://github.com/rstudio/httpuv) package.
|
||||
* Uses a [reactive](http://en.wikipedia.org/wiki/Reactive_programming) programming model that eliminates messy event handling code, so you can focus on the code that really matters.
|
||||
* Develop and redistribute your own Shiny widgets that other developers can easily drop into their own applications (coming soon!).
|
||||
@@ -39,8 +43,6 @@ devtools::install_github("rstudio/shiny")
|
||||
|
||||
To learn more we highly recommend you check out the [Shiny Tutorial](http://shiny.rstudio.com/tutorial/). The tutorial explains the framework in-depth, walks you through building a simple application, and includes extensive annotated examples.
|
||||
|
||||
We hope you enjoy using Shiny. If you have general questions about using Shiny, please use the Shiny [mailing list](https://groups.google.com/forum/#!forum/shiny-discuss). For bug reports, please use the [issue tracker](https://github.com/rstudio/shiny/issues).
|
||||
|
||||
## Bootstrap 3 migration
|
||||
|
||||
Shiny versions 0.10.2.2 and below used the Bootstrap 2 web framework. After 0.10.2.2, Shiny switched to Bootstrap 3. For most users, the upgrade should be seamless. However, if you have have customized your HTML-generating code to use features specific to Bootstrap 2, you may need to update your code to work with Bootstrap 3.
|
||||
@@ -57,9 +59,10 @@ devtools::install_version("shiny", version = "0.10.2.2")
|
||||
|
||||
The Javascript code in Shiny is minified using tools that run on Node.js. See the tools/ directory for more information.
|
||||
|
||||
## Guidelines for contributing
|
||||
|
||||
We welcome contributions to the **shiny** package. Please see our [CONTRIBUTING.md](https://github.com/rstudio/shiny/blob/master/.github/CONTRIBUTING.md) file for detailed guidelines of how to contribute.
|
||||
|
||||
## License
|
||||
|
||||
The shiny package is licensed under the GPLv3. See these files in the inst directory for additional details:
|
||||
|
||||
- COPYING - shiny package license (GPLv3)
|
||||
- NOTICE - Copyright notices for additional included software
|
||||
The shiny package as a whole is licensed under the GPLv3. See the [LICENSE](LICENSE) file for more details.
|
||||
|
||||
54
TODO-promises.md
Normal file
54
TODO-promises.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Promises TODO
|
||||
|
||||
## Documentation
|
||||
|
||||
- [x] Motivation -- why should I care about async? Why shouldn't I (what are the limitations)?
|
||||
- [x] High level technical overview
|
||||
- [ ] Cookbook-style examples
|
||||
- [ ] Top-down porting of a sync app to async
|
||||
|
||||
## Core API
|
||||
- [x] Should as.promise() convert regular values to promises? Or throw?
|
||||
- [x] If as.promise() doesn't convert regular values to promises, add promise_resolved(value) and promise_rejected(err) functions?
|
||||
|
||||
## later
|
||||
- [ ] Add support for multiple event loops
|
||||
- [x] Add timeout to run_now
|
||||
|
||||
## Error handling/debugging
|
||||
- [ ] ..stacktraceon../..stacktraceoff.. and stack traces in general
|
||||
- [x] long stack traces
|
||||
- [x] require opt-in
|
||||
- [ ] options(shiny.error) should work in promise handlers
|
||||
- [x] Detect when reactives are used across process boundaries, and error
|
||||
|
||||
## Render functions
|
||||
- [x] Non-async render functions should have their code all execute on the current tick. Otherwise order of execution will be surprising if they have side effects and explicit priorities.
|
||||
- [x] Promise domains should maybe have an onExecute, for the "sync" part that kicks off async operations to also have wrapping behavior (like capturing output). Right now, I have to start off renderPrint with promise(~resolve(TRUE)) and then execute the user code in a then(), just to get the promise behavior. Same will be true when we tackle error handling (stack trace capture).
|
||||
- [x] invisible() doesn't seem to be working correctly with renderPrint. .visible doesn't survive promise chaining, e.g. promise(~resolve(promise(~resolve(invisible("Hi"))))) %>% then(function(x, .visible) { cat(.visible) }) will print TRUE, not FALSE.
|
||||
- [x] renderDataTable should support async
|
||||
- [x] Support downloadHandler
|
||||
- [ ] Support async filename?
|
||||
- [x] Should prevent session from continuing until download completes (ref count)
|
||||
|
||||
## Flush lifecycle
|
||||
- [x] While async operations are running in a session, hold off on any further processing of inputs and scheduled task items until all operations are complete.
|
||||
- [x] Hold all outputs/errors until async operations are complete.
|
||||
- [ ] Allow both sync and async outputs to be displayed before all outputs are done. (opt-in)
|
||||
|
||||
## Testing
|
||||
- [x] App that tests that all built-in render functions support async
|
||||
- [x] Apps that test flush lifecycle, including onFlushed(once = FALSE)
|
||||
- [x] Apps that test invisible() behavior for renderPrint, both sync and async
|
||||
- [x] Apps that ensure all render functions execute synchronous code before tick is over
|
||||
- [x] App that tests async downloadHandler
|
||||
- [x] App that verifies inputs/timers don't fire for a session while it has async operations pending
|
||||
- [x] App that verifies req(FALSE), req(FALSE, cancelOutput = TRUE), validate/need, etc. all work in async
|
||||
|
||||
## External packages
|
||||
- [x] DT
|
||||
- [x] htmlwidgets: Don't require async-aware version of Shiny if not using async
|
||||
- [x] Plotly
|
||||
|
||||
## Bugs
|
||||
- [x] req(FALSE, cancelOutput = TRUE) shows grey (even without async)
|
||||
54
appveyor.yml
Normal file
54
appveyor.yml
Normal file
@@ -0,0 +1,54 @@
|
||||
# DO NOT CHANGE the "init" and "install" sections below
|
||||
|
||||
# Download script file from GitHub
|
||||
init:
|
||||
ps: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
Invoke-WebRequest http://raw.github.com/krlmlr/r-appveyor/master/scripts/appveyor-tool.ps1 -OutFile "..\appveyor-tool.ps1"
|
||||
Import-Module '..\appveyor-tool.ps1'
|
||||
|
||||
install:
|
||||
ps: Bootstrap
|
||||
|
||||
cache:
|
||||
# Bust library cache every time the description file changes
|
||||
# as appveyor cache can not be busted manually
|
||||
# This helps get around errors such as "can't update curl because it's already loaded"
|
||||
# when trying to update the existing cache
|
||||
# PR: https://github.com/rstudio/shiny/pull/2722
|
||||
- C:\RLibrary -> DESCRIPTION
|
||||
|
||||
# Adapt as necessary starting from here
|
||||
|
||||
build_script:
|
||||
- travis-tool.sh install_deps
|
||||
|
||||
test_script:
|
||||
- travis-tool.sh run_tests
|
||||
|
||||
on_failure:
|
||||
- 7z a failure.zip *.Rcheck\*
|
||||
- appveyor PushArtifact failure.zip
|
||||
|
||||
artifacts:
|
||||
- path: '*.Rcheck\**\*.log'
|
||||
name: Logs
|
||||
|
||||
- path: '*.Rcheck\**\*.out'
|
||||
name: Logs
|
||||
|
||||
- path: '*.Rcheck\**\*.fail'
|
||||
name: Logs
|
||||
|
||||
- path: '*.Rcheck\**\*.Rout'
|
||||
name: Logs
|
||||
|
||||
- path: '\*_*.tar.gz'
|
||||
name: Bits
|
||||
|
||||
- path: '\*_*.zip'
|
||||
name: Bits
|
||||
|
||||
environment:
|
||||
global:
|
||||
USE_RTOOLS: true
|
||||
221
inst/_pkgdown.yml
Normal file
221
inst/_pkgdown.yml
Normal file
@@ -0,0 +1,221 @@
|
||||
# NOTE: The main Shiny site, https://shiny.rstudio.com/, is not a pkgdown site.
|
||||
# However, as part of the build process for that site
|
||||
# (rstudio/shiny-dev-center), we do use pkgdown to generate the function
|
||||
# reference index pages for each release. This file configures the look of
|
||||
# those pages for releases from 1.4 onward. Prior to 1.4, staticdocs from
|
||||
# https://github.com/r-lib/pkgdown/releases/tag/old was used and
|
||||
# inst/staticdocs/index.r was its configuration.
|
||||
template:
|
||||
# NOTE: These templates live in shiny-dev-center
|
||||
path: _pkgdown_templates
|
||||
reference:
|
||||
- title: UI Layout
|
||||
desc: Functions for laying out the user interface for your application.
|
||||
contents:
|
||||
- absolutePanel
|
||||
- bootstrapPage
|
||||
- column
|
||||
- conditionalPanel
|
||||
- fillPage
|
||||
- fillRow
|
||||
- fixedPage
|
||||
- fluidPage
|
||||
- helpText
|
||||
- icon
|
||||
- navbarPage
|
||||
- navlistPanel
|
||||
- sidebarLayout
|
||||
- tabPanel
|
||||
- tabsetPanel
|
||||
- titlePanel
|
||||
- inputPanel
|
||||
- flowLayout
|
||||
- splitLayout
|
||||
- verticalLayout
|
||||
- wellPanel
|
||||
- withMathJax
|
||||
- title: UI Inputs
|
||||
desc: Functions for creating user interface elements that prompt the user for input values or interaction.
|
||||
contents:
|
||||
- actionButton
|
||||
- checkboxGroupInput
|
||||
- checkboxInput
|
||||
- dateInput
|
||||
- dateRangeInput
|
||||
- fileInput
|
||||
- numericInput
|
||||
- radioButtons
|
||||
- selectInput
|
||||
- varSelectInput
|
||||
- sliderInput
|
||||
- submitButton
|
||||
- textInput
|
||||
- textAreaInput
|
||||
- passwordInput
|
||||
- modalButton
|
||||
- updateActionButton
|
||||
- updateCheckboxGroupInput
|
||||
- updateCheckboxInput
|
||||
- updateDateInput
|
||||
- updateDateRangeInput
|
||||
- updateNumericInput
|
||||
- updateRadioButtons
|
||||
- updateSelectInput
|
||||
- updateSliderInput
|
||||
- updateTabsetPanel
|
||||
- insertTab
|
||||
- showTab
|
||||
- updateTextInput
|
||||
- updateTextAreaInput
|
||||
- updateQueryString
|
||||
- getQueryString
|
||||
- title: UI Outputs
|
||||
desc: Functions for creating user interface elements that, in conjunction with rendering functions, display different kinds of output from your application.
|
||||
contents:
|
||||
- htmlOutput
|
||||
- plotOutput
|
||||
- outputOptions
|
||||
- tableOutput
|
||||
- textOutput
|
||||
- downloadButton
|
||||
- Progress
|
||||
- withProgress
|
||||
- modalDialog
|
||||
- urlModal
|
||||
- showModal
|
||||
- showNotification
|
||||
- title: Interface builder functions
|
||||
desc: A sub-library for writing HTML using R functions. These functions form the foundation on which the higher level user interface functions are built, and can also be used in your Shiny UI to provide custom HTML, CSS, and JavaScript.
|
||||
contents:
|
||||
- builder
|
||||
- HTML
|
||||
- include
|
||||
- singleton
|
||||
- tag
|
||||
- validateCssUnit
|
||||
- withTags
|
||||
- htmlTemplate
|
||||
- bootstrapLib
|
||||
- suppressDependencies
|
||||
- insertUI
|
||||
- title: Rendering functions
|
||||
desc: Functions that you use in your application's server side code, assigning them to outputs that appear in your user interface.
|
||||
contents:
|
||||
- renderPlot
|
||||
- renderCachedPlot
|
||||
- renderText
|
||||
- renderPrint
|
||||
- renderDataTable
|
||||
- renderImage
|
||||
- renderTable
|
||||
- renderUI
|
||||
- downloadHandler
|
||||
- createRenderFunction
|
||||
- title: Reactive programming
|
||||
desc: A sub-library that provides reactive programming facilities for R.
|
||||
contents:
|
||||
- reactive
|
||||
- observe
|
||||
- observeEvent
|
||||
- reactiveVal
|
||||
- reactiveValues
|
||||
- reactiveValuesToList
|
||||
- is.reactivevalues
|
||||
- isolate
|
||||
- invalidateLater
|
||||
- debounce
|
||||
- reactlog
|
||||
- makeReactiveBinding
|
||||
- reactiveFileReader
|
||||
- reactivePoll
|
||||
- reactiveTimer
|
||||
- domains
|
||||
- freezeReactiveValue
|
||||
- title: Boilerplate
|
||||
desc: Functions that are required boilerplate in ui.R and server.R.
|
||||
contents:
|
||||
- shinyUI
|
||||
- shinyServer
|
||||
- title: Running
|
||||
desc: Functions that are used to run or stop Shiny applications.
|
||||
contents:
|
||||
- runApp
|
||||
- runGadget
|
||||
- runExample
|
||||
- runGadget
|
||||
- runUrl
|
||||
- stopApp
|
||||
- viewer
|
||||
- isRunning
|
||||
- loadSupport
|
||||
- title: Bookmarking state
|
||||
desc: Functions that are used for bookmarking and restoring state.
|
||||
contents:
|
||||
- bookmarkButton
|
||||
- enableBookmarking
|
||||
- setBookmarkExclude
|
||||
- showBookmarkUrlModal
|
||||
- onBookmark
|
||||
- title: Extending Shiny
|
||||
desc: Functions that are intended to be called by third-party packages that extend Shiny.
|
||||
contents:
|
||||
- createWebDependency
|
||||
- resourcePaths
|
||||
- registerInputHandler
|
||||
- removeInputHandler
|
||||
- markRenderFunction
|
||||
- title: Utility functions
|
||||
desc: Miscellaneous utilities that may be useful to advanced users or when extending Shiny.
|
||||
contents:
|
||||
- req
|
||||
- validate
|
||||
- session
|
||||
- shinyOptions
|
||||
- safeError
|
||||
- onFlush
|
||||
- restoreInput
|
||||
- applyInputHandlers
|
||||
- exprToFunction
|
||||
- installExprFunction
|
||||
- parseQueryString
|
||||
- getCurrentOutputInfo
|
||||
- plotPNG
|
||||
- sizeGrowthRatio
|
||||
- exportTestValues
|
||||
- setSerializer
|
||||
- snapshotExclude
|
||||
- snapshotPreprocessInput
|
||||
- snapshotPreprocessOutput
|
||||
- markOutputAttrs
|
||||
- repeatable
|
||||
- shinyDeprecated
|
||||
- serverInfo
|
||||
- onStop
|
||||
- diskCache
|
||||
- memoryCache
|
||||
- reexports
|
||||
- title: Plot interaction
|
||||
desc: Functions related to interactive plots
|
||||
contents:
|
||||
- brushedPoints
|
||||
- brushOpts
|
||||
- clickOpts
|
||||
- dblclickOpts
|
||||
- hoverOpts
|
||||
- nearPoints
|
||||
- title: Modules
|
||||
desc: Functions for modularizing Shiny apps
|
||||
contents:
|
||||
- NS
|
||||
- moduleServer
|
||||
- title: Embedding
|
||||
desc: Functions that are intended for third-party packages that embed Shiny applications.
|
||||
contents:
|
||||
- shinyApp
|
||||
- maskReactiveContext
|
||||
- title: Testing
|
||||
desc: Functions intended for testing of Shiny components
|
||||
contents:
|
||||
- runTests
|
||||
- testModule
|
||||
- MockShinySession
|
||||
@@ -1,4 +1,3 @@
|
||||
This small Shiny application demonstrates Shiny's automatic UI updates. Move
|
||||
the *Number of bins* slider and notice how the `renderPlot` expression is
|
||||
automatically re-evaluated when its dependant, `input$bins`, changes,
|
||||
causing a histogram with a new number of bins to be rendered.
|
||||
This small Shiny application demonstrates Shiny's automatic UI updates.
|
||||
|
||||
Move the *Number of bins* slider and notice how the `renderPlot` expression is automatically re-evaluated when its dependant, `input$bins`, changes, causing a histogram with a new number of bins to be rendered.
|
||||
|
||||
59
inst/examples/01_hello/app.R
Normal file
59
inst/examples/01_hello/app.R
Normal file
@@ -0,0 +1,59 @@
|
||||
library(shiny)
|
||||
|
||||
# Define UI for app that draws a histogram ----
|
||||
ui <- fluidPage(
|
||||
|
||||
# App title ----
|
||||
titlePanel("Hello Shiny!"),
|
||||
|
||||
# Sidebar layout with input and output definitions ----
|
||||
sidebarLayout(
|
||||
|
||||
# Sidebar panel for inputs ----
|
||||
sidebarPanel(
|
||||
|
||||
# Input: Slider for the number of bins ----
|
||||
sliderInput(inputId = "bins",
|
||||
label = "Number of bins:",
|
||||
min = 1,
|
||||
max = 50,
|
||||
value = 30)
|
||||
|
||||
),
|
||||
|
||||
# Main panel for displaying outputs ----
|
||||
mainPanel(
|
||||
|
||||
# Output: Histogram ----
|
||||
plotOutput(outputId = "distPlot")
|
||||
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Define server logic required to draw a histogram ----
|
||||
server <- function(input, output) {
|
||||
|
||||
# Histogram of the Old Faithful Geyser Data ----
|
||||
# with requested number of bins
|
||||
# This expression that generates a histogram is wrapped in a call
|
||||
# to renderPlot to indicate that:
|
||||
#
|
||||
# 1. It is "reactive" and therefore should be automatically
|
||||
# re-executed when inputs (input$bins) change
|
||||
# 2. Its output type is a plot
|
||||
output$distPlot <- renderPlot({
|
||||
|
||||
x <- faithful$waiting
|
||||
bins <- seq(min(x), max(x), length.out = input$bins + 1)
|
||||
|
||||
hist(x, breaks = bins, col = "#75AADB", border = "white",
|
||||
xlab = "Waiting time to next eruption (in mins)",
|
||||
main = "Histogram of waiting times")
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
# Create Shiny app ----
|
||||
shinyApp(ui = ui, server = server)
|
||||
@@ -1,6 +0,0 @@
|
||||
name: 01_hello
|
||||
account: admin
|
||||
server: localhost
|
||||
bundleId: 1
|
||||
url: http://localhost:3939/admin/01_hello/
|
||||
when: 1436550957.65385
|
||||
@@ -1,21 +0,0 @@
|
||||
library(shiny)
|
||||
|
||||
# Define server logic required to draw a histogram
|
||||
shinyServer(function(input, output) {
|
||||
|
||||
# Expression that generates a histogram. The expression is
|
||||
# wrapped in a call to renderPlot to indicate that:
|
||||
#
|
||||
# 1) It is "reactive" and therefore should be automatically
|
||||
# re-executed when inputs change
|
||||
# 2) Its output type is a plot
|
||||
|
||||
output$distPlot <- renderPlot({
|
||||
x <- faithful[, 2] # Old Faithful Geyser data
|
||||
bins <- seq(min(x), max(x), length.out = input$bins + 1)
|
||||
|
||||
# draw the histogram with the specified number of bins
|
||||
hist(x, breaks = bins, col = 'darkgray', border = 'white')
|
||||
})
|
||||
|
||||
})
|
||||
@@ -1,24 +0,0 @@
|
||||
library(shiny)
|
||||
|
||||
# Define UI for application that draws a histogram
|
||||
shinyUI(fluidPage(
|
||||
|
||||
# Application title
|
||||
titlePanel("Hello Shiny!"),
|
||||
|
||||
# Sidebar with a slider input for the number of bins
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
sliderInput("bins",
|
||||
"Number of bins:",
|
||||
min = 1,
|
||||
max = 50,
|
||||
value = 30)
|
||||
),
|
||||
|
||||
# Show a plot of the generated distribution
|
||||
mainPanel(
|
||||
plotOutput("distPlot")
|
||||
)
|
||||
)
|
||||
))
|
||||
@@ -1 +1 @@
|
||||
This example demonstrates output of raw text from R using the `renderPrint` function in `server.R` and the `verbatimTextOutput` function in `ui.R`. In this case, a textual summary of the data is shown using R's built-in `summary` function.
|
||||
This example demonstrates output of raw text from R using the `renderPrint` function in `server` and the `verbatimTextOutput` function in `ui`. In this case, a textual summary of the data is shown using R's built-in `summary` function.
|
||||
|
||||
64
inst/examples/02_text/app.R
Normal file
64
inst/examples/02_text/app.R
Normal file
@@ -0,0 +1,64 @@
|
||||
library(shiny)
|
||||
|
||||
# Define UI for dataset viewer app ----
|
||||
ui <- fluidPage(
|
||||
|
||||
# App title ----
|
||||
titlePanel("Shiny Text"),
|
||||
|
||||
# Sidebar layout with a input and output definitions ----
|
||||
sidebarLayout(
|
||||
|
||||
# Sidebar panel for inputs ----
|
||||
sidebarPanel(
|
||||
|
||||
# Input: Selector for choosing dataset ----
|
||||
selectInput(inputId = "dataset",
|
||||
label = "Choose a dataset:",
|
||||
choices = c("rock", "pressure", "cars")),
|
||||
|
||||
# Input: Numeric entry for number of obs to view ----
|
||||
numericInput(inputId = "obs",
|
||||
label = "Number of observations to view:",
|
||||
value = 10)
|
||||
),
|
||||
|
||||
# Main panel for displaying outputs ----
|
||||
mainPanel(
|
||||
|
||||
# Output: Verbatim text for data summary ----
|
||||
verbatimTextOutput("summary"),
|
||||
|
||||
# Output: HTML table with requested number of observations ----
|
||||
tableOutput("view")
|
||||
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Define server logic to summarize and view selected dataset ----
|
||||
server <- function(input, output) {
|
||||
|
||||
# Return the requested dataset ----
|
||||
datasetInput <- reactive({
|
||||
switch(input$dataset,
|
||||
"rock" = rock,
|
||||
"pressure" = pressure,
|
||||
"cars" = cars)
|
||||
})
|
||||
|
||||
# Generate a summary of the dataset ----
|
||||
output$summary <- renderPrint({
|
||||
dataset <- datasetInput()
|
||||
summary(dataset)
|
||||
})
|
||||
|
||||
# Show the first "n" observations ----
|
||||
output$view <- renderTable({
|
||||
head(datasetInput(), n = input$obs)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
# Create Shiny app ----
|
||||
shinyApp(ui = ui, server = server)
|
||||
@@ -1,26 +0,0 @@
|
||||
library(shiny)
|
||||
library(datasets)
|
||||
|
||||
# Define server logic required to summarize and view the selected
|
||||
# dataset
|
||||
shinyServer(function(input, output) {
|
||||
|
||||
# Return the requested dataset
|
||||
datasetInput <- reactive({
|
||||
switch(input$dataset,
|
||||
"rock" = rock,
|
||||
"pressure" = pressure,
|
||||
"cars" = cars)
|
||||
})
|
||||
|
||||
# Generate a summary of the dataset
|
||||
output$summary <- renderPrint({
|
||||
dataset <- datasetInput()
|
||||
summary(dataset)
|
||||
})
|
||||
|
||||
# Show the first "n" observations
|
||||
output$view <- renderTable({
|
||||
head(datasetInput(), n = input$obs)
|
||||
})
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
library(shiny)
|
||||
|
||||
# Define UI for dataset viewer application
|
||||
shinyUI(fluidPage(
|
||||
|
||||
# Application title
|
||||
titlePanel("Shiny Text"),
|
||||
|
||||
# Sidebar with controls to select a dataset and specify the
|
||||
# number of observations to view
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
selectInput("dataset", "Choose a dataset:",
|
||||
choices = c("rock", "pressure", "cars")),
|
||||
|
||||
numericInput("obs", "Number of observations to view:", 10)
|
||||
),
|
||||
|
||||
# Show a summary of the dataset and an HTML table with the
|
||||
# requested number of observations
|
||||
mainPanel(
|
||||
verbatimTextOutput("summary"),
|
||||
|
||||
tableOutput("view")
|
||||
)
|
||||
)
|
||||
))
|
||||
@@ -1,5 +1,5 @@
|
||||
This example demonstrates a core feature of Shiny: **reactivity**. In `server.R`, a reactive called `datasetInput` is declared.
|
||||
This example demonstrates a core feature of Shiny: **reactivity**. In the `server` function, a reactive called `datasetInput` is declared.
|
||||
|
||||
Notice that the reactive expression depends on the input expression `input$dataset`, and that it's used by both the output expression `output$summary` and `output$view`. Try changing the dataset (using *Choose a dataset*) while looking at the reactive and then at the outputs; you will see first the reactive and then its dependencies flash.
|
||||
Notice that the reactive expression depends on the input expression `input$dataset`, and that it's used by two output expressions: `output$summary` and `output$view`. Try changing the dataset (using *Choose a dataset*) while looking at the reactive and then at the outputs; you will see first the reactive and then its dependencies flash.
|
||||
|
||||
Notice also that the reactive expression doesn't just update whenever anything changes--only the inputs it depends on will trigger an update. Change the "Caption" field and notice how only the `output$caption` expression is re-evaluated; the reactive and its dependents are left alone.
|
||||
|
||||
102
inst/examples/03_reactivity/app.R
Normal file
102
inst/examples/03_reactivity/app.R
Normal file
@@ -0,0 +1,102 @@
|
||||
library(shiny)
|
||||
|
||||
# Define UI for dataset viewer app ----
|
||||
ui <- fluidPage(
|
||||
|
||||
# App title ----
|
||||
titlePanel("Reactivity"),
|
||||
|
||||
# Sidebar layout with input and output definitions ----
|
||||
sidebarLayout(
|
||||
|
||||
# Sidebar panel for inputs ----
|
||||
sidebarPanel(
|
||||
|
||||
# Input: Text for providing a caption ----
|
||||
# Note: Changes made to the caption in the textInput control
|
||||
# are updated in the output area immediately as you type
|
||||
textInput(inputId = "caption",
|
||||
label = "Caption:",
|
||||
value = "Data Summary"),
|
||||
|
||||
# Input: Selector for choosing dataset ----
|
||||
selectInput(inputId = "dataset",
|
||||
label = "Choose a dataset:",
|
||||
choices = c("rock", "pressure", "cars")),
|
||||
|
||||
# Input: Numeric entry for number of obs to view ----
|
||||
numericInput(inputId = "obs",
|
||||
label = "Number of observations to view:",
|
||||
value = 10)
|
||||
|
||||
),
|
||||
|
||||
# Main panel for displaying outputs ----
|
||||
mainPanel(
|
||||
|
||||
# Output: Formatted text for caption ----
|
||||
h3(textOutput("caption", container = span)),
|
||||
|
||||
# Output: Verbatim text for data summary ----
|
||||
verbatimTextOutput("summary"),
|
||||
|
||||
# Output: HTML table with requested number of observations ----
|
||||
tableOutput("view")
|
||||
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Define server logic to summarize and view selected dataset ----
|
||||
server <- function(input, output) {
|
||||
|
||||
# Return the requested dataset ----
|
||||
# By declaring datasetInput as a reactive expression we ensure
|
||||
# that:
|
||||
#
|
||||
# 1. It is only called when the inputs it depends on changes
|
||||
# 2. The computation and result are shared by all the callers,
|
||||
# i.e. it only executes a single time
|
||||
datasetInput <- reactive({
|
||||
switch(input$dataset,
|
||||
"rock" = rock,
|
||||
"pressure" = pressure,
|
||||
"cars" = cars)
|
||||
})
|
||||
|
||||
# Create caption ----
|
||||
# The output$caption is computed based on a reactive expression
|
||||
# that returns input$caption. When the user changes the
|
||||
# "caption" field:
|
||||
#
|
||||
# 1. This function is automatically called to recompute the output
|
||||
# 2. New caption is pushed back to the browser for re-display
|
||||
#
|
||||
# Note that because the data-oriented reactive expressions
|
||||
# below don't depend on input$caption, those expressions are
|
||||
# NOT called when input$caption changes
|
||||
output$caption <- renderText({
|
||||
input$caption
|
||||
})
|
||||
|
||||
# Generate a summary of the dataset ----
|
||||
# The output$summary depends on the datasetInput reactive
|
||||
# expression, so will be re-executed whenever datasetInput is
|
||||
# invalidated, i.e. whenever the input$dataset changes
|
||||
output$summary <- renderPrint({
|
||||
dataset <- datasetInput()
|
||||
summary(dataset)
|
||||
})
|
||||
|
||||
# Show the first "n" observations ----
|
||||
# The output$view depends on both the databaseInput reactive
|
||||
# expression and input$obs, so it will be re-executed whenever
|
||||
# input$dataset or input$obs is changed
|
||||
output$view <- renderTable({
|
||||
head(datasetInput(), n = input$obs)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
# Create Shiny app ----
|
||||
shinyApp(ui, server)
|
||||
@@ -1,53 +0,0 @@
|
||||
library(shiny)
|
||||
library(datasets)
|
||||
|
||||
# Define server logic required to summarize and view the selected
|
||||
# dataset
|
||||
shinyServer(function(input, output) {
|
||||
|
||||
# By declaring datasetInput as a reactive expression we ensure
|
||||
# that:
|
||||
#
|
||||
# 1) It is only called when the inputs it depends on changes
|
||||
# 2) The computation and result are shared by all the callers
|
||||
# (it only executes a single time)
|
||||
#
|
||||
datasetInput <- reactive({
|
||||
switch(input$dataset,
|
||||
"rock" = rock,
|
||||
"pressure" = pressure,
|
||||
"cars" = cars)
|
||||
})
|
||||
|
||||
# The output$caption is computed based on a reactive expression
|
||||
# that returns input$caption. When the user changes the
|
||||
# "caption" field:
|
||||
#
|
||||
# 1) This function is automatically called to recompute the
|
||||
# output
|
||||
# 2) The new caption is pushed back to the browser for
|
||||
# re-display
|
||||
#
|
||||
# Note that because the data-oriented reactive expressions
|
||||
# below don't depend on input$caption, those expressions are
|
||||
# NOT called when input$caption changes.
|
||||
output$caption <- renderText({
|
||||
input$caption
|
||||
})
|
||||
|
||||
# The output$summary depends on the datasetInput reactive
|
||||
# expression, so will be re-executed whenever datasetInput is
|
||||
# invalidated
|
||||
# (i.e. whenever the input$dataset changes)
|
||||
output$summary <- renderPrint({
|
||||
dataset <- datasetInput()
|
||||
summary(dataset)
|
||||
})
|
||||
|
||||
# The output$view depends on both the databaseInput reactive
|
||||
# expression and input$obs, so will be re-executed whenever
|
||||
# input$dataset or input$obs is changed.
|
||||
output$view <- renderTable({
|
||||
head(datasetInput(), n = input$obs)
|
||||
})
|
||||
})
|
||||
@@ -1,34 +0,0 @@
|
||||
library(shiny)
|
||||
|
||||
# Define UI for dataset viewer application
|
||||
shinyUI(fluidPage(
|
||||
|
||||
# Application title
|
||||
titlePanel("Reactivity"),
|
||||
|
||||
# Sidebar with controls to provide a caption, select a dataset,
|
||||
# and specify the number of observations to view. Note that
|
||||
# changes made to the caption in the textInput control are
|
||||
# updated in the output area immediately as you type
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
textInput("caption", "Caption:", "Data Summary"),
|
||||
|
||||
selectInput("dataset", "Choose a dataset:",
|
||||
choices = c("rock", "pressure", "cars")),
|
||||
|
||||
numericInput("obs", "Number of observations to view:", 10)
|
||||
),
|
||||
|
||||
|
||||
# Show the caption, a summary of the dataset and an HTML
|
||||
# table with the requested number of observations
|
||||
mainPanel(
|
||||
h3(textOutput("caption", container = span)),
|
||||
|
||||
verbatimTextOutput("summary"),
|
||||
|
||||
tableOutput("view")
|
||||
)
|
||||
)
|
||||
))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user