Replace text value upon dataset change within an existing div using d3 - d3.js

I am trying to replace text inside existing divs using d3.select when a new dataset is selected, but what I've written adds new divs every time a new dataset is selected. The use case is a leaderboard that picks up the top people groups (d.Culture) that have created objects out of a certain material (Gold, Silver, Bronze, etc.).
Page in question: https://3milychu.github.io/whatmakesart/
It takes a full minute to load (if you see the bar chart, page is loaded)
I've also tried adding .remove() and .exit() unsuccessfully per other suggestions I've viewed.
For some reason, even though the targeted divs are classes, I have to use ".myDiv" instead of "#myDiv" to see the text.
function origins(dataset) {
var totalRows = dataset.length;
console.log(totalRows);
var format = d3.format(".0%");
var origins = d3.nest()
.key(function(d) { return d.Culture; })
.rollup(function(v) { return v.length; })
.entries(dataset)
.sort(function(a,b) {return d3.descending(a.value,b.value);});
console.log(origins);
var culture1 = d3.select(".culture").selectAll(".culture1")
.data(origins)
.enter()
.append("div")
.attr("id", "culture1")
.filter(function (d, i) { return i === 0;})
.text(function(d) { return d.key + " " + format(d.value/totalRows); })
<--repeat for "culture2", "culture3", etc. -->
};
The html divs I am trying to target:
<!-- Origins -->
<div id ="origins">
<h1>Origins</h1>
<div class="culture" id="culture1"></div>
<div class="culture" id="culture2"></div>
<div class="culture" id="culture3"></div>
<div class="culture" id="culture4"></div>
<div class="culture" id="culture5"></div>
<div class="culture" id="culture6"></div>
<div class="culture" id="culture7"></div>
</div>

There are a couple of things going on here that are problematic.
First, you're appending lots of divs with the same ID when you do .attr("id", "culture1")
. This can cause all sorts of unexpected problems.
The filter function doesn't work quite as you would expect. When you do .filter(function (d, i) { return i === 0;}), you're still creating empty divs for all of the elements that do not match the filter. Use the DOM inspector in chrome and you'll see you have tons of empty divs. If you're only trying to get the first element from the array, I suggest passing only that data into the data function. If origins is going to be a different size each time you pass it into the data function, then you will need to implement removing the element on exit. But if the origins array is the same size, the existing data should just update.
That should help fix up the behavior you're seeing. It's possible there are other issues, but these are definite issues that should be fixed first.

I got the following to replace the ranks upon selection change:
function origins(dataset) {
var totalRows = dataset.length;
console.log(totalRows);
var format = d3.format(".0%");
var origins = d3.nest()
.key(function(d) { return d.Culture; })
.rollup(function(v) { return v.length; })
.entries(dataset)
.sort(function(a,b) {return d3.descending(a.value,b.value);});
// console.log(origins);
d3.select(".culture").selectAll("text").remove()
var culture1 = d3.select(".culture").selectAll("#ranks")
.data(origins.filter(function (d, i) { return i === 0;}))
.enter()
.append("text")
.attr("id", "culture1")
.text(function(d) { return d.key + " " + format(d.value/totalRows); })
.exit();
<--repeat for all ranks -->
};
The html was changed to:
<div id ="origins">
<h1>Origins</h1>
<div class="culture" id="ranks"></div>
</div>
The key was to select the element types within the div, and .remove() them prior to declaring the variable and appending. Also per Jeff's suggestion, passing through the filter at .data() prevented creating extra elements.

Related

attr() won't change an attribute unless I return it via an anonymous function

