d3js accept/revert input values - d3.js

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)

Related

How to parse a csv into a dataTable quickly and efficiently using D3 v6

What I'm trying to do
I'm trying to load a CSV with 65 thousand rows into a dataTable. Trying to make a static webpage to showcase some data I just parsed,
I'm using the following libraries
<script src="https://code.jquery.com/jquery-3.5.1.js" charset="utf-8"></script>
<script src="https://cdn.datatables.net/1.10.23/js/jquery.dataTables.min.js" charset="utf-8"> </script>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/dt/dt-1.10.23/datatables.min.css" />
What I did
Here is my code on the javascript side. My HTML just has a body and head. The head is then populated with a table
var tabulate = function (data, columns) {
var table = d3.select('body').append('table').attr("id", "example").attr("class", "display nowrap")
var thead = table.append('thead')
var tbody = table.append('tbody')
thead.append('tr')
.selectAll('th')
.data(columns)
.enter()
.append('th')
.text(function (d) {
return d
})
var rows = tbody.selectAll('tr')
.data(data)
.enter()
.append('tr')
var cells = rows.selectAll('td')
.data(function (row) {
return columns.map(function (column) {
return {
column: column,
value: row[column]
}
})
})
.enter()
.append('td')
.text(function (d) {
return d.value
})
return table;
}
d3.csv("all_engineers.csv", function (data) {
var columns = d3.keys(data[0]);
tabulate(data, columns);
$('#example').DataTable();
})
What is the problem?
It's tooooooo slow. It could be due to the big data set. I was wondering if there was an easy fix. I know D3 is super efficient and there might be something I'm not leveraging.
I also can't seem to use this code with d3 v6. I ended up using d3 v3 instead because this is the example I found that worked with datatables.
Thank you in advance for taking the time.
Update
Thanks to altocumulus
We figured out how to use d3 v6
The old implementation (using D3 V3)
jsfiddle.net/2nwasz43
the Updated Implementation (using D3 V6)
jsfiddle.net/gndv6rq0/1
Loading time before was taking more than 2 minutes before. I removed a few columns from the CSV and decided to add a loading gif to the HTML page since making this instantaneous was out of the question in terms of what I wanted to do
What really helped reduce the load time was loading the d3 data object directly into the datable instead of putting it in the DOM. I was getting page unresponsive in the latter but that seemed to have gone away in the former.
I'm using d3 version 3 instead of version 6 since the syntax seems cleaner and more familiar to me. If you want to use d3 version 6 then you would need to watch out for the API differences (look at https://jsfiddle.net/gndv6rq0/1 for reference)
What I ended up with was the following script
function hideLoader() {
$('#loading').hide();
}
setTimeout(hideLoader, 80 * 1000);
var tabulate = function (columns) {
var table = d3.select('body').select('table')
var thead = table.append('thead')
thead.append('tr')
.selectAll('th')
.data(columns)
.enter()
.append('th')
.text(function (d) {
return d
})
return table;
}
d3.csv("All_engineers_reduced.csv", function (data) {
var columns = d3.keys(data[0]);
tabulate(columns);
$('#example').DataTable({
data: data,
columns: [
{
"data": "Engineer_ID"
},
{
"data": "Arabic_Names"
},
{
"data": "Latin_Names"
},
{
"data": "Field_ID"
},
{
"data": "SubField_ID"
},
{
"data": "Field"
},
{
"data": "SubField"
}
],
processing: true,
language: {
processing: "<img src='https://media1.giphy.com/media/feN0YJbVs0fwA/giphy.gif'>"
}
});
hideLoader();
})
The loaded libraries are still
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://code.jquery.com/jquery-3.5.1.js" charset="utf-8"></script>
<script src="https://cdn.datatables.net/1.10.23/js/jquery.dataTables.min.js" charset="utf-8"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/dt/dt-1.10.23/datatables.min.css" />
You can see the final result in the link below
https://ebrahimkaram.github.io/LebaneseEngineers/

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.

Why is my pie chart showing incorrect groups when filtered with stacked bar chart in dc.js & crossfilter.js?

