Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

699 lines
23KB

  1. ---
  2. output: github_document
  3. ---
  4. # Building an HTML Widget: Frappe Charts
  5. [htmlwidget]: https://www.htmlwidgets.org/
  6. [frappe-charts]: https://frappe.io/charts
  7. [rmd-book-htmlwidget]: https://bookdown.org/yihui/rmarkdown/html-widgets.html
  8. This repository walks through the building of an [htmlwidget]
  9. around the JavaScript library [Frappe Charts][frappe-charts].
  10. Because there are many files and moving pieces involved in this process,
  11. I've created a git repository that walks through the changes
  12. at each step of the process.
  13. With my notes about each step,
  14. I've included the SHA linked to the updates made during the step.
  15. If you're viewing this on GitHub,
  16. those SHA hashes should be converted to links
  17. that will take you to a summary of which files changed at each step.
  18. This introduction focuses on the mechanics of htmlwidgets.
  19. For a much more detailed summary,
  20. the [HTML Widgets][rmd-book-htmlwidget] chapter of the R Markdown book
  21. is an excellent introduction.
  22. ```{r setup, include=FALSE}
  23. knitr::opts_chunk$set(eval = FALSE)
  24. ```
  25. ## Setup R Package
  26. * [a3f6fdd986d2b98323b5be43e323df4f6a19f1f3](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/a3f6fdd986d2b98323b5be43e323df4f6a19f1f3)
  27. Create a package for this HTML widget.
  28. We're not going to publish this, so you can call it whatever you want
  29. ```{r create-package}
  30. usethis::create_package("frappeCharts")
  31. ```
  32. Add a dev script for notes
  33. ```{r dev}
  34. dir.create("dev")
  35. file.create("dev/dev.R")
  36. rstudioapi::navigateToFile("dev/dev.R")
  37. ```
  38. ## Setup npm package
  39. * [256f0ca112b2685608f9a17a4fb4e35d279c9830](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/256f0ca112b2685608f9a17a4fb4e35d279c9830)
  40. Same process again, but this time for npm.
  41. ```bash
  42. npm init
  43. # or
  44. npm init -y
  45. ```
  46. Open `package.json` and take a look
  47. ```json
  48. {
  49. "name": "frappecharts",
  50. "version": "0.0.1",
  51. "description": "",
  52. "main": "index.js",
  53. "scripts": {
  54. "test": "echo \"Error: no test specified\" && exit 1"
  55. },
  56. "author": "",
  57. "license": "MIT"
  58. }
  59. ```
  60. From Frappe Charts [docs#installation](https://frappe.io/charts/docs#installation):
  61. ```bash
  62. npm install frappe-charts
  63. ```
  64. We now have a dependency in `package.json` and there's a `package-lock.json` file.
  65. ```json
  66. "dependencies": {
  67. "frappe-charts": "^1.3.0"
  68. }
  69. ```
  70. ## Ignore node_modules but add package-lock
  71. There's also a `node_modules/` folder with `frappe-charts/` inside.
  72. Add `node_modules` to `.Rbuildignore` and `.gitignore`.
  73. (BTW, you can and are supposed to commit `package-lock.json`.)
  74. ```{r ignore-node-module}
  75. usethis::use_build_ignore("node_modules")
  76. usethis::use_build_ignore("package.json")
  77. usethis::use_build_ignore("package-lock.json")
  78. usethis::use_git_ignore("node_modules")
  79. ```
  80. ## Scaffold the HTML widget
  81. * [38bac2c65cf54816525076690310008e62ab99a1](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/38bac2c65cf54816525076690310008e62ab99a1)
  82. ```{r htmlwidgets-scaffold}
  83. htmlwidgets::scaffoldWidget("frappeChart")
  84. ```
  85. This adds files in `inst/htmlwidgets`
  86. ```
  87. inst
  88. └── htmlwidgets
  89. ├── frappeChart.js #<< R <-> JS code
  90. └── frappeChart.yaml #<< list of dependencies
  91. ```
  92. and creates a file `R/frappeChart.R` with the functions
  93. - `frappeChart()`
  94. - `frappeChartOutput()` (for shiny)
  95. - `renderFrappeChart()` (for shiny)
  96. ## Use `npm` to get our dependencies in the right place
  97. * [7abf0224345a67217c4a476f04eafe581f0ecec0](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/7abf0224345a67217c4a476f04eafe581f0ecec0)
  98. `htmlwidgets` load dependencies in a way that's exactly the same as using a
  99. `<script>` tag in the HTML `<head>`.
  100. Look at the
  101. [documentation on Frappe Charts](https://frappe.io/charts/docs#installation)
  102. and decide which file we should use.
  103. Here's the block from their docs
  104. ```
  105. <script src="https://cdn.jsdelivr.net/npm/frappe-charts@1.2.4/dist/frappe-charts.min.iife.js"></script>
  106. <!-- or -->
  107. <script src="https://unpkg.com/frappe-charts@1.2.4/dist/frappe-charts.min.iife.js"></script>
  108. ```
  109. We need to get our dependecy into a subfolder of `inst/htmlwidgets`.
  110. Convention is `inst/htmlwidgets/lib/<dependency_name>`.
  111. Rather than creating the directoy and copying over, etc.,
  112. we can have an `npm` build script do this for us.
  113. To avoid issues with mac/windows,
  114. we'll add a dev dependency on [`cpy-cli`](https://github.com/sindresorhus/cpy-cli).
  115. Dev dependencies are node modules
  116. that are used to build a package,
  117. rather than required for the package to work.
  118. ```bash
  119. npm install cpy-cli --save-dev
  120. ```
  121. Then we create the folder `frappe-charts` under `inst/htmlwidgets/lib`
  122. that will hold the Frappe Charts JavaScript dependency.
  123. (If the library included other required files,
  124. we would move these too.)
  125. ```{r create-lib-dir}
  126. dir.create("inst/htmlwidets/lib/frappe-charts", recursive = TRUE)
  127. ```
  128. And then edit `package.json` to add a copy task.
  129. You can define scripts that are runnable with `npm run <script-name>`.
  130. For small build tasks, this is an easy to implement build solution.
  131. ```
  132. "scripts": {
  133. "copy-js": "cpy 'node_modules/frappe-charts/dist/frappe-charts.min.iife*' inst/htmlwidgets/lib/frappe-charts/",
  134. "build": "npm run copy-js"
  135. }
  136. ```
  137. Notice that running `npm run build` will also call `npm run copy-js`.
  138. If we had more build tasks related to our JavaScript dependencies,
  139. like linting or testing,
  140. we could add them as separate scripts
  141. and have them run in the build process with
  142. `npm run <task-1> && npm run <task-2>`, etc.
  143. ## Create a demo html_document_plain()
  144. * [036d454f80d6036fc1ba35db92161fd19c053635](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/036d454f80d6036fc1ba35db92161fd19c053635)
  145. ```{r create-demo-html}
  146. dir.create("dev/demo")
  147. js4shiny::js4shiny_rmd(path = "dev/demo/demo.Rmd")
  148. ```
  149. Use the example in the [Frappe Charts Docs](https://frappe.io/charts/docs).
  150. ```{r}
  151. tagList(
  152. div(id = "chart"),
  153. htmltools::htmlDependency(
  154. name = "frappe-charts",
  155. version = "1.3.0",
  156. package = "frappeCharts",
  157. src = "htmlwidgets/lib/frappe-charts",
  158. script = "frappe-charts.min.iife.js",
  159. all_files = TRUE
  160. )
  161. )
  162. ```
  163. And copy the JS into a javascript chunk.
  164. `r emo::ji("warning")` The dependencies won't be found until you build/install.
  165. ```{r build-install}
  166. devtools::document()
  167. devtools::install()
  168. ```
  169. If you get a path not found error
  170. ```
  171. Error: path for html_dependency not found: inst/htmlwidgets/lib/frappe-charts
  172. ```
  173. it's most likely because
  174. ```
  175. src = "inst/htmlwidgets/lib/frappe-charts"
  176. ```
  177. should be relative to `inst`.
  178. ## Replace the example data with another data set and example
  179. * [8fd703a08b021b8466171b83506f5fb0bf92f2ac](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/8fd703a08b021b8466171b83506f5fb0bf92f2ac)
  180. The first demo mixes chart types and we don't want to do that.
  181. Use the example from
  182. [Basic Chart](https://frappe.io/charts/docs/basic/basic_chart#adding-more-datasets).
  183. ```js
  184. const data = {
  185. labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
  186. datasets: [
  187. { name: "R", values: [18, 40, 30, 35, 8, 52, 17, -4] },
  188. { name: "Python", values: [30, 50, -10, 15, 18, 32, 27, 14] }
  189. ]
  190. }
  191. ```
  192. Then re-create this data in an R chunk ([881c12ffdbdaa017863c918f61fa6208400d6130](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/881c12ffdbdaa017863c918f61fa6208400d6130)):
  193. ```{r data-in-r}
  194. data <- list(
  195. labels = c("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"),
  196. datasets = list(
  197. list(name = "R", values = c(18, 40, 30, 35, 8, 52, 17, -4)),
  198. list(name = "Python", values = c(30, 50, -10, 15, 18, 32, 27, 14))
  199. )
  200. )
  201. ```
  202. To get the data out of R and make it available in the document,
  203. `htmlwidgets` embeds the data in a `<script type="application/json">...</script>`
  204. element in the page.
  205. Embed the data from the R chunk in a `<script>` tag with an ID
  206. so that we can find it later.
  207. ```{r embed-r-data-in-script}
  208. tags$script(
  209. id = "data",
  210. type = "application/json",
  211. htmlwidgets:::toJSON(data)
  212. )
  213. ```
  214. Change to `js4shiny::html_document_js()` so that we can see the `console.log()`
  215. from JavaScript just like R code.
  216. And then find the `<script>` tag and get it's `.textContent`.
  217. ```{js find-r-data-script-2}
  218. let rData = document.getElementById('data')
  219. rData.textContent
  220. ```
  221. Use `JSON.parse()` to turn the data into a JS object
  222. and replace the data used in the chart ([7201e436e72ebddee271cbf7c02a733ac81a5d86](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/7201e436e72ebddee271cbf7c02a733ac81a5d86)).
  223. ```{js find-r-data-script-3}
  224. let rData = document.getElementById('data')
  225. rData = JSON.parse(rData.textContent)
  226. ```
  227. Switch between `data` and `rData` and it should be the same!
  228. Change the values of the data in the R side to be random
  229. so that each re-run gives a new plot.
  230. ~~Delete the `data` in the JS side.~~
  231. Comment out the `data` on the JS side (but we'll want to see the structure later).
  232. ## Augment data to set options for the chart
  233. * [3e1d9bee03fdf621f5dc5ec46e0e92f603ebe219](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/3e1d9bee03fdf621f5dc5ec46e0e92f603ebe219)
  234. Embed `data` in another list `opts` that will carry additional options,
  235. such as `title`, `type` and `colors`.
  236. Parse the embedded `<script>` and pass the whole object to `frappe.Chart()`.
  237. Change the colors to
  238. - `#466683` (dark blue)
  239. - `#44bc96` (green)
  240. - `#d33f49` (red)
  241. - `#993d70` (purple)
  242. ## Learn about other options for line charts
  243. * [340d516ee4c7788e4f7e5089c4957ee9ffd1333e](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/340d516ee4c7788e4f7e5089c4957ee9ffd1333e)
  244. Read <https://frappe.io/charts/docs/basic/trends_regions>
  245. and add and test additional line options.
  246. Goal: shaded area chart with lines only.
  247. Make the `labels` one week and repeat 4 times.
  248. Generate `runif(7 * 4)` random numbers.
  249. ```r
  250. rep(c("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"), 4)
  251. ```
  252. Find and implement an option to reduce the number of labels on the x-axis.
  253. ## Turn on dots again and make navigable
  254. * [93d4c74f4b30a819b5c22fd7cc8ff238fc62f572](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/93d4c74f4b30a819b5c22fd7cc8ff238fc62f572)
  255. ```{r}
  256. opts <- list(
  257. title = "My AwesomeR Chart",
  258. type = "bar",
  259. height = 250,
  260. colors = c("#466683", "#44bc96"),
  261. data = data,
  262. axisOptions = list(xIsSeries = TRUE),
  263. isNavigable = TRUE
  264. )
  265. ```
  266. ## Add a real data source
  267. * [7a988739e3b5ff0572f4c16ce5110f52936550c3](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/7a988739e3b5ff0572f4c16ce5110f52936550c3)
  268. This is where you decide how much work you want to do on the R side
  269. and how much work should be done on the JavaScript side.
  270. One thing is clear though,
  271. R users should not be expected
  272. to construct the nested list data structure
  273. just to use your HTML widget.
  274. We are used to data.frames and tibbles,
  275. so these should be supported out of the box!
  276. To practice our JavaScript skills,
  277. I've chosen to do most of the work in the browser.
  278. We'll just ask our users to give us rectangular data,
  279. where the first column will provide the x axis labels
  280. and the remaining columns are series.
  281. Sidenote: As you can tell, there's a lot of validation that should happen,
  282. but that part is not as much fun so we're going to pretend
  283. our users will always give us perfectly formatted data.
  284. If you do take on an htmlwidget package project,
  285. _don't skimp on this step_.
  286. Having a friendly R API will have huge impact on the use of your widget.
  287. We'll use the `babynames` package for our demo dataset,
  288. pick two names completely at random to compare.
  289. ```{r babynames}
  290. library(dplyr)
  291. library(babynames)
  292. data <-
  293. babynames %>%
  294. filter(
  295. name %in% c("Ruth", "August"),
  296. year >= 1980
  297. ) %>%
  298. group_by(year, name) %>%
  299. summarize(n = sum(n)) %>%
  300. ungroup() %>%
  301. pivot_wider(year, name, values_from = n)
  302. ```
  303. At this point the chart won't work,
  304. but you can use the browser dev console
  305. to find the right steps to reformat the data into the expected format.
  306. We'll make the **strong** assumption that the tibble in R
  307. should always be formatted with the columns
  308. 1. `labels`
  309. 2. first series...
  310. 3. second series...
  311. 4. etc.
  312. * `repl_example('reformat-r2js-data')`
  313. <details><summary>Answer</summary>
  314. ```js
  315. const chartData = {labels: [], datasets: []}
  316. // Get keys of data, assume that first entry is for labels, the rest are data
  317. let labelColumn = Object.keys(x.data)[0]
  318. let columns = Object.keys(x.data).slice(1)
  319. // First column in x.data is the labels
  320. chartData.labels = x.data[labelColumn]
  321. // Create an appropriate object for each column, reformat data and add to chartData
  322. columns.forEach(function(col) {
  323. chartData.datasets.push({name: col, values: x.data[col]})
  324. })
  325. x.data = chartData
  326. ```
  327. </details>
  328. ## This is basically what `htmlwidgets` does, just inside a framework
  329. * [a0614d9699aefc0eda82b3e368b48370be0ae9ba](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/a0614d9699aefc0eda82b3e368b48370be0ae9ba)
  330. We now have all of the pieces of an `htmlwidget`,
  331. it's just a bit less coordinated.
  332. 1. `htmlwidgets` gives us a slightly nicer way of specifying dependencies
  333. in `inst/htmlwidgets/frappeChart.yaml`. We'll have to update that file.
  334. 2. When we started we added a `div(id = "chart")`.
  335. It would be annoying to have to make sure that each `id` is always unique.
  336. `htmlwidgets` will add this `div` for us and give each one a unique id.
  337. We won't have to write any code for this, it just happens.
  338. 3. We'll write an R function that will take input data and options and format
  339. it into a list, like the `opts` we've been using.
  340. Then we hand the data to `htmlwidgets` and it embeds it in a `<script>` tag
  341. for us.
  342. It will also find that data automatically and make it available on the JS side.
  343. 4. Finally, we wrote some code in JavaScript to initialize the chart.
  344. In the same way, we'll write some code in `inst/htmlwidgets/frappeChart.js`
  345. which is where we'll reformat the data and options passed from the R world
  346. by htmlwidgets. We also need to instantiate the chart object. For advanced
  347. usage, this is also where we'll put code that would let us update the
  348. widget in place without having to re-render the whole chart.
  349. To create the htmlwidget,
  350. we're going to work through each of these pieces
  351. and put them in the right places.
  352. # Make it an htmlwidget
  353. ## Declare dependencies
  354. * [969fd962edf0be8f98ffff1823f8e08960ffb31a](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/969fd962edf0be8f98ffff1823f8e08960ffb31a)
  355. **FILE:** `inst/htmlwidgets/frappeChart.yaml`
  356. Take the `htmltools::htmlDependency()` and
  357. turn it into `inst/htmlwidgets/frappeChart.yaml`.
  358. ```{r}
  359. rstudioapi::navigateToFile("inst/htmlwidgets/frappeChart.yaml")
  360. ```
  361. Note: keep `htmlwidgets` in `src`!
  362. ## Write the R function
  363. * [cbc25a8f7bbf7e522f369f5fec7c2517ba768656](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/cbc25a8f7bbf7e522f369f5fec7c2517ba768656)
  364. **FILE:** `R/frappeChart.r`
  365. Add appropriate arguments to `frappeChart()`.
  366. * [title](https://frappe.io/charts/docs/reference/configuration#title)
  367. * [type](https://frappe.io/charts/docs/reference/configuration#type)
  368. * [colors](https://frappe.io/charts/docs/reference/configuration#colors)?
  369. * [is_navigable](https://frappe.io/charts/docs/reference/configuration#isnavigable)
  370. Structure the argumets into `x` and pass `...` for the "extra bits".
  371. Rebuild the package,
  372. then create a new R markdown document:
  373. `js4shiny::js4shiny_doc()`.
  374. Move the code loading `dplyr`, `tidyr`, `babynames`
  375. and formatting the data.
  376. Then call `frappeCharts::frappeChart()`.
  377. Render and open dev tools in the browser to see that it "works".
  378. Meaning that the data and dependencies are included,
  379. but the chart won't.
  380. Point out the random ID.
  381. Then go back and change it so we can find the element better.
  382. ## Write JavaScript binding
  383. * [6f141c4341a2c4f8615df81887e7927d2e765f11](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/6f141c4341a2c4f8615df81887e7927d2e765f11)
  384. **FILE:** `inst/htmlwidgets/frappeChart.js`
  385. The final step is to move the Javascript we wrote before into the js binding.
  386. * Just put in `console.log(x)`, rebuild, rerender
  387. * Verify that this `x` looks the same as our `opts` from before
  388. * Copy all of the JS we wrote to reconfigure the data into the widget
  389. * Use `el` instead of `#chart`
  390. * Rebuild, rerender
  391. * it works!
  392. * Try adding other options
  393. ### Writing JavaScript in R
  394. * [8d442e3c842154adbae87dab5e9289cbb1333187](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/8d442e3c842154adbae87dab5e9289cbb1333187)
  395. The [tooltips](https://frappe.io/charts/docs/basic/annotations#tooltips)
  396. can be formatted using the `tooltipOptions` property:
  397. ```
  398. tooltipOptions: {
  399. formatTooltipX: d => (d + '').toUpperCase(),
  400. formatTooltipY: d => d + ' pts',
  401. }
  402. ```
  403. To write this in R (add to `widget_demo.R`)
  404. ```r
  405. tooltipOptions = list(
  406. formatTooltipX = htmlwidgets::JS("d => 'Year: ' + d"),
  407. formatTooltipY = htmlwidgets::JS("d => d + ' babies'")
  408. )
  409. ```
  410. ## Shiny comes for free!
  411. * [739d5945010d5e46ab3f9847fd412beb0766805d](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/739d5945010d5e46ab3f9847fd412beb0766805d)
  412. Create a basic Shiny app with
  413. 1. Slider input to pick number of values (1:26 letters)
  414. 1. A new data button that generates new data of same dimension
  415. 1. The data are reactive, `x = letters[1:n]`, `y = runif(n)`
  416. 1. Use `frappeCharts::frappeChartOutput()` linked to `frappeCharts::renderFrappeChart()`
  417. - bar plot
  418. - fix `tooltipOptions` to turn the `runif()` into a percent.
  419. `dev/shiny/app.R`
  420. Make a mistake in the spelling for `formatTooltipY`
  421. and demo how hard it is for the end user to track down what's wrong.
  422. This points to how important it is to do the validation on the R side
  423. or to do the extra work to make the R API friendly.
  424. It's also a good place to demo debug strategies for Shiny and regular widgets.
  425. Open the app in an external window,
  426. show the dev console,
  427. find the frappeCharts binding
  428. and add a breakpoint.
  429. Then reload and show how you an use the dev console there to figure things out.
  430. ## Better data updates
  431. Frappe Charts,
  432. like many JS libraries,
  433. includes a method for updating the widget
  434. without having to redraw the whole chart/plot/viz/etc.
  435. In Frappe Charts, the
  436. [full data update](https://frappe.io/charts/docs/update_state/modify_data#updating-full-data)
  437. method is
  438. ```js
  439. chart.update(data)
  440. ```
  441. where `data` is the `data` part of the initial options object.
  442. To make this work we will:
  443. 1. refactor the JS-side data processing code ([b19e33af8fdca579a8578bcd7a39c6d1e43fb32c](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/b19e33af8fdca579a8578bcd7a39c6d1e43fb32c))
  444. 1. make the created `chart` object available outside `renderValue()` ([d114592668ca63f06f593f4f247432eec218894b](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/d114592668ca63f06f593f4f247432eec218894b))
  445. 1. bind the factory function context to `el` as `widget` ([f0a3bf9fd5e60cda9b2b7ace004f360c36bf6610](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/f0a3bf9fd5e60cda9b2b7ace004f360c36bf6610))
  446. 1. Demo this by opening a rendered widget and showing `widget` as attached to the div
  447. 1. expose `chart` with a `chart()` method ([f0a3bf9fd5e60cda9b2b7ace004f360c36bf6610](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/f0a3bf9fd5e60cda9b2b7ace004f360c36bf6610))
  448. 1. Demo by finding widget div and running
  449. ```
  450. let c = $0.widget.chart()
  451. c.addDataPoint(2017, [2500, 1500])
  452. ```
  453. 1. Now, if nothing else, the `chart` object is accessible
  454. so others can use or extend it.
  455. 1. Create an update method that takes new data and updates an existing chart. ([5da4b68b5f60d8e6ee17cc8c4a009121539a2653](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/5da4b68b5f60d8e6ee17cc8c4a009121539a2653))
  456. Demo with `app.R`
  457. ```js
  458. let el = document.getElementById('chart')
  459. el.widget.update({x: ['A', 'B', 'C', 'D'], Frequency: [1, 2, 3, 4]})
  460. ```
  461. Try with various values. You can increase the number of data points
  462. but you can't add or change the series.
  463. 1. Add a custom message handler that dependes on `HTMLWidgets.shinyMode`. ([5da4b68b5f60d8e6ee17cc8c4a009121539a2653](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/5da4b68b5f60d8e6ee17cc8c4a009121539a2653))
  464. ```js
  465. // after factory function
  466. if (HTMLWidgets.shinyMode) {
  467. Shiny.addCustomMessageHandler('frappeCharts:update', function({id, data}) {
  468. let el = document.getElementById(id)
  469. el.widget.update(data)
  470. })
  471. }
  472. ```
  473. Restructure the app code so that the chart initializes with flat data (0.5).
  474. Use `session$sendCustomMessage` to trigger the update.
  475. Note that the JS function above takes `id` and `data` using destructuring.
  476. It's easy to write `function(id, data)` but this won't work because
  477. the handler can only take one argument.
  478. Demo the app, now updates are fast!
  479. 1. Write a user-friendly wrapper around `sendCustomMessage` called `updateFrappeChart()` ([4706d89183aaa9a3721599ef13c6f7af4955808b](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/4706d89183aaa9a3721599ef13c6f7af4955808b))
  480. 1. Now add an event listener to send chart navigation back to Shiny ([0b4f7ea16f378ec5a53d81260c8f9056fabbcaba](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/0b4f7ea16f378ec5a53d81260c8f9056fabbcaba))
  481. Attach the event listener during `renderValue()` and watch for the `data-select` event.
  482. Use the `el.id` to create a new id, like `el.id + '_selected'`.
  483. Send back `index` and `values` from the event.
  484. Add `verbatimTextOutput('selected')` to show `input$chart_selected`.
  485. 1. You would probably want to do some work for the user and return more meaningful values.
  486. We won't cover this in the workshop, but I've demonstrated a potential method.
  487. ([e7fe0e1d87977823e6a040434b33e6d5cdf8eac1](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/e7fe0e1d87977823e6a040434b33e6d5cdf8eac1))
  488. This function basically reverses the chart processing and
  489. and returns a list that should be a dataframe.
  490. ```js
  491. if (HTMLWidgets.shinyMode && x.isNavigable) {
  492. el.addEventListener('data-select', function(ev) {
  493. let {index, values} = ev
  494. let chart = el.widget.chart()
  495. let label = chart.data.labels[index]
  496. let names = chart.data.datasets.map(d => d.name)
  497. let data = values.reduce(function(acc, v, idx) {
  498. acc[names[idx]] = v
  499. return acc
  500. }, {})
  501. data[labelsName] = label
  502. Shiny.setInputValue(el.id + '_selected', data)
  503. })
  504. }
  505. ```
  506. 1. But now in Shiny it needs to go from a list to a data.frame.
  507. To do this we use `shiny::registerInputHandler()` in R and
  508. give the input event a type: `inputId_selected:frappeCharts-selected`.
  509. ([000de60582f277e29983f6c5803de112ca1ade99](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/000de60582f277e29983f6c5803de112ca1ade99))
  510. ```r
  511. .onLoad <- function(libname, pkgname) {
  512. shiny::registerInputHandler(
  513. type = "frappeCharts-selected",
  514. fun = function(value, session, inputName) {
  515. as.data.frame(value, stringsAsFactors = FALSE)
  516. }
  517. )
  518. }
  519. ```