How to get coordinates from a R plotly figure - r

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.

Related

In R how to replicate highchart chart with highcharter package

I need to replicate this chart bellow in my shiny app. But I am struggling to deal with the javascript part Any help would be amazing:
Clock Chart Highchart
This is the javascript code: how do I 'translate' this to R?
Any help/indication to deal with javascript in R would be amazing.
Many many tahnks guys
`/**
* Get the current time
*/
function getNow() {
var now = new Date();
return {
hours: now.getHours() + now.getMinutes() / 60,
minutes: now.getMinutes() * 12 / 60 + now.getSeconds() * 12 / 3600,
seconds: now.getSeconds() * 12 / 60
};
}
/**
* Pad numbers
*/
function pad(number, length) {
// Create an array of the remaining length + 1 and join it with 0's
return new Array((length || 2) + 1 - String(number).length).join(0) + number;
}
var now = getNow();
// Create the chart
Highcharts.chart('container', {
chart: {
type: 'gauge',
plotBackgroundColor: null,
plotBackgroundImage: null,
plotBorderWidth: 0,
plotShadow: false,
height: '80%'
},
credits: {
enabled: false
},
title: {
text: 'The Highcharts clock'
},
pane: {
background: [{
// default background
}, {
// reflex for supported browsers
backgroundColor: Highcharts.svg ? {
radialGradient: {
cx: 0.5,
cy: -0.4,
r: 1.9
},
stops: [
[0.5, 'rgba(255, 255, 255, 0.2)'],
[0.5, 'rgba(200, 200, 200, 0.2)']
]
} : null
}]
},
yAxis: {
labels: {
distance: -20
},
min: 0,
max: 12,
lineWidth: 0,
showFirstLabel: false,
minorTickInterval: 'auto',
minorTickWidth: 1,
minorTickLength: 5,
minorTickPosition: 'inside',
minorGridLineWidth: 0,
minorTickColor: '#666',
tickInterval: 1,
tickWidth: 2,
tickPosition: 'inside',
tickLength: 10,
tickColor: '#666',
title: {
text: 'Powered by<br/>Highcharts',
style: {
color: '#BBB',
fontWeight: 'normal',
fontSize: '8px',
lineHeight: '10px'
},
y: 10
}
},
tooltip: {
formatter: function () {
return this.series.chart.tooltipText;
}
},
series: [{
data: [{
id: 'hour',
y: now.hours,
dial: {
radius: '60%',
baseWidth: 4,
baseLength: '95%',
rearLength: 0
}
}, {
id: 'minute',
y: now.minutes,
dial: {
baseLength: '95%',
rearLength: 0
}
}, {
id: 'second',
y: now.seconds,
dial: {
radius: '100%',
baseWidth: 1,
rearLength: '20%'
}
}],
animation: false,
dataLabels: {
enabled: false
}
}]
},
// Move
function (chart) {
setInterval(function () {
now = getNow();
if (chart.axes) { // not destroyed
var hour = chart.get('hour'),
minute = chart.get('minute'),
second = chart.get('second'),
// run animation unless we're wrapping around from 59 to 0
animation = now.seconds === 0 ?
false : {
easing: 'easeOutBounce'
};
// Cache the tooltip text
chart.tooltipText =
pad(Math.floor(now.hours), 2) + ':' +
pad(Math.floor(now.minutes * 5), 2) + ':' +
pad(now.seconds * 5, 2);
hour.update(now.hours, true, animation);
minute.update(now.minutes, true, animation);
second.update(now.seconds, true, animation);
}
}, 1000);
});
/**
* Easing function from https://github.com/danro/easing-js/blob/master/easing.js
*/
Math.easeOutBounce = function (pos) {
if ((pos) < (1 / 2.75)) {
return (7.5625 * pos * pos);
}
if (pos < (2 / 2.75)) {
return (7.5625 * (pos -= (1.5 / 2.75)) * pos + 0.75);
}
if (pos < (2.5 / 2.75)) {
return (7.5625 * (pos -= (2.25 / 2.75)) * pos + 0.9375);
}
return (7.5625 * (pos -= (2.625 / 2.75)) * pos + 0.984375);
};`
This converts that JS into R/JS (you need to collect time in Javascript). I noticed odd vertical lines in the Viewer pane of RStudio when this runs, but these lines don't appear in my browser.
For most calls in JS for highcharter, the function or argument is identical in R. I used lubridate for the time functions in the R code. (Although, you could set the time to static values because the time isn't controlled by R code.)
After creating the graph, I used htmlwidgets::onRender to give add the animation so that it follows actual time.
If you run this without htmlwidgets, this is what you'll see. (Well, you'll see the time on the clock for your local time at the moment you render it.)
library(highcharter)
library(lubridate)
highchart() %>%
hc_chart(type = "gauge", plotBackgroundColor = NULL,
plotBackgroundImage = NULL, plotBorderWidth = 0,
plotShadow = F) %>%
hc_pane(
background = list(
backgroundColor = list(
radialGradient = list(cx = .5, cy = -.4, r = 1.9),
stops = list(
list(.5, "rgba(255, 255, 255, .2)"),
list(.5, "rgba(200, 200, 200, .2)"))
))) %>%
hc_tooltip(enabled = FALSE) %>%
hc_yAxis(
labels = list(distance = -20),
min = 0, max = 12, lineWidth = 0, showFirstLabel = F,
minorTickInterval = "auto", minorTickWidth = 1,
minorTickColor = "#666", tickColor = "#666",
minorTickPosition = "inside", minorGridLineWidth = 0,
tickInterval = 1, tickWidth = 2, tickPosition = "inside",
tickLength = 10) %>%
hc_add_series(
data = list(
list(id = "hour", y = hour(now()), dial = list(
radius = "60%", baseWidth = 4, baseLength = "95%", rearLength = 0)),
list(id = "minute", y = minute(now()), dial = list(
baseLength = "95%", rearLength = 0)),
list(id = "second", y = second(now()), dial = list(
radius = "100%", baseWidth = 1, rearLength = "20%"))),
dataLabels = list(enabled = F)) %>%
htmlwidgets::onRender("
function(el, x) {
chart = $('#' + el.id).highcharts()
$.extend($.easing, {
easeOutElastic: function (x, t, b, c, d) {
var s = 1.70158; var p = 0; var a = c;
if (t == 0) return b; if ((t /= d) == 1) return b+c;
if (!p) p = d*.3;
if (a < Math.abs(c)) { a = c; var s = p/4; }
else var s = p/(2 * Math.PI) * Math.asin (c/a);
return a * Math.pow(2, -10 * t) * Math.sin( (t * d - s) * (2 * Math.PI)/p) + c + b;
}
});
function getNow () {
var now = new Date();
return {
hours: now.getHours() + now.getMinutes() / 60,
minutes: now.getMinutes() * 12 / 60 + now.getSeconds() * 12 / 3600,
seconds: now.getSeconds() * 12 / 60
};
};
setInterval(function () {
var hour = chart.get('hour'),
minute = chart.get('minute'),
second = chart.get('second'),
now = getNow(),
/* run animation unless we're wrapping around from 59 to 0 */
animation = now.seconds == 0 ?
false : {easing: 'easeOutElastic'};
hour.update(now.hours, true, animation);
minute.update(now.minutes, true, animation);
second.update(now.seconds, true, animation);
}, 1000);
}")
In this JS, you'll see some deviation from the original code. I needed to define 'chart'. I did that using the same mechanism that is used to change any highcharter R object into it's HTML rendering: chart = $('#' + el.id).highcharts(). Since the function that sets the interval was originally part of creating the graph, it was an unnamed function. Since we're calling after we render the graph, I dropped that outer function(chart).

How do you animate axis transitions with d3 in r2d3?

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.

Add a horizontal discrete legend below the chart in ggvis

I'd like to make a horizontal legend below the ggvis chart. I can use legend properties to place it below the chart, but don't know how to make the labels horizontal below the legend title. Below is the minimum reproducible code (grabbed from the web).
df1 = data.frame(x=sample(1:10), y=sample(1:10))
df2 = data.frame(x=1:10, y=1:10)
df3 = data.frame(x=1:10, y=sqrt(1:10))
df2$id <- 1
df3$id <- 2
df4 <- rbind(df2,df3)
df4$id <- factor(df4$id)
df4 %>% ggvis(x=~x, y=~y, stroke=~id) %>% layer_lines() %>%
# make sure you use add relative scales
add_relative_scales() %>%
# values for x and y need to be between 0 and 1
# e.g for the x-axis 0 is the at far-most left point and 1 at the far-right
add_legend("stroke", title="Cylinders",
properties=legend_props(
legend=list(
x=scaled_value("x_rel", 0.2),
y=scaled_value("y_rel", -.2)
))) %>%
layer_points(x=~x, y=~y, data=df1, stroke:='black')
This is most certainly a hack, but works great:
df4 %>% ggvis(x=~x,y=~y,stroke=~id) %>% layer_lines() %>%
#make sure you use add relative scales
add_relative_scales() %>%
#values for x and y need to be between 0 and 1
#e.g for the x-axis 0 is the at far-most left point and 1 at the far-right
add_legend("stroke", title = "Cylinders", values = c(1, 1),
properties = legend_props(
legend = list(
x = scaled_value("x_rel", 0.2),
y = scaled_value("y_rel", -.2)
))) %>%
add_legend("stroke", title = " ", values = c(2, 2),
properties = legend_props(
legend = list(
x = scaled_value("x_rel", 0.23),
y = scaled_value("y_rel", -.2)
))) %>%
layer_points(x=~x,y=~y,data = df1,stroke:='black')
Basically, I'm adding another add_legend, setting the title as blank, adjusting the scale_value so that it is very close to the first legend but not overlapping. I then set the first legend with values = c(1,1) and the second with values = c(2,2) so that the two values stack on top of each other. This makes it look like one legend with horizontal values.
Seen that ggvis is dormant now, maybe you may consider switching to googleVis.
Here's a similar plot you can get by manipulating a little bit your sample data:
df5 <- df4[df4$id==1,]
colnames(df5)[2] <- "y1"
library(tidyverse)
df5 <- df5 %>%
mutate(
y0 = df1[order(df1$x),c(2)],
y2 = sqrt(x)
)
df5 <- df5[, c(1,4,2,5)]
library(googleVis)
plot_df5 <- gvisLineChart(df5, options=list(
legend="bottom",
series =
"[{labelInLegend: 'Dot', color: 'black'},
{labelInLegend: 'Cylinders: 1', color: 'blue', curveType: 'dot'},
{labelInLegend: 'Cylinders: 2', color: 'orange'}]"
)
)
plot(plot_df5)
<!-- LineChart generated in R 3.5.2 by googleVis 0.6.2 package -->
<!-- Sun Dec 30 21:21:26 2018 -->
<!-- jsHeader -->
<script type="text/javascript">
// jsData
function gvisDataLineChartID1fd8710660d () {
var data = new google.visualization.DataTable();
var datajson =
[
[
"1",
3,
1,
1
],
[
"2",
4,
2,
1.414213562
],
[
"3",
6,
3,
1.732050808
],
[
"4",
1,
4,
2
],
[
"5",
10,
5,
2.236067977
],
[
"6",
8,
6,
2.449489743
],
[
"7",
7,
7,
2.645751311
],
[
"8",
2,
8,
2.828427125
],
[
"9",
9,
9,
3
],
[
"10",
5,
10,
3.16227766
]
];
data.addColumn('string','x');
data.addColumn('number','y0');
data.addColumn('number','y1');
data.addColumn('number','y2');
data.addRows(datajson);
return(data);
}
// jsDrawChart
function drawChartLineChartID1fd8710660d() {
var data = gvisDataLineChartID1fd8710660d();
var options = {};
options["allowHtml"] = true;
options["legend"] = "bottom";
options["series"] = [{labelInLegend: 'Dot', color: 'black'},
{labelInLegend: 'Cylinders: 1', color: 'blue', curveType: 'dot'},
{labelInLegend: 'Cylinders: 2', color: 'orange'}];
var chart = new google.visualization.LineChart(
document.getElementById('LineChartID1fd8710660d')
);
chart.draw(data,options);
}
// jsDisplayChart
(function() {
var pkgs = window.__gvisPackages = window.__gvisPackages || [];
var callbacks = window.__gvisCallbacks = window.__gvisCallbacks || [];
var chartid = "corechart";
// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)
var i, newPackage = true;
for (i = 0; newPackage && i < pkgs.length; i++) {
if (pkgs[i] === chartid)
newPackage = false;
}
if (newPackage)
pkgs.push(chartid);
// Add the drawChart function to the global list of callbacks
callbacks.push(drawChartLineChartID1fd8710660d);
})();
function displayChartLineChartID1fd8710660d() {
var pkgs = window.__gvisPackages = window.__gvisPackages || [];
var callbacks = window.__gvisCallbacks = window.__gvisCallbacks || [];
window.clearTimeout(window.__gvisLoad);
// The timeout is set to 100 because otherwise the container div we are
// targeting might not be part of the document yet
window.__gvisLoad = setTimeout(function() {
var pkgCount = pkgs.length;
google.load("visualization", "1", { packages:pkgs, callback: function() {
if (pkgCount != pkgs.length) {
// Race condition where another setTimeout call snuck in after us; if
// that call added a package, we must not shift its callback
return;
}
while (callbacks.length > 0)
callbacks.shift()();
} });
}, 100);
}
// jsFooter
</script>
<!-- jsChart -->
<script type="text/javascript" src="https://www.google.com/jsapi?callback=displayChartLineChartID1fd8710660d"></script>
<!-- divChart -->
<div id="LineChartID1fd8710660d"
style="width: 500; height: automatic;">
</div>

