Show "now" line on event drops and add slight margin to the right - d3.js

1) I would like to display a vertical line with the "now" value
as well as
2) add a slight margin to the right - Currently "now" events are cropped because there is no space to the right of the last (most recent) event.
UPDATE: thanks to a suggestion in the comments, this is done easily by doing:
maxDate.add(1).hour()
with the fantastic date manipulation library at https://github.com/datejs/Datejs
e.g.:
Current:
Desired:
Questions:
how can I retrieve the x value for the "now" value from the svg so I can draw the line?
How can I add a time margin (e.g. 1 hour) or relative margin (e.g. 10%) after the last event?

To draw the line you would map the time into the scale. Then using that would draw at the returned value. You could do something like:
const chart = eventDrops({ d3 });
var now = new Date()
var x = chart.scale()
d3.select('#chart')
.append('line')
.attr('id', 'ticker')
.attr('y1', 0)
.attr('y2', 100)
.attr('x1', x(now))
.attr('x2', x(now))
.attr('transform', `translate(200, 0)`)
.style('stroke-width', 2)
.style('stroke', 'blue')
.style('fill', 'none')
As commented you would add the date margin to your chart's end date.
var maxDate = Date().today()
const chart = eventDrops({
d3,
range: {
end: maxDate.add(1).hour()
}
});

Related

Add text in rect svg and append it to arc in donut chart

