How to capture the selection in a dc.js + crossfilter + d3.js chart? - dc.js

perhaps the answer is very obvious and has nothing to do the libraries but with general javascript, JQuery or Ajax. I am very new to javascript and I was trying to implement a dashboard using flask as the backend.
Crossfilter and dc help me select ranges on the charts and see how that affects the whole dataset. If I add a:
<span class="filter"></span>
It will display the range above the chart
But that is a class "filter" inside a span object and not a variable or data that I can get inside the code. Below is what I use to display the chart (btw, the reset button does not appear at all)
<div class='span6' id='dc-close-chart'>
<h4>
close
</h4>
<p>range:
<span class="filter">
<a class="reset" href="javascript:closeChart.filterAll();dc.redrawAll();" style="display: none;">
reset
</a>
</span>
</p>
</div>
I would like to be able to do the following:
Be able to access that range and store it is a variable so I can access it and maybe post it using a submit button.
Be able to replace that label for an input textbox to modify the range and change the filter accordingly.
I've been looking around the crossfilter and dc.js forums but I didn't find anything relevant, what I want to do, is it even possible?
Below the JS code, can I create a variable that captures that?
var closeChart = dc.barChart("#dc-close-chart");
// set constants
var n_bins = 35;
d3.csv("/static/data2.csv", function (data) {
console.log(data);
data.forEach(function (d) {
d.close = d3.round(+d.close, 1);
});
// Run the data through crossfilter and load our 'facts'
var facts = crossfilter(data);
var all = facts.groupAll();
// count all the facts
dc.dataCount(".dc-data-count")
.dimension(facts)
.group(all);
// for Each chart numeric
var closeValue = facts.dimension(function (d) {
return d.close; // add the magnitude dimension
});
var closeValueGroupSum = closeValue.group()
.reduceSum(function (d) {
return d.close;
}); // sums
var closeValueGroupCount = closeValue.group()
.reduceCount(function (d) {
return d.close;
}) // counts
// extent
var closeExtent = d3.extent(data, function (d) {
return d.close;
});
// binwidth
var closebinWidth = (closeExtent[1] - closeExtent[0]) / n_bins;
//group
var closeGroup = closeValue.group(function (d) {
return Math.floor(d / closebinWidth) * closebinWidth;
});
// Setup the charts
// Magnitide Bar Graph Counted
closeChart.width(480)
.height(150)
.margins({
top: 10,
right: 10,
bottom: 20,
left: 40
})
.dimension(closeValue)
.group(closeGroup)
.transitionDuration(500)
.centerBar(true)
.gap(1) // 65 = norm
// .filter([3, 5])
.x(d3.scale.linear().domain(closeExtent).range([0, n_bins]))
.elasticY(true)
.xUnits(function () {
return n_bins;
})
.controlsUseVisibility(true)
.colors(['LimeGreen'])
.xAxis().tickFormat(function (v) {
return v;
});
// Render the Charts
dc.renderAll();
});

You can read the currently active filters using chart.filter() or chart.filters().
There isn't anything built in to parse filters from text, but if you figure out how to do that, you could apply the filter with
chart.replaceFilter([dc.filters.RangedFilter(min, max)])
RangedFilter docs.
replaceFilter docs.

Related

d3 static Cubism chart - can't get data input right