When I click on a dc.js stacked bar chart, my pie chart elsewhere on the same page doesn't show the correct groups.
I'm new to dc.js, so I've created a simple dataset to demo features I need: Alice and Bob write articles about fruit, and tag each article with a single tag. I've charted this data as follows:
Line chart showing number of articles per day
Pie chart showing total number of each tag used
Stacked bar chart showing number of tags used by author
The data set is as follows:
rawData = [
{"ID":"00000001","User":"Alice","Date":"20/02/2019","Tag":"apple"},
{"ID":"00000002","User":"Bob","Date":"17/02/2019","Tag":"dragonfruit"},
{"ID":"00000003","User":"Alice","Date":"21/02/2019","Tag":"banana"},
{"ID":"00000004","User":"Alice","Date":"22/02/2019","Tag":"cherry"},
{"ID":"00000005","User":"Bob","Date":"23/02/2019","Tag":"cherry"},
];
Illustrative JSFiddle here: https://jsfiddle.net/hv8sw6km/ and code snippet below:
/* Prepare data */
rawData = [
{"ID":"00000001","User":"Alice","Date":"20/02/2019","Tag":"apple"},
{"ID":"00000002","User":"Bob","Date":"17/02/2019","Tag":"dragonfruit"},
{"ID":"00000003","User":"Alice","Date":"21/02/2019","Tag":"banana"},
{"ID":"00000004","User":"Alice","Date":"22/02/2019","Tag":"cherry"},
{"ID":"00000005","User":"Bob","Date":"23/02/2019","Tag":"cherry"},
];
var data = [];
var parseDate = d3.timeParse("%d/%m/%Y");
rawData.forEach(function(d) {
d.Date = parseDate(d.Date);
data.push(d);
});
var ndx = crossfilter(data);
/* Set up dimensions, groups etc. */
var dateDim = ndx.dimension(function(d) {return d.Date;});
var dateGrp = dateDim.group();
var tagsDim = ndx.dimension(function(d) {return d.Tag;});
var tagsGrp = tagsDim.group();
var authorDim = ndx.dimension(function(d) { return d.User; });
/* Following stacked bar chart example at
https://dc-js.github.io/dc.js/examples/stacked-bar.html
adapted for context. */
var authorGrp = authorDim.group().reduce(
function reduceAdd(p,v) {
p[v.Tag] = (p[v.Tag] || 0) + 1;
p.total += 1;
return p;
},
function reduceRemove(p,v) {
p[v.Tag] = (p[v.Tag] || 0) - 1;
p.total -= 1;
return p;
},
function reduceInit() { return { total: 0 } }
);
var minDate = dateDim.bottom(1)[0].Date;
var maxDate = dateDim.top(1)[0].Date;
var fruitColors = d3
.scaleOrdinal()
.range(["#00CC00","#FFFF33","#CC0000","#CC00CC"])
.domain(["apple","banana","cherry","dragonfruit"]);
/* Create charts */
var articlesByDay = dc.lineChart("#chart-articlesperday");
articlesByDay
.width(500).height(200)
.dimension(dateDim)
.group(dateGrp)
.x(d3.scaleTime().domain([minDate,maxDate]));
var tagsPie = dc.pieChart("#chart-article-tags");
tagsPie
.width(150).height(150)
.dimension(tagsDim)
.group(tagsGrp)
.colors(fruitColors)
.ordering(function (d) { return d.key });
var reviewerOrdering = authorGrp
.all()
// .sort(function (a, b) { return a.value.total - b.value.total })
.map(function (d) { return d.key });
var tagsByAuthor = dc.barChart("#chart-tags-by-reviewer");
tagsByAuthor
.width(600).height(400)
.x(d3.scaleBand().domain(reviewerOrdering))
.xUnits(dc.units.ordinal)
.dimension(authorDim)
.colors(fruitColors)
.elasticY(true)
.title(function (d) { return d.key + ": " + this.layer + ": " + d.value[this.layer] });
function sel_stack(i) {
return function(d) {
return d.value[i];
};
}
var tags = tagsGrp
.all()
.sort(function(a,b) { return b.value - a.value})
.map(function (d) { return d.key });
tagsByAuthor.group(authorGrp, tags[0]);
tagsByAuthor.valueAccessor(sel_stack(tags[0]));
tags.shift(); // drop the first, as already added as .group()
tags.forEach(function (tag) {
tagsByAuthor.stack(authorGrp, tag, sel_stack(tag));
});
dc.renderAll();
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter2/1.4.7/crossfilter.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dc/3.1.1/dc.min.js"></script>
<div id="chart-articlesperday"></div>
<div id="chart-article-tags"></div>
<div id="chart-tags-by-reviewer"></div>
As you can see, Alice has made three articles, each tagged with "apple", "banana" and "cherry" respectively, and her stacked bar chart shows this.
However whenever her column of the bar chart is clicked, the pie chart instead shows her as having 1 "apple" and 2 "cherry".
It took me a very long time even to get to this point, so it may be that there's something fundamental I'm not getting about crossfilter groupings, so any insights, tips or comments are very welcome.
Indeed, this is very weird behavior, and I wouldn't know what to think except that I have faced it a few times before.
If you look at the documentation for group.all(), it warns:
This method is faster than top(Infinity) because the entire group array is returned as-is rather than selecting into a new array and sorting. Do not modify the returned array!
I guess otherwise it might start modifying the wrong bins when aggregating. (Just a guess, I haven't traced through the code.)
You have:
var tags = tagsGrp
.all()
.sort(function(a,b) { return b.value - a.value})
.map(function (d) { return d.key });
Adding .slice(), to copy the array, fixes it:
var tags = tagsGrp
.all().slice()
.sort(function(a,b) { return b.value - a.value})
.map(function (d) { return d.key });
working fork of your fiddle
We actually have an open bug where the library does this itself. Ugh! (Easy enough to fix, but a little work to produce a test case.)

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

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.

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>

Resources