Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

458 rindas
11KB

  1. ```{r setup, include=FALSE}
  2. knitr::opts_chunk$set(eval = FALSE)
  3. ```
  4. ## Setup R Package
  5. Create a package for this HTML widget.
  6. We're not going to publish this, so you can call it whatever you want
  7. ```{r create-package}
  8. usethis::create_package("frappeCharts")
  9. ```
  10. Add a dev script for notes
  11. ```{r dev}
  12. dir.create("dev")
  13. file.create("dev/dev.R")
  14. rstudioapi::navigateToFile("dev/dev.R")
  15. ```
  16. ## Setup npm package
  17. Same process again, but this time for npm.
  18. ```bash
  19. npm init
  20. # or
  21. npm init -y
  22. ```
  23. Open `package.json` and take a look
  24. ```json
  25. {
  26. "name": "frappecharts",
  27. "version": "0.0.1",
  28. "description": "",
  29. "main": "index.js",
  30. "scripts": {
  31. "test": "echo \"Error: no test specified\" && exit 1"
  32. },
  33. "author": "",
  34. "license": "MIT"
  35. }
  36. ```
  37. From Frappe Charts [docs#installation](https://frappe.io/charts/docs#installation):
  38. ```bash
  39. npm install frappe-charts
  40. ```
  41. We now have a dependency in `package.json` and there's a `package-lock.json` file.
  42. ```json
  43. "dependencies": {
  44. "frappe-charts": "^1.3.0"
  45. }
  46. ```
  47. ## Ignore node_modules but add package-lock
  48. There's also a `node_modules/` folder with `frappe-charts/` inside.
  49. Add `node_modules` to `.Rbuildignore` and `.gitignore`.
  50. (BTW, you can and are supposed to commit `package-lock.json`.)
  51. ```{r ignore-node-module}
  52. usethis::use_build_ignore("node_modules")
  53. usethis::use_build_ignore("package.json")
  54. usethis::use_build_ignore("package-lock.json")
  55. usethis::use_git_ignore("node_modules")
  56. ```
  57. ## Scaffold the HTML widget
  58. ```{r htmlwidgets-scaffold}
  59. htmlwidgets::scaffoldWidget("frappeChart")
  60. ```
  61. This adds files in `inst/htmlwidgets`
  62. ```
  63. inst
  64. └── htmlwidgets
  65. ├── frappeChart.js #<< R <-> JS code
  66. └── frappeChart.yaml #<< list of dependencies
  67. ```
  68. and creates a file `R/frappeChart.R` with the functions
  69. - `frappeChart()`
  70. - `frappeChartOutput()` (for shiny)
  71. - `renderFrappeChart()` (for shiny)
  72. ## Use `npm` to get our dependencies in the right place
  73. `htmlwidgets` load dependencies in a way that's exactly the same as using a
  74. `<script>` tag in the HTML `<head>`.
  75. Look at the documentation on Frappe Charts and decide which file we should use.
  76. Here's the block from their docs
  77. ```
  78. <script src="https://cdn.jsdelivr.net/npm/frappe-charts@1.2.4/dist/frappe-charts.min.iife.js"></script>
  79. <!-- or -->
  80. <script src="https://unpkg.com/frappe-charts@1.2.4/dist/frappe-charts.min.iife.js"></script>
  81. ```
  82. We need to get our dependecy into a subfolder of `inst/htmlwidgets`.
  83. Convention is `inst/htmlwidgets/lib/<dependency_name>`.
  84. Rather than creating the directoy and copying over, etc.,
  85. we can have an `npm` build script do this for us.
  86. To avoid issues with mac/windows,
  87. we'll add a dev dependency on [`cpy-cli`](https://github.com/sindresorhus/cpy-cli)
  88. ```bash
  89. npm install cpy-cli --save-dev
  90. ```
  91. and
  92. ```{r create-lib-dir}
  93. dir.create("inst/htmlwidets/lib/frappe-charts", recursive = TRUE)
  94. ```
  95. and then edit `package.json` to add copy tasks
  96. ```
  97. "scripts": {
  98. "copy-js": "cpy 'node_modules/frappe-charts/dist/frappe-charts.min.iife*' inst/htmlwidgets/lib/frappe-charts/",
  99. "build": "npm run copy-js"
  100. }
  101. ```
  102. ## Create a demo html_document_plain()
  103. ```{r create-demo-html}
  104. dir.create("dev/demo")
  105. js4shiny::js4shiny_rmd(path = "dev/demo/demo.Rmd")
  106. ```
  107. Use the example in the [Frappe Charts Docs](https://frappe.io/charts/docs).
  108. ```{r}
  109. tagList(
  110. div(id = "chart"),
  111. htmltools::htmlDependency(
  112. name = "frappe-charts",
  113. version = "1.3.0",
  114. package = "frappeCharts",
  115. src = "htmlwidgets/lib/frappe-charts",
  116. script = "frappe-charts.min.iife.js",
  117. all_files = TRUE
  118. )
  119. )
  120. ```
  121. And copy the JS into a javascript chunk.
  122. `r emo::ji("warning")` The dependencies won't be found until you build/install.
  123. ```{r build-install}
  124. devtools::document()
  125. devtools::install()
  126. ```
  127. If you get a path not found error
  128. ```
  129. Error: path for html_dependency not found: inst/htmlwidgets/lib/frappe-charts
  130. ```
  131. it's most likely because
  132. ```
  133. src = "inst/htmlwidgets/lib/frappe-charts"
  134. ```
  135. ## Replace the example data with another data set and example
  136. The first demo mixes chart types and we don't want to do that.
  137. Use the example from
  138. [Basic Chart](https://frappe.io/charts/docs/basic/basic_chart#adding-more-datasets).
  139. ```js
  140. const data = {
  141. labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
  142. datasets: [
  143. { name: "R", values: [18, 40, 30, 35, 8, 52, 17, -4] },
  144. { name: "Python", values: [30, 50, -10, 15, 18, 32, 27, 14] }
  145. ]
  146. }
  147. ```
  148. Then re-create this data in an R chunk:
  149. ```{r data-in-r}
  150. data <- list(
  151. labels = c("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"),
  152. datasets = list(
  153. list(name = "R", values = c(18, 40, 30, 35, 8, 52, 17, -4)),
  154. list(name = "Python", values = c(30, 50, -10, 15, 18, 32, 27, 14))
  155. )
  156. )
  157. ```
  158. To get the data out of R and make it available in the document,
  159. `htmlwidgets` embeds the data in a `<script type="application/json">...</script>`
  160. element in the page.
  161. Embed the data from the R chunk in a `<script>` tag with an ID
  162. so that we can find it later.
  163. ```{r embed-r-data-in-script}
  164. tags$script(
  165. id = "data",
  166. type = "application/json",
  167. htmlwidgets:::toJSON(data)
  168. )
  169. ```
  170. Change to `js4shiny::html_document_js()` so that we can see the `console.log()`
  171. from JavaScript just like R code.
  172. And then find the `<script>` tag and get it's `.textContent`.
  173. ```{js find-r-data-script}
  174. let rData = document.getElementById('data')
  175. rData.textContent
  176. ```
  177. Use `JSON.parse()` to turn the data into a JS object
  178. and replace the data used in the chart.
  179. ```{js find-r-data-script}
  180. let rData = document.getElementById('data')
  181. rData = JSON.parse(rData.textContent)
  182. ```
  183. Switch between `data` and `rData` and it should be the same!
  184. Change the values of the data in the R side to be random
  185. so that each re-run gives a new plot.
  186. ~~Delete the `data` in the JS side.~~
  187. Comment out the `data` on the JS side (but we'll want to see the structure later).
  188. ## Augment data to set options for the chart
  189. Embed `data` in another list `opts` that will carry additional options,
  190. such as `title`, `type` and `colors`.
  191. Parse the embedded `<script>` and pass the whole object to `frappe.Chart()`.
  192. Change the colors to
  193. - `#466683` (dark blue)
  194. - `#44bc96` (green)
  195. - `#d33f49` (red)
  196. - `#993d70` (purple)
  197. ## Learn about other options for line charts
  198. Read <https://frappe.io/charts/docs/basic/trends_regions>
  199. and add and test additional line options.
  200. Goal: shaded area chart with lines only.
  201. Make the `labels` one week and repeat 4 times.
  202. Generate `runif(7 * 4)` random numbers.
  203. ```r
  204. rep(c("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"), 4)
  205. ```
  206. Find and implement an option to reduce the number of labels on the x-axis.
  207. ## Turn on dots again and make navigable
  208. ```{r}
  209. opts <- list(
  210. title = "My AwesomeR Chart",
  211. type = "bar",
  212. height = 250,
  213. colors = c("#466683", "#44bc96"),
  214. data = data,
  215. axisOptions = list(xIsSeries = TRUE),
  216. isNavigable = TRUE
  217. )
  218. ```
  219. ## Add a real data source
  220. Using the `babynames` package, pick two names to compare.
  221. ```{r babynames}
  222. library(dplyr)
  223. library(babynames)
  224. data <-
  225. babynames %>%
  226. filter(
  227. name %in% c("Ruth", "August"),
  228. year >= 1980
  229. ) %>%
  230. group_by(year, name) %>%
  231. summarize(n = sum(n)) %>%
  232. ungroup() %>%
  233. pivot_wider(year, name, values_from = n)
  234. ```
  235. At this point the chart won't work,
  236. but you can use the browser dev console
  237. to find the right steps to reformat the data into the expected format.
  238. We'll make the **strong** assumption that the tibble in R
  239. should always be formatted with the columns
  240. 1. `labels`
  241. 2. first data set...
  242. 3. second data set...
  243. 4. etc.
  244. `repl_example("reformat-r2js-data")`
  245. <details><summary>Answer</summary>
  246. ```js
  247. const chartData = {labels: [], datasets: []}
  248. // Get keys of data, assume that first entry is for labels, the rest are data
  249. let labelColumn = Object.keys(x.data)[0]
  250. let columns = Object.keys(x.data).slice(1)
  251. // First column in x.data is the labels
  252. chartData.labels = x.data[labelColumn]
  253. // Create an appropriate object for each column, reformat data and add to chartData
  254. columns.forEach(function(col) {
  255. chartData.datasets.push({name: col, values: x.data[col]})
  256. })
  257. x.data = chartData
  258. ```
  259. </details>
  260. ## This is basically what `htmlwidgets` does, just inside a framework
  261. We now have all of the pieces of an `htmlwidget`,
  262. it's just a bit less coordinated.
  263. 1. `htmlwidgets` gives us a slightly nicer way of specifying dependencies
  264. in `inst/htmlwidgets/frappeChart.yaml`. We'll have to update that file.
  265. 2. When we started we added a `div(id = "chart")`.
  266. It would be annoying to have to make sure that each `id` is always unique.
  267. `htmlwidgets` will add this `div` for us and give each one a unique id.
  268. We won't have to write any code for this, it just happens.
  269. 3. We'll write an R function that will take input data and options and format
  270. it into a list, like the `opts` we've been using.
  271. Then we hand the data to `htmlwidgets` and it embeds it in a `<script>` tag
  272. for us.
  273. It will also find that data automatically and make it available on the JS side.
  274. 4. Finally, we wrote some code in JavaScript to initialize the chart.
  275. In the same way, we'll write some code in `inst/htmlwidgets/frappeChart.js`
  276. which is where we'll reformat the data and options passed from the R world
  277. by htmlwidgets. We also need to instantiate the chart object. For advanced
  278. usage, this is also where we'll put code that would let us update the
  279. widget in place without having to re-render the whole chart.
  280. To create the htmlwidget,
  281. we're going to work through each of these pieces
  282. and put them in the right places.
  283. # Make it an htmlwidget
  284. ## Declare dependencies
  285. **FILE:** `inst/htmlwidgets/frappeChart.yaml`
  286. Take the `htmltools::htmlDependency()` and
  287. turn it into `inst/htmlwidgets/frappeChart.yaml`.
  288. ```{r}
  289. rstudioapi::navigateToFile("inst/htmlwidgets/frappeChart.yaml")
  290. ```
  291. Note: keep `htmlwidgets` in `src`!
  292. ## Write the R function
  293. **FILE:** `R/frappeChart.r`
  294. Add appropriate arguments to `frappeChart()`.
  295. * [title](https://frappe.io/charts/docs/reference/configuration#title)
  296. * [type](https://frappe.io/charts/docs/reference/configuration#type)
  297. * [colors](https://frappe.io/charts/docs/reference/configuration#colors)?
  298. * [is_navigable](https://frappe.io/charts/docs/reference/configuration#isnavigable)
  299. Structure the argumets into `x` and pass `...` for the "extra bits".
  300. Rebuild the package,
  301. then create a new R markdown document:
  302. `js4shiny::js4shiny_doc()`.
  303. Move the code loading `dplyr`, `tidyr`, `babynames`
  304. and formatting the data.
  305. Then call `frappeCharts::frappeChart()`.
  306. Render and open dev tools in the browser to see that it "works".
  307. Meaning that the data and dependencies are included,
  308. but the chart won't.
  309. Point out the random ID.
  310. Then go back and change it so we can find the element better.
  311. ## Write JavaScript binding
  312. **FILE:** `inst/htmlwidgets/frappeChart.js`
  313. The final step is to move the Javascript we wrote before into the js binding.
  314. * Just put in `console.log(x)`, rebuild, rerender
  315. * Verify that this `x` looks the same as our `opts` from before
  316. * Copy all of the JS we wrote to reconfigure the data into the widget
  317. * Use `el` instead of `#chart`
  318. * Rebuild, rerender
  319. * it works!
  320. * Try adding other options
  321. ### Writing JavaScript in R
  322. The [tooltips](https://frappe.io/charts/docs/basic/annotations#tooltips)
  323. can be formatted using the `tooltipOptions` property:
  324. ```
  325. tooltipOptions: {
  326. formatTooltipX: d => (d + '').toUpperCase(),
  327. formatTooltipY: d => d + ' pts',
  328. }
  329. ```
  330. To write this in R (add to `widget_demo.R`)
  331. ```r
  332. tooltipOptions = list(
  333. formatTooltipX = htmlwidgets::JS("d => 'Year: ' + d"),
  334. formatTooltipY = htmlwidgets::JS("d => d + ' babies'")
  335. )
  336. ```