I am using dc to create a line graph where capacity is on the y-axis and week is on the x-axis. For weeks, the range is 1-52, but there is no data from weeks 2-40. I only have data for week 1 and 41-52, but my line graph is still creating a line when there is no data:
How do I get it so the line graph will break if there are no values? So it wouldn't be one connected line. Here is my code for reference
let chart = dc.lineChart("#chart");
let ndx = crossfilter(results);
let weekDimension = ndx.dimension(function (d) {
return d.week = +d.week;
});
function reduceAdd(p, v) {
++p.count;
p.total += v.capacity;
p.average = p.total / p.count;
return p;
}
function reduceRemove(p, v) {
--p.count;
p.total -= v.capacity;
p.average = p.count ? p.total / p.count : 0;
return p;
}
function reduceInitial() {
return { count: 0, total: 0, average: 0 };
}
let capacityGroup = weekDimension.group().reduce(reduceAdd, reduceRemove, reduceInitial);
chart.width(360)
.height(200)
.margins({ top: 20, right: 20, bottom: 50, left: 30 })
.mouseZoomable(false)
.x(d3.scale.linear().domain([1, 52]))
.renderHorizontalGridLines(true)
.brushOn(false)
.dimension(weekDimension)
.valueAccessor(function (d) {
return d.value.average;
})
.group(capacityGroup);
dc.renderAll('chart');
This is how results would look like
{month : "1", capacity: "48"}
{month : "1", capacity: "60"}
{month : "42", capacity: "67"}
{month : "42", capacity: "60"}
{month : "43", capacity: "66"}
{month : "44", capacity: "52"}
{month : "45", capacity: "63"}
{month : "46", capacity: "67"}
{month : "47", capacity: "80"}
{month : "48", capacity: "61"}
{month : "48", capacity: "66"}
{month : "49", capacity: "54"}
{month : "50", capacity: "69"}
I have tried to add .defined(d => { return d.y != null; }); and .defined(d => !isNaN(d.value)); but that didn't do anything... Any help will be greatly appreciated
As we discussed in the comments, the important problem is that dc.js will only draw the data it receives. It doesn't know if data is missing, so we will need to fill in the nulls in order to draw gaps in the line.
I linked to a previous question, where the data is timestamps. The answer there uses a d3 time interval to generate the missing timestamps.
However, your data uses integers for keys (even though it represents weeks), so we will need to change the function a little bit:
function fill_ints(group, fillval, stride = 1) { // 1
return {
all: function() {
var orig = group.all();
var target = d3.range(orig[0].key, orig[orig.length-1].key, stride); // 2
var result = [];
for(var oi = 0, ti = 0; oi < orig.length && ti < target.length;) {
if(orig[oi].key <= target[ti]) {
result.push(orig[oi]);
if(orig[oi++].key === target[ti])
++ti;
} else {
result.push({key: target[ti], value: fillval});
++ti;
}
} // 3
if(oi<orig.length) // 4
Array.prototype.push.apply(result, orig.slice(oi));
if(ti<target.length) // 5
result = [...result, ...target.slice(ti).map(t => ({key: t, value: fillval}))];
return result;
}
};
}
This function takes a group, the value to fill, and a stride, i.e. the desired gap between entries.
It reads the current data, and generates the desired keys using d3.range.
It walks both arrays, adding any missing entries to a copy of the group data.
If there are any leftover entries from the original group data, it appends it.
If there are any remaining targets, it generates those.
Now we wrap our original group using this function, creating a "fake group":
const filledGroup = fill_ints(capacityGroup, {average: null});
and pass it to the chart instead:
.group(filledGroup);
One weakness of using LineChart.defined(), and the underlying d3.line.defined, is that it takes two points to make a line. If you have isolated points, as week 1 is isolated in your original data, then it won't be shown at all.
In this demo fiddle, I have avoided the problem by adding data for week 2.
But what about isolated dots?
I was curious how to solve the "isolated dots problem" so I tried showing the built-in dots that are usually used for a mouseover effect:
chart.on('pretransition', chart => {
const all = chart.group().all();
isolated = all.reduce((p, kv, i) => {
return (kv.value.average !== null &&
(i==0 || all[i-1].value.average == null) &&
((i==all.length-1 || all[i+1].value.average == null))) ?
{...p, [kv.key]: true} : p;
}, {});
chart.g().selectAll('circle.dot')
.filter(d => isolated[d.data.key])
.style('fill-opacity', 0.75)
.on('mousemove mouseout', null)
})
This works but it currently relies on disabling the interactivity of those dots so they don't disappear.
Related
Sorry for the title, it is hard to sumarize what I am trying to achieve in one sentence.
I have a bar chart that uses a crossfilter that is also used by 6 other charts. My data looks as follows (note: this is only a small sample, there are more keys in each object)
var data = [
{ clientime : '20210603000000', calendarweek : '22', x : 9, y : 4 },
{ clientime : '20210603000007', calendarweek : '22', x : 5, y : 5 },
{ clientime : '20210607000000', calendarweek : '23', x : 1, y : 2 },
{ clientime : '20210607000007', calendarweek : '23', x : 5, y : 5 },
{ clientime : '20210615000000', calendarweek : '24', x : 10, y : 20 },
{ clientime : '20210615000011', calendarweek : '24', x : 5, y : 5 },
];
The category for each bar is the calendarweek and I wan the the value to be the sum of all x devided by the sum of all y.
According to the above sample I would like to see 3 bars.
Bar '22' should have the value `sum(9,5)/sum(4,5)` = 1.556
Bar '23' should have the value `sum(1,5)/sum(2,5)` = 0.857
Bar '24' should have the value `sum(10,5)/sum(20,5)` = 0.6
My first intention wasto use the reduce function where I would add or remove the sums in a custom dictionary.
var x_key = "calendarweek";
var dim = crossfilter.dimension( d => d[x_key] );
var grp = dim.group().reduce(
( p, v ) => {
p.sum_x += v.x;
p.sum_y += v.y;
return p;
},
( p, v ) => {
p.sum_x -= v.x;
p.sum_y -= v.y;
return p;
},
() => ( { sum_x : 0, sum_y : 0 } )
);
var chart = dc.barChart( "#chart" );
chart
.width( 490 )
.height( 280 )
.dimension( dim )
.group( grp )
.x( d3.scaleBand() )
.xUnits( dc.units.ordinal )
.elasticX( true )
.elasticY( true )
.controlsUseVisibility( true )
.margins( {
top : 10, right : 50, bottom : 20, left : 40,
} );
grp.all() does seem to look fine but from here on out I am not sure how to set the data correctly to chart. Using the created group no bars are shown at all because I creted an object in the reduce function that dc.js does not understand.
Additionally I would like to still be able to limit the bars to N entries.
Thanks to Gordon in the comments pointing me into the right direction I was able to solve my problem using the valueAccesor.
chart_mttrhist.valueAccessor( function( d ) {
return ( d.value.sum_x / d.value.sum_y );
} );
Please note that the code in the question changed a little.
I know there are a lot of questions on that specific subject on SO but none of the solutions seem to work in my case.
This is my data :
var theData = [{
"value": "190.79",
"age_days": "22",
"criteria": "FX"
}, {
"value": "18.43",
"age_days": "22",
"criteria": "FX"
}, {...}]
I put the data into buckets as such :
var getAge = (d) => {
if ((d.age_days) <= 7) {
return (["1w", "2w", "1m", "3m", "6m", "1y", "All"]);
} else if ((d.age_days) <= 14) {
return (["2w", "1m", "3m", "6m", "1y", "All"]);
} else if ((d.age_days) <= 30) {
return (["1m", "3m", "6m", "1y", "All"]);
} else if ((d.age_days) <= 90) {
return (["3m", "6m", "1y", "All"]);
} else if ((d.age_days) <= 180) {
return (["6m", "1y", "All"]);
} else if ((d.age_days) <= 360) {
return (["1y", "All"]);
} else {
return (["All"]);
}
};
var ndx = crossfilter(theData);
var dims = {};
var groups = {};
dims.age = ndx.dimension(getAge,true);
groups.age = {};
groups.age.valueSum = dims.age.group().reduceSum((d) => d.value);
I then try to order the group using the fake group approach :
var sort_group = (source_group, order) => {
return {
all: () => {
let g = source_group.all();
let map = {};
g.forEach(function (kv) {
map[kv.key] = kv.value;
});
return order
.filter((k) => map.hasOwnProperty(k))
.map((k) => {
return {key: k, value: map[k]}
});
}
};
};
var the_order = ["1w", "2w", "1m", "3m", "6m", "1y", "All"];
var the_sorted_age_group = sort_group(groups.age.valueSum, the_order);
then I create the barChart using
theAgeChart
.height(200)
.width(400)
.dimension(dims.age)
.group(the_sorted_age_group)
.valueAccessor((d) => d.value)
.x(d3.scaleBand())
.xUnits(dc.units.ordinal);
But it still comes out using the default sort :
I've created a jsFiddle here which contains everything.
How can I get my bars sorted as I want them to be sorted ?
When elasticX is true or the x scale domain is not set, the coordinate grid mixin will generate the X domain
if (_chart.elasticX() || _x.domain().length === 0) {
_x.domain(_chart._ordinalXDomain());
}
(source)
That totally makes sense, but it always sorts the domain when it generates it:
_chart._ordinalXDomain = function () {
var groups = _chart._computeOrderedGroups(_chart.data());
return groups.map(_chart.keyAccessor());
};
(source)
I guess we could consider not sorting the domain when ordering is null.
Anyway, one workaround is to set the domain yourself:
.x(d3.scaleBand().domain(the_order))
You don't need to sort the group for a bar chart. (For a line chart, the group order must agree with the scale domain, but it doesn't matter for the bar chart.)
With the domain set, this also works:
.group(groups.age.valueSum)
Fork of your fiddle.
I guess the moral of the story is that it's complicated to generate charts automatically. Most of the time one does want the X domain sorted, but what's the best way to allow the user to provide their own sort?
I would not say this is the best way, but there is a way to make it work.
I just started with dc.js and was looking at the NASDAQ example on the main site: https://dc-js.github.io/dc.js/
I created a Fiddle with some sample dummy data and just the two relevant charts for this question.
Similar to the NASDAQ example, I want to have a bubble chart with the Y-Axis being the % Change in value over a timespan controlled by a brush in a different chart. The code for the NASDAQ example does the following:
var yearlyPerformanceGroup = yearlyDimension.group().reduce(
/* callback for when data is added to the current filter results */
function (p, v) {
++p.count;
p.absGain += v.close - v.open;
p.fluctuation += Math.abs(v.close - v.open);
p.sumIndex += (v.open + v.close) / 2;
p.avgIndex = p.sumIndex / p.count;
p.percentageGain = p.avgIndex ? (p.absGain / p.avgIndex) * 100 : 0;
p.fluctuationPercentage = p.avgIndex ? (p.fluctuation / p.avgIndex) * 100 : 0;
return p;
},
/* callback for when data is removed from the current filter results */
function (p, v) {
--p.count;
p.absGain -= v.close - v.open;
p.fluctuation -= Math.abs(v.close - v.open);
p.sumIndex -= (v.open + v.close) / 2;
p.avgIndex = p.count ? p.sumIndex / p.count : 0;
p.percentageGain = p.avgIndex ? (p.absGain / p.avgIndex) * 100 : 0;
p.fluctuationPercentage = p.avgIndex ? (p.fluctuation / p.avgIndex) * 100 : 0;
return p;
},
/* initialize p */
function () {
return {
count: 0,
absGain: 0,
fluctuation: 0,
fluctuationPercentage: 0,
sumIndex: 0,
avgIndex: 0,
percentageGain: 0
};
}
);
which I currently interpret as summing(close-open) across all data and dividing by the average of the average daily index. But this is not a percent change formula I am familiar with. (e.g. (new-old)/old x 100)
While it seems to work for the NASDAQ example, my data would be more like the following:
country_id,received_week,product_type,my_quantity,my_revenue,country_other_quantity
3,2017-04-02,1,1,361,93881
1,2017-04-02,4,45,140,93881
2,2017-04-02,4,2,30,93881
3,2017-04-02,3,1,462,93881
2,2017-04-02,3,48,497,93881
etc.. over many months and product_types.
Let's say I was interested in computing the percent change for a particular Country. How do I get the start and end quantities for a given country so I can compute change as end-start/start * 100?
I was thinking of something such as the following (assuming I set up the proper dimensions and everything)
var country_dim = ndx.dimension(function (d) { return d['country_id']; })
var received_day_dim = ndx.dimension(function (d) { return d['received_day']; })
var date_min = received_day_dim.bottom(1)[0]['received_day']
var date_max = received_day_dim.top(1)[0]['received_day']
Then in my custom reduce function currently in the vein of the example (wrong):
var statsByCountry = country_dim.group().reduce(
function (p, v) {
++p.count;
p.units += +v["my_units"];
p.example_rate = +v['my_units']/(v['quantity_unpacked']*90) //place holder for total units per day per country
p.sumRate += p.opp_buy_rate;
p.avgRate = p.opp_buy_rate/p.count;
p.percentageGain = p.avgRate ? (p.opp_buy_rate / p.avgRate) * 100 : 0;
p.dollars += +v["quantity_unpacked"]/2;
// p.max_date = v['received_week'].max();
// p.min_date
//dateDimension.top(Infinity)[dateDimension.top(Infinity).length - 1]['distance'] - dateDimension.top(Infinity)[0]['distance']
return p;
},
function (p, v) {
--p.count;
if (v.region_id > 2) {
p.test -= 100;
}
p.units -= +v["quantity_unpacked"];
p.opp_buy_rate = +v['quantity_unpacked']/(v['quantity_unpacked']*90) //place holder for total units per day per country
p.sumRate -= p.opp_buy_rate;
p.avgRate = p.count ? p.opp_buy_rate/p.count : 0;
p.percentageGain = p.avgRate ? (p.opp_buy_rate / p.avgRate) * 100 : 0;
p.dollars -= +v["quantity_unpacked"]/2;
// p.max_date = v['received_week'].max();
return p;
},
function () {
return {quantity_unpacked: 0,
count: 0,
units: 0,
opp_buy_rate: 0,
sumRate: 0,
avgRate: 0,
percentageGain: 0,
dollars: 0,
test: 0
};//, dollars: 0}
}
);
and my chart:
country_bubble
.width(990)
.height(250)
.margins({top:10, right: 50, bottom: 30, left:80})
.dimension(country_dim)
.group(statsByCountry)
.keyAccessor(function (p) {
return p.value.units;
})
.valueAccessor(function (p) { //y alue
return p.value.percentageGain;
})
.radiusValueAccessor(function (p) { //radius
return p.value.dollars/10000000;
})
.maxBubbleRelativeSize(0.05)
.elasticX(true)
.elasticY(true)
.elasticRadius(true)
.x(d3.scale.linear())
.y(d3.scale.linear())
// .x(d3.scale.linear().domain([0, 1.2*bubble_xmax]))
// .y(d3.scale.linear().domain([0, 10000000]))
.r(d3.scale.linear().domain([0, 10]))
.yAxisPadding('25%')
.xAxisPadding('15%')
.renderHorizontalGridLines(true)
.renderVerticalGridLines(true)
.on('renderlet', function(chart, filter){
chart.svg().select(".chart-body").attr("clip-path",null);
});
Originally thought of having something similar to the following in statsbycountry:
if (v.received_day == date_min) {
p.start_value += v.my_quantity;
}
if (v.received_day == date_max) {
p.end_value += v.my_quantity;
}
This seems a bit clumsy? But if I do this, I don't think this will continually update as other filters change (say time or product)? Ethan suggested I use fake groups, but I'm a bit lost.
With the working fiddle, we can demonstrate one way to do this. I don't really think this is the best way to go about it, but it is the Crossfilter way.
First you need to maintain an ordered array of all data in a group as part of the group using your custom reduce function:
var statsByCountry = country_dim.group().reduce(
function(p, v) {
++p.count;
p.units += +v["my_quantity"];
p.country_rate = p.units / (1.0 * v['country_other_quantity']) //hopefully total sum of my_quantity divided by the fixed country_other_quantity for that week
p.percent_change = 50 //placeholder for now, ideally this would be the change in units over the timespan brush on the bottom chart
p.dollars += +v["my_revenue"];
i = bisect(p.data, v, 0, p.data.length);
p.data.splice(i, 0, v);
return p;
},
function(p, v) {
--p.count;
p.units -= +v["my_quantity"];
p.country_rate = p.units / (1.0 * v['country_other_quantity']) //hopefully total sum of my_quantity divided by the fixed country_other_quantity for that week
p.percent_change = 50 //placeholder for now, ideally this would be the change in units over the timespan brush on the bottom chart
p.dollars -= +v["my_revenue"];
i = bisect(p.data, v, 0, p.data.length);
p.data.splice(i, 1)
return p;
},
function() {
return {
data: [],
count: 0,
units: 0,
country_rate: 0,
dollars: 0,
percent_change: 0
}; //, dollars: 0}
}
);
Above, I've updated your reduce function to maintain this ordered array (ordered by received_week) under the .data property. It uses Crossfilter's bisect function to maintain order efficiently.
Then in your valueAccessor you want to actually calculate your change in value based on this data:
.valueAccessor(function(p) { //y alue
// Calculate change in units/day from first day to last day.
var firstDay = p.value.data[p.value.data.length-1].received_week.toString();
var lastDay = p.value.data[0].received_week.toString();
var firstDayUnits = d3.sum(p.value.data, function(d) { return d.received_week.toString() === firstDay ? d.my_quantity : 0 })
var lastDayUnits = d3.sum(p.value.data, function(d) { return d.received_week.toString() === lastDay ? d.my_quantity : 0 })
return lastDayUnits - firstDayUnits;
})
You do this in the value accessor because it only runs once per filter change, whereas the reduce functions run once per record added or removed, which can be thousands of times per filter.
If you want to calculate % change, you can do this here as well, but the key question for % calculations is always "% of what?" and the answer to that question wasn't clear to me from your question.
It's worth noting that with this approach your group structure is going to get really big as you are storing your entire data set in the groups. If you are having performance problems while filtering, I would still recommend moving away from this approach and towards one based on a fake group.
Working updated fiddle: https://jsfiddle.net/vysbxd1h/1/
Trying to modify the code at the below link to create an animated scatter which shows data points moving over time.
http://bost.ocks.org/mike/nations/
Struggling to wrap my head around the data interpolation section
// Interpolates the dataset for the given (fractional) year.
function interpolateData(year) {
return nations.map(function(d) {
return {
employee: d.employee,
category: d.category,
x:interpolateValues(d.x, year),
y:interpolateValues(d.y, year)
};
});
}
// Finds (and possibly interpolates) the value for the specified year.
function interpolateValues(values, year) {
var i = bisect.left(values, year, 0, values.length - 1),
a = values[i];
if (i > 0) {
var b = values[i - 1],
t = (year - a[0]) / (b[0] - a[0]);
return a[1] * (1 - t) + b[1] * t;
}
return a[1];
}
});
Code is expecting the data in this format, with the x and y values containing an array with the corresponding year and value.
d: Object
category: "1"
employee: "12017512"
x: Array[63]
y: Array[63]
The data I'm passing in is in this format, with a record for each each year.
d: Object
category: "1"
employee: "12017512"
x: 2697.3199999999993
y: 24
year: "2015"
How do I modify the code to accept the data in the format I have?
Managed to solve my issue. The answer if its helps anyone was to use underscore.js and the following code.
I also found the following jsfiddle helpful.
http://jsfiddle.net/bgAzH/1/
groupData = _.map(_.groupBy(data, 'dimension1', 'dimension2'), function (b) {
return _.extend(_.pick(b[0], 'dimension1', 'dimension2'), {
x: _.map(b, function (elem) {
return _.pick(elem, 'time', 'x')
}),
y: _.map(b, function (elem) {
return _.pick(elem, 'time', 'y')
})
});
});
Then when passing the array of objects into the interpolate values functions I converted the objects to simple arrays.
var values = values.map(function (obj) { return [Number(obj.time), obj[element]] });
I need to create the following array dynamically, for example:
var data = { point: [
{ x:5, y:8 },
{ x:8, y:10},
]};
console.log(data.point[0].x); // 5 n=0
console.log(data.point[1].y); // 10 n=1
At some point my application needs to expand the array to more than 2 items (n=0, n=1). Please let me know how to do that (i.e. n = 9 ).
You could use Array.push method to add element to an array.
var point = {x:1,y:1};
data.point.push(point);
you can use method 'push' like this code
var data = { point: [
{ x:5, y:8 },
{ x:8, y:10},
]};
console.log(data.point[0].x); // 5 n=0
console.log(data.point[1].y); // 10 n=1
data.point.push({x:4,y:3});
console.log(JSON.stringify(data.point));
You could do something like this:
var data = {'points' : []};
function addPoint(x, y) {
data.points.push({'x' : x, 'y' : y});
}
function getPoint(index) {
return data.points[index];
}
addPoint(10, 20);
addPoint(5, 3);
var p = getPoint(1);
alert(p.x + ", " + p.y); //alerts => "5, 3"
This would work for any arbitrary number of points.
Update
To clear the array
function clearPoints() {
data.points = [];
}
Update 2
These little functions will work okay if you have a simple page. If this points handling is going to end up being part of a larger system, it may be better to do something like this:
var data = {
'points' : [],
addPoint : function(x, y) {
this.points.push({
'x' : x,
'y' : y
});
},
getPoint : function(index) {
return this.points[index];
},
clearPoints : function() {
this.points = [];
},
removePoint : function(index) {
this.points.splice(index, 1);
}
};
Example usage:
alert(data.points.length); // => 0
data.addPoint(1, 2);
data.addPoint(8, 12);
data.addPoint(3, 7);
alert(data.points.length); // => 3
var p = data.getPoint(2); //p = {x:3, y:7};
data.removePoint(1);
alert(data.points.length); // => 2
data.clearPoints();
alert(data.points.length); // => 0
It may allow you to keep your point handling a little cleaner and easier to use and update.
You can either use the push() function as stated, or add additional items to the array using an index, which is preferred in the Google Style Guide. The latter method does require that you have a pointer to the last index.
Note that since assigning values to an array is faster than using
push() you should use assignment where possible.
for(var i=lastIndex; i < numOfNewPoints; i++){
data.point[i] = {x:4, y:3};
}