D3.js stacked bar chart broken transition - dynamic

I am trying to get a stacked bar chart to transition in the same way as this bar chart - http://www.animatedcreations.net/d3/animatedBarChart_Slide.html
I have been following Mike Bostock's "A bar chart, part 2" example, and things are OK up to transitioning the stacked bars in and out.
My broken example is here - http://www.animatedcreations.net/d3/stackedBarChart7.html
I am reasonably sure the problem is with how I set up the data, as shown below. I am even wondering if the data needs to be transformed to be in columns rather than layers?
Insight much appreciated :) Thanks.
From redraw():
// stack the new data
var stacked = d3.layout.stack()(["act1", "act2","act3","other"].map(function(activity){
return stats.map(function(d) {
return {x:(d.hour), y: +d[activity]};
});
}));
// update x axis
x.domain(stacked[0].map(function(d) { return d.x; }));
var valgroup = graph.selectAll("g.valgroup").data(stacked);
// want the data in d. var rect contains the data AND functions.
// I am guessing this is where it all breaks??
var rect = valgroup.selectAll("rect")
.data((function(d) { return d; }), (function(d) { return d.x; }));
// new data set. slide by hour on x axis.

In this problem, the transitions are clearly the trickiest part, so I prefered to go from the simple bar example you provided and go to the stacked bar chart using Mike Bostock's example.
The principal problem with the stacked implementation you provide is that the information is "reversed" as you would want each bar to be in a different element of the data array, this way you can identify your data by its time stamp.
So, first, let's define some data with an array of values for each element:
function next () {
return {
time: ++t,
value: d3.range(3).map(getRand)
};
}
Then, inside of the redraw() function:
First format the data for the bar stacks:
customData = data.map(function(d){
y0=0
return {value:d.value.map(function(d){return {y0:y0, y1: y0+=d}}), time:d.time}
})
Then create the group for each stack of bar
var state = graph.selectAll(".g")
.data(customData, function(d) { return d.time; });
var stateEnter = state.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + x(d.time+1) + ",0)"; });
Then, add the stack of bars of the group:
stateEnter.selectAll("rect")
.data(function(d) { return d.value; })
.enter().append("rect")
.attr("width", barWidth)
.attr("y", function(d) {return y(d.y1); })
.attr("height", function(d) { return y(d.y0) - y(d.y1); })
.attr("class", "bar")
.style("fill", function(d,i) { return color(i); });
Move every bar group to update the x values:
state.transition().duration(1000)
.attr("transform", function(d) {console.log(d); return "translate(" + x(d.time) + ",0)"; });
Remove old values
state.exit()
.attr("transform", function(d) {return "translate(" + x(d.time) + ",0)";})
.remove()
Here is a working example.
PS: Next time, please provide jsFiddles so we don't have to go to the source code of your page to provide a solution. Also, try to minimize as much as possible the code of your example (remove axis, useless parsing etc) so we can concentrate on what is wrong. Also, in the process you will often find the problem by yourself as you isolate it.

Related

Vue Leaflet Setting custom TileLayer bounds

I'm trying to display a large number of images using leaflet and I'm having problems making the tileLayers display properly.
I'm extending the TileLayer class and overriding the createTile function like this:
`
let imageLayer = TileLayer.extend({
createTile: function (coords, done) {
const image = window.L.DomUtil.create("img");
image.src = `http://placekitten.com/g/200/300`;
image.crossOrigin = "";
image.onload = function () {
this.onload = function () {};
done(null, image);
};
return image;
}
});
Result
I want to change the position of the images and to only display them in areas I have already defined (for example between offsetting the image at 0,0 to be at 1,1 or using latLng
([8273.300000000001, 10618.02] , [9232.653535353536, 9658.666464646465])).
Ive tried a bunch of different methods and almost always came back to bounds, which don't work and setting the bounds doesn't change anything.
I don't want to use ImageOverlays because the images I'm trying to display are sliced and named with the leaflet naming scheme (0_0_1.png).

Chartist.js - point value and X label in tooltip?