var k = -1;
someCircle.attr("cy", someScaleObject(++k));
All circles will have the same y alignment whereas expected behavior is to have each circle drawn below the other. Oddly enough, if I return someScaleObject(++k) via an anonymous function, this has the desired effect. Why? The author also declares a d variable within the scope of the anonymous function despite having no use for it. Can anyone explain why?
// Works as expected
someCircle.attr("cy", function () {
return someScaleObject(++k);
});
The full code can be found below and is drawn from D3 for the Impatient
// keys.js
function makeKeys() {
var ds1 = [["Mary", 1], ["Jane", 4], ["Anne", 2]];
var ds2 = [["Anne", 5], ["Jane", 3]];
var scX = d3.scaleLinear().domain([0, 6]).range([50, 300]),
scY = d3.scaleLinear().domain([0, 3]).range([50, 150]);
var j = -1, k = -1;
var svg = d3.select("#keys");
svg.selectAll("text")
.data(ds1).enter().append("text")
.attr("x", 20).attr("y", function (d) {
return scY(++j);
}).text(function (d) {
return d[0];
});
svg.selectAll("circle").data(ds1).enter().append("circle")
.attr("r", 5).attr("fill", "red")
.attr("cx", function (d) {
return scX(d[1]);
})
.attr("cy", function (d) {
return scY(++k); // this is what concerns me
});
svg.on("click", function () {
var cs = svg.selectAll("circle").data(ds2, function (d) {
return d[0];
});
cs.transition().duration(1000).attr("cx", function (d) {
return scX(d[1]);
})
cs.exit().attr("fill", "blue");
})
}
And the html file
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="d3.js"></script>
<script src="keys.js"></script>
</head>
<body onload="makeKeys()">
<svg id="keys" width="300" height="150"></svg>
</body>
</html>
In the following:
someCircle.attr("cy", someScaleObject(++k));
You aren't passing a function to selection.attr(), you execute someScaleObject(++k) and pass its return value instead. If you pass something other than a function to selection.attr() for the second parameter, you use the same value for all elements - this is why your circles are all positioned the same.
You could pass a function without executing it:
someCircle.attr("cy", someScaleObject);
However, you want to pass some integer to this function, D3 passes three variables to the function passed to selection.attr() - the datum, the current index, and the group of DOM nodes in the selection. These are passed in this order. In your case you want to pass some other variable to the function, so this method won't work.
You could build someScaleObject to track or access k itself from within the function - such that it doesn't need to be passed as a parameter. Or, we can nest the function inside an anonymous function:
someCircle.attr("cy", function() { return someScaleObject(++k) });
This allows us to pass parameters other than the datum, index, and group of nodes to the function we want to execute. This way selection.attr() is passed a function, that when executed for each element in the selection, execute someScaleObject the intended number of times.
However....
While the above is useful in terms of D3, in your specific situation, the need for passing k to the scale object is unclear, as k represents the index of each element. So instead of:
var k = -1;
selection.attr("cy", function() { return scY(++k); })
We can simply use:
selection.attr("cy", function(d,i) { return scY(i); })
Because the second parameter of a function passed to .attr() is the index of the element in the selection. D3 already tracks this. I'm actually not aware of a use case where the index is something that needs to be tracked externally.

How do I filter a stacked line chart by stack in dc.js?

