christophergandrud / networkD3

D3 JavaScript Network Graphs from R
http://christophergandrud.github.io/networkD3
652 stars 268 forks source link

allow for custom CSS and/or JavaScript #197

Open cjyetman opened 7 years ago

cjyetman commented 7 years ago

Many of the questions or requests I see about networkD3 here and on stackoverflow etc. involve some desire to stylize the output beyond the existing parameters available. The more inventive suggestions/answers tend to suggest injecting JavaScript through the linkDistance or clickAction argument, which often works, but is definitely not how it was intended to work, not ideal, and probably liable to muck something up eventually. The alternative, adding more and more parameters to the functions for every imaginable type of stylization, is not ideal either.

I propose facilitating a way for the user to pass custom CSS and/or JavaScript when calling the networkD3 functions. This would allow near limitless stylization without inflating the argument list with a bunch of new parameters.

There is a way to do this, suggested by @timelyportfolio on stackoverflow, with the htmltools package. That would require moving htmltools from Suggests to Imports, and it may impact how it shows up in RMarkdown, Shiny, etc.. Otherwise, I'd be happy to hear any suggestions on how to inject custom CSS into a htmlwidget using the htmlwidgets package... I think it might be possible by writing a temp CSS file and adding it as a dependency, but it would probably be much more preferable to be able to simply pass it as a character vector.

For example, the forceNetwork function could accept an argument css and the end of the function could be modified to something like...

widget <- htmlwidgets::createWidget(
        name = "forceNetwork",
        x = list(links = LinksDF, nodes = NodesDF, options = options),
        width = width,
        height = height,
        htmlwidgets::sizingPolicy(padding = 10, browser.fill = TRUE),
        package = "networkD3"
)

htmltools::browsable(
  htmltools::tagList(
    htmltools::tags$head(
      htmltools::tags$style(css)
    ),
    widget
  )
)

then a user would be able to do something like this...

extracss <- "
  body{background-color: #DAE3F9 !important}
  .nodetext{fill: #000000}
  .legend text{fill: #FF0000}
"

forceNetwork(Links = MisLinks, Nodes = MisNodes, Source = "source",
             Target = "target", Value = "value", NodeID = "name",
             Group = "group", css = extracss)

If this is implemented, it would make sense to also...

  1. make sure that all the elements created by the JavaScript are logically classed and or id'ed
  2. document that somewhere in the help files so that users can easily figure out how to target certain elements with CSS

JavaScript injection would be a bit more complicated, but could allow, theoretically, the user to add more interaction than just the clickAction, among other things.

cjyetman commented 7 years ago

this would also resolve these issues: #106, #100, and possibly others

timelyportfolio commented 7 years ago

@cjyetman I very much like this approach. My main concern would be many networkD3 or other widgets on a page. Here is one solution for custom styling with specificity http://www.buildingwidgets.com/blog/2016/9/7/custom-styling-for-htmlwidgets. onRender is not always called again on resize. I cannot remember if this is the case with networkD3. This solution is also robust to that issue. Let me know if I can help.

cjyetman commented 7 years ago

thanks @timelyportfolio I knew you'd have something intelligent to say about it

I was hoping to keep the CSS argument to something as simple as a single string, but then we would need something robust/intelligent enough for parsing the CSS and adding the elementId to each rule (no nested CSS rules, boo!). By the way, an ID that starts with a digit, like that created by htmlwidgets:::createWidgetId(), is not valid CSS and a query for it will fail on some browsers, so I prepended 'hw'. Maybe we could make the css option a character vector with one rule per element (and a rule with no selector will select the whole widget, or technically the element that contains it)? So the user could pass ...

css <- c("{ background-color: #DAE3F9 !important }", 
         ".nodetext { fill: #000000 }", 
         ".legend text { fill: #FF0000 }")

and the function would do something like this...

# create widget
hw <- htmlwidgets::createWidget(
  name = "forceNetwork",
  x = list(links = LinksDF, nodes = NodesDF, options = options),
  width = width,
  height = height,
  htmlwidgets::sizingPolicy(padding = 10, browser.fill = TRUE),
  package = "networkD3"
)

if (!is.null(css)) {
  hw$elementId <- paste0('hw', htmlwidgets:::createWidgetId())
  specificcss <- paste(paste0('#', hw$elementId, ' ', css), collapse = '\n')

  hw <- htmlwidgets::prependContent(
    hw,
    htmltools::tags$style(specificcss)
  )
}

hw
cjyetman commented 7 years ago

One major disadvantage to this that I realized is that CSS does not work on elements rendered to canvas, so a reliance on styling/formatting through CSS may not work in the future.