I wanted to add labels to each arc in donut chart. I've added by taking the centroid of each arc and adding, but somehow it is not adding in correct position. I can't figure it out so I need some help regarding it. I've added my code in codepen. The link is here.
My donut should look like this.
Sample code is:
svg.selectAll(".dataText")
.data(data_ready)
.enter()
.each(function (d) {
var centroid = arc.centroid(d);
d3.select(this)
.append('rect')
.attr("class", "dataBG_" + d.data.value.label)
.attr('x', (centroid[0]) - 28)
.attr('y', (centroid[1]) - 5)
.attr('rx', '10px')
.attr('ry', '10px')
.attr("width", 50)
.attr("height", 20)
.style('fill', d.data.value.color)
.style("opacity", 1.0);
d3.select(this)
.append('text')
.attr("class", "dataText_" + d.data.value.label)
.style('fill', 'white')
.style("font-size", "11px")
.attr("dx", (centroid[0]) - 7)
.attr("dy", centroid[1] + 7)
.text(Math.round((d.data.value.value)) + "%");
});
Thanks in advance.
The difference between the "bad" state on codepen and the desired state is that in the one you don't like, you take the centroid and then you center your text on it. The centroid of a thick arc is the midpoint of the arc that runs from the midpoint of one line-segment cap to the other. This is roughly "center of mass" of the shape if it had some finite thickness and were a physical object. I don't think it's what you want. What you want is the midpoint of the outer arc. There's no function to generate it, but it's easy enough to calculate. Also, I think you want to justify your text differently for arcs whose text-anchor point is on the left hand of the chart from those on the right half. I'm going copy your code and modify it, with comments explaining.
// for some reason I couldn't get Math.Pi to work in d3.js, so
// I'm just going to calculate it once here in the one-shot setup
var piValue = Math.acos(-1);
// also, I'm noting the inner radius here and calculating the
// the outer radius (this is similar to what you do in codepen.)
var innerRadius = 40
var thickness = 30
var outerRadius = innerRadius + thickness
svg.selectAll(".dataText")
.data(data_ready)
.enter()
.each(function (d) {
// I'm renaming "centroid" to "anchor - just a
// point that relates to where you want to put
// the label, regardless of what it means geometrically.
// no more call to arc.centroid
// var centroid = arc.centroid(d);
// calculate the angle halfway between startAngle and
// endAngle. We can just average them because the convention
// seems to be that angles always increase, even if you
// if you pass the 2*pi/0 angle, and that endAngle
// is always greater than startAngle. I subtract piValue
// before dividing by 2 because in "real" trigonometry, the
// convention is that a ray that points in the 0 valued
// angles are measured against the positive x-axis, which
// is angle 0. In D3.pie conventions, the 0-angle points upward
// along the y-axis. Subtracting pi/2 to all angles before
// doing any trigonometry fixes that, because x and y
// are handled normally.
var bisectAngle = (d.startAngle + d.endAngle - piValue) / 2.0
var anchor = [ outerRadius * Math.cos(bisectAngle), outerRadius * Math.sin(bisectAngle) ];
d3.select(this)
.append('rect')
.attr("class", "dataBG_" + d.data.value.label)
// now if you stopped and didn't change anything more, you'd
// have something kind of close to what you want, but to get
// it closer, you want the labels to "swing out" from the
// from the circle - to the left on the left half of the
// the chart and to the right on the right half. So, I'm
// replacing your code with fixed offsets to code that is
// sensitive to which side we're on. You probably also want
// to replace the constants with something related to the
// the dynamic size of the label background, but I leave
// that as an "exercise for the reader".
// .attr('x', anchor[0] - 28)
// .attr('y', anchor[1] - 5)
.attr('x', anchor[0] < 0 ? anchor[0] - 48 : anchor[0] - 2)
.attr('y', anchor[1] - 10
.attr('rx', '10px')
.attr('ry', '10px')
.attr("width", 50)
.attr("height", 20)
.style('fill', d.data.value.color)
.style("opacity", 1.0);
d3.select(this)
.append('text')
.attr("class", "dataText_" + d.data.value.label)
.style('fill', 'white')
.style("font-size", "11px")
// changing the text centering code to match the box
// box-centering code above. Again, rather than constants,
// you're probably going to want something a that
// that adjusts to the size of the background box
// .attr("dx", anchor[0] - 7)
// .attr("dy", anchor[1] + 7)
.attr("dx", anchor[0] < 0 ? anchor[0] - 28 : anchor[0] + 14)
.attr("dy", anchor[1] + 4)
.text(Math.round((d.data.value.value)) + "%");
});
I tested. this code on your codepen example. I apologize if I affected your example for everyone - I'm not familiar with codepen and I don't know the collaboration rules. This is all just meant by way of suggestion, it can be made a lot more efficient with a few tweaks, but I wanted to keep it parallel to make it clear what I was changing and why. Hope this gives you some good ideas.

The axis label at x=0 does not show up

I am using D3 to draw a line chart. The value at x=0 does not show up.
The code for the axis is shown below.
const xScale = d3
.scaleTime()
.domain(d3.extent(data[0].series, d => d.time))
.range([xPadding, width - xPadding]);
const xAxis = d3
.axisBottom(xScale)
.ticks(4)
.tickSizeOuter(0)
.tickSizeInner(0)
.tickFormat(d3.timeFormat('%Y'));
I am not sure why it is not showing up the label at x=0, which is 2014. On checking the SVG, only three tick marks are displayed, but the one at x=0 is not in the SVG element.
CodePen for this: https://codepen.io/vijayst/pen/bLJYoK?editors=1111
I see different solutions which have their pros and cons. The third solution should be the cleanest and most generic.
Add the left tick manually:
Since d3 handles itself the location of x-axis ticks, one way of doing so would (if the data set is fixed) would be to manually add the missing tick:
svg
.append("g")
.append("text")
.text("2014-02-01") // can be retrieved from data instead of being harcoded
.style("font-size", 10)
.style("font-family", "sans-serif")
.attr("transform", "translate(0," + (height - yPadding + 10) + ")")
which looks great, but in this case you might have problems if for a given dataset, d3 chooses to display a tick close to the left edge of the axis. Both d3's tick and the label we've included could overlap.
Modify the x-scale to start before the first day of the year:
An other solution would be to increase the x-axis range on the left to make it start one month before the first point's date. To try this out, we can replace:
.domain(d3.extent(data[0].series, d => d.time))
with
.domain(d3.extent([new Date(2013, 12), new Date(2019, 1)]))
which allow d3 to legitimately include a "year-tick" for 2014 at the begin of the x-axis.
but in this case, the first point will have an offset with the begin of the x-axis range.
Push a specific tick to ticks auto-generated by d3:
An other solution: we can push a specific tick to the ticks auto-generated by d3. But this requires to modify the format of ticks to "%Y-%m".
In order to do this, replace:
.tickFormat(d3.timeFormat("%Y"));
with
.tickFormat(d3.timeFormat("%Y-%m"));
we can then push a new specific tick to the set of ticks generated by d3:
var ticks = xScale.ticks();
ticks.push(new Date(2014, 1, 1));
xAxis.tickValues(ticks);
and include some padding in the left and the right of the chart since now tick labels have a part displayed outside the graph:
const svg = d3
.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.style("padding-left", 15)
.style("padding-right", 15);

Live updating line chart - Wont smoothly scroll off screen (wiggles)

I have a line chart that I've built for displaying live data. I've read through https://bost.ocks.org/mike/path/ and I can get it to smoothly come on screen, but for the life of me, I cant get it to not 'wiggle' when I remove a datapoint and the corresponding line section from the screen.
Here is the section of code where I am updating the line and x-axis.
lineselection.datum(data)
.transition()
.duration(1000)
.ease(d3.easeLinear)
.attr('d', line)
.attr('transform', 'translate(' + (xScale.range()[0]/data.length) + ',0)');
xScale = d3.scaleTime()
.domain([time, start_time])
.range([width - (margin * 2), 0])
.clamp(true);
g.selectAll('.x.axis')
.transition()
.duration(1000)
ease(d3.easeLinear)
.call(xAxis);
This produces a nice smooth line chart, where the new section of line is drawn outside of the clip-path, and then translated left into view. The x-axis scrolls along wonderfully with it.
In my data update method, I do a check to see if we are in my data view window (ie, we only want to view the last, say 60 seconds of data). If a point falls outside the time frame, we remove it, and update the start_time to reflect.
if ((time_frame + start_time) < time){
for (d = 0; d < data.length; d++){
if ((data[d].x + time_frame) < time){
data.shift();
}
else{
start_time = data[d].x;
break;
}
}
}
I am newer to both JS and D3, so please be gentle. Any help would be appreciated. Am I missing something obvious?
Here is a jsfiddle. https://jsfiddle.net/3kn56mb7/2/
Edit. Added a jsfiddle.
Here is the updated jsfiddle. https://jsfiddle.net/3kn56mb7/12/
Thank you elias for helping me get to this!
Here is my draw function. I used selection.interrupt() to overcome a jitter.
function draw(){
var line = d3.line()
.x(function(d) { return xScale(d.x); })
.y(function(d) { return yScale(d.y); });
var lineselection = svg.selectAll('.line_')
.select('path');
lineselection.interrupt()
.transition()
.duration(duration)
.ease(d3.easeLinear)
.attr('transform', 'translate(' + -(xScale.range()[0]/((duration / 100)-2)) + ',' + margin + ')');
if (data[0].x < time - time_frame - duration ){
console.log('shift');
data.shift();
}
lineselection.attr('d',line)
.attr('transform', 'translate(0,' + margin + ')');
start_time = time - (duration * 2) - time_frame;
xScale.domain([time, time + (duration * 2) - time_frame])
.range([width - (margin *2),0]);
d3.select('body')
.select('svg')
.select('g')
.selectAll('.x.axis')
.transition()
.duration(duration)
.ease(d3.easeLinear)
.call(xAxis);
}
EDIT: I updated the code to reflect the selection.interrupt() change as well as included the entire draw function. I also updated the jsfiddle link.
EDIT 2: Fixed a margin error and updated the jsfiddle link.
EDIT 3: I came back to use this as a reference. I am looking at this bit of code a year later, and couldnt figure out why I did some weird math. I must have been breathing diesel fumes. In anycase, I redid the math so that it made sense (why was I dividing duration by 100 and subtracting 2)?
Changed this: https://jsfiddle.net/3kn56mb7/11/
.attr('transform', 'translate(' + -(xScale.range()[0]/((duration / 100)-2)) + ',' + margin + ')');
To this: https://jsfiddle.net/3kn56mb7/12/
.attr('transform', 'translate(' + -(xScale(data[data.length-1].x) -xScale.range()[0]) + ',' + margin + ')');
I am now taking the last data point every draw cycle, and getting its time (x value) and converting that to a position in the range. Then I subtract the max range out (width-margin*2). Make the difference negative so it moves left.
The basic problem is that your data elements' indices are changing by removing the obsolete elements. I've faced the very same issue in a project of mine, and solved it in the following way:
I recommend that doing what you now in the transition in two separate steps:
when removing the element, draw a new path with the new indices - do this immediately, without a transition, so the wiggling does not appear
after this has been done, a transition can happen, which is then only a translation, during which no elements get deleted

Axis with either dynamic tickFormats or multipe axis with different timeScales

I'd like to either:
dynamically adjust the tickFormat of an D3 timeaxis depending on current zoom, like first showing years -> zoom in -> show months -> zoom in -> show days and so on up to seconds.
I've tried a bit. See this fiddle.
var w = 700,
h = 50,
xY = d3.time.scale().range([0, w]),
xAxisY = d3.svg.axis()
.scale(xY)
.orient("bottom")
.ticks(10)
.tickSize(10, 1)
var svgY = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g");
svgY.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + 10 + ")");
svgY.append("svg:rect")
.attr("class", "pane")
.attr("width", w)
.attr("height", h)
.call(d3.behavior.zoom().on("zoom", zoom));
xY.domain([new Date(2000, 0, 1), new Date(2014, 0, 0)]);
xY.ticks(d3.time.minute, 1);
draw();
function draw() {
console.log("drawing");
svgY.select("g.x.axis").call(xAxisY);
}
function zoom() {
console.log("zooming");
d3.event.transform(xY); // TODO d3.behavior.zoom should support extents
draw();
}
It would be nice if the label format could be adjusted. If I zoom into minutely interval, th complete time shold be displayed ( 12:01 ) instead of sth like 12:00..............10......20......30....
or ( preferred, if possible ) :
add multiple x-axis with different formats below each other. In this case, the labels should disappear on overlap, eg:
-------2012--------------------------------------------------------------2013-----------------
--12----01----02----03----04----05----06----07----08----09----10----11----12----01----02------
Below the monthly axis there should appear a daily and so on. If I zoom into an detailled interval, the overlapping labels should disappear.
For this case I tried playing around with the above fiddle, simply adding futher axis / svgs but they react independently, eg: I have a yearly axis and a monthly axis. Zooming on the first also zooms the second and vice versa, but when I first change from first to second, both axis do a "jump". I think the second one moves back to its initial state on first moving.
Any ways to accomblish this?
In both cases, I'm surprised why the tick-lines have gone away?
In many examples the axis look like:
|2012 |2011 |2010
but the vertical lines are gone in the fiddled example!?