I'm trying to make a static cubism chart like this http://bl.ocks.org/bae25/10797393
The csv file ("cubism_test.csv") looks something like this:
date,one,two,three,four,five
2018-06-01,132.54,18.44,68.36,0,56.63
2018-06-02,146.64,19.18,71.74,0,59.66
2018-06-03,160.77,117.98,75.15,0,62.71
2018-06-04,193.29,171.53,78.59,0,65.76
2018-06-05,275.92,78.64,82.05,0,68.82
<script>
// create context and horizon
var context = cubism.context()
.size(30)
.stop();
var horizon = context.horizon()
.extent([0,2]);
d3.csv("cubism_test.csv", function(data)
{
var format = d3.time.format("%Y-%m-%d");
data.forEach(function(d, i)
{
d.date = format.parse(d.date);
d.one= +d.one;
d.two= +d.two;
d.three= +d.three;
d.four= +d.four;
d.five= +d.five;
})
console.log(data);
// define metric accessor
context.metric(function(start,stop,step,callback)
{
var values = data;
console.log(values);
callback(null, values);
}, name);
d3.select("#graph").selectAll(".horizon")
.data(data)
.enter()
.append("div")
.attr("class", "horizon")
.call(horizon);
// set rule
d3.select("#body").append("div")
.attr("class", "rule")
.call(context.rule());
// set focus
context.on("focus", function(i) {
d3.selectAll(".value")
.style( "right", i == null ? null : context.size() - i + "px");
});
// set axis
var axis = context.axis()
d3.select("#graph").append("div").attr("class", "axis").append("g").call(axis);
});
</script>
Obviously this isn't working, but I don't know to fix it. I can't find a proper recourse on how to work with d3 data. The ones I've found are very basic and tell you how to use data to make simple circles, but not time series.
I don't know how to tell d3 to use the column headers as names or get it to use the values in the columns as the values for each cubism/horizon chart.
Your advice would be highly appreciated.

Custom legend based on color of bars in a dc.js composite chart

