I am trying to make a 'Movie-Chart' with ChartJs v3.9.1 where I add and remove data:
https://codepen.io/ipax77/pen/YzLrNEM
<...>
let chartdata = { x: label, y: stepdata.Winrate };
chart.data.labels.push(label);
chart.data.datasets.forEach(dataset => {
if (dataset.label == stepdata.Commander) {
dataset.data.push(chartdata);
dataadded = true;
}
});
chart.update();
<...>
if (chart.data.labels.length > 6) {
let removelabel = chart.data.labels[0];
chart.data.labels.splice(0, 1);
chart.data.datasets.forEach(dataset => {
const index = dataset.data.findIndex(obj => obj.x == removelabel);
if (index > -1) {
dataset.data.splice(index, 1);
}
});
Somehow the animation for new datapoints starts from the xAxis rather than the previouse datapoint. Is there a way to fix this?
I tried working with timescale axis and chartjs-adapter-date-fns which fixes the problem, but then the animation for lines that don't start with the first label are messed up.
You could disable animation on the y-axis by defining options.animation as follows:
options: {
responsive: true,
animation: {
y: {
duration: 0
}
},
Related
I am using Mapbox GL JS to capture frame by frame video of the animation of a geoJson (similar to what is described here: https://docs.mapbox.com/mapbox-gl-js/example/animate-a-line/).
The strategy for encoding mapbox animations into mp4 here are described here:
https://github.com/mapbox/mapbox-gl-js/issues/5297 and https://github.com/mapbox/mapbox-gl-js/pull/10172 .
I would like to show the distance the polyline has covered as I draw each frame. I need the distance to be in the GL itself (as opposed to, for example, an HTML element on top of the canvas), since that's where I'm capturing the video from.
Can someone help describe a performant strategy for doing this to me?
Aside from tearing apart the Mapbox GL JS, why not simply try drawing directly onto the mapbox canvas?
In this example, there are two canvases, created identically.
With the second, there is another requestAnimationFrame loop that adds an overlay.
I've also shown how it can be recorded with MediaRecorder.
const canvas1 = document.getElementById('canvas1')
const canvas2 = document.getElementById('canvas2')
const video = document.getElementById('output')
const status = document.getElementById('status')
let x = 0 // Value to be displayed
const setupCanvas = (canvas) => {
canvas.height = 300
canvas.width = 300
const canvas1Ctx = canvas.getContext('2d')
canvas1Ctx.fillStyle = 'black'
canvas1Ctx.fillRect(300, 100, 100, 100)
const animateCanvas = () => {
x += 2;
if (x > 300) x = 10
canvas1Ctx.fillStyle = 'rgba(20,20,20,1)'
canvas1Ctx.fillRect(0, 0, canvas.width, canvas.height)
canvas1Ctx.beginPath()
canvas1Ctx.arc(x, 100, 20, 0, 2 * Math.PI)
canvas1Ctx.fillStyle = 'rgba(250,0,0,1)'
canvas1Ctx.fill()
requestAnimationFrame(animateCanvas)
}
animateCanvas()
}
const addOverlay = (canvas) => {
const canvasCtx = canvas2.getContext('2d')
function animateOverlay() {
canvasCtx.font = '48px serif'
canvasCtx.fillStyle = 'white'
canvasCtx.fillText(`X: ${x}`, 10, 50)
requestAnimationFrame(animateOverlay)
}
animateOverlay()
}
const initVideoCapture = () => {
var videoStream = canvas2.captureStream(30)
var mediaRecorder = new MediaRecorder(videoStream)
var chunks = []
mediaRecorder.ondataavailable = function(e) {
chunks.push(e.data)
}
mediaRecorder.onstop = function(e) {
var blob = new Blob(chunks, {
'type': 'video/mp4'
})
chunks = []
var videoURL = URL.createObjectURL(blob)
video.src = videoURL
}
mediaRecorder.ondataavailable = function(e) {
chunks.push(e.data)
}
mediaRecorder.start()
status.textContent = 'Recording...'
setTimeout(function() {
mediaRecorder.stop()
status.textContent = 'Complete'
}, 5000)
}
setupCanvas(canvas1)
setupCanvas(canvas2)
addOverlay(canvas2)
initVideoCapture()
<canvas id="canvas1"></canvas>
<canvas id="canvas2"></canvas>
<video id="output" autoplay controls></video>
<p>Status: <span id="status">Loading</span></p>
Try this :
// show the distance a polyline has covered while animating it in mapbox gl js
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v9',
center: [-74.5, 40],
zoom: 9,
})
var geojson = {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: [
[-74.5, 40],
[-74.45, 40.7],
[-74.36, 40.8],
[-74.36, 41.2],
],
},
}
map.on('load', function () {
map.addLayer({
id: 'route',
type: 'line',
source: {
type: 'geojson',
data: geojson,
},
layout: {
'line-join': 'round',
'line-cap': 'round',
},
paint: {
'line-color': '#888',
'line-width': 8,
},
})
var lineDistance = turf.lineDistance(geojson, 'kilometers')
var arc = []
var maxTravelTime = 0
for (var i = 0; i < lineDistance; i += lineDistance / 200) {
var segment = turf.along(geojson, i, 'kilometers')
arc.push(segment.geometry.coordinates)
}
map.addLayer({
id: 'point',
type: 'symbol',
source: {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {},
geometry: {
type: 'Point',
coordinates: arc[0],
},
},
],
},
},
layout: {
'icon-image': 'car',
'icon-rotate': ['get', 'bearing'],
'icon-rotation-alignment': 'map',
'icon-allow-overlap': true,
'icon-ignore-placement': true,
},
})
function animate() {
map.getSource('point').setData(geojson)
if (maxTravelTime < lineDistance) {
maxTravelTime += lineDistance / 200
} else {
maxTravelTime = 0
}
requestAnimationFrame(animate)
}
animate()
})
I am trying to display a stacked bar chart with dates as xAxis. it display the number of sport session by type of sport.
The idea is to have for a specific time range the number of sessions displayed. For example for the last 4 weeks, the number of sessions per day will be displayed, and for the last 12 weeks, it will display the number of sessions per week.
These values are being calculated and displayed fine. The issue is that they are displayed as a 1px wide bar, instead of a "wide" automatically calculated bar width.
If someone have an idea how this fix this kind of issue... please help!
Data are structured as follows. I only show concerned data
const sessions_summary = [
{
activity_name: 'regular_biking',
date_time: '2020-03-18T15:57:47.853Z',
// ...
},
{
activity_name: 'swimming',
date_time: '2020-03-18T15:57:47.853Z'
},
{
activity_name: 'running',
date_time: '2020-03-19T15:57:47.853Z'
},
// ...
];
Crossfilter:
const ndx = crossfilter(sessions_summary);
const Dimension = ndx.dimension(function(d) {
return d3.timeDay(new Date(d.date_time));
});
Scaletime:
const today = new Date(new Date().toDateString());
const minDate = d3.timeDay(
new Date(
new Date().setDate(
today.getDate() - parseFloat(timeranges[timerange_name]) // 7 or 30 or 90 or 180 or 360 : number of days, depends on the interval selected in Select Entry
)
)
);
let maxDate = d3.timeDay(today);
maxDate = d3.timeDay.offset(maxDate, 1);
const scaletime = d3.scaleTime().domain([minDate, maxDate]);
Chart.x(scaletime);
const interval = intervals[timerange_name]; // d3.timeDay or d3.timeWeek or d3.timeMonth, depending on the choice made in Select Entry
Chart.xUnits(interval);
Group:
const types = [...new Set(sessions_summary.map(session => session.type))];
Group = Dimension.group(function(k) {
return interval(k);
}).reduce(
function(p, v) {
if (v.type in p.types) {
p.types[v.type]++;
} else {
p.types[v.type] = 1;
}
return p;
},
function(p, v) {
p.types[v.type]--;
if (p.types[v.type] === 0) {
delete p.types[v.type];
}
return p;
},
function() {
return {
types: {}
};
}
);
Chart.group(Group, types[0], sel_stack(types[0])).render();
for (let i = 1; i < types.length; i++) {
Chart.stack(Group, types[i], sel_stack(types[i]));
}
Bar Chart:
const Chart = dc.barChart('#sessions_chart');
Chart.width(968)
.height(240)
.elasticY(true)
.margins({
left: 40,
top: 10,
right: 20,
bottom: 40
})
.gap(5)
.centerBar(true)
.round(d3.timeDay.round)
.alwaysUseRounding(true)
.xUnits(d3.timeDays)
.brushOn(false)
.renderHorizontalGridLines(true)
.renderVerticalGridLines(false)
.dimension(Dimension)
.title(d => {
return (
'Date: ' +
new Date(d.key).toDateString() +
'\n' +
'Sessions: ' +
Object.keys(d.value.types)
);
});
Chart.legend(
dc
.legend()
.x(40)
.y(465)
.gap(10)
.horizontal(true)
.autoItemWidth(true)
);
Chart.render();
Complete code can be found on JSFiddle
Thanks in advance
[SOLVED]
The issue was the double xUnits, and the wrong use of d3.TimeDay instead of d3.TimeDays.
I am working with chartjs, I am trying to animate chart from right to left or left to right on load.
var canvas = document.getElementById('chart_canvas');
var ctx = canvas.getContext('2d');
// Generate random data to plot
var DATA_POINT_NUM = 10;
var data = {
labels: [],
datasets: [
{
data: [],
},
]
}
for (var i=0; i<DATA_POINT_NUM; i++) {
data.datasets[0].data.push(Math.random()*10);
data.labels.push(String.fromCharCode(65+i));
}
var oldDraw = Chart.controllers.line.prototype.draw;
Chart.controllers.line.prototype.draw = function(animationFraction) {
var animationConfig = this.chart.options.animation;
if (animationConfig.xAxis === true) {
var ctx = this.chart.chart.ctx;
var hShift = (1-animationFraction)*ctx.canvas.width;
ctx.save();
ctx.setTransform(1, 0, 0, 1, hShift,0);
if (animationConfig.yAxis === true) {
oldDraw.call(this, animationFraction);
} else {
oldDraw.call(this, 1);
}
ctx.restore();
} else if (animationConfig.yAxis === true) {
oldDraw.call(this, animationFraction);
} else {
oldDraw.call(this, 1);
}
}
var lineChart = new Chart(ctx, {
type: 'line',
data: data,
options: {
animation: {
duration: 5000,
xAxis: true,
yAxis: true,
}
}
});
Example 1
The above code works fine on windows, but I'm facing issue on mac devices.While animating from left to right the data displays incorrectly means that the data moves to upward from x axis.How to fix this issue?
I am attaching screenshot.
Screenshot
Please change setTransform to transform.
Try the following code
var canvas = document.getElementById('chart_canvas');
var ctx = canvas.getContext('2d');
// Generate random data to plot
var DATA_POINT_NUM = 10;
var data = {
labels: [],
datasets: [
{
data: [],
},
]
}
for (var i=0; i<DATA_POINT_NUM; i++) {
data.datasets[0].data.push(Math.random()*10);
data.labels.push(String.fromCharCode(65+i));
}
var oldDraw = Chart.controllers.line.prototype.draw;
Chart.controllers.line.prototype.draw = function(animationFraction) {
var animationConfig = this.chart.options.animation;
if (animationConfig.xAxis === true) {
var ctx = this.chart.chart.ctx;
var hShift = (1-animationFraction)*ctx.canvas.width;
ctx.save();
ctx.transform(1, 0, 0, 1, hShift,0);
if (animationConfig.yAxis === true) {
oldDraw.call(this, animationFraction);
} else {
oldDraw.call(this, 1);
}
ctx.restore();
} else if (animationConfig.yAxis === true) {
oldDraw.call(this, animationFraction);
} else {
oldDraw.call(this, 1);
}
}
var lineChart = new Chart(ctx, {
type: 'line',
data: data,
options: {
animation: {
duration: 5000,
xAxis: true,
yAxis: true,
}
}
});
I'm using a dimple bar chart legend to filter the chart's data as given in this fiddle https://jsfiddle.net/fbpnzy9u/.
var svg = dimple.newSvg("#chartContainer", 590, 400);
var data = [
{ Animal: "Cats", Value: (Math.random() * 1000000) },
{ Animal: "Dogs", Value: (Math.random() * 1000000) },
{ Animal: "Mice", Value: (Math.random() * 1000000) }
];
var myChart = new dimple.chart(svg, data);
myChart.setBounds(60, 30, 510, 305)
var x = myChart.addCategoryAxis("x", "Animal");
x.addOrderRule(["Cats", "Dogs", "Mice"]);
myChart.addMeasureAxis("y", "Value");
myChart.addSeries("Animal", dimple.plot.bar);
var legend = myChart.addLegend(500,10,100, 100, "right");
myChart.draw();
d3.select("#btn").on("click", function() {
myChart.data = [
{ Animal: "Cats", Value: (Math.random() * 1000000) },
{ Animal: "Dogs", Value: (Math.random() * 1000000) },
{ Animal: "Mice", Value: (Math.random() * 1000000) }
];
myChart.draw(1000);
});
// filter
myChart.legends = [];
// Get a unique list of y values to use when filtering
var filterValues = dimple.getUniqueValues(data, "Animal");
// Get all the rectangles from our now orphaned legend
legend.shapes.selectAll('rect').on("click", function (e) {
// This indicates whether the item is already visible or not
var hide = false;
var newFilters = [];
//If the filters contain the clicked shape hide it
filterValues.forEach(function (f) {
if (f === e.aggField.slice(-1)[0]) {
hide = true;
} else {
newFilters.push(f);
}
});
if (hide) {
d3.select(this).style("opacity", 0.2);
} else {
newFilters.push(e.aggField.slice(-1)[0]);
d3.select(this).style("opacity", 0.8);
}
// // Update the filters
filterValues = newFilters;
//Filter the data
myChart.data = dimple.filterData(data, "Animal", filterValues);
myChart.draw(800);
});
Although the filtering happens as expected, it throws a d3 error on to the console:
Error: attribute x: Expected length, "NaN"
Any idea as to what may be causing this error?
Not sure what might be causing the error.
What my intuition tells me is that since the shapes from the previous set of data are being accessed when the chart is being redrawn with the new data since they're separate objects in memory. That is, the data generates the series which lives in the chart's svg object but is in no way tied to changes of the data until a chart is redrawn. If this is true, that could be why it's not finding values for the axes in order to draw the shapes that live there. (I wonder if there's a fail silently option in the future).
Anyway, if you are redrawing the chart, you could do this workaround:
if (oldChartData.length > newChartData.length) {
chart.svg.selectAll('*').remove();
createChart(newChartData);
}
It's dirty but it works.
Edit: Here's the related github issue.
I was looking for a fix/solution for that and found this topic:
http://highslide.com/forum/viewtopic.php?t=3030
function fixElement(el) {
var stl = el.style;
stl.position = 'fixed';
stl.top = (parseInt(stl.top) - hs.page.scrollTop) +'px';
stl.left = (parseInt(stl.left) - hs.page.scrollLeft) +'px';
}
function unfixElement(el) {
var stl = el.style;
stl.position = 'absolute';
stl.top = (parseInt(stl.top) + hs.page.scrollTop) +'px';
stl.left = (parseInt(stl.left) + hs.page.scrollLeft) +'px';
}
if (!hs.ie || hs.ieVersion() > 6) {
hs.Expander.prototype.onAfterExpand = function() {
fixElement (this.wrapper);
if (this.outline) fixElement(this.outline.table);
};
hs.Expander.prototype.onBeforeClose = function() {
unfixElement (this.wrapper);
if (this.outline) unfixElement(this.outline.table);
};
}
Got a small hack on the forum topic, but the hack does not work on any Internet Explorer that I tried (IE7, IE8 and IE9).
Does anyone has a "fix" on the hack to make it work on IE?
I think it's something related on this part of the code, this condition:
if (!hs.ie || hs.ieVersion() > 6)
I removed and it worked, but maybe this code could be changed.
Thank you.
The below code is what you need. Live demo: http://www.highslide.com/studies/position-fixed.html
Note: requires highslide-full.js
// Highslide fixed popup mod. Requires the "Events" component.
if (!hs.ie || hs.uaVersion > 6) hs.extend ( hs.Expander.prototype, {
fix: function(on) {
var sign = on ? -1 : 1,
stl = this.wrapper.style;
if (!on) hs.getPageSize(); // recalculate scroll positions
hs.setStyles (this.wrapper, {
position: on ? 'fixed' : 'absolute',
zoom: 1, // IE7 hasLayout bug,
left: (parseInt(stl.left) + sign * hs.page.scrollLeft) +'px',
top: (parseInt(stl.top) + sign * hs.page.scrollTop) +'px'
});
if (this.outline) {
stl = this.outline.table.style;
hs.setStyles (this.outline.table, {
position: on ? 'fixed' : 'absolute',
zoom: 1, // IE7 hasLayout bug,
left: (parseInt(stl.left) + sign * hs.page.scrollLeft) +'px',
top: (parseInt(stl.top) + sign * hs.page.scrollTop) +'px'
});
}
this.fixed = on; // flag for use on dragging
},
onAfterExpand: function() {
this.fix(true); // fix the popup to viewport coordinates
},
onBeforeClose: function() {
this.fix(false); // unfix to get the animation right
},
onDrop: function() {
this.fix(true); // fix it again after dragging
},
onDrag: function(sender, args) {
//if (this.fixed) { // only unfix it on the first drag event
this.fix(true);
//}
}
});