I have a Chartist.js line chart with points and I'm using tooltip so when you hover over a point you get the value for that point. I'd also like to add the label for the X axis in there so this...
$chart.on('mouseenter', '.ct-point', function () {
const $point = $(this);
const label = Number($point.attr('ct:label')); // doesn't work
const value = Number($point.attr('ct:value')); // does work
$toolTip.html(value + ' at ' + label).show();
});
...would show the value of the point plus the index (X axis label).
I'm 3 years late but maybe someone else still has the same problem.
I had a similar issue with a bar chart that showed amounts of euro by date. The tooltip only showed the amount for each bar (y-value), but I also wanted it to show the date for that bar (x-value).
Chartist.js and the tooltip plugin come with an inbuilt solution for this - though this is neither intuitive nor well documented. So there is no special CSS or JS hacking involved. You only have to modify your series data array.
Before doing so, my data array looked like this (simplified):
var regularData= [];
// [...]
regularData.push(427615440);
regularData.push(428134236);
regularData.push(428543629);
// [...]
var graphData = {
series: regularData
};
new Chartist.Bar('#chart-container', graphData, options);
So in this case i had a bar chart where the bars each had a value, which was also shown in the tooltip. Now I wanted to add the date.
Solution
Instead of pushing the values directly to the array, you need to push container objects to it, that contain key-value pairs for "meta" and "value", where meta specifies the x-value and value specifies the y-value. Note: the "meta" value can be any value, it does not need to be consistent with the x-value in your data/axis.
var labeledData = [];
// [...]
labeledData.push({meta: "15.12.", value: 427615440});
labeledData.push({meta: "16.12.", value: 428134236});
labeledData.push({meta: "17.12.", value: 428543629});
// [...]
var graphData = {
series: [labeledData]
};
new Chartist.Bar('#chart-container', graphData, options);
And here we go! The mouesover-tooltip now also displays the date instead of the y-value only! Hope this helps. :)
Sidenote: I left out my tooltip formatting (euro values) and the customized CSS (bold y-value) for simplicity.

A slider to separate 2 content divs

