I have been trying to make a bar chart where I present data from a data set that has several ocurrences of years. I want to show the year in axis X and on axis Y show the number of occurrences of every year. I have been trying the following code on Observable, but I am not sure why it is not working. I tried checking other S.O. posts but couldnt succeed.
There are a few issues going on here that largely stem from the fact that you are working within an Observable notebook, rather than a vanilla Javascript environment. I think it's worth taking a good look at Learning Observable to make sure you understand the basics.
In particular, you can't type:
let x = 1
let y = 2
into a single cell. This is effectively what you're trying to do in your main cell that defines the chart and you get a syntax error. What you might do instead is type:
x = 1
into one cell and then
y = 2
into a different cell. You could then reference those values in other cells.
You also might combine things like:
x_plus_y = {
let x = 1;
let y = 2;
return x+y;
}
The value of x_plus_y would then be available for use in other cells.
Now, I suppose what you're trying to create a count by year of astronaut missions using https://think.cs.vt.edu/corgis/csv/astronauts/.
Assuming so, this Observable Notebook shows a couple of ways to accomplish this.
Finally, I think that it is a mistake to use the BarChart component as you are trying to do. That component was really more of an experimental stepping stone to their more complete, higher level plotting library called Observable Plot. I think it's much easier to get where you want with Plot and you have more control. It' also easy to use Plot in a vanilla environment, like so:
let div = d3.select('#plot')
fetch(
"https://think.cs.vt.edu/corgis/datasets/csv/astronauts/astronauts.csv"
).then(async function (r) {
let astronauts = d3.csvParse(await r.text(), d3.autoType);
div.append(() => Plot.plot({
y: {
grid: true
},
x: { tickFormat: (y) => (y % 10 == 0 ? d3.format('d')(y) : "") },
marks: [
Plot.barY(astronauts, Plot.groupX({ y: "count" }, { x: "Mission.Year" })),
Plot.ruleY([0])
]
}))
});
<script src="https://cdn.jsdelivr.net/npm/d3#7"></script>
<script src="https://cdn.jsdelivr.net/npm/#observablehq/plot#0.6"></script>
<div id="plot"></div>
Related
I have a line chart as shown in the fiddle http://jsfiddle.net/djmartin_umich/qBr7y/
The graph works fine and plots as expected. But I need one change to be made so that the plots become triangular shaped and I could see a series of irregular triangles. I mean after every point in Y, it should drop to 0 and start afresh. I know we could achieve this by explicitly adding data points to point to 0. But, just wondering if we could do that without creating additional data points.
HTML:
<div id="line-chart"></div>
<div id="log">Incoming Data:</div>
JS:
var startDate = new Date("2011-11-14T16:17:54Z");
var currDate = moment(startDate);
var cf = crossfilter([{date: startDate, quantity: 1}]);
AddData();
var timeDimension = cf.dimension(function(d){ return d.date; });
var totalGroup = timeDimension.group().reduceSum(function(d){ return d.quantity; });
var lineChart = dc.lineChart("#line-chart")
.brushOn(false)
.width(800)
.height(200)
.elasticY(true)
.x(d3.time.scale().domain([startDate, currDate]))
.dimension(timeDimension)
.group(totalGroup);
dc.renderAll();
window.setInterval(function(){
AddData();
lineChart.x(d3.time.scale().domain([startDate, currDate]));
dc.renderAll();
}, 800);
function AddData(){
var q = Math.floor(Math.random() * 6) + 1;
currDate = currDate.add('day', 5);
cf.add( [{date: currDate.clone().toDate(), quantity: q}]);
$("#log").append(q + ", ");
}
CSS:
#log{
clear:both;
}
Thanks,
Vicky
You can use a "fake group" to achieve this effect.
This is a general-purpose technique for preprocessing data that allows you to change what the chart sees without modifying the data in the crossfilter. In this case, we want to add a data point immediately after each point returned by the crossfilter group.
The fake group wraps the crossfilter group in an object that works like a group. Since in most cases dc.js only needs to call group.all(), this is pretty easy:
function drop_to_zero_group(key_incrementor, group) {
return {
all: function() {
var _all = group.all(), result = [];
_all.forEach(function(kv) {
result.push(kv);
result.push({key: key_incrementor(kv.key), value: 0});
})
return result;
}
}
}
The fake group here produces two data points for each one it reads. The first is just a duplicate (or reference) of the original, and the second has its key incremented by a user-specified function.
It might make sense to parameterize this function by the zero value as well, but I mostly wanted to pull out the date incrementor, since that involves another trick. Here is a date incrementor:
function increment_date(date) {
return new Date(date.getTime()+1);
}
This uses date.getTime() to get the integer value (in milliseconds since the beginning of 1970), adds one, and converts back to a date.
Actually, the first time I tried this, I forgot to include +1, and it still worked! But I don't recommend that, since dc.js is likely to get confused if there is more than one point with the same x value.
Apply the fake group by wrapping the group before passing it to the chart
lineChart
.group(drop_to_zero_group(increment_date, totalGroup));
Here's a fork of DJ's fiddle: http://jsfiddle.net/gordonwoodhull/dwfgma8j/4/
FWIW I also changed dc.renderAll() to dc.redrawAll() in order to enable animated transitions instead of blinking white and rendering from scratch each time. The transitions are not perfect but I think it's still better than the blink. I have a fix but it's a breaking change so it will go into dc.js 2.1.
All I want to do is set a simple Global Option that formats numbers with commas for Y AXIS and Tooltips. I have tried a million examples and I can not get this to work.
Version: Chart.js/2.2.2
I would like to format all numbers with commas for Y axis and tooltip values using a simple global option. The reason why this would be easier using global is that I am generating and dynamically sending in the JSON data and adding more options to that would be a pain, dynamically when all the numbers need to just behave the same.
After talking to the developer I have a really nice global method for setting y axis and tooltip number formatting. I hope this can help someone else too!
You can definitely use global options to control that.
For instance, here's how you would update so that all linear scales (the default Y axis) will format using dollar signs and nice numbers
Chart.scaleService.updateScaleDefaults('linear', {
ticks: {
callback: function(tick) {
return '$' + tick.toLocaleString();
}
}
});
Here's how you could also do the same for a tooltip
Chart.defaults.global.tooltips.callbacks.label = function(tooltipItem, data) {
var dataset = data.datasets[tooltipItem.datasetIndex];
var datasetLabel = dataset.label || '';
return datasetLabel + ": $" + dataset.data[tooltipItem.index].toLocaleString();
};
Then I asked about including simple flags for formatting within the axes and dataset settings:
I've hesitated to push to include these in the core because there are so many different options and trying to support each and every one would dramatically increase the size of the already large library. It may make sense to encourage someone to create a library of formatters that essentially automates the creation of these functions to speed up development.
References:
https://github.com/chartjs/Chart.js/issues/3294#issuecomment-246532871
https://jsfiddle.net/ChrisGo/k4Lxncvm/
I have a bar chart in c3js that uses category ticks on the x-axis.
The tick values I want to display are set upon loading the chart:
axis: {
x: {
tick:{
rotate: -35,
values: getTicks(displayData), //Return the tick values
multiline:false
},
type: 'categorized'
}
}
The reason for setting the ticks manually on load, is I need to update them later.
I need to allow users to update the x axis range dynamically, without loading new data, and I want the number of ticks displayed to remain the same, but with different values obviously.
To change the x axis range, I use this function:
chart.axis.range({min: {x: minRange}, max: {x: maxRange}});
There is no function called chart.axis.values.
Any ideas on how to change tick values dynamically?
EDIT -
To be really clear, I do not wish to update the chart's values. That includes the x and y axis values. I only wish to change what ticks are displayed.
Something like this should work.
axis: {
x: {
type: 'timeseries',
tick: {
values: function(x) { return customTicks(x) },
format: function(x) { return customFormat(x); },
}
}
}
customTicks() must return an array of Dates for this to work.
function customTicks(x) {
start = x[0];
end = x[1];
result = [];
for (var i = new Date(start); i <= end; i.setDate(i.getDate() + 1)) {
result.push(new Date(i));
}
return result;
}
customFormat() takes the date and returns the string for that date. This gives you the flexibility to customize the format as needed.
function customFormat(x) {
}
It may not be particularly elegant but does the job. One benefit of this approach is that you can use the same functions on more than one chart and they will all exhibit identical behavior always.
Problem #1 with this approach is that it seems to call customTicks() for every data point just as it does for customFormat().
Problem #2 is that it generates a lot of these errors:
d3.v3.min.js:1 [Violation] Added non-passive event listener to a scroll-blocking 'touchstart' event. Consider marking event handler as 'passive' to make the page more responsive. See https://www.chromestatus.com/feature/5745543795965952
This has already been flagged on Stackoverflow at Added non-passive event listener to a scroll-blocking 'touchstart' event
I am having the same difficulty updating the Y-Axis ticks values dynamically. I searched through the c3.js documents and was expecting to get something like chart.axis.values similar to chart.axis.range or chart.axis.max / chart.axis.min.
After digging into the Chart object on the console. I somehow managed to resolve my problem, but with a bit hacky way.
Please check the fiddle, I have played with the chart.internal which contains the chart's values on which graph is plotted. I changed those values and used chart.flush() method to redraw the chart.
According to https://github.com/c3js/c3/issues/827:
chart.internal.config.axis_x_tick_values = getTicks(displayData);
This works for me.
My issue here is that I have a crossfilter group reduce function for calculating savings, but when a filter is applied on a chart, it seems to not pass the correct filter across. I also have a similar case working example.
I have created two jsbin's out of which one of them has correct behavior.
I have data for two years in this case 2010 and 2014. But in the Savings(not working) chart the carrier pie chart doesn't filter by year whereas it does in the DIFOT(working) chart.
The links are :
DIFOT(Working) : http://jsbin.com/bagohavehu/2/edit
Savings (Not working as expected) : http://jsbin.com/yudametulo/2/edit
Thanks a lot for your time and effort.
Regards,
Animesh
To track these calculation problems down, you really need to use the debugger and put breakpoints in the reduction functions or in the draw functions to see what reduced values you ended up with.
I find using the browser's debugger very difficult (impossible?) to do in jsbin, so I have again pasted your code into two jsfiddles:
parseFloat version: http://jsfiddle.net/gordonwoodhull/aqhLv0qc/
parseInt version: http://jsfiddle.net/gordonwoodhull/9bnejpLx/1/
Now, we set a breakpoint in chart.redraw (dc.js:1139) to see what values it's trying to plot on clicking the slice.
The first time the breakpoint is hit isn't interesting, because it's just the yearly chart we clicked on, but the second hit is the carrier chart, which reveals a lot. Printing _chart.group().all() for the parseInt version:
Now, for the parseFloat version:
Since the calculations come out exact for the int version, you end up with 1 - 0/0 === NaN, and somewhere along the way dc.js or d3.js silently forces NaN to zero.
But since float calculations are never exact, you end up with 1 - 0/-3e-15 === 1.
You want to avoid dividing by zero, or anything close to zero, so adding a check in your reduction function produces (I think) the desired result:
var group = Dim.group().reduce(
function(p,v){
p.current += parseFloat(v.currentPrice);
p.compare += parseFloat(v.comparePrice);
if(Math.abs(p.compare) > 0.01) {
p.savings = p.current/p.compare;
p.result = 1-p.savings;
}
else p.result = 0;
return p;
},
function(p,v){
p.current -= parseFloat(v.currentPrice);
p.compare -= parseFloat(v.comparePrice);
if(Math.abs(p.compare) > 0.01) {
p.savings = p.current/p.compare;
p.result = 1-p.savings;
}
else p.result = 0;
return p;
},
function(){return { current: 0, compare: 0,savings:0, result:0};}
);
Working version (I think): http://jsfiddle.net/gordonwoodhull/y2sf7y18/1/
The topic of filling area between series has been discussed quite a lot. I've seen some solutions using 'arearange' series ( adding dummy serie with area range to add the fill color ) or using 'stacked area' ( using dummy area serie with stacking: true, transparent under the actual series, then add another stacked area with the needed color ). An example can be seen here.
but my problem is quite specific : I need to fill the area between to series which don't share the same yAxis, thus I can't add a dummy serie since I can't figure which of the yAxis to use.
( The same problem occurs when series don't share the same xAxis reference values, as seen in this example )
For example, let's say I want to fill the area on this chart where the blue 'rainfall' line is under the green 'temperature' line : JSFiddle example
How can I do that ?
I've accepted Sebastian answer and I publish here its implementation :
I've created a function that generates a dummy series with the type 'arearange', using the data of the first series and a modified version of the data in the second series in order to map them on the main yAxis :
/**
*generate a dummy arearange series in order to fill area between our two series.
*/
function createRangeSerie(data1, min1, max1, data2, min2, max2)
{
//we will apply a linear transfomation to values from the second series
var b = min1-min2;
var a = (max1 - b) / max2;
var rangeData = [];
$.each(data1, function(index, value) {
var value2 = a*data2[index] + b;
rangeData.push([value, value2]);
});
//return the third series, used to fill the area between our 2 series.
return {
name: 'areaRangeSerie',
type: 'arearange',
data: rangeData
};
};
then I use it to add the new area in my chart as seen in this fiddle example :
http://jsfiddle.net/2me4Z/3/
Thanks again guys!
LIMITATIONS :
What I was afraid of occurs : in the case I want to fill only if the first curve is under the second (or vice versa). there is a problem on the adjacent points. As you can see in the updated JSfiddle. So this solution is still not perfect, I'll improve it.
LAST VERSION :
In my final version, I've added the intersection point to my arearange serie. Now the result is perfect. You can see the result an the algorithm here in this JSFiddle
as you can see, I4ve changed the xAxis so I've got values for computations instead of categories.
Edit : here is the function to find the intersection point from two lines ( each defined by two points so we need 4 arguments : line1Point1 line1Point2, line2Point1, line2Point2). I don't sue pixel coordinates but I compute intersection from my x and y values since the resulting point will be mapped by the highchart library the same way.
function intersectionPoint(l1Pt1, l1Pt2, l2Pt1, l2Pt2){
console.log(l1Pt1, l1Pt2, l2Pt1, l2Pt2);
//compute A B and C for the first line: AX + BY = C
A1 = l1Pt2.y-l1Pt1.y;
B1 = l1Pt1.x-l1Pt2.x;
C1 = A1*l1Pt1.x + B1 *l1Pt1.y;
//compute A B and C for the second line
A2 = l2Pt2.y - l2Pt1.y;
B2 = l2Pt1.x - l2Pt2.x;
C2 = A2*l2Pt1.x + B2*l2Pt1.y;
var delta = A1*B2 - A2*B1;
if(delta == 0) {
console.log("Lines are parallel");
return null;
}
else
{
return {
x: (B2*C1 - B1*C2)/delta,
y: (A1*C2 - A2*C1)/delta
};
}
};
I'm really expecting highchart to give a full official support for it since it will become again more complex when using logarithmic axis X(
You can use the same two linked axis (the same ranges /ticks) but with different data, and then use additional series with arearange type: http://www.highcharts.com/demo/arearange
I think your options are pretty much one of these two:
1) Normalize your data before sending it to the chart so that they can use the same axis.
2) Develop a complex script to determine where the series are in relation to each other and create your dummy series accordingly.
HOWEVER.
It's extremely important to consider the fact that with two series using two separate axes, measuring two different things on two different scales....
The interaction between the 2 lines is completely meaningless.
It is one of the major common pitfalls of data visualization to highlight the interaction between two such lines, but the interaction is entirely dependent on the mostly arbitrary scaling of the two completely different axis measurements.
FWIW.