D3 - Vertical axis is not displaying correctly

I have a CSV file containing the French population by department. I can correctly display a map colored with population density but I encounter problems with the associated legend.
You can see the current result here: http://i.stack.imgur.com/Y26JT.jpg
After loading the CSV, here is my code to add the legend :
legend = svg.append('g')
.attr('transform', 'translate(525, 150)')
.attr('id', 'legend');
legend.selectAll('.colorbar')
.data(d3.range(9))
.enter().append('svg:rect')
.attr('y', function(d) { return d * 20 + 'px'; })
.attr('height', '20px')
.attr('width', '20px')
.attr('x', '0px')
.attr("class", function(d) { return "q" + d + "-9"; })
.attr('stroke', 'none');
legendScale = d3.scale.linear()
.domain([0, d3.max(csv, function(e) { return +e.POP; })])
.range([0, 9 * 20]);
legendAxis = d3.svg.axis()
.scale(legendScale)
.orient('right')
.tickSize(1);
legendLabels = svg.append('g')
.attr('transform', 'translate(550, 150)')
.attr('class', 'y axis')
.call(legendAxis);
Colors are obtain using ColorBrewer CSS (https://github.com/mbostock/d3/tree/master/lib/colorbrewer)
I have two problems:
The Hyphen '-' (a SVG line) is not displayed for each value of my axis
I cannot choose the number of values in my axis, I would like one at the beginning of each color block.
Thanks in advance for any help.
The reason your solution isn't showing the 'hyphen' (svg lines) is because you have the tickSize set very small. Try not setting it and it will default to 6 (according to the API docs - https://github.com/mbostock/d3/wiki/SVG-Axes).
To choose the number of values in your axis, you can add a call to ".ticks(N)", where N is the number of ticks you want. D3 will try to show that many ticks. You could also call ".tickValues([...])" and pass in the exact array of values to use for the ticks.
Here's a JSFiddle that corrects the issues in your example: http://jsfiddle.net/TRkGK/3/
And a sample of the part that fixes your issues:
var legendAxis = d3.svg.axis()
.scale(legendScale)
.orient('right')
.tickSize(6) // Tick size controls the width of the svg lines used as ticks
.ticks(9); // This tells it to 'try' to use 9 ticks
UPDATED:
You also want to make sure you're setting the CSS correctly. Here's what I use:
.y.axis line { stroke: #ccc; }
.y.axis path { display: none; }
In your example, when you add the larger tickSize, you are seeing the path in which the tick lines are defined. If you hide the path and give the lines a color, you'll see the ticks rather than the area in which the ticks are defined.
Hope this helps!
It looks like it could be a css issue, did you look at reblace's css in his fiddle?

Resources