I am making a stacked line chart for a dashboard:
var json = [...]
var timeFormat = d3.time.format.iso;
json = json.map(function(c){
c.date = timeFormat.parse(c.date);
return c;
});
var data = crossfilter(json);
var days = data.dimension(function (d) {
return d.date;
});
var minDate = days.bottom(1)[0].date;
var maxDate = days.top(1)[0].date;
var lineValues = days.group().reduce(function (acc, cur) {
acc[cur.line] = (acc[cur.line] || 0) + 1
return acc;
}, function (acc, cur) {
acc[cur.line] = (acc[cur.line] || 0) - 1
return acc;
}, function () {
return {};
});
var personChart = dc.lineChart("#graph");
personChart
.turnOnControls(true)
.width(600).height(350)
.dimension(days)
.group(lineValues, "completed")
.valueAccessor(function (d) {
return d.value.completed || 0;
})
.stack(lineValues, "assigned", function (d) {
return d.value.assigned || 0;
})
.stack(lineValues, "inactive", function (d) {
return d.value.inactive || 0;
})
.stack(lineValues, "active", function (d) {
return d.value.active || 0;
})
.stack(lineValues, "new", function (d) {
return d.value.new || 0;
})
.stack(lineValues, "temp", function (d) {
return d.value.temp || 0;
})
.elasticY(true)
.renderArea(true)
.x(d3.time.scale().domain([minDate, maxDate]))
.ordinalColors(colorScale)
.legend(dc.legend().x(50).y(10).itemHeight(13).gap(5).horizontal(true));
dc.renderAll();
Fiddle here
It is working fine so far, but I reached an obstacle. I need to implement an option to filter the chart by individual stacks. Is this possible in dc.js? I can modify and rewrite the entire code if necessary as well as ask my client to remodel the data differently, if needed. There are other fields in the data that I filter on for other charts so preserving that functionality is important.
By design, dc.js has a lot of "leaky abstractions", so there is usually a way to get at the data you want, and customize the behavior by dropping down to d3, even if it's functionality that wasn't anticipated by the library.
Your workaround of using a pie chart is pretty reasonable, but I agree that clicking on the legend would be better.
Here's one way to do that:
var categories = data.dimension(function (d) {
return d.line;
});
personChart
.on('renderlet', function(chart) {
chart.selectAll('.dc-legend-item')
.on('click', function(d) {
categories.filter(d.name);
dc.redrawAll();
})
});
Basically, once the chart is done drawing, we select the legend items and replace the click behavior which our own, which filters on another dimension we've created for the purpose.
This does rely on the text of the legend matching the value you want to filter on. You might have to customize the undocumented interface .legendables() between the legend and its chart, if this doesn't match your actual use case, but it works here.
This fork of your fiddle demonstrates the functionality: https://jsfiddle.net/gordonwoodhull/gqj00v27/8/
I've also added a pie chart just to illustrate what is going on. You can have the legend filter via the pie chart by doing
catPie.filter(d.name);
instead of
categories.filter(d.name);
This way you can see the resulting filter in the slices of the pie. You also can get the toggle behavior of being able to click a second time to go back to the null selection, and clicking on multiple categories. Leave a comment if the toggle behavior is desired and I try to come up with a way to add that without using the pie chart.
Sometimes it seems like the legend should be its own independent chart type...

d3js accept/revert input values

I fill a set of text input fields displaying values of an associative array using d3js. The user changes some input values.
var obj = {a: 'Hello', b: 'World'}
var kk = function(d){return d.key}
var kv = function(d){return d.value}
var upd = function(c){
c.select('div.label').text(kk)
c.select('div.input input').attr("value", kv)
}
var data = d3.entries(obj)
var prop = d3.select("div.container").selectAll("div.prop").data(data, kk)
upd(prop)
var eprop = prop.enter().append("div").attr("class", "prop")
eprop.append("div").attr("class", "label")
eprop.append("div").attr("class", "input").append("input").attr("name", kk)
upd(eprop)
prop.exit().remove()
What are the best practices to
a) update the original associative array from DOM (opposite direction)
b) revert the DOM values to the original ones
My current solutions are
a) iterating over the input fields d3.select("div.container").selectAll("div.prop").select('div.input input').each(function(){obj[this.name]=this.value})
b) assigning [] as selection data and then back the original data (I have not found anything like force update or refresh)
Edit (generalized)
Input fields displaying values of an associative array is only a special case, you can imagine any d3js layout applied on a model with user interaction.
But generally:
a) accept the values from the DOM back to the source data
b) revert the DOM to the actual source data
Is there any d3js built-in support or a d3js plugin for this?
You really need to provide some code because from your question it's tough to tell how you are using d3 to manage your inputs. I'm assuming you are data-binding in some fashion so I just coded this up for fun. It's shows some of the cools ways to use enter/append, data() and datum() to manage groups of input and their values:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
</head>
<body>
<button id="report">Report</button>
<button id="reset">Reset</button>
<script>
var data = [{
v: Math.random()
}, {
v: Math.random()
}, {
v: Math.random()
}, {
v: Math.random()
}, {
v: Math.random()
}, {
v: Math.random()
}];
var inputs = d3.select('body')
.append('div')
.selectAll('input')
.data(data)
.enter()
.append('input')
.attr('type','text')
.attr('value', function(d) {
return d.v;
})
.on('blur', function(d) {
d._v = d.v;
d.v = this.value;
});
d3.select('#report')
.on('click',function(){
alert(inputs.data().map(function(d){
return d.v;
}).join("\n"));
});
d3.select('#reset')
.on('click',function(){
inputs.each(function(d){
if (d._v){
this.value = d._v;
d.v = d._v;
d._v = null;
}
});
});
</script>
</body>
</html>
I found simple and built-in solutions for accept:
prop.select('div.input input').datum(function(d){obj[d.key]=(d.value=this.value);return d})
and revert:
upd(prop.datum(function(d){return(d)}))
I tried this already before, but there was an error on this line:
c.select('div.input input').attr("value", kv)
The correct code is here:
c.select('div.input input').property("value", kv)