d3.js: why don't the columns in my column chart contain the full data?

I am using a slightly modified reusable column chart script from https://gist.github.com/llad/3766585
Works great. However, I was just trying to add some tooltips on hover, that would display additional data from the JSON file for each column, but the additional data is not there. I look in the inspector and just see the two values used to calculate x and y. Is there something obvious in the way the script is written that is not binding the full data set?
The script:
function columnChart() {
var margin = {top: 30, right: 10, bottom: 50, left: 50},
width = 420,
height = 420,
xRoundBands = 0.2,
xValue = function(d) { return d[0]; },
yValue = function(d) { return d[1]; },
xScale = d3.scale.ordinal(),
yScale = d3.scale.linear(),
xFormat = '',
yFormat = '',
yAxis = d3.svg.axis().scale(yScale).orient("left"),
xAxis = d3.svg.axis().scale(xScale);
function chart(selection) {
selection.each(function(data) {
// Convert data to standard representation greedily;
// this is needed for nondeterministic accessors.
data = data.map(function(d, i) {
return [xValue.call(data, d, i), yValue.call(data, d, i)];
});
// Update the x-scale.
xScale
.domain(data.map(function(d) { return d[0];} ))
.rangeRoundBands([0, width - margin.left - margin.right], xRoundBands);
// Update the y-scale.
yScale
.domain(d3.extent(data.map(function(d) { return d[1];} )))
.range([height - margin.top - margin.bottom, 0])
.nice();
// Select the svg element, if it exists.
var svg = d3.select(this).selectAll("svg").data([data]);
// Otherwise, create the skeletal chart.
var gEnter = svg.enter().append("svg").append("g");
gEnter.append("g").attr("class", "bars");
gEnter.append("g").attr("class", "y axis");
gEnter.append("g").attr("class", "x axis");
gEnter.append("g").attr("class", "x axis zero");
// Update the outer dimensions.
svg .attr("width", width)
.attr("height", height);
// Update the inner dimensions.
var g = svg.select("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Update the bars.
var bar = svg.select(".bars").selectAll(".bar").data(data);
bar.enter().append("rect");
bar.exit().remove();
bar.attr("class", function(d, i) { return d[1] < 0 ? "bar negative" : "bar positive"; })
.attr("x", function(d) { return X(d); })
.attr("y", function(d, i) { return d[1] < 0 ? Y0() : Y(d); })
.attr("width", xScale.rangeBand())
.attr("height", function(d, i) { return Math.abs( Y(d) - Y0() ); })
.on("click", function(d, i)
{
d3.selectAll('.bar').classed('fade', true);
d3.select(this).classed("sel", true).classed("fade", false);
})
.on("mouseover", function(d, i)
{
d3.select(this).classed("hover", true);
})
.on("mouseout", function(d, i)
{
d3.select(this).classed("hover", false);
});
// x axis at the bottom of the chart
g.select(".x.axis")
.attr("transform", "translate(0," + (height - margin.top - margin.bottom) + ")")
.call(xAxis.orient("bottom").tickFormat(xFormat));
// zero line
g.select(".x.axis.zero")
.attr("transform", "translate(0," + Y0() + ")")
.call(xAxis.tickFormat("").tickSize(0));
// Update the y-axis.
g.select(".y.axis")
.call(yAxis);
// Horizontal grid
g.insert("g", ".bars")
.attr("class", "grid horizontal")
.call(d3.svg.axis().scale(yScale)
.orient("left")
.tickSize(-(width-margin.left-margin.right), 0, 0)
.tickFormat("")
);
});
}
// The x-accessor for the path generator; xScale ∘ xValue.
function X(d) {
return xScale(d[0]);
}
function Y0() {
return yScale(0);
}
// The x-accessor for the path generator; yScale ∘ yValue.
function Y(d) {
return yScale(d[1]);
}
chart.margin = function(_) {
if (!arguments.length) return margin;
margin = _;
return chart;
};
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
return chart;
};
chart.x = function(_) {
if (!arguments.length) return xValue;
xValue = _;
return chart;
};
chart.y = function(_) {
if (!arguments.length) return yValue;
yValue = _;
return chart;
};
chart.yTickFormat = function(_) {
if (!arguments.length) return yFormat;
yFormat = _;
return chart;
};
chart.xTickFormat = function(_) {
if (!arguments.length) return xFormat;
xFormat = _;
return chart;
};
return chart;
}
The initialization:
function renderGraph(view){
var chartWidth = mainWidth();
var chartHeight = 400;
var parseDate = d3.time.format("%Y-%m-%d").parse;
var xFormat = d3.time.format("%b %e");
var data = [];
var req = $.ajax({
url: '/data/column-data.json',
type: 'GET',
dataType: 'json',
success: function(response) {
data = response;
}
});
$.when(req).done(function() {
d3.select("#columnChart")
.datum(data.widgets)
.call(columnChart()
.width(chartWidth)
.height(chartHeight)
.x(function(d, i) { return parseDate(d[0]); })
.xTickFormat(xFormat)
.y(function(d, i) {
var yData;
if (view === 'thisView'){
yData = d[1];
}else if (view === 'thatView'){
yData = d[2];
}
return yData;
}));
});
}
and the data looks like this:
{ "widgets" : [
["2013-09-15", 1, 66622, 1, 3],
["2013-09-16", 0, 0, 0, 0],
["2013-09-17", 2, 76316, 2, 2],
["2013-09-18", 4, 291244, 8, 12],
["2013-09-19", 1, 74674, 2, 2],
["2013-09-20", 5, 287965, 7, 5],
["2013-09-21", 0, 0, 0, 0],
["2013-09-22", 0, 0, 0, 0],
["2013-09-23", 7, 459249, 15, 22],
["2013-09-24", 2, 317320, 1, 6],
["2013-09-25", 3, 100269, 3, 10],
["2013-09-26", 4, 181080, 8, 4],
["2013-09-27", 1, 38056, 1, 1],
["2013-09-28", 0, 0, 0, 0],
["2013-09-29", 0, 0, 0, 0],
["2013-09-30", 3, 449334, 2, 13],
["2013-10-01", 9, 403929, 5, 15],
["2013-10-02", 4, 222512, 7, 12],
["2013-10-03", 1, 196012, 3, 9],
["2013-10-04", 2, 391716, 2, 8],
["2013-10-05", 0, 0, 0, 0],
["2013-10-06", 0, 0, 0, 0],
["2013-10-07", 4, 260312, 8, 14],
["2013-10-08", 1, 34350, 1, 1],
["2013-10-09", 3, 179067, 9, 18],
["2013-10-10", 2, 124250, 8, 19],
["2013-10-11", 2, 381186, 4, 9],
["2013-10-12", 0, 0, 0, 0],
["2013-10-13", 0, 0, 0, 0],
["2013-10-14", 5, 393400, 11, 17]
]
}
The tooltip should display the full data for any one column. The web inspector shows an array of 2 for each column (I'm expecting an array of 5)
How do I modify the script to bind the full data for each column?
Thanks!
UPDATE:
I still haven't solved this, though #adam-pearce got very close. I've put together a couple jsbin's to help.
A bin for the original script is here: http://jsbin.com/EjugosA/2/edit
and the script with Adam's answer is here: http://jsbin.com/IPoDutA/1/edit
this just has the following commented out:
//data = data.map(function(d, i) {
// return [xValue.call(data, d, i), yValue.call(data, d, i)];
//});
You can see in the second that both x and y axis fail to render, though the full data set I wanted is bound to each bar. The web inspector shows the following error:
'undefined' is not a function (evaluating 'd.getMonth()')" which points to line 8410 in d3.v3.js, in d3_time_formats.
How to I modify this script to get the full data without breaking the axis?
data = data.map(function(d, i) {
return [xValue.call(data, d, i), yValue.call(data, d, i)];
});
Removes everything but the first two columns from data. Looking over the code, I think it would still work if you commented out those three lines.
In responce to your comment:
// x axis at the bottom of the chart
g.select(".x.axis")
.attr("transform", "translate(0," + (height - margin.top - margin.bottom) + ")")
.call(xAxis.orient("bottom"));//.tickFormat(xFormat));
You're trying to print strings as dates which doesn't work without parsing the strings first.

Changing bonsai code dynamically

I have this part of code
<script>
var xhr = new XMLHttpRequest();
xhr.open('POST', 'getNewUsers.php',false);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send();
var json;
if (xhr.readyState == 4 && xhr.status == 200) { // If file is loaded correctly.
json = JSON.parse(xhr.responseText);
alert(json.en);
}
else if(xhr.readyState == 4 && xhr.status != 200) { // En cas d'erreur !
alert( ' Une erreur est survenue !\n\nCode :' + xhr.status + '\nTexte : ' + xhr.statusText);
}
</script>
<script id="bs">
function Sector(x, y, radius, startAngle, endAngle) {
SpecialAttrPath.call(this, {
radius: 0,
startAngle: startAngle,
endAngle: endAngle
});
this.attr({
x: x,
y: y,
radius: radius,
startAngle: startAngle,
endAngle: endAngle
});
}
Sector.prototype = Object.create(SpecialAttrPath.prototype);
Sector.prototype._make = function() {
var attr = this._attributes,
radius = attr.radius,
startAngle = attr.startAngle,
endAngle = attr.endAngle;
var startX, startY, endX, endY;
var diffAngle = Math.abs(endAngle - startAngle);
this.startX = startX = radius * Math.cos(startAngle);
this.startY = startY = radius * Math.sin(startAngle);
if (diffAngle < Math.PI*2) {
endX = radius * Math.cos(endAngle);
endY = radius * Math.sin(endAngle);
} else { // angles differ by more than 2*PI: draw a full circle
endX = startX;
endY = startY - .0001;
}
this.endX = endX;
this.endY = endY;
this.radiusExtentX = radius * Math.cos(startAngle + (endAngle - startAngle)/2);
this.radiusExtentY = radius * Math.sin(startAngle + (endAngle - startAngle)/2);
return this.moveTo(0, 0)
.lineTo(startX, startY)
.arcTo(radius, radius, 0, (diffAngle < Math.PI) ? 0 : 1, 1, endX, endY)
.lineTo(0, 0);
};
Sector.prototype.getDimensions = function() {
var x = this.attr('x'),
y = this.attr('y'),
left = Math.min(x, x + this.startX, x + this.endX, x + this.radiusExtentX),
top = Math.min(y, y + this.startY, y + this.endY, y + this.radiusExtentY),
right = Math.max(x, x + this.startX, x + this.endX, x + this.radiusExtentX),
bottom = Math.max(y, y + this.startY, y + this.endY, y + this.radiusExtentY);
console.log(y, y + this.startY, y + this.endY, y + this.radiusExtentY)
return {
left: left,
top: top,
width: right - left,
height: bottom - top
};
};
PieChart.BASE_COLOR = color('red');
function PieChart(data) {
this.angle = 0;
this.labelY = 30;
this.kolor = PieChart.BASE_COLOR.clone();
var n = 0;
for (var i in data) {
this.slice(i, data[i], n++);
}
}
PieChart.prototype = {
slice: function(name, value, i) {
var start = this.angle,
end = start + (Math.PI*2) * value/100,
// Increase hue by .1 with each slice (max of 10 will work)
kolor = this.kolor = this.kolor.clone().hue(this.kolor.hue()+.1);
var s = new Sector(
400, 200, 150,
start,
end
);
var animDelay = (i * 200) + 'ms';
var label = this.label(name, value, kolor);
label.attr({ opacity: 0 });
s.stroke('#FFF', 3);
s.fill(kolor);
s.attr({
endAngle: start,
radius: 0
}).addTo(stage).on('mouseover', over).on('mouseout', out);
label.on('mouseover', over).on('mouseout', out);
function over() {
label.text.attr('fontWeight', 'bold');
label.animate('.2s', {
x: 40
});
s.animate('.2s', {
radius: 170,
fillColor: kolor.lighter(.1)
}, {
easing: 'sineOut'
});
}
function out() {
label.text.attr('fontWeight', '');
label.animate('.2s', {
x: 30
});
s.animate('.2s', {
radius: 150,
fillColor: kolor
});
}
s.animate('.4s', {
radius: 150,
startAngle: start,
endAngle: end
}, {
easing: 'sineOut',
delay: animDelay
});
label.animate('.4s', {
opacity: 1
}, { delay: animDelay });
this.angle = end;
},
label: function(name, v, fill) {
var g = new Group().attr({
x: 30,
y: this.labelY,
cursor: 'pointer'
});
var t = new Text(name + ' (' + v + '%)').addTo(g);
var r = new Rect(0, 0, 20, 20, 5).fill(fill).addTo(g);
t.attr({
x: 30,
y: 17,
textFillColor: 'black',
fontFamily: 'Arial',
fontSize: '14'
});
g.addTo(stage);
this.labelY += 30;
g.text = t;
return g;
}
};
new PieChart({
English: json.en,
French: 20,
German: 30,
Dutch: 5,
Spanish: 19,
Others: 18
})
</script>
The problem is I would like to change the pie dynamically using Json, in the demi it is shown with integers but here it is also an integer. This is the line that cause the problem for rendering the pie.
new PieChart({
English: json.en,
BonsaiJS is executed in a separate execution environment (mostly worker) and because of that it can't reach objects that were defined outside (in your case the json variable) of the BonsaiJS movie code (in your example <script id="bs">).
You can read about the special execution of BonsaiJS here: http://docs.bonsaijs.org/overview/Execution.html. If you want to pass data from your page to the Bonsai movie execution you can do the following:
Dynamically communicate through sendMessage with the execution context (described here: http://docs.bonsaijs.org/overview/Communication.html)
If you just have to pass data into your context once you can do that through bonsai.run(myNode, {myJson: json}); and access it from within your movie code through stage.options.myJson (documented on the bottom of http://docs.bonsaijs.org/overview/Execution.html).
You also have the third option to move the XMLHttpRequest code into the movie-code and do the request from there. Every client-side bonsai execution context (worker, iframe) does support that.

Resources