How to decide dimensions and groups in dc.js? - d3.js

I am new to dc.js and facing issues in deciding dimensions and groups. I have data like this
this.data = [
{Type:'Type1', Day:1, Count: 20},
{Type:'Type2', Day:1, Count: 10},
{Type:'Type1', Day:2, Count: 30},
{Type:'Type2', Day:2, Count: 10}
]
I have to show a composite chart of two linecharts one for type Type1 and other for Type2. My x-axis will be Day. So one of my dimensions will be Day
var ndx = crossfilter(this.data);
var dayDim = ndx.dimension(function(d) { return d.Day; })
How the grouping will be done? If I do it on Count, the total count of a particular Day shows up which I don't want.

Your question isn't entirely clear, but it sounds like you want to group by both Type and Day
One way to do it is to use composite keys:
var typeDayDimension = ndx.dimension(function(d) {return [d.Type, d.Day]; }),
typeDayGroup = typeDayDimension.group().reduceSum(function(d) { return d.Count; });
Then you could use the series chart to generate two line charts inside a composite chart.
var chart = dc.seriesChart("#test");
chart
.width(768)
.height(480)
.chart(function(c) { return dc.lineChart(c); })
// ...
.dimension(typeDayDimension)
.group(typeDayGroup)
.seriesAccessor(function(d) {return d.key[0];})
.keyAccessor(function(d) {return +d.key[1];}) // convert to number
// ...
See the series chart example for more details.

Although what Gordon suggested is working perfectly fine, if you want to achieve the same result using composite chart then you can use group.reduce(add, remove, initial) method.
function reduceAdd(p, v) {
if (v.Type === "Type1") {
p.docCount += v.Count;
}
return p;
}
function reduceRemove(p, v) {
if (v.Type === "Type1") {
p.docCount -= v.Count;
}
return p;
}
function reduceInitial() {
return { docCount: 0 };
}
Here's an example: http://jsfiddle.net/curtisp/7frw79q6
Quoting Gordon:
Series chart is just a composite chart with the automatic splitting of the data and generation of the child charts.

Related

Time Series chart to filter multi line chart(rendering multi lines for multiple data items)

