I have a line chart and data in the form
[{
time: "2016-4-29"
total: 23242
},
{
time: "2016-5-16
total: 3322
}
...
]
I'm trying to filter on the x-axis with the brush, however, since I don't have every single date, if I brush in a small range, the filter handler seems to return an empty array for my filters
I've set up my line chart's x-axis like so:
.x(d3.time.scale().domain([minDate,maxDate]))
is there a way to make it so a user can only filter on dates that are in the dataset?
I would like the brush to snap to dates in the dataset.
it seems like whats happening is that you are able to brush between ticks..so it doesn't know what it selected.
I'm going to answer the easier question: How do I create a brush that will not allow nothing to be selected?
In other words, if the brush contains no data, do not allow it to take.
There are two parts to the solution. First, since any chart with a brush will remove the old filter and then add the new filter, we can set up the addFilterHandler to reject any filter that does not contain non-zero bins:
spendHistChart.addFilterHandler(function(filters, filter) {
var binsIn = spendHistChart.group().all().filter(function(kv) {
return filter.isFiltered(kv.key) && kv.value;
});
console.log('non-empty bins in range', binsIn.length);
return binsIn.length ? [filter] : [];
});
That's the straightforward part, and incidentally I think you could probably modify it to snap the brush to existing data. (I haven't tried it, though.)
The more tricky part is that this won't get rid of the brush, it just doesn't apply the filter. So the chart will end up in an inconsistent state.
We need to detect when the brush action has finished, and if there is no filter at that point, explicitly tell the chart to clear the filter:
spendHistChart.brush().on('brushend.no-empty', function() {
if(!spendHistChart.filters().length)
window.setTimeout(function() {
spendHistChart.filterAll().redraw();
}, 100);
});
We need a brief delay here, because if we respond to brushend synchronously, the chart may still be responding to it, causing bickering and dissatisfaction.
As a bonus, you get kind of a "nah-ah" animation because of the unintentional remove-brush animation.
demo fiddle
Related
I have a dynamically growing timeseries I need to display in a zoomable/panable chart.
Try it out here (in fact: my first jsFiddle ever :) ) :
https://jsfiddle.net/Herkules001/L12k5zwx/29/
I tried to do it the same way as described here: https://dc-js.github.io/dc.js/examples/replacing-data.html
However, each time the chart updates, the zoom and filter are lost on the focus chart. (The brush is preserved on the range chart however.)
How can I add data without resetting the views and losing the zoom?
var chart = dc.lineChart("#test");
var zoom = dc.lineChart("#zoom");
//d3.csv("morley.csv", function(error, experiments) {
var experiments = d3.csvParse(d3.select('pre#data').text());
experiments.forEach(function(x) {
x.Speed = +x.Speed;
});
var ndx = crossfilter(experiments),
runDimension = ndx.dimension(function(d) {return +d.Run;}),
speedSumGroup = runDimension.group().reduceSum(function(d) {return d.Speed * d.Run / 1000;});
chart
.width(768)
.height(400)
.x(d3.scaleLinear().domain([6,20]))
.brushOn(false)
.yAxisLabel("This is the Y Axis!")
.dimension(runDimension)
.group(speedSumGroup)
.rangeChart(zoom);
zoom
.width(768)
.height(80)
.x(d3.scaleLinear().domain([6,20]))
.brushOn(true)
.yAxisLabel("")
.dimension(runDimension)
.group(speedSumGroup);
zoom.render();
chart.render();
var run = 21;
setInterval(
() => {
var chartfilter = chart.filters();
var zoomfilter = zoom.filters();
chart.filter(null);
zoom.filter(null);
ndx.add([{Expt: 6, Run: run++, Speed: 100 + 5 * run}]);
chart.x(d3.scaleLinear().domain([6,run]));
zoom.x(d3.scaleLinear().domain([6,run]));
chart.filter([chartfilter]);
zoom.filter([zoomfilter]);
chart.render();
zoom.render();
},
1000);
//});
In this case, if you are just adding data, you don't need to do the complicated clearing and restoring of filters demonstrated in the example you cited.
That part is only necessary because crossfilter.remove() originally would remove the data that matched the current filters. An awkward interface, almost never what you want.
If you're only adding data, you don't have to worry about any of that:
setInterval(
() => {
ndx.add([{Expt: 6, Run: run++, Speed: 5000 + 5 * run}]);
chart.redraw();
zoom.redraw();
},
5000);
Note that you'll get less flicker, and decent animated transitions, by using redraw instead of render. I also added evadeDomainFilter to avoid lines being clipped before the edge of the chart.
Fork of your fiddle
Removing data
If you use the predicate form of crossfilter.remove() you don't have to worry about saving and restoring filters:
ndx.remove(d => d.Run < run-20);
However, this does expose other bugs in dc.js. Seems like elasticY does not work, similar to what's described in this issue. And you get some weird animations.
Here's a demo with remove enabled.
In the end, dc.js has some pretty neat features, and there is usually a way to get it to do what you want, but it sure is quirky. It's a very complicated domain and in my experience you are going to find some of these quirks in any fully featured charting library.
Update: I fixed the replacing data example, that one is just ndx.remove(() => true) now.
zooming issues
As Joerg pointed out in the comments,
when the chart is not zoomed, it would be nice to have it also grow to show new data as it arrives
the X domain was clipped or even reversed if the focus reached outside the original domain of the chart
We can address these issues by adding a preRedraw event handler. That's the ideal place to adjust the domain; for example you can implement elasticX manually if you need to. (As you'll see in a second, we do!)
First, a naive attempt that's easy to understand:
chart.on('preRedraw', () => {
chart.elasticX(!zoom.filters().length);
});
We can turn elasticX on and off based on whether the range chart has an active filter.
This works and it's nice and simple, but why does the chart get so confused when you try to focus on a domain that wasn't in the original chart?
Welp, it records the original domain (source). So that it can restore to that domain if the focus is cleared, and also to stop you from zooming or panning past the edge of the graph.
But notice from the source link above that we have an escape hatch. It records the original domain when the X scale is set. So, instead of setting elasticX, we can calculate the extent of the data, set the domain of the scale, and tell the chart that the scale is new:
chart.on('preRedraw', () => {
if(!zoom.filters().length) {
var xExtent = d3.extent(speedSumGroup.all(), kv => kv.key);
chart.x(chart.x().domain(xExtent));
}
});
New fiddle with zooming issues fixed.
There is still one glitch which Joerg points out: if you are moving the brush while data comes in, the brush handles occasionally will occasionally stray from the ends of the brush. In my experience, these kinds of glitches are pretty common in D3 (and dynamic charting in general), because it's difficult to think about data changing during user interaction. It probably could be fixed inside the library (perhaps an interrupted transition?) but I'm not going to get into that here.
I'm trying to access the currently-hovered series data and color via JavaScript. The data is available to the legend and tooltip, but I'm not sure how to directly access it.
It's possible to place the legend in an external container, but their code creates a lot of additional containers/wrappers which makes formatting difficult. This Github question addresses it, but no answer was provided.
Perhaps events could be used to detect changes in the legend text or tspan elements and then grab the new text, but I'm not sure how to do this (using amCharts events) and how efficient it would be (especially with multiple series and/or charts with synced cursors).
Another idea was to get the data based on cursor position, but this seems inefficient (cursorpositionchanged fires too often - on mouse/cursor movement even when the series data hasn't changed). Maybe it could be done more efficiently based on change in dateAxis value? For example, using the positionchanged event listener:
chart.cursor.lineX.events.on('positionchanged', function() {
// get series data and do something with it
});
At least when using chart.cursor.xAxis = dateAxis, the positionchanged event only seems to fire when the cursor jumps to a new value. So it would be more efficient than an event that fired on mouse/cursor movement.
Any suggestions would be appreciated.
UPDATE
By currently-hovered, I am referring to the series data and color accessible via the tooltip (for example) with the mouse over the chart.
Examples: CandlestickSeries and LineSeries
One method you can try is to set an adapter for tooltipText on the object of concern. Since this may run multiple times especially via a chart cursor, perhaps keep track of changes to the tooltip via monitoring the unique value, e.g. in the samples provided that would be the date field. The data you're looking for can be found in the adapter's target.tooltipDataItem. The color, if on the series, will be target.tooltipDataItem.component.fill (in the case of the line series example, the target is the line series and has no change of color, so you can just use target.fill), otherwise e.g. in the case of CandleStick series the color would be on the candle stick, or column, i.e. via target.tooltipDataItem.column.fill.
Sample adapter for LineSeries:
var tooltipDate;
series.adapter.add("tooltipText", function(text, target) {
// data via target.tooltipDataItem.dataContext
console.log('text adapter; color: ', target.tooltipDataItem.component.fill.hex);
if (tooltipDate !== target.tooltipDataItem.dataContext.date) {
console.log('new tooltip date, do something');
tooltipDate = target.tooltipDataItem.dataContext.date;
}
// note: in this case: component === target
return text;
});
Demo:
https://codepen.io/team/amcharts/pen/9f621f6a0e5d0441fe55b99a25094e2b
Sample Candlestick series adapter:
var tooltipDate;
series.adapter.add("tooltipText", function(text, target) {
// data via target.tooltipDataItem.dataContext
console.log('text adapter; color: ', target.tooltipDataItem.column.fill.hex);
if (tooltipDate !== target.tooltipDataItem.dataContext.date) {
console.log('new tooltip date, do something');
tooltipDate = target.tooltipDataItem.dataContext.date;
}
return text;
});
Demo:
https://codepen.io/team/amcharts/pen/80343b59241b72cf8246c266d70281a7
Let us know if this is making sense, and if the adapter route is a good point in time to capture changes, data, color, as well as if it's efficient enough a manner to go about this.
I've got 2 pie charts with data like:
data: [
{diseaseType: 'Cancer', diseaseDetails: 'Lung cancer', quantity: 100},
{diseaseType: 'Diebetes', diseaseDetails: 'Unspecific', quantity: 650},
{diseaseType: 'Cancer', diseaseDetails: 'Breast cancer', quantity: 80}
]
i'm tying to get list of filters and able to remove them by user like (it's only controlled test code):
this.diseasePieChart.filters().splice(0, 1)
dc.renderAll()
it's updating first chart, but second (connected with first) not, it's stay like it was before remove filter.
Second chart i'm rendering like this:
self.diseasePieChart.on('filtered.monitor', function (chart) {
// create dimensions etc and render second chart
}
I also tried to do again crossfilter(data) after filter remove. When i'm calling dc.filterAll all filters are reset.
thanks for any help !
The correct entry points for changing filters are chart.filter() or chart.replaceFilter(), depending on whether you are trying to toggle individual items or change the entire array of filters at once.
As you found out, manipulating the array of filters inside the chart might affect the way the chart draws, but it won't convey the change to the crossfilter dimension and the other charts.
Note that as documented in the link above, the accepted type for the parameter for each of these functions is a little surprising:
The filter parameter can take one of these forms:
A single value: the value will be toggled (added if it is not present in the current filters, removed if it is present)
An array containing a single array of values ([[value,value,value]]): each value is toggled
When appropriate for the chart, a dc filter object such as
dc.filters.RangedFilter for the dc.coordinateGridMixin charts
dc.filters.TwoDimensionalFilter for the heat map
dc.filters.RangedTwoDimensionalFilter for the scatter plot
null: the filter will be reset using the resetFilterHandler
So if you want to get the array, remove an item, and then set it back, you could either:
var filters = chart.filters().slice(0); // copy the array of filters
filters.splice(0,1)
chart.replaceFilter([filters])
.redrawGroup();
or (using the toggle feature):
chart.filter(chart.filters()[0])
.redrawGroup();
Note that you usually want to redraw, not render, after changing a filter. This will allow the animated transitions to display, and is a little bit quicker.
Also, chart.redrawGroup is the same as dc.redrawAll() but it's a little safer in case you have more chart groups in the future.
I want to use the mouse zoom functionality on seriesChart and have it filter for other charts of the same group.
When I enable the zoom with .mouseZoomable(true) on seriesChart, and zoom the chart, the other charts become empty.
This doesn't happen when I enable it on a LineChart.
Here is a simple example: https://codepen.io/udeste/pen/ZKeXmX
(Zoom the second chart with the mouse. All is working. But when you zoom the first chart the other charts go blank.)
What am I doing wrong? Is it a dc.seriesChart bug?
It's because dc.seriesChart required you to supply that strange dimension, but it didn't change the filter function accordingly.
You specified seriesDimension like so:
var seriesDimension = ndx.dimension(function(d) {
return [+d.Expt, +d.Hours];
});
But when you zoom, the dc.coordinateGridMixin just filters using a regular dc.filters.RangedFilter, which does not know about these kinds of two-dimensional "multikeys".
Probably since the series chart requires this kind of input, it should redefine the filter handler to also deal with multikeys. But until then, you can work around it by providing your own filterHandler:
seriesChart.filterHandler(function(dimension, filters) {
if(filters.length === 0) // 1
dimension.filter(null);
else {
console.assert(filters.length===1); // 2
console.assert(filters[0].filterType==='RangedFilter');
dimension.filter(function(d) { // 3
return filters[0][0] <= d[1] && d[1] < filters[0][1];
})
}
});
What this does:
Checks if this event is because the filters have been cleared, and clears the dimension's filter if so.
Asserts that the filter is what is expected. coordinateGridMixin will always supply a single dc.filters.RangedFilter but who knows what else could happen.
Supplies a substitute filter function that checks if the part of the key used by the keyAccessor falls within the range (instead of comparing the array with the range, which will always return false).
Here's a working fork of your codepen.
(Incidentally, it looks like this examples slams into a known issue where line segments off the edge of the chart are dropped instead of clipping the segments. It won't be quite as bad if there are more points. I don't think I've found a good workaround. Hopefully we'll fix this soon.)
I am trying to filter data on my choropleth chart from a bargraph. Strange thing is that it is not showing correct value on selecting a bar from the accompanying bar chart.
Here is the jsfiddle: https://jsfiddle.net/anmolkoul/jk8LammL/
The script code begins from line 4794
If i select WIN004 from the bar chart, it should highlight only five states and the tooltip should reflect the values for the data. Some states are highlighted for whom WIN004 does not exist.
I changed the properties of the choropleth from
.colors(d3.scale.quantize().range(["#F90D00", "#F63F00", "#F36F01", "#F09E01", "#EDCB02", "#DDEA03", "#ADE703", "#7EE404", "#50E104", "#24DE05", "#05DB11"]))
.colorDomain([-1, 1])
To
.colors(d3.scale.linear().range(["green", "white", "red"]))
.colorDomain([-2, 0, 2])
But i get a lot of white states where its hard to discern what has been highlighted. The tool tip for some white-ed-out states show -0.00 :/
Here is the fiddle http://jsfiddle.net/anmolkoul/jk8LammL/1/
So i guess either its a problem with my color range or how my data is getting parsed.
I would ideally like to specify the data ranges in the .colorDomain based on the top and bottom values of the riskIndicator dimension. My functions are not working though. Should i use d3.max or riskIndicator.top here?
EDIT:
I got the color domain dynamic by using the min and max values but still the graph is not performing as expected? Could this be an issue with the geochoropleth chart? I further took a working geochoropleth example and ported my data to it and even that gave me the same issue of representing data incorrectly. I thoughit could be a data problem but i validated using a couple of good BI tools and their map charts displayed data correctly.
Could this be an issue with the dc choropleth?
Thank you.
Anmol
This has the same root cause as the issue in this question:
Crossfilter showing negative numbers on dc.js with no negative numbers in the dataset
In short, floating point numbers don't always cancel out to zero when added and subtracted. This "fake group" will ensure they snap to zero when they get close:
function snap_to_zero(source_group) {
return {
all:function () {
return source_group.all().map(function(d) {
return {key: d.key,
value: (Math.abs(d.value)<1e-6) ? 0 : d.value};
});
}
};
}
Added it to the FAQ!