Scoping in r2d3 visualization - d3.selectAll vs svg.selectAll - r

Why does d3.selectAll('rect')... not work as seen in the script below in the mouseover function, but svg.selectAll('rect')... does?
svg is the special pre-set selector from r2d3.
Additionally, I have noticed that running e.g. d3.selectAll('rect').remove() from the browser console does not work when running the visualization from Rstudio.
This is from the r2d3 example, as sample.js:
// !preview r2d3 data=c(0.3, 0.6, 0.8, 0.95, 0.40, 0.20)
var barHeight = Math.floor(height / data.length);
svg.selectAll('rect')
.data(data)
.enter().append('rect')
.attr('width', function(d) { return d * width; })
.attr('height', barHeight)
.attr('y', function(d, i) { return i * barHeight; })
.attr('fill', 'steelblue')
.on('mouseover', function(d){
d3.select(this).attr('fill', 'red')
//svg.selectAll('rect').attr('fill', 'red')
d3.selectAll('rect').attr('fill', 'red')
})
Run from R via r2d3::r2d3("sample.js", data=c(0.3, 0.6, 0.8, 0.95, 0.40, 0.20))

By default r2d3 places the visualization in a shadow DOM. Both d3.select and d3.selectAll start their search at the root element of the DOM, but do not venture into a shadow DOM's child nodes.
Neither select nor selectAll will cross into a shadow tree from the current tree being searched. As svg is a selection of an element within the shadow DOM, the rectangles can be found with svg.selectAll("rect") but not d3.selectAll("rect") (the rects aren't "shadowed" relative to the SVG).
The easiest solution is to not create a shadow DOM by setting the r2d3 shadow option to false.
eg (using the base example from r2d3's documentation):
r2d3(options(r2d3.shadow = FALSE), data=c(0.3, 0.6, 0.8, 0.95, 0.40, 0.20), script = "barchart.js")
Of course in doing so you lose the encapsulation offered by the shadow root, but this might be preferable or neutral depending on the situation.

I've found that sometimes the r2d3.shadow option doesn't always work well, so here's another possible solution. The parent of the event target will be the svg/g that you've placed it in.
// !preview r2d3 data=c(0.3, 0.6, 0.8, 0.95, 0.40, 0.20)
var barHeight = Math.floor(height / data.length);
svg.selectAll('rect')
.data(data)
.enter().append('rect')
.attr('width', function(d) { return d * width; })
.attr('height', barHeight)
.attr('y', function(d, i) { return i * barHeight; })
.attr('fill', 'steelblue')
.on('mouseover', function(d){
const parent = d3.select(d.target.parentElement); // this is the original svg
//svg.selectAll('rect').attr('fill', 'red')
parent.selectAll('rect').attr('fill', 'blue')
d3.select(this).attr('fill', 'red')
})

Related

Is it possible to change borderColor for area bands (e_band2) in echarts4r?

Goal:
In my plot, I would like to have different colors for the border and the area of an area band. The plot was created with echarts4r using the e_band2() function.
Problem:
In the documentation I read that area bands can be customized through the itemStyle argument of the function. This works fine for all the other options (borderWidth, borderType, shadowBlur, shadowColor) I tested, but not for borderColor. However, the borderColor option seems to work at least partially, because the symbol in the legend has the desired border color, but not the area band in the plot. Does anyone know if there is another way to change the border color or is this a bug?
Reprex:
library(echarts4r)
library(dplyr)
data(EuStockMarkets)
as.data.frame(EuStockMarkets) |>
dplyr::slice_head(n = 200) |>
dplyr::mutate(day = 1:dplyr::n()) |>
e_charts(day) |>
e_band2(DAX, SMI, itemStyle = list(
borderWidth = 1,
color = "green",
borderColor = "red"
)) |>
e_y_axis(scale = TRUE)
For some reason, it's hardcoded to ignore your style settings for the stroke color (in SVG, that's the lines) by echarts4r.
However, you can change it back.
I've taken the code from the echarts4r package for the JS function renderBand and modified it to use the settings that are defined in the plot call (which is what you wanted and expected).
Note that I've made this a string.
renderBands2 <- "
function renderBands2(params, api) {
if (params.context.rendered) return;
params.context.rendered = true;
/* set polygon vertices */
let points = [];
let i = 0;
while (typeof api.value(0,i) != 'undefined' && !isNaN(api.value(0,i))) {
points.push(api.coord([api.value(0,i), api.value(1,i)])); /* lo */
i++;
}
for (var k = i-1; k > -1 ; k--) {
points.push(api.coord([api.value(0,k), api.value(2,k)])); /* up */
}
return {
type: 'polygon',
shape: {
points: echarts.graphic.clipPointsByRect(points, {
x: params.coordSys.x, y: params.coordSys.y,
width: params.coordSys.width, height: params.coordSys.height
})}, style: api.style()
};
}"
Apply this to your plot with htmlwidgets::onRender. This calls for the plot to be re-rendered with this new function for the data series.
as.data.frame(EuStockMarkets) |>
dplyr::slice_head(n = 200) |>
dplyr::mutate(day = 1:dplyr::n()) |>
e_charts(day) |>
e_band2(DAX, SMI, itemStyle = list(
borderWidth = 1, color = "green", borderColor = "red")) |>
e_y_axis(scale = TRUE) %>%
htmlwidgets::onRender(paste0(
"function(el, data) {
", renderBands2,
"
chart = get_e_charts(el.id);
opts = chart.getOption();
opts.series[0].renderItem = renderBands2;
chart.setOption(opts, true); /* all better! */
}"))

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.

How to attach multiple react-spring springs on a single component?

I'm trying to learn how to use react-spring. Let's say that I have three divs to animate.
<a.div style={trans1}>
<a.div style={trans2}>
<a.div style={trans3}>
and trans1 has the following configuration…
const [clicked, toggle] = useState(null)
const { x } = useSpring({
from: { x: 0 },
x: clicked ? 1 : 0,
config: { duration: 500 },
})
const trans1 = {
transform: x
.interpolate({
range: [0, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 1],
output: [1, 0.97, 0.9, 1.1, 0.9, 1.1, 1.03, 1],
})
.interpolate((x) => `scale(${x})`),
}
What's the best way to implement the same type of animation on the second and third divs without duplicating all that code? How do I make multiple instances of the same spring for use on multiple Dom objects without triggering them all at the same time? I certainly don't want to duplicate a full set of code for each item, right?
Do I need to create a function that accepts a parameter that can switch the arguments in the config on the fly? 🤷🏽‍♂️
Any help is appreciated.
Here's a live example: https://codesandbox.io/s/optimistic-bassi-brnle
How do I make the left & right sides animate one at a time without creating duplicate code?
The first possibility would be to separate the style and give it to more than one div. Its drawback is, that they would behave exactly the same at the same time.
const style = {
opacity: x.interpolate({ range: [0, 1], output: [0.3, 1] }),
transform: x
.interpolate({
range: [0, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 1],
output: [1, 0.97, 0.9, 1.1, 0.9, 1.1, 1.03, 1]
})
.interpolate(x => `scale(${x})`)
};
return (
<div onClick={() => toggle(!state)}>
<animated.div
style={style}>
click
</animated.div>
<animated.div
style={style}>
click
</animated.div>
</div>
)
The second option is, that you create a new component with the click and spring logic. This way you write the logic once and you can use it multiple time. I introduced a text attribute also to make different text for the component.
const AnimText = ({text}) => {
const [state, toggle] = useState(true)
const { x } = useSpring({ from: { x: 0 }, x: state ? 1 : 0, config: { duration: 1000 } })
return (
<div onClick={() => toggle(!state)}>
<animated.div
style={{
opacity: x.interpolate({ range: [0, 1], output: [0.3, 1] }),
transform: x
.interpolate({
range: [0, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 1],
output: [1, 0.97, 0.9, 1.1, 0.9, 1.1, 1.03, 1]
})
.interpolate(x => `scale(${x})`)
}}>
{text}
</animated.div>
</div>
)
}
function Demo() {
return (
<div>
<AnimText text={'click1'}/>
<AnimText text={'click2'}/>
<AnimText text={'click3'}/>
</div>
)
}
here is the example: https://codesandbox.io/s/divine-water-n1b6x

Use R data.frame object in d3.js using r2d3

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');

Changing line thickness and opacity in scatterplot on onRender() htmlWidgets in R

I am hoping to make a plot using the R package htmlwidgets' onRender() function in which a user can click on a point and a line is drawn. I have the crux of it working right now where a gray line is drawn at its default thickness and probably its default opaqueness.
However, I have been stuck on changing the thickness of the line (and possibly changing the opaqueness of the line, although it may be working and I cannot see it since the line is so thin). I want the line to be very thick and rather transparent. I tried several parameters and approaches for line width and opaqueness (some of which are commented out below), but it seems they do not make a difference. Any ideas what I may be missing? Thank you.
library(plotly)
library(broom)
dat <- mtcars
dat$mpg <- dat$mpg * 10
p <- ggplot(data = dat, aes(x=disp,y=mpg)) + geom_point(size=0.5)
ggplotly(p) %>%
onRender("
function(el, x, data) {
// reduce the opacity of every trace except for the hover one
el.on('plotly_click', function(e) {
var trace1 = {
x: [100, 400],
y: [100, 400],
mode: 'lines',
//line: dict(color: 'gray', width: 100)
marker: {
color: 'gray',
size: 200,
width: 1000,
opacity: 0.5
}
}
Plotly.addTraces(el.id, trace1);
})
}
", data=dat)
The opacity would need to be in the trace object. Your line object has some syntax issues which prevents Javascript from reading it.
gp %>% onRender("
function(el, x, data) {
el.on('plotly_click', function(e) {
var trace1 = {
x: [100, 400],
y: [100, 400],
mode: 'lines',
line: {
color: 'gray',
width: 100
},
opacity: 0.8,
}
Plotly.addTraces(el.id, trace1);
})
}", data=dat)

Resources