I need some help with d3 in scala js please. Not sure how to go about using d3.json function.
I have this:
val rectXFunVpc = (d: Vpcs,
i: Int) => {
println(s"rectXFunVpc i:$i")
i * 30
}
d3.json(
"json-file.json", callback = (e: Any, json: Any) => {
val jsonAsString: String = JSON.stringify(json.asInstanceOf[js.Any])
println(s"jsonAsString: $jsonAsString")
val pickledJson = read[domain.DescribeVpcsObject](jsonAsString)
println(s"pickledJson:$pickledJson")
val dataArray: js.Array[Vpcs] = pickledJson.Vpcs.asInstanceOf[js.Array[Vpcs]]
println(s"dataArray:$dataArray")
val sel: Update[Vpcs] = svg.selectAll("rect").data(dataArray)
sel.enter()
.append("rect")
.attr("x", rectXFunVpc)
.attr("y", 20)
.attr("width", 20)
.attr("height", 10)
.style("fill", rectColorFun)
print()
}
)
couple of issues:
The rects are not drawn so it looks like the dataArray is not right but when I console.log it I think I am getting a proper js.Array[Vpcs]
The rectXFuncVpc is never called (my printlns in there are not
printed in browser console)
the return type of Unit is forcing me to put the print() as the last statement of the function
Is there a chance someone could provide me an example please?
Hard to figure out without the Vpcs json format.
Here is your json example with a simple json:
https://github.com/spaced/scala-js-d3-example-app/tree/json_example
To enforce a Unit , you can use ()
Related
var cs1 = drawCircles(svg1,data,d=>d["A"],d=>d["B"],sc1);
Trying to get an intuitive understanding for how d is used when binding.
The above function calls .append('circle') and uses the two access functions d=>d["A"],d=>d["B"] to place the circles.
I understand that d essentially acts as an iterable on 'data' and that here we are implicitly returning d['B']. What I don't understand is why we cant simply pass d in a non function way e.g. drawCircles(...,d['A'] ,...). When I've tried using d in this manner it returns d is not defined.
Using a simpler example,
svg.selectAll('circle').data(data).enter.append('circle')
.attr('id', (d) => {return d['name']})
Why can't I simply supply d['name'] for the 2nd arg of .attr()
How can I reason about the use of d solely as a function argument?
Thanks!
The 2nd Parameter of .style()/.attr()
When using something like:
.attr("fill", someParameter);
D3 checks to see if someParameter is a function or a constant.
If someParameter is a constant, "all elements are given the same attribute value", afterall we're using a constant. (see also, D3 documentation).
If someParameter is a function, D3 uses function.apply() (see also, MDN's function.apply() documentation) to call that function for each item in the selection, providing it with 3 parameters:
the current datum (by convention d),
the index of the current item (by convention i),
and the group of nodes in the selection (inconsistent convention, I'll use nodes here).
The use of apply also allows specification of this, which is the current element (which is also: nodes[i]).
The use of function.apply() defines d, i, and nodes only within the function provided.
This makes sense, if you provide a constant, there is nothing to apply, er, apply to and no need to.
What happens when you supply d['name'] as the 2nd argument for .style()/.attr()
If using:
.attr("fill", d.color)`
d in the above has no relation to the datum. If you haven't declared d and given it the property color yourself, it'll be undefined here. D3 doesn't call a function here with apply to define d - you aren't providing a function to do so with.
Only if d.color evaluates to function(d) { return d.color; } would you be able to do what you are asking about. This would be a very unusual form with D3.
If d is undefined you'll likely throw an error when accessing d.color as you've seen. If d is defined, but d.color isn't a function, it'll be treated as a constant and every element will gain a property with the same value.
Consequently, this is why we see the format:
.attr("fill", function(d,i,nodes) { return ... });
Stop Here
It is possible in theory, but not advisable, to accomplish what you think should be possible.
I'm only sharing because
I've had this lying around for a while
It shows how much of a workaround is required to achieve the pattern you are asking about (without pre-defining your functions as properties of some object named d).
Again it's not advisable to use - but you can, technically, dynamically create an accessor function for a given property, or nested property, of the datum with a proxy.
With this you could use the form:
.attr("fill", d.color)
When accessing any of the proxy (d) properties (here color) it would need to return the appropriate accessor function (function(d) { return d.color;}) which would then be passed to .attr() and have the appropriate datum bound to it. You can only use the property, you wouldn't be able to use d.x + 2.
// create a proxy `d` to return accessor functions,
var d = new Proxy({},{ get: f })
var data = [
{style:{fill:"steelblue",stroke:{color:"crimson", width:4}},width: 30, y: 50, x: 10},
{style:{fill:"yellow",stroke:{color:"orange", width:2}},width: 20, y: 50, x: 50},
{style:{fill:"crimson",stroke:{color:"steelblue", width:8}},width: 30, y: 50, x: 80}
]
var svg = d3.select("body").append("svg");
svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("x", d.x)
.attr("y", d.y)
.attr("width", d.width)
.attr("height",d.width)
.attr("fill",d.style.fill)
.attr("stroke-width",d.style.stroke.width)
.attr("stroke", d.style.stroke.color);
// To resolve paths (https://stackoverflow.com/a/45322101/7106086):
function resolve(path, obj) {
return path.reduce(function(prev, curr) {
return prev ? prev[curr] : null
}, obj || self)
}
// Proxy to dynamically access properties of any depth:
function f(obj, prop) {
if(prop in obj) return obj[prop];
else {
var g = function() {
var accessor = function(d) {
return resolve(accessor._path_,d);
}
// keep track of path:
if(obj._path_) accessor._path_ = [...obj._path_,prop];
else (accessor._path_) = [prop];
return accessor;
}
return obj[prop] = new Proxy(g(), {get:f});
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I have been using the d3 v5 .join update pattern. It's great, but I have a situation in my code where the data is not being passed down as expected and I am at a loss for what is going wrong. My code looks like this :
let binnedWrap = wrap.selectAll('.attr-wrap').data(sortedBins).join('g').attr('class', d=> d.key + ' attr-wrap');
binnedWrap.attr('transform', (d, i)=> 'translate(0,'+(i * (height + 5))+')');
let label = binnedWrap.append('text').text(d=> d.key).attr('y', 40).attr('x', 80).style('text-anchor', 'end');
let branchGroup = binnedWrap.selectAll('g.branch-bin').data(d=> {
///THIS IS RIGHT
console.log('data before the branch bins',d);
return d.branches}).join('g').classed('branch-bin', true);
branchGroup.attr('transform', (d, i)=> 'translate('+(100 + branchScale(i))+')');
This works as expected. The data that consoles is correct and it creates a group classed 'branch-bin' for each branch element in d.branches
BUT- when I attempt to use the branch data within each of the 'branch-bin' group, I am not getting the expected d.branches data:
let continDist = branchGroup.filter(f=> f.type === 'continuous');
var lineGen = d3.line()
.y((d, i)=> {
console.log('y',d, i)
let y = d3.scaleLinear().domain([0, 16]).range([0, height]);
return y(i);
})
.x(d=> {
let x = d3.scaleLinear().domain([0, 100]).range([0, 100]);
return x(Object.entries(d).length - 2);
});
//THIS IS RIGHT//
console.log(continDist.data())
continDist.append('path').data((d, i)=> {
console.log('supposed to be data in d branch', d, i);
return lineGen(d.bins)});
The output looks the same as the above console. The path is not being passed the branch data.
Any idea what is going on here would be much appreciated!
I made the mistake of using
continDist.append('path').data((d, i)=>lineGen(d.bins));
instead of continDist.append('path').attr('d', (d, i)=>lineGen(d.bins));
after assigning d as an attr of path it worked as expected.
d3 newb is trying something else:
I want to add a date label to my bar chart only if the date is not the same as in the previous bar.
bar.append("text")
.attr("visibility", function(d,i){
if(d.Datum == data[i-1].Datum) return "hidden"})
.attr("x", padding)
.attr("y", barHeight-2)
.attr("fill", "Gainsboro")
.text(function(d){ return d.Datum})
So I thought I add a visibility to my text label. however I cannot access the previous date from the data-object... probably this is an easy one for someone not newbie as me...
data example (before CSV import)
Datum,Name,Kategorie,Betrag
01/10/15,,Lohn,1586.7
02/10/15,,lunch,-4.55
So assuming that after d3 parses the text file you are left with data like:
var data = [
{
Datum: "01/10/15",
Name: "",
Kategorie: "Lohn",
Betrag: 1586.7
},{
...
}
];
I'd pre-process the data to contain a bool about whether or not it is the first instance of that date:
data.forEach(function(d,i){
d.isFirstInstanceOfDate = (i === 0 || d.Dataum !== data[i-1].Datum);
});
Then assuming that bar is a selection of gs elements (which already contain a rect), I'd filter them and only append the text on the first instance:
bar
.filter(function(d){
return d.isFirstInstanceOfDate
})
.append('text')
...
The index starts from 0. By subtracting 1 from it the first time you get an error. You need to check whether i > 0 so that you don't do out of bounds.
For example:
.style('visibility', function (d, i) {
if (i > 0) {
if (d.datetime === data[i - 1].datetime) {
return 'hidden';
}
}
return 'visible';
});
Here are two sample fiddles:
http://jsfiddle.net/00drii/y079vw7e/
http://jsfiddle.net/00drii/dmhs0gza/
Is it possible to (and if yes - how to fix render()) to perform properly updating divs after click on [x] with d3?
var id_names = {},
render = function (id) {
var names = id_names[id],
divs = d3.select("#filelist").selectAll("div").data(names),
denter = divs.enter().append("div");
denter.append("div")
.classed("txt", true);
denter.append("div")
.classed("del", true);
divs.select("div .txt")
.text(function (d, i) {return i + " :: " + d});
divs.select("div .del")
.text("[x]")
.on("click", function (d, i) {
// remove element from array
id_names[id].splice(i, 1)
render(id)
});
divs.exit().remove()
};
// {id: [name, nameN]}
id_names[11] = ["aaa", "bbb", "cc"]
render(11)
I think the problem is that you bind the data to divs, and you have more divs in the denters.
So if you add some class (or id) to the containing divs, you get the desired effect (I hope).
See here:
JSFiddle
Or here, the core changes:
divs = d3.select("#filelist").selectAll(".container")
.data(name),
denter = divs.enter().append("div")
.attr('class', 'container');
(A side note: I am not a fan of modifying in a function that is in an outside scope, as you are doing it with id_name here...). This may lead to very nasty side effects...)
Hope this helps!
I have a multi-line chart representing 8 different series of values, for a given date:
http://bl.ocks.org/eoiny/8548406
I have managed to filter out series1 and append circles for each data-point for series1 only, using:
var filtered = city
.filter(function(d){
return d.name == "series1"
})
filtered
.selectAll('circle')
.data(
function(d){return d.values}
)
.enter().append('circle')
.attr({
cx: function(d,i){
return x(d.date)
},
cy: function(d,i){
return y(d.pindex)
},
r: 5
})
However I am trying to append 4 circles to my series1 line, one for each of the following values only:
min value in series1,
max value in series1,
1st value in series1,
last value in series1.
I approached this problem by looking at the "filtered" array and I tried using something like this to catch the min & max values to start with:
.attr("visibility", function(d) {
if (d.pindex == d3.max(filtered, function(d) { return d.pindex; })) {return "visible"}
if (d.pindex == d3.min(filtered, function(d) { return d.pindex; })) {return "visible"}
else { return "hidden" }
;})
But I'm somehow getting muddled up by the fact that the data I need is in an object within the filtered array. I know that filtered should look like this:
[{
name: "series1",
values: [{date: "2005-01-01",
pindex: "100"},
{date: "2005-02-01"
pindex: "100.4"}, ...etc for all data points i.e. dates
]
}]
So I tried something like this:
d.pindex == d3.max(filtered, function(d) { return d.values.pindex; })
but I'm still getting a bit lost. Does anyone have any ideas?
In general, you probably want to filter your data rather than DOM elements. So instead of using city.filter you might use cities.filter to get the data array you're interested in. More importantly, you probably want to filter the data passed to the new circle selection, rather than creating all circles and then selectively showing or hiding them. I might try:
filtered
.selectAll('circle')
.data(function(d){
var points = d.values;
// create the array of desired points, starting with the easy ones
var circleData = [
// first
points[0],
// last
points[points.length - 1]
];
// now find min and max
function getValue(d) { return d.pindex; }
// d3.max returns the max value, *not* the object that contains it
var maxVal = d3.max(points, getValue);
// Note that you might have multiple points with the max. If you
// don't want them all, just take maxPoints[0]
var maxPoints = points.filter(function(d) { return d.pindex === maxVal; });
// same for min
var minVal = d3.min(points, getValue);
var minPoints = points.filter(function(d) { return d.pindex === minVal; });
// stick them all together
return circleData.concat(maxPoints).concat(minPoints);
})
.enter().append('circle')
// etc
Key points:
Filter your data, not your DOM. It's less expensive processing, easier to debug, and generally much easier to get your head around.
d3.min and d3.max don't return the object with the max value, they return the value itself, hence your TypeError.