I am trying to create a simple bar chart where the X axis is a date
scale and the Y a total count of events that occurred on a date.
Additionally I’m trying to add the capability to brush and zoom into the
chart so that there would be an initial X axis for years, then months
and finally days, but I am having an awful time trying to do this.
eventData.forEach(function(d) {
d.event_stat_data_ontime = $(d.event_stat_data_ontime).text();
//get rid of time portion
var tempDate = new Date(d.event_entry_date);
d.event_entry_date = new Date(tempDate.toDateString());
});
eventData = [ {event_entry_date: "Mon Feb 05 2020 23:13:39 GMT-0500 (Eastern Standard Time)", event_type: "Type 01"},
{event_entry_date: "Tue Feb 04 2020 00:14:40 GMT-0500 (Eastern Standard Time", event_type: "Type 02"},
{event_entry_date: "Thu Feb 20 2020 21:01:46 GMT-0500 (Eastern Standard Time)", event_type: "Type 01"},
{event_entry_date: "Fri Feb 21 2020 21:08:20 GMT-0500 (Eastern Standard Time)", event_type: "Type 01"},
{event_entry_date: "Mon Feb 03 2020 11:12:16 GMT-0500 (Eastern Standard Time)", event_type: "Type 03"} ];
eventData.forEach(function(d) {
d.event_entry_date = new Date(d.event_entry_date);
});
var ndx = crossfilter(eventData);
var dateDim = ndx.dimension( function(d) { return d.event_entry_date; } );
var dateGroup = dateDim.group().reduceCount( function(d) { return d.event_entry_date; });
var minDate = d3.timeMonth.offset(dateDim.bottom(1)[0].event_entry_date, -1);
var maxDate = d3.timeMonth.offset(dateDim.top(1)[0].event_entry_date, 1);
timeChart
.width( $('#dc-time-chart').parent().innerWidth() )
.transitionDuration(500)
.dimension(dateDim)
.group(dateGroup)
.elasticY(true)
.x(d3.scaleTime().domain([minDate, maxDate]))
.renderHorizontalGridLines(true)
dc.js doesn't have any built-in feature to zoom on brushing.
I can think of a few reasons for this:
The brush in dc.js is used for selection
You either end up with the brush covering the whole chart (ugly), or you have to remove the selection brush after zooming
You have to have some other way to restore the zoom, because once you are zoomed in, there is no way to select outside the chart to zoom out
Whatever the reason, the range and focus charts are the built-in solution for this.
If you still want to hack it, you can respond to the filtered event and change the domain of the x scale:
timeChart.on('filtered', function() {
if(timeChart.filter()) {
timeChart.x().domain(timeChart.filter());
timeChart.filter(null);
timeChart.redraw();
}
})
This takes the filter, applies it to the domain, and the removes the filter. This is a solution for problem #2 above. It's kind of ugly, but not as ugly as leaving the whole chart brushed afterward. (Also once the whole chart is brushed, there's no way to brush within that.)
By itself, this will not work right because dc.js charts brush continuously, while the mouse is being dragged.
There's an old feature request to have an option to only brush on "brush end", that is, when the mouse is released. We can borrow some code from a pull request that implements this feature.
We'll add this method to change the selection without filtering:
timeChart._nonFilteredBrushing = function () {
var extent = timeChart.extendBrush();
timeChart.fadeDeselectedArea();
};
And then change the brush event handlers to only filter on brush end:
timeChart.brush().on('brush', timeChart._nonFilteredBrushing);
timeChart.brush().on('end.filter', timeChart._brushing);
Unfortunately, this step has to be done after the chart is rendered.
Finally, we'll need to add a button to reset the domain of the chart, a solution for problem #3 above:
<a href='javascript:timeChart.focus(null)'>reset</a>
Here is a demo fiddle.
As for problem #1, how do we deal with selection, since this is different from the behavior of other dc.js charts?
1. No selection or filtering in this chart. That's what I implemented here.
2. Filter to the X domain of this chart, but don't show the brush. Maybe not difficult, but might require hiding the brush instead of removing the filter.
side note: quantizing to days.
Here's the idiomatic way to bin by days, rather than creating strings and then parsing them.
Use d3.timeDay in your dimension key function:
var dateDim = ndx.dimension( function(d) { return d3.timeDay(d.event_entry_date); } );
Use d3.timeDays for your xUnits so that dc.js knows how many bars there are and how wide to make them:
.xUnits(d3.timeDays)
Related
Using different dimensions of the same dataset, there are three dc.js Line Charts on screen.
When user clicks a datapoint on any lineChart, I wish to locate and return the data values for that corresponding point from all other charts, including the one clicked on.
I am also attempting (on mouseover) to change the circle fill color to red for the datapoint being hovered, as well as for the corresponding datapoint (same "x" value) for all other charts.
I am using the .filter() method but haven't been successful getting the desired data. The error message is: "Uncaught TypeError: myCSV[i].filter is not a function"
Full jsFiddle demo/example
lc1.on('renderlet', function(lc1) {
var allDots1 = lc1.selectAll('circle.dot');
var allDots2 = lc2.selectAll('circle.dot');
var allDots3 = lc3.selectAll('circle.dot');
allDots1.on('click', function(d) {
var d2find = d.x;
var d2find2 = d3.select(this).datum();
console.log(myCSV);
alert('Captured:'+"\nX-axis (Date): "+d2find2.x +"\nY-axis (Value): "+ d2find2.y +"\nDesired: display corresponding values from all three charts for this (date/time) datapoint");
allDots2.filter(d=>d.x == d2find2).attr('fill','red');
findAllPoints(d2find2);
});//END allDots1.on(click);
function findAllPoints(datum) {
var objOut = {};
var arrLines=['car','bike','moto'];
for (var i = 0; i < 3; i++) {
thisSrx = arrLines[i];
console.log('thisSrx: '+thisSrx);
console.log(myCSV[i].date)
console.log(datum.x);
//loop thru myCSV obj, compare myCSV[i].date to clicked "x" val
//build objOut with data from each graph at same "X" (d/t) as clicked
objOut[i] = myCSV[i].filter(e => e.date === datum.x)[0][thisSrx];
}
$('#msg').html( JSON.stringify(objOut) );
console.log( JSON.stringify(objOut) );
}//END fn findAllPoints()
});//END lc1.on(renderlet)
myCSV contains all three data points, so I don't see the need to loop through the three charts independently - findAllPoints is going to find the same array entry for all three data series anyway.
The main problem you have here is that date objects don't compare equal if they have the same value. This is because == (and ===) evaluate object identity if the operands are objects:
> var d1 = new Date(), d2 = new Date(d1)
undefined
> d1
Mon Feb 13 2017 09:03:53 GMT-0500 (EST)
> d2
Mon Feb 13 2017 09:03:53 GMT-0500 (EST)
> d1==d2
false
> d1.getTime()===d2.getTime()
true
There are two ways to deal with this.
Approach 1: use second event argument
If the items in all the charts match up item by item, you can just use the index.
All d3 callbacks pass both the datum and the index. So you can modify your callback like this:
allDots1.on('click', function(d,i) {
// ...
allDots2.filter((d,j)=> j===i).attr('fill','red').style('fill-opacity', 1);
alert(JSON.stringify(myCSV[i]));
});
http://jsfiddle.net/gordonwoodhull/drbtmL77/7/
Approach 2: compare by date
If the different charts might have different data indices, you probably want to compare by date, but use Date.getTime() to get an integer you can compare with ===:
allDots1.on('click', function(d) {
var d2find = d.x;
// ...
allDots2.filter(d=> d.x.getTime()===d2find.getTime()).attr('fill','red').style('fill-opacity', 1);
var row = myCSV.filter(r=>r.date.getTime()===d2find.getTime())
alert(JSON.stringify(row));
});
http://jsfiddle.net/gordonwoodhull/drbtmL77/10/
Note that in either case, you're going to need to also change the opacity of the dot in the other charts, because otherwise they don't show until they are hovered.
Not sure when you want to reset this - I guess it might make more sense to show the corresponding dots on mouseover and hide them on mouseout. Hopefully this is enough to get you started!
I would like to create a bar chart based on dates in x-axis. Labels should be displayed as month (i.e. Jan, Jan'17 - preferred). Within my data I have always first date of following months, i.e. 01Jan, 01Feb, 01Mar. I have created a chart but I am not able to make it aligned.
var chart = dc.barChart("#" + el.id);
var chCategory = ndx.dimension(function(d) {return d[chCategoryName];});
chValues = chCategory.group().reduceSum(
return parseFloat(d[chValueName]);});
//set range for x-axis
var minDate = chCategory.bottom(1)[0][chCategoryName];
var maxDate = chCategory.top(1)[0][chCategoryName];
chart
.width(800)
.height(200)
.x(d3.time.scale().domain([minDate,maxDate]))
.xUnits(d3.time.months)
.dimension(chCategory)
.group(chValues)
.renderHorizontalGridLines(true)
// .centerBar(true) //does not look better
.controlsUseVisibility(true)
.ordinalColors(arrColors)
.transitionDuration(1000)
.margins({top: 10, left: 80, right: 5, bottom: 20})
I have already read post: dc.js x-axis will not display ticks as months, shows decimals instead
but I am not able to implement it in a way that will keep correct sorting for different years.
dc.js takes the domain pretty literally - the x axis stretches exactly from the beginning to the end, disregarding the width of the bars or their placement. It's a design bug.
Here are two workarounds.
keep bars centered and add padding
If you're using elasticX you can manually correct it like this:
chart.centerBar(true)
.xAxisPadding(15).xAxisPaddingUnit('day')
If you're just setting the domain manually, that's
minDate = d3.time.day.offset(minDate, -15);
maxDate = d3.time.day.offset(maxDate, 15);
align the ticks to the left of bars and correct the right side of the domain
You don't say what problem you run into when you don't center the bars. But I know the right bar can get clipped.
If you want the elasticX effect, you can implement it manually like this, offsetting the right side by a month (example):
function calc_domain(chart) {
var min = d3.min(chart.group().all(), function(kv) { return kv.key; }),
max = d3.max(chart.group().all(), function(kv) { return kv.key; });
max = d3.time.month.offset(max, 1);
chart.x().domain([min, max]);
}
chart.on('preRender', calc_domain);
chart.on('preRedraw', calc_domain);
Or without elasticX that's just:
maxDate = d3.time.month.offset(maxDate, 1);
Good day all
I am having a bit of trouble. I have a graph that ranges from 2012 to 2015. I need my year to display at my origin once I zoom in. At this stage my the year only displays at the beginning of every year. It takes the place of January. Is it possible to do this?
chart3.addListener("zoomed", function(e) {
var date = $( ".chartdiv3 .amChartsPeriodSelector .amChartsInputField" )
.map(function() {
return $(this).val();
}).get();
$(this).html(date.join(' <i style="color: #F47C00;">to</i> '));
chart3.panels[0].titles[1].text = "period - " + date[0] + " to " + date[1];
chart3.panels[0].validateData();
});
chart3.write('chartdiv3');
Ok. So changing the labelFunction depending on zoom is pretty easy.
Just take a look at this fiddle. (I used this as template).
The other approach is not as easy as i thought. You can modify the labels when the zoomed event is fired, however i haven't found a way to get the year for the first label. (it's an amcharts object containing the svg but no date, just the plain text for the label)
I have a Dimple.JS scatter plot with a time-based (in years) X-axis. I'd like (in a similar manner to this D3 question) to be able to shade in an arbitrary area (ideally the start and end positions wouldn't necessarily be data points in the series).
Is there an existing function that will let me supply a year and give me the X co-ordinate the correct position on the scale in the SVG, which I can then use the construct my rectangle (I tried to look at the source code to figure out how dimple does it's positioning...)?
Alternatively, if it's more practical to use points already plotted on the chart, what's the correct way to use d3.select with dimple to access a specific one? My series has a date field (dd/mm/yyyy) so I have SVG elements like this:
<circle id="All_Wed Mar 18 1931 00:00:00 GMT+0000 (GMT)__" class="series0 bubble All Wed_Mar_18_1931_00:00:00_GMT+0000_(GMT) " cx="465.0000000006503" cy="362.1714285714286" r="2" opacity="0.8" fill="#e90e0e" stroke="#c20b0b"></circle>
… my guess was I should use mySeries.shapes.select(id) to access that, but for:
mySeries.shapes.select("#All_Wed Mar 18 1931 00:00:00 GMT+0000 (GMT)__");
or (if I escape it, unless there's a silly syntax error):
mySeries.shapes.select("#All_Wed Mar\ 18\ 1931\ 00:00:00\ GMT+0000\ (GMT)__");
I get "Not a valid selector".
(Thanks)
You need to use a non-public method of the axes to do this, so it may not work this way in future versions (>1.1.5) however between you and me, I don't think the scale method of the axis is going to be disappearing any time soon.
The _scale method is the raw d3 scale method added once the draw method of the chart is called so it can convert the values for you. I've created a fiddle to illustrate the solution. This will need a little tweaking if you are dealing with negative values or log axes:
// Draw a simple chart
var svg = dimple.newSvg("body", 800, 600);
var data = [
{ "a":300, "b":2000, "c":"a" },
{ "a":400, "b":3000, "c":"b" },
{ "a":340, "b":2200, "c":"c" },
{ "a":300, "b":5000, "c":"d" }
];
var chart = new dimple.chart(svg, data);
var x = chart.addMeasureAxis("x", "a");
var y = chart.addMeasureAxis("y", "b");
chart.addSeries("c", dimple.plot.bubble);
chart.draw();
// Draw a grey region using the following co-ordinates
var fromX = x._scale(210),
toX = x._scale(320),
fromY = y._scale(2200),
toY = y._scale(3100)
svg.append("rect")
.attr("x", fromX)
.attr("y", toY)
.attr("width", toX - fromX)
.attr("height", fromY - toY)
.style("fill", "grey")
.style("opacity", 0.2);
Here's the fiddle: http://jsfiddle.net/T6ZDL/7/
I'm able to zoom as in block 4015254. But as per my other questions I'm trying to zoom into the data, not the vector diagram.
I think I'm most of the way there but need a way to know what the new x-axis endpoints are.
For example, block 4015254 starts with an x-axis ranging from January 1999 to January 2003. After zooming in a bit, the x-axis now ranges from about February 2000 to about November 2000. Those are the endpoints I'm looking for.
I'm assuming I'll need to pass my chart's endpoints into a scaling function or d3.time.scale or d3.scale.linear.
The idea is to grab those endpoints and, upon zoomend recalculate the bar chart's granularity based upon the new range. Here's some pseudocode:
function zoomEndFunction(scope) {
return function() {
var that = scope;
var startDate = xAxis.scale()(0); // <---- this doesn't work
var endDate = xAxis.scale()(that.chartWidth); // <---- nor this
that.redrawChart(startDate, endDate);
}
}