R Markdown - Highcharter - Animate graph when visible, rather on page load? - r

I am writing a report in R Markdown, it contains multiple animated highcharts.
The animations work fine, however they all run when the html page loads (after knitting), instead of when the user scrolls to it, so essentially the animation is pointless as the user never sees it.
An example of an animated chart is at the bottom of this question.
Is there a way to make it animate when it appears? All the examples I have found use jsfiddle and I am using R Markdown.
Many thanks
library(dplyr)
library(stringr)
library(purrr)
n <- 5
set.seed(123)
df <- data.frame(x = seq_len(n) - 1) %>%
mutate(
y = 10 + x + 10 * sin(x),
y = round(y, 1),
z = (x*y) - median(x*y),
e = 10 * abs(rnorm(length(x))) + 2,
e = round(e, 1),
low = y - e,
high = y + e,
value = y,
name = sample(fruit[str_length(fruit) <= 5], size = n),
color = rep(colors, length.out = n),
segmentColor = rep(colors2, length.out = n)
)
hcs <- c("line") %>%
map(create_hc)
hcs

Ok, I worked out how to do it myself, going to post the answer here in case someone stumbles across this post in the future.
First of all, I found NOTHING on how to do this in R.
So, I decided to do this in JS, AFTER I had knitted the R Markdown document to HTML, as it wouldn't work in R Markdown.
Once it is a HTML file, open it in TextEdit or Notepad, and add the following code just before one of the charts:
<script>
(function (H) {
var pendingRenders = [];
// https://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport/7557433#7557433
function isElementInViewport(el) {
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (
window.innerHeight ||
document.documentElement.clientHeight
) &&
rect.right <= (
window.innerWidth ||
document.documentElement.clientWidth
)
);
}
H.wrap(H.Series.prototype, 'render', function deferRender(proceed) {
var series = this,
renderTo = this.chart.container.parentNode;
// It is appeared, render it
if (isElementInViewport(renderTo) || !series.options.animation) {
proceed.call(series);
// It is not appeared, halt renering until appear
} else {
pendingRenders.push({
element: renderTo,
appear: function () {
proceed.call(series);
}
});
}
});
function recalculate() {
pendingRenders.forEach(function (item) {
if (isElementInViewport(item.element)) {
item.appear();
H.erase(pendingRenders, item);
}
});
}
if (window.addEventListener) {
['DOMContentLoaded', 'load', 'scroll', 'resize']
.forEach(function (eventType) {
addEventListener(eventType, recalculate, false);
});
}
}(Highcharts));
</script>
The charts then animate when you scroll to them, rather than when you open the HTML file.
Note: The JSFIDDLE I got the code from was from here:
https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/studies/appear/

Related

Increase size of one of the series in R highchart

I'm trying to show a line and % changes in a single highchart plot, but the changes are very little and It can't be seen in the plot. I made a simplified code to show my problem:
a <- c(300,200, 400, 10, 40, 80)
b <- c(0.8, 2, -2, -1.5, -1.1, 2)
d<-cbind(a,b)
dt <- seq(as.Date("2018-01-01"), as.Date("2018-01-06"), by = "days")
ts <- xts(d, dt )
highchart(type="stock") %>%
hc_add_series(ts$a,
type = "line",
color="black") %>%
hc_add_series(ts$b,
type = "lollipop",
color="red")
I need to increase the size of "ts$b" in the plot, how can I do it? I also tried with two axis, but It seems doesn't solve the problem.
I see two solutions to achieve that.
The first you mentioned - using two yAxis and manipulating their height and top distance.
Example JS code:
yAxis: [{
height: '90%',
opposite: false
},
{
visible: false,
top: '83%',
height: '15%',
}
]
Demo:
https://jsfiddle.net/BlackLabel/0826r7sh/
Another way is using a modified logarithmic axis. Negative values can't be plotted on a log axis, because by nature, the axis will only show positive values. In that case you need to use a custom extension according to the following thread:
Highcharts negative logarithmic scale solution stopped working
(function(H) {
H.addEvent(H.Axis, 'afterInit', function() {
const logarithmic = this.logarithmic;
if (logarithmic && this.options.custom.allowNegativeLog) {
// Avoid errors on negative numbers on a log axis
this.positiveValuesOnly = false;
// Override the converter functions
logarithmic.log2lin = num => {
const isNegative = num < 0;
let adjustedNum = Math.abs(num);
if (adjustedNum < 10) {
adjustedNum += (10 - adjustedNum) / 10;
}
const result = Math.log(adjustedNum) / Math.LN10;
return isNegative ? -result : result;
};
logarithmic.lin2log = num => {
const isNegative = num < 0;
let result = Math.pow(10, Math.abs(num));
if (result < 10) {
result = (10 * (result - 1)) / (10 - 1);
}
return isNegative ? -result : result;
};
}
});
}(Highcharts));
.
yAxis: {
type: 'logarithmic',
custom: {
allowNegativeLog: true
}
},
Demo
https://jsfiddle.net/BlackLabel/nw6osucm/