I implemented a composite chart with two bar charts in which one bar chart consists of bars with different colored bars.
Now, I want to create a custom legend that represents each color bar (similar to https://dc-js.github.io/dc.js/examples/pie-external-labels.html used for pie chart).
Below is the code snippet of what I've done so far:
var buttonPress = dc.barChart(composite)
.dimension(joyTimeDimension)
//.renderlet(colorRenderlet)
//.colors('red')
.colors(colorbrewer.Set1[5])
.colorDomain([101, 105])
.colorAccessor(function (d) {
return d.value;
})
.group(btnGroup, "Button Press")
.keyAccessor(function(d) {return d.key[0];})
.valueAccessor(function (d) {
return d.value;
})
.title( function(d){
return [
"Time: "+d.key[0],
"button Name: "+d.key[1],
"button: "+ d.value
].join('\n')
});
var joyStick = dc.barChart(composite)
.dimension(joyTimeDimension)
.colors('blue')
.group(stepperGroup,"Joy Stick Movement")
.keyAccessor(function(d) {return d.key[0];})
.title( function(d){
return [
"Time: "+d.key[0],
"Stepper Position: "+ d.value
].join('\n')
});
composite
.width(1200)
.transitionDuration(500)
.margins({top: 30, right: 50, bottom: 25, left: 40})
.x(d3.time.scale().domain([startDate,currDate]))
.xUnits(function(){return 150;})
//.xUnits(d3.time.second)
.elasticY(true)
.legend(dc.legend().x(1000).y(4).itemHeight(13).gap(5))
.renderHorizontalGridLines(true)
.renderTitle(true)
.shareTitle(false)
.compose([buttonPress, joyStick])
.brushOn(false)
Is there a way to create a custom legend for this scenario?
Thanks in advance.
Let me provide a little bit of background about how the legend is built.
The legend in dc.js is really not all that sophisticated. It just calls .legendables() on the chart, and the chart decides what items to display in the legend.
Each chart has its own special-purpose code for this.
If we look at the source for compositeChart.legendables(), it's just recursively getting the legendables for each child chart and concatenating them:
_chart.legendables = function () {
return _children.reduce(function (items, child) {
if (_shareColors) {
child.colors(_chart.colors());
}
items.push.apply(items, child.legendables());
return items;
}, []);
};
The pie chart creates a legendable for each pie slice:
_chart.legendables = function () {
return _chart.data().map(function (d, i) {
var legendable = {name: d.key, data: d.value, others: d.others, chart: _chart};
legendable.color = _chart.getColor(d, i);
return legendable;
});
};
The legendables for the bar chart come from the stack mixin, which creates a legendable for each stack:
_chart.legendables = function () {
return _stack.map(function (layer, i) {
return {
chart: _chart,
name: layer.name,
hidden: layer.hidden || false,
color: _chart.getColor.call(layer, layer.values, i)
};
});
};
Given that there's currently no way to get a bar chart to display a pie chart's legend, I think the easiest thing to do is override legendables for your bar chart with its custom colors:
buttonPress.legendables = function() {
return btnGroup.all().map(function(kv) {
return {
chart: buttonPress,
// display the value as the text (not sure what you want here)
name: kv.value,
// apply the chart's color scale to get the color
color: buttonPress.colors()(kv.value)
};
})
};
There are probably some more details to be worked out, such as what if the same value occurs twice? I am assuming you can just read the input data from the group and .map() it, but you might need to generate your data a different way.
But this should give the general idea. Lmk if it doesn't work and I'll be glad to follow up.

How can I use 2 range sliders at the same time?

I want to filter data in the table based on the age and height at the same time using 2 range sliders.
I have implemented 2 range sliders (Age and Height) using d3.slider.js and a dc.dataTable. I want to use these 2 range sliders at the same time, but it seems that they are not working properly.
Also, under the table, there is the text "49 selected out of 49 records". The numbers are not changing while using the sliders.
Code:
var dataTable = dc.dataTable("table#list");
var dispatch = d3.dispatch('load','filter');
d3.json('data.json',function(json){
dispatch.load(json)
});
dispatch.on('load',function(json) {
var formatNumber = d3.format( ",d");
var facts = crossfilter(json);
var dimensionAge = facts.dimension(function(d) {
return +d.age;
});
var accessorAge = function(d) {
return d.age;
};
var dimensionHeight = facts.dimension(function(d) {
return +d.height;
});
var accessorHeight = function(d) {
return d.height;
};
var range = d3.extent(json, accessorAge);
var range2 = d3.extent(json, accessorHeight);
var all = facts.groupAll();
d3.select("div#slider3")
.call(d3.slider().axis(true).min(range[0]).max(range[1]).value(range)
.on("slide", function(evt,value) {
dispatch.filter(value);
d3.select("#slider3textmin").text(Math.floor(value[0]));
d3.select("#slider3textmax").text(Math.floor(value[1]))
}))
d3.select("div#slider4")
.call(d3.slider().axis(true).min(range2[0]).max(range2[1]).value(range2)
.on("slide", function(evt,value) {
dispatch.filter(value);
d3.select("#slider4textmin").text(Math.floor(value[0]));
d3.select("#slider4textmax").text(Math.floor(value[1]))
}))
FieldNames = [
"",
"Age",
"Weight",
"Height",
"Eye Color",
"Hair Color",
"Race",
"Sex",
"Annual Income"
];
d3.select("tr#FieldNames").selectAll("th")
.data(FieldNames)
.enter()
.append("th")
.append("text")
.text(function(d){
return d;
});
dataTable
.dimension(dimensionAge)
.group(function(d) {
return d.sex;
})
.columns([
function(d) {return "";},
function(d) {return d.age;},
function(d) {return d.weight;},
function(d) {return d.height;},
function(d) {return d.eyeColor;},
function(d) {return d.hairColor;},
function(d) {return d.race;},
function(d) {return d.sex;},
function(d) {return formatNumber(d.annualIncome);}
]);
dispatch.on('filter',function(value){
dataTable.replaceFilter(dc.filters.RangedFilter(value[0], value[1]));
dataTable.redraw();
})
dc.dataCount(".dc-data-count")
.dimension(facts)
.group(all);
dc.renderAll();
});
Link to the website
Plunker
Original response on the dc.js users group.
Nice use of d3.slider.js - I haven't seen that used with dc.js before.
At a quick glance, I see two problems here. First, you're using one
dispatch for both sliders, so both sliders are filtering the age,
since that's the dimension of the table. You'd probably want to create
another dimension for filtering by height, and you don't really need
to attach that to a chart.
Second, instead of just redrawing the chart with dataTable.redraw(),
you probably want to call dataTable.redrawGroup() so that all charts
in its chart group get redrawn, including the dataCount.
Specifically:
you'll need two filter events in your dispatch
var dispatch = d3.dispatch('load','filterAge','filterHeight');
the age slider will call filterAge
dispatch.filterAge(value);
and the height slider will call filterHeight
dispatch.filterHeight(value);
the current filter event handler will now handle filterAge and it will call redrawGroup
dispatch.on('filterAge',function(value){
dataTable.replaceFilter(dc.filters.RangedFilter(value[0], value[1]));
dataTable.redrawGroup();
})
we add another filterHeight handler which directly filters dimensionHeight and also redraws the chart group
dispatch.on('filterHeight',function(value){
dimensionHeight.filter([value[0], value[1]]);
dataTable.redrawGroup();
})
Reset All will also have to clear dimensionHeight. (Since this dimension isn't used by any chart, dc.filterAll() won't find it.)
Reset All
Fork of your plunker.
this for reset all, the 49 selected out of 49 records already change correcly
replace this
Reset All
to this
Reset All
add this after dispatch on load
dispatch.on('load',function(json) {
//your code
})
function sololo(){
//table
dispatch.filterAge([0,100]);
dispatch.filterHeight([0,100]);
//text slider
d3.select("#slider4textmin").text(0)
d3.select("#slider4textmax").text(0)
d3.select("#slider3textmin").text(0);
d3.select("#slider3textmax").text(0)
//slider
d3.select('#slider3').select('#handle-one').style('left','0%')
d3.select('#slider3').select('#handle-two') .style('right','0%')
d3.select('#slider3').select('div').style('left','0%').style('right','0%')
d3.select('#slider4').select('#handle-one').style('left','0%')
d3.select('#slider4').select('#handle-two') .style('right','0%')
d3.select('#slider4').select('div').style('left','0%').style('right','0%')
}

dc.js multiple select menu with checkboxes

I have a dataset which consists of 5 columns -> country, id, value and sector. I was able to create a row chart in dc.js using the value and country, where country is my dimension.
var rowChart = dc.rowChart('#rowChart');
d3.csv('data.csv', function(data){
data.forEach(function(d){
d.country = d.country;
d.id = d.id;
d.value = +d.value;
d.sector = d.sector;
});
var height = 300;
var width = 300;
var ndx = crossfilter(data)
var countryDim = data.dimension(function (d) {
return d.country;
});
var countryGroup = countryDim.group().reduceSum(function (d) {
return d.value
})
rowChart
.width(300)
.height(900)
.margins({top: 10, right: 10, bottom: -1, left: 30})
.dimension(countryDim)
.group(countryGroup)
.colors('#86BC25')
.ordering(function (d) { return -d.value; })
.elasticX(true)
.xAxis();
rowChart
.title(function (d) { return d.value;})
.renderTitleLabel(true)
.titleLabelOffsetX(10);
dc.renderAll();
});
and this is my data in csv
country,id,value,sector
USA,0982,10,high
AUS,0983,9,high
IND,0982,10,high
CHN,0982,8,high
CUB,0986,5,middle
FIN,0987,low
i tried creating a jsfiddle, but does not seem to work, sorry my first time
http://jsfiddle.net/i8rice/2r76bjt7/4/
I want to be able to create two drop down with check boxes. One to filter the row chart by country and another by sector. So if I first filter the sector by 'high' in the drop down menu the row chart will get filtered and the other drop down menu should only show me the 5 'high' countries.
I know this is achievable using dc.selectMenu but I wan that drop down check box style. I was wondering if this is possible with dc.js?
Sorry I am very new to asking questions and in d3.js, dc.js and crossfilter.
Thanks to Gordon the check box within the drop down menu was working. However upon discussing with a few others, they have suggested that the check box, once ticked, is not calling the event handler, so wrote this, which is pretty much the same as the one within dc.js
selectField.on('postRender', function() {
$('#menuselect select').change(function(){
console.log($(this).val())
if ($(this).val() && $(this).val() != "") {
selectField.replaceFilter([$(this).val()]);
} else {
selectField.filterAll();
}
dc.events.trigger(function () {
dc.redrawAll();
});
}).multipleSelect({ placeholder: "Select Country"})
});
And everything worked, well, tested it on local. I don't know of any other ways as I am still new to this.

Click table, update line, hover over line, update table

I'm new to D3, but loving it so far. But I know my solutions lack... elegance.
I am trying to have 2 controls, a table and a graph displaying the data represented by the cells of the table. If you click on a cell on the table, the associated line should be highlighted. If you hover over a line the associate table cell will change color. Eventually there will be a third control showing detail data specific to that cel. Unfortunately I've only managed to make this work if I use static calls to the update function. If I try to be clever and dynamic the whole thing breaks.
I've tried to minimize my example as much as I can below. The click table->update line works because the calls to SelectData() that updates everything uses constant data. However the mouseover on the lines doesn't work. Eventually I need the table to be more dynamic too, but for now, how do I fix this?
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.lineDefault {
fill: none;
stroke: red;
stroke-width: 1.5px;
stroke-dasharray: 4,4;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
</style>
<body>
<div id="wrap">
<table>
<tr>
<td id="dataBlock" onclick="SelectData(0)">1</td>
<td id="dataBlock" onclick="SelectData(1)">2</td>
</tr>
<tr>
<td id="dataBlock" onclick="SelectData(2)">3</td>
<td id="dataBlock" onclick="SelectData(3)">4</td>
</tr>
</table>
<div>
<svg class="chart"></svg>
</div>
</div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 600, height = 600;
var maxx = 100,
maxy = 100;
var linedata = {};
linedata[0] = [[0, 50 ],[ 50, 60 ],[100, 100]];
linedata[1] = [[0, 40 ],[ 40, 40 ],[100, 90 ]];
linedata[2] = [[0, 20 ],[ 50, 30 ],[100, 90 ]];
linedata[3] = [[0, 0 ],[ 60, 30 ],[100, 30 ]];
var activeElement = 0;
var graphlines = {};
var numlines = 0;
chart = d3.select(".chart").attr("viewBox", "0 0 600 600").append("g");
var x = d3.scale.linear().domain([0, maxx]).range([0, width]);
var y = d3.scale.linear().domain([0, maxy]).range([height, 0]);
var xAxis = d3.svg.axis().scale(x).orient("bottom");
var yAxis = d3.svg.axis().scale(y).orient("left");
var line = d3.svg.line()
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[1]); });
for (var i = 0; i < 4; i++) {
graphlines[i] = chart
.append("path")
.datum(linedata[i])
.attr("class", "lineDefault")
.attr("id", "linedata")
.attr("d", line)
.on("mouseover", SelectData(i));
numlines++;
}
function SelectData(n) {
d3.selectAll("td").transition()
.style("background-color", function(d, i) {
return i == n ? "#c99" : "#fff";
});
activeElement = n;
for (var i = 0; i<numlines; i++) {
if (i == n) {
graphlines[i]
.style("stroke-dasharray", "1,0")
.transition()
.style("stroke-width", "3")
.style("stroke", "steelblue");
} else {
graphlines[i]
.style("stroke-dasharray", "4,4")
.transition()
.style("stroke-width", "1.5")
.style("stroke", "red");
}
}
}
</script>
The mouseclick on the table effects the lines, the mouseover on the lines does not effect the table. Also I will accept any berating on my elegance and pointers to fixing them.
First, the reason why your mouseover method wasn't working is because in this line:
.on("mouseover", SelectData(i));
you are calling the SelectData method at the time you call the .on() method. So when you're done initializing, your last item is selected, but there is no function listening to the mouseover event. What you want is to pass the function name to the .on() method. If you make that function take two parameters (usually named d and i), then d3 will automatically pass the index value as the second parameter. I wrote up a rather long discussion about passing functions as parameters for someone else recently, you may find it useful.
Furthermore, you're really not taking advantage of the d3 selection structure, which can do in one call all the things you're using for-loops to do. I'd suggest taking the time to read through some tutorials on how the d3 selections work.
Now, to your main question:
The usual solution for selecting an element whose data matches the data of a clicked element is to give all elements a class based on a unique ID from the data. Then you can easily select all the elements associated with a given data object. If the user selects an object with d.name=Sue, and you initialized all the elements with that name as a class name, then you can do d3.select("path.Sue") and also d3.select("td.Sue") to find the correct ones.
Your example data doesn't seem to have any unique data id value, just the index number. So you don't even need a unique class, you can just use the nth-of-type(i+1) CSS selector. (It's i+1 because the CSS counting starts at 1, while the data count starts at 0.)
However, I would suggest that you use a special CSS class to apply all your highlighting styles. That way, it will be easy to select the currently highlighted values to remove that class, instead of having to loop through everything to test whether or not it matches. You can use CSS transitions to transition the style after a class changes.
Here's a fiddle version of your code, tidied up to use d3 properly and with the above method for highlighting data.
http://fiddle.jshell.net/g8z5h/
I haven't implemented the live version of your table, but I recommend you read the tutorial on nested selections to figure out how that will work.
I did give each data block it's own row so that the nth-of-type() selector would work properly (the CSS numbering of elements only works if they are all siblings, they can't be table data elements split across multiple rows). If you need the original layout to work, you'll have to give elements class names based on their index value, and use those to select. I also moved the click event binding into the code, because JSFiddle wraps all its code in a window load event, so the SelectData function wasn't visible outside it.
I have been referring this since i wanted to achieve the same thing in angular2 using d3. Here is my code...
private drawLine() {
this.line = d3Shape.line()
.y((d: any) => this.y(d.rank))
.defined(function(d : any) { return d.rank})
.x((d: any) => this.x(d.year));
var color = function(i) {
var colors = ["#35559C","#D9469C","#70D45B","#915ABB","#FF7C26","#50C5F6","#ECBE4B"];
return colors[i % colors.length];
};
var i = 0;
var j=1;
for(let d of this.lineData){
this.g.append('path')
.attr('d', this.line(d.values))
.attr("class", "line")
.attr("stroke", color(i))
.attr("transform", "translate(0,"+this.margin.top+")")
.attr("id","row-"+j)
.on("mouseover",function(){
d3.select(this)
.attr("class","line1");
d3.select("tr#"+this.id)
.style("background-color","gainsboro")
//console.log( d3.select("tr#"+this.id))
})
.on('mouseout', function(){
d3.select(this)
.attr("class","line");
d3.select("tr#"+this.id)
.style("background-color","")
});
j++;
i++;
if(i > 9) {
i = 0;
}
}
}
private mouseover(event){
d3.select(event.currentTarget)
.style("background-color","gainsboro")
d3.select("path#"+event.currentTarget.id)
.attr("class","line1");
}
private mouseout(event){
d3.select(event.currentTarget)
.style("background-color","")
d3.select("path#"+event.currentTarget.id)
.attr("class","line");
}
while creating line i assigned id to it and the same id to every row in table.
<tr *ngFor="let object of jsonFile; let i = index" id="{{'row-'+[i+1]}}"
(mouseover)="mouseover($event)" (mouseout)="mouseout($event)">
<td *ngFor="let column of columnHeaders" [ngStyle]="{'width':
column.length}">
<div *ngIf="isString(column.columnType) == true">
{{object[column.id] | trim}}
</div>
<div *ngIf="isString(column.columnType) == false">
{{object[column.id]}}
</div>
</td>
</tr>
and called the mouseover and mouseout function in table row.
I would like to have some recommendations if m wrong somewhere.. :)

Resources