I have a bar chart (stacked by not yet complete) and the columns are all grouping into the left of the chart.
What is causing this?
Plnkr: https://plnkr.co/edit/DMRJfbD4ZF3xCiPdFedy?p=preview
data.forEach(function(d) {
var y0_positive = 0;
var y0_negative = 0;
d.components = keys.map(function(key) {
if (d[key] >= 0) {
// if we have a positive value, add to the postive
return {key: key, y1: y0_positive, y0: y0_positive += d[key] };
} else if (d[key] < 0) {
// if value is negative, add to the negative value
return {key: key, y0: y0_negative, y1: y0_negative += d[key] };
}
})
})
Kev
There is absolutely nothing wrong with your chart. It's correct, and the data is being accurately shown.
I know it seems the opposite, but this is the reason: you are using a time scale, and a time scale is not an ordinal scale.
Let's see your dates. The first data point is:
"date":"2016-11-03 00:00:00"
Corresponding to 3rd of November. But all the other data points are from 2016-10-06, the 6th of October.
So, the time scale will show all the dates between these two extremes (from 6th of October to 3rd of November) evenly spaced, and all your bars will be squeezed to the left, at the 6th of October (except for a little bar at the right, corresponding to the 3rd of November), because this is the correct and expected outcome when you use a time scale!
Now, see what happens if we simply delete the first object (the 3rd of November) in your data: https://plnkr.co/edit/rhmh2zxEnwO4SLTLvfNu?p=preview
If you don't want to show the time span in the correct proportion, use a ordinal scale instead, like scaleOrdinal or scaleBand.
Related
I don't know if this is possible in dc.js and crossfilter.js, but I decided to ask anyways.
I combined a scatterplot and a barChart example from dc to make an interactive dashboard:
var chart1 = dc.scatterPlot("#test1");
var chart2 = dc.scatterPlot("#test2");
d3.csv("output.csv", function(error, data) {
data.forEach(function (x) {
x.x = +x.x;
x.y = +x.y;
x.z = +x.z;
});
var ndx = crossfilter(data),
dim1 = ndx.dimension(function (d) {
return [d.x, d.y];
}),
dim2 = ndx.dimension(function (d) {
return Math.floor(parseFloat(d.z) * 10) / 10;
}),
group1 = dim1.group(),
group2 = dim2.group(),
chart1.width(300)
.height(300)
.x(d3.scale.linear().domain([-2, 2]))
.y(d3.scale.linear().domain([-2, 2]))
.yAxisLabel("y")
.xAxisLabel("x")
.clipPadding(10)
.dimension(dim1)
//.excludedOpacity(0.5)
.excludedColor('#ddd')
.group(group1)
.symbolSize([2.5]);
chart2
.width(600)
.dimension(dim2)
.group(group2)
.x(d3.scale.linear().domain([0,3]))
.elasticY(true)
.controlsUseVisibility(false)
.barPadding([0.1])
.outerPadding([0.05]);
chart2.xAxis().tickFormat(function(d) {return d}); // convert back to base unit
chart2.yAxis().ticks(10);
dc.renderAll();
});
Result when brushing the bar chart:
I want to change the filtering so that when I brush the bar chart, brushed points in the scatterplot will have an opacity value, which is 1 in the middle of the brush, and decreases towards end of the range of brush.
The other points (outside the brush) should just be grey, instead of invisible as in the current script. Illustration:
Is this possible to do with the dc.js and crossfilter.js?
PS: The attached scatterplot isn't the desired outcome. It is not filtered based on opacity. I just attached it to show how the other points(grey) should look like after brushing the bar chart.
I couldn't get this working with animated transitions, because there is something I am missing about how to interrupt transitions, and the original dc.scatterPlot is already applying opacity transitions.
So, to start off, let's turn transitions on the original scatter plot:
chart1
.transitionDuration(0)
We also need to add Z to the input data for the scatter plot. Although it would make more sense to add it to the value, it's easy to add it to the key (and the scatter plot will ignore extra elements in the key):
dim1 = ndx.dimension(function (d) {
return [d.x, d.y, d.z];
}),
Then we can add a handler to to the scatter plot to apply opacity to the dots, based on the range of the filter in the bar chart:
chart1.on('pretransition', function(chart) {
var range = chart2.filter(); // 1
console.assert(!range || range.filterType==='RangedFilter'); // 2
var mid, div; // 3
if(range) {
mid = (range[0] + range[1])/2;
div = (range[1] - range[0])/2;
}
chart1.selectAll('path.symbol') // 4
.attr('opacity', function(d) {
if(range) { // 5
if(d.key[2] < range[0] || range[1] < d.key[2])
op = 0; // 6
else
op = 1 - Math.abs(d.key[2] - mid)/div; // 7
//console.log(mid, div, d.key[2], op);
return op;
}
else return 1;
})
});
Get the current brush/filter from the bar chart
It should either be null or it should be a RangedFilter
Find the midpoint and the distance from the midpoint to the edges of the brush
Now apply opacity to all symbols in the scatter plot
If there is an active brush, apply opacity (otherwise 1)
If the symbol is outside the brush, opacity is 0
Otherwise the opacity is linear based on the distance from the midpoint
You could probably use d3.ease to map the distance [0,1] to opacity [0,1] using a curve instead of linearly. This might be nice so that it emphasizes the points closer to the midpoint
This demo is not all that cool because the data is purely random, but it shows the idea:
https://jsfiddle.net/gordonwoodhull/qq31xcoj/64/
EDIT: alright, it's a total abuse of dc.js, but if you really want to use it without filtering, and displaying the excluded points in grey, you can do that too.
This will disable filtering on the bar chart:
chart2.filterHandler(function(_, filters) { return filters; });
Then apply opacity and color to the scatter plot like this instead:
chart1.selectAll('path.symbol')
.attr('opacity', function(d) {
if(range && range.isFiltered(d.key[2]))
return 1 - Math.abs(d.key[2] - mid)/div;
else return 1;
})
.attr('fill', function(d) {
if(!range || range.isFiltered(d.key[2]))
return chart1.getColor(d);
else return '#ccc';
})
With this data it's tricky to see the difference between the light blue dots and the grey dots. Maybe it will work better with non-random data, maybe not. Maybe another color will help.
Again, you might as well use straight D3, since this disables most of what dc.js and crossfilter do. But you'd have to start from scratch to ask that question.
Updated fiddle.
EDIT 2: sort the dots by filteredness like this:
.sort(function(d) {
return range && range.isFiltered(d.key[2]) ? 1 : 0;
})
Fiddle 3
I have a scatter plot that charts unique events per date on my X axis and when they happened on each day, by the second, on the Y axis.
To accomplish this I took the date data that I use for the X axis, and ran it through a function that would output the exact second of the day in which the event happened (0 to 86400). In that sense, my Y axis goes from 0 seconds to 86400 seconds.
This allowed me to represent each even uniquely as a dot on my chart down to the second.
I'm pretty happy with the outcome of my chart:
The only issue is my Y label goes from 0 to 86400. I would like to change the text to a familiar HH AM/PM format, perhaps something along the lines of "0 to 23, where ie: if (y value = 3600) return "1 AM".
My scatterplot looks like this:
scatterPlot
.width(window.innerWidth-100)
.height(window.innerHeight/2)
.x(d3.time.scale().domain([minDate, maxDate]))
.brushOn(false)
.mouseZoomable(true)
// .title(function (d) {
// return "Event: " + d['text'];
// })
.clipPadding(5)
.yAxisLabel("Hour of the day")
.elasticY(true)
.dimension(eventsByDateAndHourDim)
.group(eventsByDateAndHourGroup);
I tried to do this:
scatterPlot.yAxis().tickFormat(function (d) {
if (domain = 43200)
return "12 PM";
})
But that only changes all labels to "12 PM". I'm not sure how to refer to the actual value of my Y domain.
I tried using the group "if (eventsByDateAndHourGroup = 43200)..." but that didn't work either.
The parameter to the function passed to the tickFormat() method is the Y coordinate which needs to be formatted.
So you should be able to do something like
scatterPlot.yAxis().tickFormat(function (d) {
var hour = Math.floor(d/3600),
pm = hour > 11;
if(pm) hour -= 12;
if(hour === 0) hour = 12;
return String(hour) + (pm ? ' PM' : ' AM');
});
Or you can use d3.time.format.utc for a more robust, elegant solution:
var hourFormat = d3.time.format.utc('%I %p');
scatterPlot.yAxis().tickFormat(function (d) {
var date = new Date(d*1000);
return hourFormat(date);
});
This is somewhat less efficient, because Date objects are slow, but it probably doesn't matter for processing a dozen ticks.
I use dc.js lineChart and barChart. Now I need to mark the maximum and minimum values on my lineChart with 'renderArea(true)'.
I want something like in the picture below or maybe something else, but I don't know how to add this feature.
Update:
Gordon's answer is perfect. Unfortunately, my chart doesn't show the hint with 'mouseover' on marked points
One more update:
How can I redraw these points after zooming?
This isn't something supported directly by dc.js, but you can annotate the chart with a renderlet. Gladly, dc.js makes it easy to escape out to d3 when you need custom annotations like this.
We'll use the fact that by default the line chart draws invisible dots at each data point (which only appear when they are hovered over). We'll grab the coordinates from those and use them to draw or update our own dots in another layer.
Usually we'd want to use a pretransition event handler, but those dots don't seem to have positions until after the transition, so we'll have to handle the renderlet event instead:
chart.on('renderlet', function(chart) { // 1
// create a layer for the highlights, only once
// insert it after the tooltip/dots layer
var highlightLayer = chart.select('g.chart-body') // 2
.selectAll('g.highlight-dots').data([0]);
highlightLayer
.enter().insert('g', 'g.dc-tooltip-list').attr('class', 'highlight-dots');
chart.selectAll('g.dc-tooltip').each(function(_, stacki) { // 3
var dots = d3.select(this).selectAll('circle.dot'); // 4
var data = dots.data();
var mini = 0, maxi = 0;
data.forEach(function(d, i) { // 5
if(i===0) return;
if(d.y < data[mini].y)
mini = i;
if(d.y > data[maxi].y)
maxi = i;
});
var highlightData = [mini, maxi].map(function(i) { // 6
var dot = dots.filter(function(_, j) { return j === i; });
return {
x: dot.attr('cx'),
y: dot.attr('cy'),
color: dot.attr('fill')
}
});
var highlights = highlightLayer.selectAll('circle.minmax-highlight._' + stacki).data(highlightData);
highlights
.enter().append('circle') // 7
.attr({
class: 'minmax-highlight _' + stacki,
r: 10,
'fill-opacity': 0.2,
'stroke-opacity': 0.8
});
highlights.attr({ // 8
cx: function(d) { return d.x; },
cy: function(d) { return d.y; },
stroke: function(d) { return d.color; },
fill: function(d) { return d.color; }
});
});
});
This is fairly complicated, so let's look at it step-by-step:
We're listening for the renderlet event, which fires after everything has transitioned
We'll create another layer. The .data([0]).enter().insert(stuff) trick is a degenerate case of the d3 general update pattern that just makes sure an item is added exactly once. We specify the selector for the existing tooltip/dots layer as the second parameter to .insert(), in order to put this layer before in DOM order, which means behind. Also, we'll hold onto the update selection because that is either the inserted node or the existing node.
We iterate through each of the stacks of tooltip-dots
In each stack, we'll select all the existing dots,
and iterate over all their data, finding the minimum and maximum indices mini and maxi.
Now we'll create a two-element data array for binding to the min/max highlight dots, pulling data from the existing dots
Now we're finally ready to draw stuff. We'll use the same degenerate update pattern to draw two dots with class minmax-highlight _1, _2, etc.
And use the color and positions that we remembered in step 6
Note that the min and max for each stack is not necessarily the same as the total min and max, so the highlighted points for a higher stack might not be the highest or lowest points.
Not so simple, but not too hard if you're willing to do some d3 hacking.
Example fiddle: http://jsfiddle.net/gordonwoodhull/7vptdou5/31/
I have been working with Mike Bostock's Sortable Bar Chart, which nicely staggers the sorting of an ordinal axis.
The initial sort is done from the left (starting A, B, C…) , but the reverse is done with the same order (A, B, C…) rather than from the left. This means that the letters start moving from seemingly random positions and the eye can't follow it as easily as the first sort.
I have been trying to resolve this, but I suspect that this may be a limitation of ordinal scales: the sort order is that specified in setting the domain. The sorting element of the code is at the bottom.
var x0 = x.domain(data.sort(this.checked
? function(a, b) { return b.frequency - a.frequency; }
: function(a, b) { return d3.ascending(a.letter, b.letter); })
.map(function(d) { return d.letter; }))
.copy();
var transition = svg.transition().duration(750),
delay = function(d, i) { return i * 50; };
transition.selectAll(".bar")
.delay(delay)
.attr("x", function(d) { return x0(d.letter); });
transition.select(".x.axis")
.call(xAxis)
.selectAll("g")
.delay(delay);
The x0 scale is created as a copy of the x scale to provide a static reference point for the transition of the bars, as x is tweened during the transition. In the process, I think the x.domain is also set to the target sort order. I thought that the i values would be reset on setting a new domain, but it appears not: they persist through the change in domain order.
How can I change the sort order so that the sort always starts with the left-most category? I've tried (unsuccessfully) creating an artificial sort order on the "g", trying to work with multiple domains, etc. I can achieve this using a linear scale, but the ordinal scale should be a more concise and more elegant solution!
I've created a fiddle so you can experiment with this.
The start of the transition as per your specification would be determined by the order before updating. That is, the element with the lowest index before the update should be first. To do that, you simply need to store and reference the old index for the transition:
.each(function(d, i) { this.old_i = i; })
// ...
delay = function(d, i) { return this.old_i * 250; }
Note that you need to save the index for both the bars and the axis ticks/labels, as you are transitioning both.
Complete example here. Note that I'm also rebinding the data on update -- this is necessary to make it work when changing back to the original data, as the index doesn't change if you only change the scale.
I have an array, fullset(24), containing plot data for the last 24h, by the hour. This array i feed to jqPlot to create a bar graph. Works fine. But I want to show only a subset of the data, say the business hours (8-17). I do this, rather clumsily, by creating a new array containing a subset and some additional trickery with the ticks, like so:
var ticks = [];
var subset = [];
for (i = 8; i < 17; i++)
{
subset[i - 8] = fullset[i][1];
ticks.push(sprintf("%d-%d", i, i + 1));
}
But is there a better way? Is it possible to somehow tell jqPlot to show only a subset of the full set?
On the axes settings, I have set a minimum and maximum, but not sure if this will do the same as you are looking for.
axes: {
xaxis: {
min:8,
max:16,
},
},