Highcharter tooltip not working with many series

I'm trying to create a line chart with many series (more than 50) using the highcharter package in R.
When I use fewer series (less than 40) my tooltip works correctly, but when the number of series increase, the tooltip stops displaying the category of the x-axis and starts simply counting from zero to the number of ticks. Here is the code to reproduce:
library(dplyr)
library(highcharter)
test1<-data.frame(Value=rnorm(300,0,1),Type=rep(c('A','B','C'),each=100),Period=rep(seq(as.Date('2020-01-01'),as.Date('2020-04-09'),by=1),3))
test2<-data.frame(Value=rnorm(500,0,1),Type=rep(as.character(1:50),each=10),Period=rep(seq(as.Date('2020-01-01'),as.Date('2020-01-10'),by=1),50))
category1<-unique(format(test1$Period,'%d/%b/%y'))
category2<-unique(format(test2$Period,'%d/%b/%y'))
hchart(test1,type='line',hcaes(x=as.character(Period),y=Value,group=Type)) %>%
hc_xAxis(type='category',categories=as.list(category1),title='',labels=list(rotation=-30)) %>%
hc_legend(align='left',maxHeight=100) %>%
hc_plotOptions(line=list(marker=list(enabled=F))) %>%
hc_tooltip(shared=T,
formatter=JS("function() {
var s = '';
$.each(this.points, function(i, point) {
if (point.y !== 0) {
s += '<br><b>'+ point.series.name +': </b>'+ Highcharts.numberFormat(point.y,2,',','.');
}
});
return this.x + s;
}"))
hchart(test2,type='line',hcaes(x=as.character(Period),y=Value,group=Type)) %>%
hc_xAxis(type='category',categories=as.list(category2),title='',labels=list(rotation=-30)) %>%
hc_legend(align='left',maxHeight=100) %>%
hc_plotOptions(line=list(marker=list(enabled=F))) %>%
hc_tooltip(shared=T,
formatter=JS("function() {
var s = '';
$.each(this.points, function(i, point) {
if (point.y !== 0) {
s += '<br><b>'+ point.series.name +': </b>'+ Highcharts.numberFormat(point.y,2,',','.');
}
});
return this.x + s;
}"))
And here are the outputs:
I don't know if this is a limitation because of the volume of information, but it is a strange behavior that I could not solve.

paperjs - How to apply brightness/contrast like effects on the raster image?

I have tried to apply the image adjustment options using paper.js, but it will only apply to the fillcolor.
Does anyone know how to apply brightness, contrast or other image adjustments to the raster image?
For example:
var url = 'http://images.com/q.jpg';
var raster = new paper.Raster(url);
raster.brightness = .5;
Are there any pre-defined functions available for image adjustment in paper.js?
Nope, but you can play with blend modes or opacity.
I would advise using specialized WebGL libraries like glfx or webgl-filter for image effects (I didn't try them, but they seem powerful).
function reDrawImage(lightness = 10,contrast = 1.5) {
const raster = paper.project.activeLayer.children[0] as paper.Raster
const ctx: CanvasRenderingContext2D = currentRaster.getContext(true)
const imageData = ctx.getImageData(0, 0, currentRaster.width, currentRaster.height)
for (let i = 0; i < imageData.data.length; i += 4) {
imageData.data[i] = saturate_cast(imageData.data[i] * (contrast + lightness));
imageData.data[i+1] = saturate_cast(imageData.data[i+1] * (contrast + lightness));
imageData.data[i+2] = saturate_cast(imageData.data[i+2] * (contrast + lightness));
}
raster.setImageData(imageData, new Point(0, 0))
}
function saturate_cast(num: number) {
if (num > 255) {
return 255
}
if (num < 0) {
return 0
}
return num
}

d3 map - After using blur filter, zoom does not work properly

I am using the blur effect on the d3 map as given here: http://geoexamples.blogspot.in/2014/01/d3-map-styling-tutorial-ii-giving-style.html?
But after using this method (because of how the data is loaded..using datum) my zoom functionality behaves randomly. Irrespective of where I click it zooms to the same point. Also, the animations have become very slow after using the filter.
Is there any other way to achieve blur? Or a solution to this problem?
Any help?
Thanks.
This is the code for the world creation in case when filtering is required (use of datum as per the code on the above site).
d3.json("world-110m2.json", function(error, world) {
g.insert("path")
.datum(topojson.feature(world, world.objects.land))
.attr("d", path);
g.insert("path")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("d", path)
.append("path");
g.selectAll("path")
.on("click", click);})
This is the code used in case filtering is not required (No use of datum - maybe the datum is causing the issue)
d3.json("world-110m2.json", function(error,topology) {
g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries)
.geometries)
.enter()
.append("path")
.attr("d",path)
.on("click", click);)}
This is the zoom function: got the code from here: http://bl.ocks.org/mbostock/2206590
function click(d) {
var x, y, k;
var centered;
if (d && centered !== d) {
var centroid = path.centroid(d);
x = centroid[0];
y = centroid[1];
k = 4;
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
if (active === d) return reset();
g.selectAll(".active").classed("active", false);
d3.select(this).classed("active", active = d);
var b = path.bounds(d);
g.selectAll("path")
.classed("active", centered && function(d) { return d === centered; });
g.transition()
.duration(750)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
.style("stroke-width", 1.5 / k + "px");
}
The blur filter consumes lots of resources, as indicated in the post. Speciallly if you combine it with other filters.
One solution would be using Canvas instead of SVG. Here you have some filters using the Canvas element. It should be possible to achieve the same result.
I can't find why the zoom stops working, but the performance is slower because you use all the data, so you are applying the filter to all the data instead of using only the part of the word you are showing, so you are using a much bigger image when you zoom.

Passing a variable to setTimeout()

I've installed a Jcarousel on Drupal 7 but I need it to scroll horizontally to both sides when the client hovers over the arrows.
I've been trying to pass a variable to the Timeout function and it doesn't seem to work.
In the following code Timeout recognizes only: var n = function () {c.next();};
I need to be able to tell timeout to either scroll left or right using c.prev() or c.next() depending on which arrow the user clicked.
var c = this;
var k = 1;
var n = function () {c.next();};
if (k == 1) n = function () {c.prev();};
if (k == 5) n = function () {c.next();};
this.timer = window.setTimeout(n, 500)
I've also tried to do it this way and it doesn't work either.
var c = this;
var k = 5;
this.timer = window.setTimeout(function() {c.nextprev(k);}, 500)
...
nextprev: function(k) {
if (k === 1) return "prev()";
if (k === 5) return "next()";
}
Any help or guideline will be appreciated!
Try this, it doesn't feel 100% right, but introduces some techniques you seem to need:
c.nextprev to execute immediately and return a function that will do what was really needed, capturing c and k as a closure...
c.nextprev = function(k){
return function(){
// I feel like prev and next might be backwards... think about that
if (k === 1) c.prev();
if (k === 5) c.next();
// do nothing if k not 1 nor 5
}
};
c.timer = window.setTimeout(c.nextprev(k), 500);
...or maybe just do this without all the preceding code....
Here bind sets "this" back to c.
setTimeout( (k === 5)? c.next.bind(c): ((k === 1)? c.prev.bind(c): function(){} ) );

Resources