I'm trying to create a histogram using D3 which has nice animated transitions for both the bars and the axes. It's straightforward to get the bars working, but I'm struggling to see how to do the same thing with the axes. In the example below the transition looks like it is happening, but it's actually adding a new axis each time without removing the old one.
My ultimate goal is to do the development of widgets like this using R2D3 and then hand over the javascript to someone else to implement in an app in Java, so I need to make sure it is transferable and doesn't use R/shiny/R2D3 specific things in the javascript file.
This is the hist.js script
// !preview r2d3 data=data.frame(density = c(10,20,5), from = c(0, 1, 3), to = c(1, 3, 4))
//
// r2d3: https://rstudio.github.io/r2d3
//
var margin = {left:40, right:30, top:10, bottom:30, axis_offset:10};
var min_from = d3.min(data, function(d) {return d.from;});
var max_to = d3.max(data, function(d) {return d.to;});
var max_density = d3.max(data, function(d) { return d.density;});
svg.append('g')
.attr('transform', 'translate('+(margin.left - margin.axis_offset)+', 0)')
.attr("class", "y_axis");
svg.append('g')
.attr('transform', 'translate(0, '+(height - margin.bottom + margin.axis_offset)+')')
.attr("class", "x_axis");
var x = d3.scaleLinear()
.domain([min_from, max_to])
.range([margin.left, width-margin.right])
.nice();
var y = d3.scaleLinear()
.domain([0, max_density])
.range([height-margin.bottom, margin.top])
.nice();
svg.selectAll('.y_axis')
.transition()
.duration(500)
.call(d3.axisLeft(y));
svg.selectAll('.x_axis')
.transition()
.duration(500)
.call(d3.axisBottom(x));
var bars = svg.selectAll('rect').data(data);
bars.enter().append('rect')
.attr('x', function(d) { return x(d.from); })
.attr('width', function(d) { return x(d.to) - x(d.from)-1;})
.attr('y', function(d) { return y(d.density); })
.attr('height', function(d) { return y(0) - y(d.density); })
.attr('fill', 'steelblue');
bars.exit().remove();
bars.transition()
.duration(500)
.attr('x', function(d) { return x(d.from); })
.attr('width', function(d) { return x(d.to) - x(d.from)-1;})
.attr('y', function(d) { return y(d.density); })
.attr('height', function(d) { return y(0) - y(d.density); });
and this is my shiny app which runs it
library(shiny)
library(r2d3)
library(data.table)
library(jsonlite)
get_hist <- function(x) {
buckets <- seq(0, mean(x)+3*sd(x), length.out = 21)
h <- hist(x, breaks = c(buckets, Inf), plot = FALSE)
y <- data.table(count = h$counts, from = head(h$breaks, -1), to = head(shift(h$breaks, -1), -1))[-.N]
y[, density := count/(to-from)]
y[]
}
new_data <- function() {
sh <- 1
rgamma(10, sh, 1/sh)
}
ui <- fluidPage(
actionButton("add_data", "Add more data"),
d3Output('d3_hist')
)
server <- function(input, output, session) {
samp <- reactiveVal(new_data())
observeEvent(input$add_data, {
samp(c(samp(), new_data()))
})
output$d3_hist <- renderD3({
y <- get_hist(samp())
r2d3(data = toJSON(y), script = 'hist.js')
})
}
shinyApp(ui, server)
Every time you update the data, you create a new g element for each axis. This creates multiple g elements with the class x_axis / y_axis, all of which you call the axis generators on:
svg.append('g') // append a new g every update for x axis
.attr('transform', 'translate(0, '+(height - margin.bottom + margin.axis_offset)+')')
.attr("class", "x_axis");
svg.selectAll('.x_axis') // call axis generator for every x axis g.
.transition()
.duration(500)
.call(d3.axisBottom(x));
The setup with r2d3 is a bit different in that usually you'd create a single g for each axis and then use an update function to update the data and the axes. Here the entire javascript script runs every time, so we need to avoid appending a g for each axis once we have one:
For example:
if(svg.select(".y_axis").empty()) {
svg.append('g')
.attr('transform', 'translate('+(margin.left - margin.axis_offset)+', 0)')
.attr("class", "y_axis");
}
if(svg.select(".x_axis").empty()) {
svg.append('g')
.attr('transform', 'translate(0, '+(height - margin.bottom + margin.axis_offset)+')')
.attr("class", "x_axis");
}
There are different approaches that could be taken here in the javascript, but this is probably the most straight forward. Ideally r2d3 would more easily allow you run an update function rather than the entire script every update.
Related
I'm trying to understand how an R data frame is passed to a d3.js script using the package r2d3. An extension of the r2d3 bar chart example to access variables in data.frame object would be helpful.
R code:
library(r2d3)
data <- data.frame(nums = c(0.3, 0.6, 0.8, 0.95, 0.40, 0.20))
r2d3(data, script = "test.js")
js code:
var barHeight = Math.floor(height / data.length);
svg.selectAll('rect')
.data(data.nums)
.enter().append('rect')
.attr('width', function(d) { return d * width; })
.attr('height', barHeight)
.attr('y', function(d, i) { return i * barHeight; })
.attr('fill', 'steelblue');
Error:
Error: svg.selectAll(...).data(...).enter is not a function in (test.js#5:4)
TypeError: svg.selectAll(...).data(...).enter is not a function
This error occurs because data.nums is undefined.
Your data variable is not an object containing an array as follows:
var data = {
nums: [0.3,0.6,0.8,0.95,0.40,0.20]
}
But, rather an array containing objects:
var data = [
{nums:0.3},
{nums:0.6},
{nums:0.8},
{nums:0.95},
{nums:0.40},
{nums:0.2}
]
To keep you r side code, we just need to pass the data array itself to selection.data() and access the nums property of each datum:
var barHeight = Math.floor(height / data.length);
svg.selectAll('rect')
.data(data)
.enter().append('rect')
.attr('width', function(d) { return d.nums * width; })
.attr('height', barHeight)
.attr('y', function(d, i) { return i * barHeight; })
.attr('fill', 'steelblue');
I have been struggling like mad to solve an apparently basic question.
Imagine you have a scatter plot, with say ... 10 markers.
I suppose this plot has been generated using plotly within a Shiny environment.
One can easily get the coordinates of these markers using the event_data("plotly_click") code.
Now imagine you do not need the coordinates of these markers, but the coordinates generated by a mouse click but precisely where no marker exists (for example because you would like to set a new marker exactly there, and you would like to re-use the information coming from that mouse click).
I cannot obtain such a behavior using onclick(), or whatever.
Any idea ?
You could add a D3 event listener to your plot
Plotly.d3.select('.plotly').on('click', function(d, i) {})
and then
retrieve the relative x and y values based on the click position (d3.event.layerX resp. layerY)
adjusting for the relative graph position (document.getElementsByClassName('bg')[0].attributes['x'])
and finally calculating the new values based on the axis ranges (myPlot.layout.xaxis.range[0])
The new x and y value are then pushed to the existing graph
Plotly.extendTraces(myPlot, {
x: [[x]],
y: [[y]]
}, [1]);
Complete R code
library("plotly")
library("htmlwidgets")
p <- plot_ly(x = c( -2, 0, 2 ),y = c( -2, 1, 2), type = 'scatter' ,mode = 'lines+markers') %>%
add_trace(x=c(-1,0.4,2),y=c(2, 0, -1),type='scatter',mode='lines+markers') %>%
layout(hovermode='closest')
javascript <- "
var myPlot = document.getElementsByClassName('plotly')[0];
Number.prototype.between = function (min, max) {
return this >= min && this <= max;
};
Plotly.d3.select('.plotly').on('click', function(d, i) {
var e = Plotly.d3.event;
var bg = document.getElementsByClassName('bg')[0];
var x = ((e.layerX - bg.attributes['x'].value + 4) / (bg.attributes['width'].value)) * (myPlot.layout.xaxis.range[1] - myPlot.layout.xaxis.range[0]) + myPlot.layout.xaxis.range[0];
var y =((e.layerY - bg.attributes['y'].value + 4) / (bg.attributes['height'].value)) * (myPlot.layout.yaxis.range[0] - myPlot.layout.yaxis.range[1]) + myPlot.layout.yaxis.range[1]
if (x.between(myPlot.layout.xaxis.range[0], myPlot.layout.xaxis.range[1]) &&
y.between(myPlot.layout.yaxis.range[0], myPlot.layout.yaxis.range[1])) {
Plotly.extendTraces(myPlot, {
x: [[x]],
y: [[y]]
}, [1]);
}
});"
p <- htmlwidgets::prependContent(p, onStaticRenderComplete(javascript), data=list(''))
p
Interactive Javascript example
var traces = [{
x: [1, 2, 3, 4],
y: [10, 15, 13, 17],
mode: 'markers',
type: 'scatter'
}];
traces.push({
x: [2, 3, 4, 5],
y: [16, 5, 11, 9],
mode: 'markers',
type: 'scatter'
});
traces.push({
x: [1, 2, 3, 4],
y: [12, 9, 15, 12],
mode: 'markers',
type: 'scatter'
});
traces.push({
x: [],
y: [],
mode: 'lines+markers',
type: 'scatter'
});
var myPlot = document.getElementById('myPlot')
Plotly.newPlot('myPlot', traces, {hovermode: 'closest'});
Number.prototype.between = function(min, max) {
return this >= min && this <= max;
};
Plotly.d3.select(".plotly").on('click', function(d, i) {
var e = Plotly.d3.event;
var bg = document.getElementsByClassName('bg')[0];
var x = ((e.layerX - bg.attributes['x'].value + 4) / (bg.attributes['width'].value)) * (myPlot.layout.xaxis.range[1] - myPlot.layout.xaxis.range[0]) + myPlot.layout.xaxis.range[0];
var y = ((e.layerY - bg.attributes['y'].value + 4) / (bg.attributes['height'].value)) * (myPlot.layout.yaxis.range[0] - myPlot.layout.yaxis.range[1]) + myPlot.layout.yaxis.range[1]
if (x.between(myPlot.layout.xaxis.range[0], myPlot.layout.xaxis.range[1]) &&
y.between(myPlot.layout.yaxis.range[0], myPlot.layout.yaxis.range[1])) {
Plotly.extendTraces(myPlot, {
x: [
[x]
],
y: [
[y]
]
}, [3]);
}
});
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<div id="myPlot" style="width:100%;height:100%"></div>
Shiny example
library(shiny)
library("plotly")
library("htmlwidgets")
ui <- fluidPage(
plotlyOutput("plot")
)
server <- function(input, output) {
javascript <- "
function(el, x){
Number.prototype.between = function (min, max) {
return this >= min && this <= max;
};
Plotly.d3.select('.plotly').on('click', function(d, i) {
var e = Plotly.d3.event;
var bg = document.getElementsByClassName('bg')[0];
var x = ((e.layerX - bg.attributes['x'].value + 4) / (bg.attributes['width'].value)) * (el.layout.xaxis.range[1] - el.layout.xaxis.range[0]) + el.layout.xaxis.range[0];
var y =((e.layerY - bg.attributes['y'].value + 4) / (bg.attributes['height'].value)) * (el.layout.yaxis.range[0] - el.layout.yaxis.range[1]) + el.layout.yaxis.range[1]
if (x.between(el.layout.xaxis.range[0], el.layout.xaxis.range[1]) && y.between(el.layout.yaxis.range[0], el.layout.yaxis.range[1])) {
Plotly.extendTraces(el, {
x: [[x]],
y: [[y]]
}, [1]);
}
});
}"
output$plot <- renderPlotly({
plot_ly(x = c( -2, 0, 2 ),y = c( -2, 1, 2), type = 'scatter' ,mode = 'lines+markers') %>%
add_trace(x=c(-1,0.4,2),y=c(2, 0, -1),type='scatter',mode='lines+markers') %>%
layout(hovermode='closest') %>% onRender(javascript)
})
}
shinyApp(ui = ui, server = server)
The solution by Maximilian does not work on Plotly.js versions later than 1.42.0. Trying to fetch
var bg = document.getElementsByClassName('bg')[0];
returns undefined. The solution works using version 1.41.3.
This answer is most likely more suited to be a comment but my reputation does not meet the minimum requirement of 50.
I would like to change the shape and size of the clicked point in the below plot. How to achieve it? For this toy plot, I have reduced the number of points from original 100k to 2k. So, the expected solution should be highly scalable and do not deviate from the original plot i.e., all the colors before and after the update of the click point should be the same.
library(shiny)
library(plotly)
df <- data.frame(X=runif(2000,0,2), Y=runif(2000,0,20),
Type=c(rep(c('Type1','Type2'),600),
rep(c('Type3','Type4'),400)),
Val=sample(LETTERS,2000,replace=TRUE))
# table(df$Type, df$Val)
ui <- fluidPage(
title = 'Select experiment',
sidebarLayout(
sidebarPanel(
checkboxGroupInput("SelType", "Select Types to plot:",
choices = unique(df$Type),
selected = NA)
),
mainPanel(
plotlyOutput("plot", width = "400px"),
verbatimTextOutput("click")
)
)
)
server <- function(input, output, session) {
output$plot <- renderPlotly({
if(length(input$SelType) != 0){
df <- subset(df, Type %in% input$SelType)
p <- ggplot(df, aes(X, Y, col = as.factor(Val))) +
geom_point()
}else{
p <- ggplot(df, aes(X, Y, col = as.factor(Val))) +
geom_point()
}
ggplotly(p) %>% layout(height = 800, width = 800)
})
output$click <- renderPrint({
d <- event_data("plotly_click")
if (is.null(d)) "Click events appear here (double-click to clear)"
else cat("Selected point associated with value: ", d$Val)
})
}
shinyApp(ui, server)
A related question has been asked here, but that approach of highlighting the point with a color does not work(when the number of levels of a variable is high, it is difficult to hard code a color which might be already present in the plot).
Plotly's restyle function won't help us here but we can still use the onclick event together with a little bit of JavaScript. The code has acceptable performance for 10,000 points.
We can get the point which was clicked on in JavaScript using:
var point = document.getElementsByClassName('scatterlayer')[0].getElementsByClassName('scatter')[data.points[0].curveNumber].getElementsByClassName('point')[data.points[0].pointNumber];
(scatterlayer is the layer where all the scatterplot elements are located,
scatter[n] is the n-th scatter plot and point[p] is the p-th point in it)
Now we just make this point a lot bigger (or whatever other shape/transformation you want):
point.setAttribute('d', 'M10,0A10,10 0 1,1 0,-10A10,10 0 0,1 10,0Z');
In order to get the possibility to revert everything, we store the unaltered info about the point together with the rest of the Plotly information:
var plotly_div = document.getElementsByClassName('plotly')[0];
plotly_div.backup = {curveNumber: data.points[0].curveNumber,
pointNumber: data.points[0].pointNumber,
d: point.attributes['d'].value
}
and later we can restore the point:
var old_point = document.getElementsByClassName('scatterlayer')[0].getElementsByClassName('scatter')[plotly_div.backup.curveNumber].getElementsByClassName('point')[plotly_div.backup.pointNumber]
old_point.setAttribute('d', plotly_div.backup.d);
Now we can add all the code to the plotly widget.
javascript <- "
function(el, x){
el.on('plotly_click', function(data) {
var point = document.getElementsByClassName('scatterlayer')[0].getElementsByClassName('scatter')[data.points[0].curveNumber].getElementsByClassName('point')[data.points[0].pointNumber];
var plotly_div = document.getElementsByClassName('plotly')[0];
if (plotly_div.backup !== undefined) {
var old_point = document.getElementsByClassName('scatterlayer')[0].getElementsByClassName('scatter')[plotly_div.backup.curveNumber].getElementsByClassName('point')[plotly_div.backup.pointNumber]
if (old_point !== undefined) {
old_point.setAttribute('d', plotly_div.backup.d);
}
}
plotly_div.backup = {curveNumber: data.points[0].curveNumber,
pointNumber: data.points[0].pointNumber,
d: point.attributes['d'].value,
style: point.attributes['style'].value
}
point.setAttribute('d', 'M10,0A10,10 0 1,1 0,-10A10,10 0 0,1 10,0Z');
});
}"
[...]
ggplotly(p) %>% onRender(javascript)
Alternatively you could make a new SVG element based on the location of the clicked point but in the color and shape you would like.
You can try it here without R/Shiny.
//create some random data
var data = [];
for (var i = 0; i < 10; i += 1) {
data.push({x: [],
y: [],
mode: 'markers',
type: 'scatter'});
for (var p = 0; p < 200; p += 1) {
data[i].x.push(Math.random());
data[i].y.push(Math.random());
}
}
//create the plot
var myDiv = document.getElementById('myDiv');
Plotly.newPlot(myDiv, data, layout = { hovermode:'closest'});
//add the same click event as the snippet above
myDiv.on('plotly_click', function(data) {
//let's check if some traces are hidden
var traces = document.getElementsByClassName('legend')[0].getElementsByClassName('traces');
var realCurveNumber = data.points[0].curveNumber;
for (var i = 0; i < data.points[0].curveNumber; i += 1) {
if (traces[i].style['opacity'] < 1) {
realCurveNumber -= 1
}
}
data.points[0].curveNumber = realCurveNumber;
var point = document.getElementsByClassName('scatterlayer')[0].getElementsByClassName('scatter')[data.points[0].curveNumber].getElementsByClassName('point')[data.points[0].pointNumber];
var plotly_div = document.getElementsByClassName('plotly')[0];
if (plotly_div.backup !== undefined) {
var old_point = document.getElementsByClassName('scatterlayer')[0].getElementsByClassName('scatter')[plotly_div.backup.curveNumber].getElementsByClassName('point')[plotly_div.backup.pointNumber]
if (old_point !== undefined) {
old_point.setAttribute('d', plotly_div.backup.d);
}
}
plotly_div.backup = {curveNumber: data.points[0].curveNumber,
pointNumber: data.points[0].pointNumber,
d: point.attributes['d'].value,
style: point.attributes['style'].value
}
point.setAttribute('d', 'M10,0A10,10 0 1,1 0,-10A10,10 0 0,1 10,0Z');
});
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<div id="myDiv">
I am using d3.js , when calling box plot function.
It's working fine with small dataset. But its not working with large
dataset (900 000 data).
I tried with many data set but its only working with small set of
data and its getting hang on large set of data.
I tried to debug the d3.js script for box plot but was not able to
find where the dataset becomes null.
Does the performance of d3.js graphs degrade with increase in dataset? How can I optimize it?
Here's the code :
function box(g) {
g.each(function(data, i) {
//d = d.map(value).sort(d3.ascending);
//var boxIndex = data[0];
//var boxIndex = 1;
var d = data[1].sort(d3.ascending);
var t = tableData.sort(function(a, b){return a-b;});
// console.log(boxIndex);
//console.log(d);
var g = d3.select(this),
n = t.length;
min = 0,
max = d[n-1];
// Compute quartiles. Must return exactly 3 elements.
var quartileData = [];
quartileData[0]=d[1];
quartileData[1]=parseFloat(d[2]);
quartileData[2]=d[3];
var data1 = parseFloat(d[1]);
var data2 = parseFloat(d[3]);
var common = data2-data1;
var value = common.toFixed(5);
// Compute whiskers. Must return exactly 2 elements, or null.
var whiskerData= [];
whiskerData[0] = (data1 - ((value)*1.5)).toFixed(5);
whiskerData[1]= (data2 + ((value)*1.5)).toFixed(5);
// Compute whiskers. Must return exactly 2 elements, or null.
var whiskerIndices = [];
var isRangeFound = false;
whiskerIndices[0] = $.map( t , function( val,i )
{
if(whiskerData[0]< parseFloat(val) && !isRangeFound)
{
isRangeFound = true;
return i;
}
});
var isRangeFound1 = false;
whiskerIndices[1] = $.map( t , function( val,i )
{
if(whiskerData[1] < parseFloat(val) && !isRangeFound1)
{
isRangeFound1 = true;
return i-1;
}
});
// Compute outliers. If no whiskers are specified, all data are "outliers".
// We compute the outliers as indices, so that we can join across transitions!
var outlierIndices = whiskerIndices
? d3.range(0, whiskerIndices[0]).concat(d3.range(parseInt(whiskerIndices[1]) + 1, n))
: d3.range(n);
// Compute the new x-scale.
var x1 = d3.scale.linear()
.domain(domain && domain.call(this,t, i) || [minBound, maxBound])
.range([height, 0]);
// Retrieve the old x-scale, if this is an update.
var x0 = this.__chart__ || d3.scale.linear()
.domain([0, Infinity])
// .domain([0, max])
.range(x1.range());
// Stash the new scale.
this.__chart__ = x1;
// Note: the box, median, and box tick elements are fixed in number,
// so we only have to handle enter and update. In contrast, the outliers
// and other elements are variable, so we need to exit them! Variable
// elements also fade in and out.
// Update center line: the vertical line spanning the whiskers.
var center = g.selectAll("line.center")
.data(whiskerData ? [whiskerData] : []);
//vertical line
center.enter().insert("line", "rect")
.attr("class", "center")
.attr("x1", width / 2)
.attr("y1", function(d) { return x0(d[0]); })
.attr("x2", width / 2)
.attr("y2", function(d) { return x0(d[1]); })
.style("opacity", 1e-6)
.transition()
.duration(duration)
.style("opacity", 1)
.attr("y1", function(d) { return x1(d[0]); })
.attr("y2", function(d) { return x1(d[1]); });
center.transition()
.duration(duration)
.style("opacity", 1)
.attr("y1", function(d) { return x1(d[0]); })
.attr("y2", function(d) { return x1(d[1]); });
center.exit().transition()
.duration(duration)
.style("opacity", 1e-6)
.attr("y1", function(d) { return x1(d[0]); })
.attr("y2", function(d) { return x1(d[1]); })
.remove();
// Update innerquartile box.
var box = g.selectAll("rect.box")
.data([quartileData]);
box.enter().append("rect")
.attr("class", "box")
.attr("x", 0)
.attr("y", function(d) { return x0(d[2]); })
.attr("width", width)
.attr("height", function(d) { return x0(d[0]) - x0(d[2]); })
.transition()
.duration(duration)
.attr("y", function(d) { return x1(d[2]); })
.attr("height", function(d) { return x1(d[0]) - x1(d[2]); });
box.transition()
.duration(duration)
.attr("y", function(d) { return x1(d[2]); })
.attr("height", function(d) { return x1(d[0]) - x1(d[2]); });
// Update median line.
var medianLine = g.selectAll("line.median")
.data((quartileData[1]));
medianLine.enter().append("line")
.attr("class", "median")
.attr("x1", 0)
.attr("y1", x0)
.attr("x2", width)
.attr("y2", x0)
.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1);
medianLine.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1);
// Update whiskers.
var whisker = g.selectAll("line.whisker")
.data(whiskerData || []);
whisker.enter().insert("line", "circle, text")
.attr("class", "whisker")
.attr("x1", 0)
.attr("y1", x0)
.attr("x2", 0 + width)
.attr("y2", x0)
.style("opacity", 1e-6)
.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1)
.style("opacity", 1);
whisker.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1)
.style("opacity", 1);
whisker.exit().transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1)
.style("opacity", 1e-6)
.remove();
var outlier = g.selectAll("circle.outlier")
.data(outlierIndices, Number);
outlier.enter().insert("circle", "text")
.attr("class", "outlier")
.attr("r", 5)
.attr("cx", width / 2)
.attr("cy", function(i) { return x0(t[i]); })
.style("opacity", 1e-6)
.transition()
.duration(duration)
.attr("cy", function(i) { return x1(t[i]); })
.style("opacity", 1);
outlier.transition()
.duration(duration)
.attr("cy", function(i) { return x1(t[i]); })
.style("opacity", 1);
outlier.exit().transition()
.duration(duration)
.attr("cy", function(i) { return x1(t[i]); })
.style("opacity", 1e-6)
.remove();
// Compute the tick format.
var format = tickFormat || x1.tickFormat(8);
// Update box ticks.
var boxTick = g.selectAll("text.box")
.data(quartileData);
if(showLabels == true) {
boxTick.enter().append("text")
.attr("class", "box")
.attr("dy", ".3em")
.attr("dx", function(d, i) { return i & 1 ? 6 : -6 ;})
.attr("x", function(d, i) { return i & 1 ? + width : 0; })
.attr("y", x0)
.attr("text-anchor", function(d, i) { return i & 1 ? "start" : "end"; })
.text(format)
.transition()
.duration(duration)
.attr("y", x1);
}
boxTick.transition()
.duration(duration)
.text(format)
.attr("y", x1);
// Update whisker ticks. These are handled separately from the box
// ticks because they may or may not exist, and we want don't want
// to join box ticks pre-transition with whisker ticks post-.
var whiskerTick = g.selectAll("text.whisker")
.data(whiskerData || []);
if(showLabels == true) {
whiskerTick.enter().append("text")
.attr("class", "whisker")
.attr("dy", ".3em")
.attr("dx", 6)
.attr("x", width)
.attr("y", x0)
.text(format)
.style("opacity", 1e-6)
.transition()
.duration(duration)
.attr("y", x1)
.style("opacity", 1);
}
whiskerTick.transition()
.duration(duration)
.text(format)
.attr("y", x1)
.style("opacity", 1);
whiskerTick.exit().transition()
.duration(duration)
.attr("y", x1)
.style("opacity", 1e-6)
.remove();
});
d3.timer.flush();
}
function getBoxPlotChart(csvFetch,chartDiv,outilerTotalValue,tableDataValue)
{
var labels = true;
$("#" + chartDiv).empty();
var margin = {top: 30, right: 50, bottom: 70, left: 50};
var width = 410 - margin.left - margin.right;
var height = 380 - margin.top - margin.bottom;
var min = Infinity,
max = -Infinity;
tableData = tableDataValue;
var data = [];
data[0] = [];
// add more rows if your csv file has more columns
// add here the header of the csv file
data[0][0] = "";
// add more rows if your csv file has more columns
data[0][1] = [];
var v1 = parseFloat(csvFetch.firstQuartile),
v2 = parseFloat(csvFetch.medianValue),
v3=parseFloat(csvFetch.thirdQuartile),
v4=parseFloat(csvFetch.minValue),
v5=parseFloat(csvFetch.maxValue);
outlierData = outilerTotalValue;
var rowMax = Math.max(v1, Math.max(v2, Math.max(v3,v5)));
//var rowMin = Math.min(v4, Math.max(v2, Math.max(v3,v1)));
var rowMin=0;
// add more variables if your csv file has more columns
var data1 = parseFloat(v1);
var data2 = parseFloat(v3);
var absDiff = Math.abs(data1-data2);
var iqrValue = absDiff * 1.5;
//var common = data2-data1;
//var value = common.toFixed(5);
// Compute whiskers. Must return exactly 2 elements, or null.
var fence= [];
fence[0] = data1 - iqrValue;
fence[1]= data2 + iqrValue;
var lowerFence=fence[0];
var upperFence=fence[1];
/*End : To correct Y axis labelling : Pranjal */
var rowMax1 = Math.max(v1, Math.max(v2, Math.max(v3,v5)));
var rowMin1 = Math.min(v4, Math.max(v2, Math.max(v3,v1)));
minBound=Math.floor(Math.min(lowerFence,rowMin1));
maxBound=Math.ceil(Math.max(upperFence,rowMax1));
data[0][1].push(v1);
data[0][1].push(v2);
data[0][1].push(v3);
data[0][1].push(v4);
data[0][1].push(v5);
// add more rows if your csv file has more columns
if (rowMax > max) max = rowMax;
if (rowMin < min) min = rowMin;
var chart = d3.box()
//.whiskers(iqr(1.5))
.height(height)
.domain([minBound, maxBound])
.showLabels(labels);
var svg = d3.select("#" + chartDiv).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("class", "box")
.append("g")
.attr("transform", "translate(" + (margin.left+200) + "," + margin.top + ")");
// the x-axis
var x = d3.scale.ordinal()
.domain( data.map(function(d) { console.log(d); return d[0]; } ) )
.rangeRoundBands([0 , width], 0.7, 0.3);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
// the y-axis
var y = d3.scale.linear()
.domain([minBound, maxBound])
.range([height + margin.top, 0 + margin.top]);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
// draw the boxplots
svg.selectAll(".box")
.data(data)
.enter().append("g")
.attr("transform", function(d) { return "translate(" + x(d[0]) + "," + margin.top + ")"; } )
.call(chart.width(x.rangeBand()));
/*// add a title
svg.append("text")
.attr("x", (width / 2))
.attr("y", 0 + (margin.top / 2))
.attr("text-anchor", "middle")
.style("font-size", "18px");*/
/* Start: Bug: attribute label should present : Salman : 11/03/2016 */
// .text(csvFetch.attributeName+" "+"Vs"+" "+"Distribution");
/* End: Bug: attribute label should present : Salman : 11/03/2016 */
// draw y axis labelling
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text") // and text1
.attr("transform", "rotate(0)")
.attr("y", 6)
.attr("dy", ".71em")
/* Start: Bug: y axis label was getting hidden : Salman : 11/03/2016 */
.style("text-anchor", "start")
/* End: Bug: y axis label was getting hidden : Salman : 11/03/2016 */
.text(csvFetch.attributeName);
// draw x axis labelling
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height + margin.top + 10) + ")")
.call(xAxis)
.append("text") // text label for the x axis
.attr("x", (width / 2)+350 )
.attr("y", 10 )
.attr("dy", ".71em");
// Returns a function to compute the interquartile range.
/*function iqr(k) {
return function(d, i) {
var q1 =d[1];
var q3 = d[3];
var iqr = (q3 - q1)*k;
};
}*/
}
I would like to add several vertical lines indicating particular events in a line chart created with nPlot. Any suggestions?
library(reshape2)
library(ggplot2)
library(rCharts)
ecm <- reshape2::melt(economics[,c('date', 'uempmed', 'psavert')], id = 'date')
p7 <- nPlot(value ~ date, group = 'variable', data = ecm, type = 'lineWithFocusChart')
Final goal: add vertical lines with labels at 'date' = '1967-11-30', '2001-11-30', '1968-01-31', etc.
Ok, so this should answer the question. I think you will notice that this is generally considered well beyond the skill level of an average R user. Eventually, it would be nice to write a custom chart to handle this behavior well and make easy and available to the general R user. I would like to generalize both the dashed and the vertical lines to handle in R. Here is the rCharts viewer demo and the live code example.
library(reshape2)
library(ggplot2)
library(rCharts)
ecm <- reshape2::melt(economics[,c('date', 'uempmed', 'psavert')], id = 'date')
p7 <- nPlot(value ~ date, group = 'variable', data = ecm, type = 'lineWithFocusChart')
#let's add this to make date handling easier
p7$xAxis( tickFormat="#!function(d) {return d3.time.format('%b %Y')(new Date( d * 86400000 ));}!#" )
#grab template from
#https://github.com/ramnathv/rCharts/blob/master/inst/libraries/nvd3/layouts/chart.html
#modify to add callback on graph render
p7$setTemplate(script = sprintf("
<script type='text/javascript'>
$(document).ready(function(){
draw{{chartId}}( );
});
function draw{{chartId}}( ){
var opts = {{{ opts }}};
var data = {{{ data }}};
if(!(opts.type==='pieChart' || opts.type==='sparklinePlus' || opts.type==='bulletChart')) {
var data = d3.nest()
.key(function(d){
//return opts.group === undefined ? 'main' : d[opts.group]
//instead of main would think a better default is opts.x
return opts.group === undefined ? opts.y : d[opts.group];
})
.entries(data);
}
if (opts.disabled != undefined){
data.map(function(d, i){
d.disabled = opts.disabled[i]
})
}
nv.addGraph(function() {
chart = nv.models[opts.type]()
.width(opts.width)
.height(opts.height)
if (opts.type != 'bulletChart'){
chart
.x(function(d) { return d[opts.x] })
.y(function(d) { return d[opts.y] })
}
{{{ chart }}}
{{{ xAxis }}}
{{{ x2Axis }}}
{{{ yAxis }}}
d3.select('#' + opts.id)
.append('svg')
.datum(data)
.transition().duration(500)
.call(chart);
chart.dispatch.brush.on('brushstart',function(){ drawVerticalLines( opts ) });
chart.dispatch.brush.on(
'brushend',
function(){ window.setTimeout(
function() {drawVerticalLines( opts )},
250
)}
);
nv.utils.windowResize(chart.update);
return chart;
},%s);
};
%s
</script>
"
,
#here is where you can type your vertical line/label function
"function() { drawVerticalLines( opts ) }"
,
#add the afterScript here if using with shiny
"
function drawVerticalLines( opts ){
if (!(d3.select('#' + opts.id + ' .nvd3 .nv-focus .nv-linesWrap').select('.vertical-lines')[0][0])) {
d3.select('#' + opts.id + ' .nvd3 .nv-focus .nv-linesWrap').append('g')
.attr('class', 'vertical-lines')
}
vertLines = d3.select('#' + opts.id + ' .nvd3 .nv-focus .nv-linesWrap').select('.vertical-lines').selectAll('.vertical-line')
.data(
[
{ 'date' : new Date('1967-11-30'),
'label' : 'something to highlight 1967'
} ,
{ 'date' : new Date('2001-11-30'),
'label' : 'something to highlight 2001'
}
] )
var vertG = vertLines.enter()
.append('g')
.attr('class', 'vertical-line')
vertG.append('svg:line')
vertG.append('text')
vertLines.exit().remove()
vertLines.selectAll('line')
.attr('x1', function(d){
return chart.xAxis.scale()(d.date/60/60/24/1000)
})
.attr('x2', function(d){ return chart.xAxis.scale()(d.date/60/60/24/1000) })
.attr('y1', chart.yAxis.scale().range()[0] )
.attr('y2', chart.yAxis.scale().range()[1] )
.style('stroke', 'red')
vertLines.selectAll('text')
.text( function(d) { return d.label })
.attr('dy', '1em')
//x placement ; change dy above for minor adjustments but mainly
// change the d.date/60/60/24/1000
//y placement ; change 2 to where you want vertical placement
//rotate -90 but feel free to change to what you would like
.attr('transform', function(d){
return 'translate(' +
chart.xAxis.scale()(d.date/60/60/24/1000) +
',' +
chart.yAxis.scale()(2) +
') rotate(-90)'
})
//also you can style however you would like
//here is an example changing the font size
.style('font-size','80%')
}
"
))
p7
Let me know your thoughts.