What is the right way to filter multi line chart using a time series chart as filter?
I need a time series chart for my focus chart that it is shown the image below. Whenever I brush on time series chart my focus chart needs to be filtered with respect to time series chart.
The time series chart needs to contain only X axis and time as its dimension and it should be interactive with focus chart with respect to time.
var totalNumber = null;
// ------ main chart function -------
function makeCompetitiveGraphs(error, keywords_data) {
errorHandle(error);
cleanedData = getCompositeChartData(keywords_data);
console.log("===", cleanedData);
minDate = moment.min(cleanedData.timeStamp);
maxDate = moment.max(cleanedData.timeStamp);
margins = { top: 27, right: 27, bottom: 36, left: 54 };
// create composite chart.
var composite = dc.compositeChart('#competitiveChart');
// create cross filter
var cf = crossfilter(cleanedData.keywordData);
// create dimensions.
var keywordDateDimension = cf.dimension(function (dp) { return dp.date;
});
var Group = keywordDateDimension.group();
// compose for key words
composeCharts = composeKeywords(dc, composite, keywordDateDimension);
// create chart.
composite
.width(width())
.height(height())
.transitionDuration(1000)
.x(d3.time.scale().domain([minDate, maxDate]))
.ordering(function (d) { return d.value; })
.elasticY(true)
.elasticX(true)
.margins(margins)
.legend(
dc.legend()
.x(1100)
.y(10)
.itemHeight(16)
.gap(8)
.horizontal(false)
)
.renderHorizontalGridLines(true)
.brushOn(false);
// compose the chart array.
composite.compose(composeCharts);
// render the chart
composite.render();
function getCompositeChartData(keywords) {
let momentTimeStamps = [];
let totalKeywordPerDay = [];
let allKeywords = [];
// clean data for d3js chart's
keywords.forEach((kob) => {
kob.sd.forEach((ob) => {
allKeywords.push({
name: kob.kn,
total: ob.value.total__,
date: new Date(moment(ob._id.mention_created_date_, "MMM-DD-YYYY-hh")._d),
});
momentTimeStamps.push(moment(moment(ob._id.mention_created_date_, "MMM-DD-
YYYY-hh")._d));
totalKeywordPerDay.push(ob.value.total__);
});
});
console.log("--------", allKeywords)
// apply date filter.
allKeywords = limiteDataToDateFilter(allKeywords);
return { "keywordData": allKeywords, "timeStamp": momentTimeStamps,
"totalKeywordPerDay": totalKeywordPerDay };
}
function limiteDataToDateFilter(allKeywords) {
cleanedDateWithDates = [];
allKeywords.forEach(element => {
if (moment(element.date).isAfter(moment().date(1).month(6)) &&
moment(element.date).isBefore(moment().date(30).month(8))) {
cleanedDateWithDates.push(element);
}
});
return cleanedDateWithDates;
}
function getReduce(keyword, keywordDateDimension) {
return keywordDateDimension.group().reduceSum(function (dp) {
return dp.name === keyword ? dp.total : 0;
});
}
function composeKeywords(dc, composite, keywordDateDimension) {
composeChartsData = []
keywordsParams.forEach(keyword => {
keyword.chart = dc.lineChart(composite)
.dimension(keywordDateDimension)
.colors(keyword.color)
.group(getReduce(keyword.word, keywordDateDimension), keyword.word)
.interpolate('basis')
composeChartsData.push(keyword.chart);
});
return composeChartsData;
}

Reduce number of datapoints using crossfilter

Let's say I have a 100 years worth of monthly data, total of 1200 data points, see bottom.
To plot a tiny overview line chart (e.g. just 100 data points) I have to do it manually by grouping. For instance, group the data by year, then get the average of 12 months value, iterate through every group, then finally reduced the data points to 100.
Instead of this approach, is there a convenient way using crossfilter or any other library?
[
{ date: 1900-01, value: 72000000000},
{ date: 1900-02, value: 58000000000},
{ date: 1900-03, value: 2900000000},
{ date: 1900-04, value: 31000000000},
{ date: 1900-05, value: 33000000000},
...
{ date: 1999-11, value: 30000000000},
{ date: 1999-12, value: 10000000000},
]
It's going to be the same algorithm no matter which library you use, just different ways of specifying it. In this case d3.nest is probably the easiest way to do this, but if you want quick filtering, the crossfilter way isn't too bad.
The difference between using d3.nest and crossfilter is that we're not constructing an array of values, just a single value. So we'll maintain both sum and count.
We'll also need to specify what happens when a row is removed from a bin.
var parse = d3.timeParse("%Y-%m");
data.forEach(function(d) {
// it's best to convert fields before passing to crossfilter
// because crossfilter will look at them many times
d.date = parse(d.key);
});
var cf = crossfilter(data);
var yearDim = cf.dimension(d => d3.timeYear(d.date));
var yearAvgGroup = yearDim.group().reduce(
function(p, v) { // add
p.sum += v.value;
++p.count;
p.avg = p.sum/p.count;
return p;
},
function(p, v) { // remove
p.sum -= v.value;
--p.count;
p.avg = p.count ? p.sum/p.count : 0;
return p;
},
function() { // init
return {sum: 0, count: 0, avg: 0};
}
);
Now yearAvgGroup.all() will return an array of key/value pairs, where the key is the year, and the value contains sum, count, and avg.
Crossfilter doesn't make this problem particularly convenient to solve, but reductio has a helper function for this:
var yearAvgGroup = yearDim.group();
reductio().avg(d => d.value);
Note: it doesn't matter unless you have ton of data, but it's more efficient to only compute sum and count in the group, and compute the average when it's needed.
If you're using dc.js, you can use valueAccessor for this:
// remove avg lines from the above, and
chart.dimension(yearDim)
.group(yearAvgGroup)
.valueAccessor(kv => kv.value.sum / kv.value.count);
Assuming your question is only concerned with producing the data, you can use d3-nest, without crossfilter, to average each year:
Parsing the date value, you can then format the date as a year to create a key. This groups values by key, then we rollup those values with a function to calculate the mean for a given year:
var parse = d3.timeParse("%Y-%m"); // takes: "1900-01"
var format = d3.timeFormat("%Y"); // gives: "1900"
var means = d3.nest()
.key(function(d) { return format(parse(d.date)); })
.rollup(function(values) { return d3.mean(values, function(d) {return d.value; }) })
.entries(data);
Which gives us the following structure:
[
{
"key": "1900",
"value": 39380000000
},
{
"key": "1999",
"value": 20000000000
}
]
var data = [
{ date: "1900-01", value: 72000000000},
{ date: "1900-02", value: 58000000000},
{ date: "1900-03", value: 2900000000},
{ date: "1900-04", value: 31000000000},
{ date: "1900-05", value: 33000000000},
{ date: "1999-11", value: 30000000000},
{ date: "1999-12", value: 10000000000},
];
var parse = d3.timeParse("%Y-%m");
var format = d3.timeFormat("%Y");
var means = d3.nest()
.key(function(d) { return format(parse(d.date)); })
.rollup(function(values) { return d3.mean(values, function(d) {return d.value; }) })
.entries(data);
console.log(means);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

dc.js bubble chart - multidimension grouping issue and unable to get custom reducer to work

I'm currently trying to produce a dashboard in dc.js for my master's thesis and I have hit a real roadblock today if anyone could please help it would be much appreciated. I'm new to Javascript and dc so I'll try my best to explain...
My data format (Probe Request with visible SSID):
{"vendor":"Huawei Technologies Co.Ltd","SSID":"eduroam","timestamp":"2018-07-10 12:25:26","longitude":-1.9361,"mac":"dc:d9:16:##:##:##","packet":"PR-REQ","latitude":52.4505,"identifier":"Client"}
My data format (Probe Request with Broadcast / protected SSID):
{"vendor":"Nokia","SSID":"Broadcast","timestamp":"2018-07-10 12:25:26","longitude":-1.9361,"mac":"dc:d9:16:##:##:##","packet":"PR-REQ","latitude":52.4505,"identifier":"Client"}
I'm trying to produce a bubble chart which will display vendors as a bubble (size denoted by volume of packets collected for that vendor) then plot the bubble against X axis unprotected (any SSID != broadcast) & Y axis protected (packets where "Broadcast" is in the data)
Sketch of what I mean
What I've managed to get so far
I've managed to get a bar/ row/pie charts to work as they only require me to use one dimension and run them through a group. But I think I'm fundamentally misunderstanding how to pass multiple dimensions to a group.
for each at the top adds a new value of 0 / 1 to triple if Broadcast is present in the data.
Then I'm using a custom reducer to count 0 / 1 related to unpro & pro which will be used to plot the X / Y
I've tried reworking the nasdaq example and I'm getting nowhere
Code:
queue()
.defer(d3.json, "/uniquedevices")
.await(plotVendor);
function plotVendor(error, packetsJson) {
var packets = packetsJson;
packets.forEach(function (d) {
if(d["SSID"] == "Broadcast") {
d.unpro = 0;
d.pro = 1;
} else {
d.unpro = 1;
d.pro = 0;
}
});
var ndx = crossfilter(packets);
var vendorDimension = ndx.dimension(function(d) {
return [ d.vendor, d.unpro, d.pro ];
});
var vendorGroup = vendorDimension.group().reduce(
function (p, v) {
++p.count;
p.numun += v.unpro;
p.numpr += v.pro;
return p;
},
function (p, v) {
--p.count;
p.numun -= v.unpro;
p.numpr -= v.pro;
return p;
},
function () {
return {
numun: 0,
numpr: 0
};
}
);
var vendorBubble = dc.bubbleChart("#vendorBubble");
vendorBubble
.width(990)
.height(250)
.transitionDuration(1500)
.margins({top: 10, right: 50, bottom: 30, left: 40})
.dimension(vendorDimension)
.group(vendorGroup)
.yAxisPadding(100)
.xAxisPadding(500)
.keyAccessor(function (p) {
return p.key[1];
})
.valueAccessor(function (p) {
return p.key[2];
})
.radiusValueAccessor(function (d) { return Object.keys(d).length;
})
.maxBubbleRelativeSize(0.3)
.x(d3.scale.linear().domain([0, 10]))
.y(d3.scale.linear().domain([0, 10]))
.r(d3.scale.linear().domain([0, 20]))
dc.renderAll();
};
Here is a fiddle: http://jsfiddle.net/adamistheanswer/tm9fzc4r/1/
I think you are aggregating the data right and the missing bits are
your accessors should look inside of value (that's where crossfilter aggregates)
.keyAccessor(function (p) {
return p.value.numpr;
})
.valueAccessor(function (p) {
return p.value.numun;
})
.radiusValueAccessor(function (d) {
return d.value.count;
})
your key should just be the vendor - crossfilter dimensions aren't geometric dimensions, they are what you filter and bin on:
var vendorDimension = ndx.dimension(function(d) {
return d.vendor;
});
you probably need to initialize count because ++undefined is NaN:
function () { // reduce-init
return {
count: 0,
numun: 0,
numpr: 0
};
}
Fork of your fiddle, with all the dependencies added, wrapping function disabled, and elasticX/elasticY (probably not what you want but easier to debug):
https://jsfiddle.net/gordonwoodhull/spw5oxkj/16/

Crossfilter isn't applying filter to multi line-chart. What am I missing?

I am new to using crossfilter, dc.js, and d3.js. I am struggling to get the filters to apply to my composite line chart. I've gone through several tutorials, but apparently am missing something as the charts don't change or look different at all if I remove the dimension with the filter applied.
Here is an example of my data:
var data = array(
{
price:{value: 38}
shipment:{start_date: "2017-12-06", end_date: "2018-01-15"}
side:"sell"
},
{
price:{value: 44}
shipment:{start_date: "2017-10-08", end_date: "2018-01-15"}
side:"sell"
},
{
price:{value: 38}
shipment:{start_date: "2017-11-15", end_date: "2018-01-15"}
side:"buy"
},
{
price:{value: 38}
shipment:{start_date: "2017-10-25", end_date: "2018-01-15"}
side:"buy"
}
);
And here is where I declare my dimensions:
` var crossFilteredData = crossfilter(data);
// Dimension by start_date
var dateDimension = crossFilteredData.dimension(function(d) {
var date = Date.parse(d.shipment.start_date);
return date;
});
// Dimension by side
var sideDimension = crossFilteredData.dimension(function(d) {
console.log(d.side);
return d.side;
});
sideDimension.filter("buy");
sideDimension.top(Infinity);`
After declaring my dimensions and applying a filter to the sideDimension, I am building my group and calculating a date's max price and min price for each day:
var performanceByDateGroup = dateDimension.group().reduce(
function (p, v) {
++p.count;
p.sum += v.price.value;
// Calculate Min
if (p.minPrice > v.price.value) {
p.minPrice = v.price.value;
}
// Calculate Max
if (p.maxPrice < v.price.value) {
p.maxPrice = v.price.value;
}
return p;
},
function (p, v) {
--p.count;
p.sum -= v.price.value;
return p;
},
function () {
return {
count: 0,
sum: 0,
minPrice: 1000,
maxPrice: 0
};
}
);
Lastly, I put the dimension and groups into the composite line chart:
priceChart
.width(960)
.height(400)
.margins({top: 10, right: 10, bottom: 40, left: 10})
.transitionDuration(500)
.elasticY(true)
.renderHorizontalGridLines(true)
.yAxisLabel('Price')
.shareTitle(false)
.x(d3.time.scale().domain([Date.parse("2017-11-01"), Date.parse("2018-03-31")]))
.xAxisLabel('Shipment Start Date')
.legend(dc.legend().x(40).y(0).itemHeight(16).gap(4))
.compose([
dc.lineChart(priceChart)
.dimension(dateDimension)
.group(performanceByDateGroup, 'Min Price')
.colors('red')
.renderTitle(true)
.title(function(d) {
return 'Min: $' + d.value.minPrice.toFixed(2);
})
.valueAccessor(function (d) {
return d.value.minPrice;
}),
dc.lineChart(priceChart)
.dimension(dateDimension)
.group(performanceByDateGroup, 'Max Price')
.colors('green')
.renderTitle(true)
.title(function(d) {
return 'Max: $' + d.value.maxPrice.toFixed(2);
})
.valueAccessor(function (d) {
return d.value.maxPrice;
})
])
.brushOn(false);
dc.renderAll();
The chart shows all the plotted points, as if the entire sideDimension variable is not being recognized at all. If I remove the sideDimension variable and filter, the chart looks the exact same.
I greatly appreciate any help or suggestions you can offer.
It's difficult, but not impossible to calculate min and max values using a crossfilter reduction.
When crossfilter is evaluating a group, it will first add all the records and then remove the records that don't match the filters. This is so that the result is consistent whether or not the filters existed when the dimension was created. (For example, you want zeros for values that exist but are filtered out.)
In this case, you are not doing anything with minPrice and maxPrice inside of your reduceRemove function:
function (p, v) {
--p.count;
p.sum -= v.price.value;
return p;
},
So as we observe, the records are added but never removed.
However, the situation is worse than this, because min and max are more complicated aggregations than sums and averages. Think about it: you can remember the min and max, but when those are removed, what value do you fall back on?
reductio has handy functions for doing min and max, or if you want to do it yourself, this example shows how.

Adding Filter in dc.js from another dataset

I'm pushing two datasets into the same page.
They're both coming from separate mongodb tables, but the records are linked by a primary key 'plankey'.
I want to add a filter from this graph to the one in the second dataset.
Main chart function:
function loadProjectData(productName) {
// $('#progress_dialog').show();
buildDataLoaded = false;
$.get('/getbuildresults.json?product=' + productName, function (data) {
stats = data;
if (stats != null && stats.length > 0) {
// Convert dates to real dates
stats.forEach(function (d) {
d.builddate = new Date(d.builddate);
});
// feed it through crossfilter
ndx = crossfilter(stats);
overall = ndx.dimension(function (d) {
return d3.time.month(d.builddate);
});
overallGroup = overall.group().reduce(buildReduceAdd, buildReduceRemove, buildReduceInitial);
//Test Types Graph Data Sorter
testTypesDimension = ndx.dimension(function (d) {
return d3.time.week(d.builddate)
})
}
overallChart = dc.compositeChart("#overall-timeline-chart")
.width(chartWidth) // (optional) define chart width, :default = 200
.height(250) // (optional) define chart height, :default = 200
.transitionDuration(500) // (optional) define chart transition duration, :default = 500
.margins({
top: 10,
right: 50,
bottom: 30,
left: 40
})
.dimension(failedTestDimension)
.group(failedTestDimensionGroup)
.elasticY(true)
.mouseZoomable(false)
.elasticX(false)
.renderHorizontalGridLines(true)
.x(d3.time.scale().domain(timelineDomain))
.round(d3.time.month.round)
.xUnits(d3.time.months)
.title(function (d) {
return "Value: " + d.value;
})
.renderTitle(true)
.brushOn(true);
// Loop through available plans and create chart for each and then compose
var charts = [];
var plans = buildGroup.all();
plans.forEach(function (plan) {
charts.push(dc.lineChart(overallChart).dimension(failedTestDimension).group(failedTestDimensionGroup)
.valueAccessor(function (d) {
return d.value.result[plan.key] ? d.value.result[plan.key].failed : null;
}));
});
overallChart.compose(charts);
Second graph function (this is where I want to add the filter from the above graph):
function loadTestResultsData() {
// $('#progress_dialog').show();
testDataLoaded = false;
$.get('/gettestresults.json', function(data) {
stats = data;
if (stats != null && stats.length > 0) {
// Convert dates to real dates
stats.forEach(function (d) {
d.rundate = new Date(d.rundate);
});
// feed it through crossfilter
support_ndx = crossfilter(stats);
//Support Cases Chart
// Dimension and Group for monthly support cases
supportCasesPerMonth = support_ndx.dimension(function(d){ return d.methodName });
supportCasesPerMonthGroup = supportCasesPerMonth.group();
buildTypesChart = dc.rowChart("#failed-tests-chart")
.width(750) // (optional) define chart width, :default = 200
.height(300) // (optional) define chart height, :default = 200
.group(supportCasesPerMonthGroup) // set group
.dimension(supportCasesPerMonth) // set dimension
// (optional) define margins
.margins({
top: 20,
left: 10,
right: 10,
bottom: 20
})
// (optional) define color array for slices
.colors(['#3182bd', '#6baed6', '#9ecae1', '#c6dbef', '#dadaeb'])
// (optional) set gap between rows, default is 5
.gap(7);
}
testDataLoaded = true;
dataLoaded();
});
};
You have two basic approaches. The first to be prefered.
1) Join the data first. I would suggest using something like queue
queue()
.defer(d3.json, '/getbuildresults.json?product=' + productName)
.defer(d3.json, '/gettestresults.json')
.await(ready);
function ready(error, products, stats) {
var productMap = {};
products.forEach(function (d) {
d.builddate = new Date(d.builddate);
productMap[d.plankey] = d;
});
stats.forEach(function (d) {
d.rundate = new Date(d.rundate);
$.extend(d, productMap[d.plankey]);
});
ndx = crossfilter(stats);
// build other dimensions/groups
// build charts
});
2) Your other option is to link the charts by using a trigger to filter on the plankey. So on both data sets, create a crossfilter linked dimension for plankey. Then, on the filter trigger from the second chart, see what plankeys have been set with something like
var keys = C2PlanKeysDim.all()
.filter(function(d){return d.value>0;})
.map(function(d){return d.key;});`
Then on chart 1, filter by the key on C1PlanKeysDim or whatever you call it, and trigger a chart redraw to take into account the filter.

Resources