I want to create an "image comparison slider" for contents. Example:
Codepen Link
// Call & init
$(document).ready(function(){
$('.ba-slider').each(function(){
var cur = $(this);
// Adjust the slider
var width = cur.width()+'px';
cur.find('.resize img').css('width', width);
// Bind dragging events
drags(cur.find('.handle'), cur.find('.resize'), cur);
});
});
// Update sliders on resize.
// Because we all do this: i.imgur.com/YkbaV.gif
$(window).resize(function(){
$('.ba-slider').each(function(){
var cur = $(this);
var width = cur.width()+'px';
cur.find('.resize img').css('width', width);
});
});
function drags(dragElement, resizeElement, container) {
// Initialize the dragging event on mousedown.
dragElement.on('mousedown touchstart', function(e) {
dragElement.addClass('draggable');
resizeElement.addClass('resizable');
// Check if it's a mouse or touch event and pass along the correct value
var startX = (e.pageX) ? e.pageX : e.originalEvent.touches[0].pageX;
// Get the initial position
var dragWidth = dragElement.outerWidth(),
posX = dragElement.offset().left + dragWidth - startX,
containerOffset = container.offset().left,
containerWidth = container.outerWidth();
// Set limits
minLeft = containerOffset + 10;
maxLeft = containerOffset + containerWidth - dragWidth - 10;
// Calculate the dragging distance on mousemove.
dragElement.parents().on("mousemove touchmove", function(e) {
// Check if it's a mouse or touch event and pass along the correct value
var moveX = (e.pageX) ? e.pageX : e.originalEvent.touches[0].pageX;
leftValue = moveX + posX - dragWidth;
// Prevent going off limits
if ( leftValue < minLeft) {
leftValue = minLeft;
} else if (leftValue > maxLeft) {
leftValue = maxLeft;
}
// Translate the handle's left value to masked divs width.
widthValue = (leftValue + dragWidth/2 - containerOffset)*100/containerWidth+'%';
// Set the new values for the slider and the handle.
// Bind mouseup events to stop dragging.
$('.draggable').css('left', widthValue).on('mouseup touchend touchcancel', function () {
$(this).removeClass('draggable');
resizeElement.removeClass('resizable');
});
$('.resizable').css('width', widthValue);
}).on('mouseup touchend touchcancel', function(){
dragElement.removeClass('draggable');
resizeElement.removeClass('resizable');
});
e.preventDefault();
}).on('mouseup touchend touchcancel', function(e){
dragElement.removeClass('draggable');
resizeElement.removeClass('resizable');
});
}
Same as above, but instead of images, I want to create content divs with text, images, html. Like 2 pages with a slider to compare both.
Your comparison script utilizes a resizing a div, so there's no way (that I know of) to achieve this without some considerable trickery (or how to achieve the same effect without resizing).
The only way to prevent the text from automatically wrapping is by using ...
white-space: nowrap;
... but that would just result in one continuous line of text.
Breaks <br> could be inserted from there, but that would just look awful and negate any resizing.
Personally, my approach would be to render the text within a canvas element first (https://www.w3schools.com/graphics/canvas_text.asp).
Since a canvas is treated nearly identical to that of an image, it wouldn't require any changes to the existing comparison script.

Panning the map to certain extent javascript API

I want to limit map extent to the initial extent of the map and limit user from panning more than certain extent.
I tried following but nothing has changed:
map = new Map( "map" , {
basemap: "gray",
center: [-85.416, 49.000],
zoom : 6,
logo: false,
sliderStyle: "small"
});
dojo.connect(map, "onExtentChange", function (){
var initExtent = map.extent;
var extent = map.extent.getCenter();
if(initExtent.contains(extent)){}
else{map.setExtent(initExtent)}
});
Just to flesh out Simon's answer somewhat, and give an example. Ideally you need two variables at the same scope as map:
initExtent to store the boundary of your valid extent, and
validExtent to store the last valid extent found while panning, so that you can bounce back to it.
I've used the newer dojo.on event syntax as well for this example, it's probably a good idea to move to this as per the documentation's recommendation - I assume ESRI will discontinue the older style at some point.
var map;
var validExtent;
var initExtent;
[...]
require(['dojo/on'], function(on) {
on(map, 'pan', function(evt) {
if ( !initExtent.contains(evt.extent) ) {
console.log('Outside bounds!');
} else {
console.log('Updated extent');
validExtent = evt.extent;
}
});
on(map, 'pan-end', function(evt) {
if ( !initExtent.contains(evt.extent) ) {
map.setExtent(validExtent);
}
});
});
You can do the same with the zoom events, or use extent-change if you want to trap everything. Up to you.
It looks like your extent changed function is setting the initial extent variable to the maps current extent and then checking if that extent contains the current extents centre point - which of course it always will.
Instead, declare initExtent at the same scope of the map variable. Then, change the on load event to set this global scope variable rather than a local variable. In the extent changed function, don't update the value of initExtent, simply check the initExtent contains the entire of the current extent.
Alternatively you could compare each bound of the current extent to each bound of the initExtent, e.g. is initExtent.xmin < map.extent.xmin and if any are, create a new extent setting any exceeded bounds to the initExtent values.
The only problem is these techniques will allow the initExtent to be exceeded briefly, but will then snap the extent back once the extent changed function fires and catches up.
I originally posted this solution on gis.stackexchange in answer to this question: https://gis.stackexchange.com/a/199366
Here's a code sample from that post:
//This function limits the extent of the map to prevent users from scrolling
//far away from the initial extent.
function limitMapExtent(map) {
var initialExtent = map.extent;
map.on('extent-change', function(event) {
//If the map has moved to the point where it's center is
//outside the initial boundaries, then move it back to the
//edge where it moved out
var currentCenter = map.extent.getCenter();
if (!initialExtent.contains(currentCenter) &&
event.delta.x !== 0 && event.delta.y !== 0) {
var newCenter = map.extent.getCenter();
//check each side of the initial extent and if the
//current center is outside that extent,
//set the new center to be on the edge that it went out on
if (currentCenter.x < initialExtent.xmin) {
newCenter.x = initialExtent.xmin;
}
if (currentCenter.x > initialExtent.xmax) {
newCenter.x = initialExtent.xmax;
}
if (currentCenter.y < initialExtent.ymin) {
newCenter.y = initialExtent.ymin;
}
if (currentCenter.y > initialExtent.ymax) {
newCenter.y = initialExtent.ymax;
}
map.centerAt(newCenter);
}
});
}
And here's a working jsFiddle example: http://jsfiddle.net/sirhcybe/aL1p24xy/

D3 - best way to dynamically update a Pie chart?

I am trying to dynamically update a Pie chart and I am not 100% sure if this is the best way to do it. What do you say?
var arcsenter = arcs.enter().append("g")
.attr("class", "arc");
arcsenter.append("path")
.attr("d", arc)
.attr("fill", function (d) {
return color(d.data);
})
.on("click", function (evt) {
alert(JSON.stringify(evt));
});
Fiddle - http://jsfiddle.net/hsfid/dr7X3/embedded/result/