diff --git a/R/hiveNetwork.R b/R/hiveNetwork.R new file mode 100644 index 00000000..b758920f --- /dev/null +++ b/R/hiveNetwork.R @@ -0,0 +1,91 @@ +#' Create a D3 JavaScript hive plot representation of a network. +#' +#' @TODO +#' 1. Think about bindings from data.frame to: axis and group +#' 2. Limit to ? axis? +#' +#' @export +hiveNetwork <- function(nodes, + links, + source = NULL, + target = NULL, + linksize = NULL, + linkcolour = NULL, + nodeID = NULL, + x = NULL, + y = NULL, + nodesize = NULL, + nodecolour = NULL, + height = NULL, + width = NULL) +{ + + # some checks ---- + if (!is.data.frame(links)) + stop("Links must be a data frame class object.") + + if (!is.data.frame(nodes)) + stop("Nodes must be a data frame class object.") + + # helper - rescale attributes to play nice with axis + .rescale <- function(x) (x - min(x))/(max(x) - min(x)) + + # axis binding + if (is.null(nodes$x)) { + indeces <- 1:nrow(nodes) - 1 + indeces <- cbind( indeces %in% links$source & !indeces %in% links$target, + indeces %in% links$source & indeces %in% links$target, + !indeces %in% links$source & indeces %in% links$target) + nodes$x <- apply(indeces, 1, which) + } + + # radius binding + nodes$y <- .rescale(nodes$y) + + # node size binding + if (is.null(nodes$nodesize)) { + nodes$nodesize <- 5 + } + + # node size binding + if (is.null(nodes$nodecolour)) { + nodes$nodecolour <- nodes$x + } + + # link size binding + if (is.null(links$linksize)) { + links$linksize <- 0.5 + } + + # link colour binding + if (is.null(links$linkcolour)) { + links$linkcolour <- links$source + } + + # create options ---- + options = list() + + # create widget + htmlwidgets::createWidget( + name = "hiveNetwork", + x = list(links = links, nodes = nodes, options = options), + width = width, + height = height, + htmlwidgets::sizingPolicy(padding = 10, browser.fill = TRUE), + package = "networkD3" + ) +} + +#' @rdname networkD3-shiny +#' @export +hiveNetworkOutput <- function(outputId, width = "100%", height = "500px") { + shinyWidgetOutput(outputId, "hiveNetwork", width, height, + package = "networkD3") +} + +#' @rdname networkD3-shiny +#' @export +renderHiveNetwork <- function(expr, env = parent.frame(), quoted = FALSE) { + if (!quoted) { expr <- substitute(expr) } # force quoted + shinyRenderWidget(expr, forceNetworkOutput, env, quoted = TRUE) +} diff --git a/inst/htmlwidgets/hiveNetwork.js b/inst/htmlwidgets/hiveNetwork.js new file mode 100644 index 00000000..ec57a265 --- /dev/null +++ b/inst/htmlwidgets/hiveNetwork.js @@ -0,0 +1,130 @@ +HTMLWidgets.widget({ + + name: "hiveNetwork", + + type: "output", + + initialize: function(el, width, height) { + + var svg = d3.select(el).append("svg") + .attr("viewBox", "0 0 " + width + " " + height) + .attr("preserveAspectRatio", "xMidYMid meet") + .append("g") + .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); + + return svg; + }, + + resize: function(el, width, height, instance) { + // handle with viewBox + }, + + renderValue: function(el, x, svg) { + + // Convert radians to degrees + function degrees(radians) { + return radians / Math.PI * 180 - 90; + } + + // get the width and height + var width = el.offsetWidth; + var height = el.offsetHeight; + var innerRadius = 40; + var outerRadius = Math.min(width, height)/2 - 20; + + var angle = d3.scale.ordinal().domain(d3.range(4)).rangePoints([0, 2 * Math.PI]), + radius = d3.scale.linear().range([innerRadius, outerRadius]), + color = d3.scale.category10().domain(d3.range(20)); + + // alias options + // var options = x.options; + + // convert links and nodes data frames to d3 friendly format + var nodes = HTMLWidgets.dataframeToD3(x.nodes); + var tmp = HTMLWidgets.dataframeToD3(x.links); + + // create links associative array from nodes + var links = Array(tmp.length); + for (var i = 0; i < tmp.length; i++) { + links[i] = { "source" : nodes[tmp[i].source], + "target" : nodes[tmp[i].target], + "Linksize" : tmp[i].Linksize, + "Linkcolour" : tmp[i].Linkcolour + }; + } + + // map elements + svg.selectAll(".axis") + .data(d3.range(3)) + .enter().append("line") + .attr("class", "axis") + .attr("transform", function(d) { return "rotate(" + degrees(angle(d)) + ")"; }) + .attr("x1", radius.range()[0]) + .attr("x2", radius.range()[1]) + .style("stroke", '#000') + .style('stroke-width', '2px'); + + svg.selectAll(".link") + .data(links) + .enter().append("path") + .attr("class", "link") + .attr("d", d3.hive.link() + .angle(function(d) { return angle(d.x); }) + .radius(function(d) { return radius(d.y); })) + .style("stroke", function(d) { return color(d.source.x); }) + // .style("stroke", "#000") + .style('stroke-width', '0.5px') + .style("fill", "none"); + + svg.selectAll(".node") + .data(nodes) + .enter().append("circle") + .attr("class", "node") + .attr("transform", function(d) { return "rotate(" + degrees(angle(d.x)) + ")"; }) + .attr("cx", function(d) { return radius(d.y); }) + .style("fill", function(d) { return color(d.x); }) + .attr("r", function(d) { return d.nodesize; }) + .style("stroke", '#000') + .on("mouseenter", function(d) { + d3.select(this) + .transition() + .duration(50) + .style("stroke-width", 3) + .attr("r", 15); + + d3.selectAll(".link") + .data(links) + .style("stroke-width", function (dl) { + if(dl.source == d){ + return 5; + } else if(dl.target == d){ + return 5; + } else { + return 0.5; + } + }); + /* + .style("stroke", function (dl) { + if(dl.source == d){ + return color(d.x); + } else if(dl.target == d){ + return color(d.x); + } else { + return "#000"; + } + }); + */ + }) + .on("mouseleave", function(d){ + d3.select(this) + .transition() + .duration(50) + .style("stroke-width", 1.5) + .attr("r", function(d) { return d.nodesize; }); + + d3.selectAll(".link") + .style("stroke-width", 0.5); + // .style("stroke", "#000") + }); + }, +}); diff --git a/inst/htmlwidgets/hiveNetwork.yaml b/inst/htmlwidgets/hiveNetwork.yaml new file mode 100644 index 00000000..5a9b9163 --- /dev/null +++ b/inst/htmlwidgets/hiveNetwork.yaml @@ -0,0 +1,9 @@ +dependencies: + - name: d3 + version: 3.5.2 + src: "htmlwidgets/lib/d3-3.5.2" + script: d3.min.js + - name: hive + version: 1.0 + src: "htmlwidgets/lib" + script: d3.hive.min.js diff --git a/inst/htmlwidgets/lib/d3.hive.min.js b/inst/htmlwidgets/lib/d3.hive.min.js new file mode 100644 index 00000000..84de45a6 --- /dev/null +++ b/inst/htmlwidgets/lib/d3.hive.min.js @@ -0,0 +1 @@ +d3.hive={},d3.hive.link=function(){function t(t,s){var u,h=a(r,this,t,s),i=a(n,this,t,s);h.a>i.a&&(u=i,i=h,h=u),i.a-h.a>Math.PI&&(h.a+=2*Math.PI);var e=h.a+(i.a-h.a)/3,c=i.a-(i.a-h.a)/3;return h.r0-h.r1||i.r0-i.r1?"M"+Math.cos(h.a)*h.r0+","+Math.sin(h.a)*h.r0+"L"+Math.cos(h.a)*h.r1+","+Math.sin(h.a)*h.r1+"C"+Math.cos(e)*h.r1+","+Math.sin(e)*h.r1+" "+Math.cos(c)*i.r1+","+Math.sin(c)*i.r1+" "+Math.cos(i.a)*i.r1+","+Math.sin(i.a)*i.r1+"L"+Math.cos(i.a)*i.r0+","+Math.sin(i.a)*i.r0+"C"+Math.cos(c)*i.r0+","+Math.sin(c)*i.r0+" "+Math.cos(e)*h.r0+","+Math.sin(e)*h.r0+" "+Math.cos(h.a)*h.r0+","+Math.sin(h.a)*h.r0:"M"+Math.cos(h.a)*h.r0+","+Math.sin(h.a)*h.r0+"C"+Math.cos(e)*h.r1+","+Math.sin(e)*h.r1+" "+Math.cos(c)*i.r1+","+Math.sin(c)*i.r1+" "+Math.cos(i.a)*i.r1+","+Math.sin(i.a)*i.r1}function a(t,a,r,n){var e=t.call(a,r,n),c=+("function"==typeof s?s.call(a,e,n):s)+i,o=+("function"==typeof u?u.call(a,e,n):u),M=u===h?o:+("function"==typeof h?h.call(a,e,n):h);return{r0:o,r1:M,a:c}}var r=function(t){return t.source},n=function(t){return t.target},s=function(t){return t.angle},u=function(t){return t.radius},h=u,i=-Math.PI/2;return t.source=function(a){return arguments.length?(r=a,t):r},t.target=function(a){return arguments.length?(n=a,t):n},t.angle=function(a){return arguments.length?(s=a,t):s},t.radius=function(a){return arguments.length?(u=h=a,t):u},t.startRadius=function(a){return arguments.length?(u=a,t):u},t.endRadius=function(a){return arguments.length?(h=a,t):h},t}; \ No newline at end of file