D3 sort() with CSV data

I am trying all kinds of ways to make .sort() work on my csv dataset. No luck.
I'd just like to sort my data by a "value" column.
This is the function I'm running inside my d3.csv api call and before I select the dom and append my divs:
dataset = dataset.sort(function (a,b) {return d3.ascending(a.value, b.value); });
Before I get to the .sort, I clean the data:
dataset.forEach(function(d) {
d.funded_month = parseDate(d.funded_month);
d.value = +d.value;
});
};
Everything seems in order. When I console.log(d3.ascending(a.value, b.value)), I get the right outputs:
-1 d32.html:138
1 d32.html:138
-1 d32.html:138
1 d32.html:138
etc..
Yet the bars data doesn't sort.
It is not clear from the provided code but I will hazard a guess you are not handling async nature of d3.csv.
This plunkr shows your sort code working fine. Note where the data object is declared, populated, and used.
here is a partial listing. I have added buttons that re-order data. To achieve this we need to put the ordering logic inside render rather than inside the d3.csv callback.
<script type="text/javascript">
var data = [];
d3.csv("data.csv",
function(error, rows) {
rows.forEach(function(r) {
data.push({
expense: +r.expense,
category: r.category
})
});
render();
});
function render(d3Comparator) {
if(d3Comparator) data = data.sort(function(a, b) {
return d3[d3Comparator](a.expense, b.expense);
});
d3.select("body").selectAll("div.h-bar") // <-B
.data(data)
.enter().append("div")
.attr("class", "h-bar")
.append("span");
d3.select("body").selectAll("div.h-bar") // <-C
.data(data)
.exit().remove();
d3.select("body").selectAll("div.h-bar") // <-D
.style("width", function(d) {
return (d.expense * 5) + "px";
})
.select("span")
.text(function(d) {
return d.category;
});
}
</script>
<button onclick="render('ascending')">Sort ascending!</button>
<button onclick="render('descending')">Sort descending!</button>

Format for D3's data() binding

What is the required format for things passed to d3's .data()?
In this jsfiddle, I try to create several <div> elements for each metric. Unfortunately, nothing happens. I'm assuming this is related to an incorrect data structure?
http://jsfiddle.net/GppWz/
The main issue here is that you are trying to use a hash as a data source, while d3 wants your data in array format.
If you can, modify your data source so that you are receiving data in array format. If this is not possible, you can use the d3.entries function to convert the object into an array:
var listContainers = d3.select('#lists').selectAll('div')
.data(d3.entries(data))
.enter().append('div')
.attr('class', 'listContainer');
listContainers.append('h5')
.text(function(d) {
return d.key;
});
var item = listContainers.selectAll('.item').data(function(d) {
return d.value;
}).enter()
.append('div')
.attr('class', 'item')
.text(function(d) {
return 'average_dif = ' + d.average_dif;
});
// ...

Resources