I'm trying to get drag functionality to work on D3, and have copied the code directly from the developer's example.
However it seems the origin (what is being clicked) is not being passed correctly into the variable d, which leads to the error: 'Cannot read property 'x' of undefined'
The relevant code:
var drag = d3.behavior.drag()
.on("drag", function(d,i) {
d.x += d3.event.dx
d.y += d3.event.dy
d3.select(this).attr("transform", function(d,i){
return "translate(" + [ d.x,d.y ] + ")"
})
});
var svg = d3.select("body").append("svg")
.attr("width", 1000)
.attr("height", 300);
var group = svg.append("svg:g")
.attr("transform", "translate(10, 10)")
.attr("id", "group");
var rect1 = group.append("svg:rect")
.attr("rx", 6)
.attr("ry", 6)
.attr("x", 5/2)
.attr("y", 5/2)
.attr("id", "rect")
.attr("width", 250)
.attr("height", 125)
.style("fill", 'white')
.style("stroke", d3.scale.category20c())
.style('stroke-width', 5)
.call(drag);
Usually, in D3 you create elements out of some sort of datasets. In your case you have just one (perhaps, one day you'll want more than that). Here's how you can do it:
var data = [{x: 2.5, y: 2.5}], // here's a dataset that has one item in it
rects = group.selectAll('rect').data(data) // do a data join on 'rect' nodes
.enter().append('rect') // for all new items append new nodes with the following attributes:
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; })
... // other attributes here to modify
.call(drag);
As for the 'drag' event handler:
var drag = d3.behavior.drag()
.on('drag', function (d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this)
.attr('transform', 'translate(' + d.x + ',' + d.y + ')');
});
Oleg's got it, I just wanted to mention one other thing you might do in your case.
Since you only have a single rect, you can bind data directly to it with .datum() and not bother with computing a join or having an enter selection:
var rect1 = svg.append('rect')
.datum([{x: 2.5, y: 2.5}])
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; })
//... other attributes here
.call(drag);
Related
I have a data object to show stacked bars with an initial animation that plays upon loading the page, essentially where fruits correspond to orchards:
[{Apple=1.0, Orange=2.0, Lettuce=1.0, orchard=小明, Blueberry=1.0}, {Apple=1.0, Orange=1.0, Lettuce=1.0, orchard=小陈, Blueberry=1.0}, {Apple=1.0, Orange=1.0, Lettuce=1.0, orchard=小虎, Blueberry=1.0}, {Orange=1.0, Lettuce=1.0, orchard=小桃, Blueberry=1.0, Apple=1.0}]
The below code works fine if each orchard includes every fruit-type, but upon let's say removing Apples from everyone except for 小明, the y0 values for multiple orchards become 'NaN'. I am asking for a way to remove NaN values or find a way to skip them in d3js so that other fruits can still be displayed.
My rectangle code can be found below:
var rect = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d) { return x(d.x); })
.attr("height", function(d) { return y(d.y0) - y(0); })
.attr("y", function(d) { return y(0); })
.attr("width", x.rangeBand())
.on("mouseover", function() { tooltip.style("display", null); })
.on("mouseout", function() { tooltip.style("display", "none"); })
.on("mousemove", function(d) {
var xPosition = d3.mouse(this)[0] - 15;
var yPosition = d3.mouse(this)[1] - 25;
tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")");
tooltip.select("text").text(d.x + ": " + d.y);
});
svg.selectAll("rect")
.transition()
.duration(800)
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); })
.delay(function(d,i){console.log(i) ; return(i*90)});
I am curious about using something like
rect.filter(function(d) {
return d.y == 0;
})
.remove();
But this does not seem to work. Unfortunately I am using d3js v3.
The issue was solved by reformatting how my d3 viz read keys. In my answer to my other question you can see how the keys variable was reformatted.
I don't have my v3 version saved on-file, but it had something to do with how stackedLayout works in v3. Not sure, but if you have a similar issue, I suggest reformatting how your variables are stacked to follow the same philosophy of using
.data(d3.stack().keys(keys)(data))
I have a parallel coordinates (parallel axes) chart that is 90% working, but I'm having issues with entering new data. I am getting the error, "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node."
I am copying the re-drawing of the axes from when the chart is initialized exactly, so that should be working fine. It seems like it's a problem with my identifier function when adding new data (potentially), although, my exit function works fine with it.
When I console log axes.enter(), the output is correct (correct element being added in correct spot). However, there's the insert error that seems to stop it from rendering.
function updateAxis(newFilters, selectedAxis, newData) {
dimensions = d3.keys(newData[0]).filter(function(d) {
return d != "PID" && d != "Age" && (y[d] = d3.scaleLinear()
.domain(d3.extent(data, function(p) { return +p[d]; }))
.range([height, 0]))
});
x.domain(dimensions);
var axes = d3.selectAll(".dimension")
.data(dimensions, d => d);
axes
.transition('update')
.duration(500)
.each(function(d) { d3.select(this).call(axis.scale(y[d])); })
.attr("transform", function(d, i) {
return "translate(" + x(d) + ")";
});
axes.selectAll("text")
.text(function(d) { return d; });
foreground
.transition('update')
.duration(500)
.attr("d", path);
background
.transition('update')
.duration(500)
.attr("d", path);
/********start axis enter() updates*************/
axes.enter().append("axes")
.attr("class", "axis")
.each(function(d) { d3.select(this).call(axis.scale(y[d])); })
.append("text")
.style("text-anchor", "end")
.attr("y", -9)
.text(function(d) { return d; })
.style("fill", "black");
axes.append("axes")
.attr("class", "dimension")
.attr("transform", function(d) { console.log ("axes.enter() transform: ", d); return "translate(" + x(d) + ")"; })
.call(d3.drag()
.subject(function(d) {
//console.log(d);
return {x: x(d)}; })
.on("start", function(d) {
dragging[d] = x(d);
background.attr("visibility", "hidden");
})
.on("drag", function(d) {
dragging[d] = Math.min(width, Math.max(0, d3.event.x));
foreground.attr("d", path);
dimensions.sort(function(a, b) { return position(a) - position(b); });
x.domain(dimensions);
axes.attr("transform", function(d) { return "translate(" + position(d) + ")"; })
})
.on("end", function(d) {
delete dragging[d];
transition(d3.select(this)).attr("transform", "translate(" + x(d) + ")");
transition(foreground).attr("d", path);
background
.attr("d", path)
.transition()
.delay(500)
.duration(0)
.attr("visibility", null);
}));
// Add and store a brush for each axis.
axes.append("axes")
.attr("class", "brush")
.each(function(d) {
d3.select(this).call(y[d].brush = d3.brushY()
.extent([[-10,0], [10,height]])
.on("brush", brush)
.on("end", brush)
)
})
.selectAll("rect")
.attr("x", -8)
.attr("width", 16);
/********end axis enter() updates*************/
axes.exit()
.transition('exit-transition')
.duration(500)
.attr("opacity", 0)
.remove();
};
Expected output is re-drawing the lines and adding in the path data (foreground and background). Foreground and background are added correctly, but the axes are not getting re-drawn correctly. So the foreground/background updates, there is space allocated in the visualization for the axis, all other axes are moved to match the new foreground/background, it's just the enter() axes themselves not being added back in. I suspect it's related to this "insert" error.
Unfortunately I can't share the entire visualization because the data I'm working on is protected, but the newData file is updating correctly and the dimensions re-add the axes in the correct place. Essentially, the axes is being added back in the order it was removed in (so it's being added back "in place" as much as possible).
I can't figure out what I'm doing wrong - appreciate all help/advice! Let me know if I need to add more details.
Anyone managed to make the example
Sankey diagram with horizontal and vertical node movement
work in v4
since sankey.relayout() is not available anymore
d3.drag has no .origin anymore.
My attempt do the wildest things while attempting to drag a node and since some behaviors have changed in both sankey and drag specifications I'm unable to figure how to migrate the example to v4.
var graph = { "nodes": [...], "links": [...] };
var layout = d3.sankey();
layout
.extent([[1, 1], [width - 1, height - 6]])
.nodeId(function (d) {
return d.id;
})
.nodeWidth(12)
.nodePadding(padding)
.nodeAlign(d3.sankeyRight)
layout(graph);
// Add Links
var link = svg.append("g")
.attr("fill", "none")
.selectAll(".link")
.data(graph.links)
.enter()
.append("g")
.attr("class", "link")
link.append("path")
.attr("d", d3.sankeyLinkHorizontal())
.attr("stroke", "#000")
.style("stroke-width", function (d) {
return d.width;
})
.append("title")
.text("Some text");
// Drag behavior for node elements
var drag = d3.drag()
//.origin(function (d) { return d; }) // Deprecated but maybe unnecessary
.on("drag", dragmove)
// Add nodes
var node = svg.append("g")
.selectAll(".node")
.data(graph.nodes)
.enter()
.append("g")
.attr("transform", function (d) {
return "translate(" + [d.x0, d.y0] + ")";
})
.call(drag) // The g element should be now draggable
// Add element inside g element
node.append("rect")
.attr("height", function (d) { return d.y1 - d.y0; })
.attr("width", function (d) { return d.x1 - d.x0; })
.attr("fill", ...)
node.append("text")
.attr("x", function (d) { return (d.x1 - d.x0) - 6; })
.attr("y", function (d) { return (d.y1 - d.y0) / 2; })
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.text(function (d) { return d.name; })
.filter(function (d) { return d.x0 < width / 2; })
.attr("x", function (d) { return (d.x1 - d.x0) + 6 })
.attr("text-anchor", "start");
// Called by d3.drag
function dragmove(d) {
var dx = Math.round(d.x = Math.max(0, Math.min(width , evt.x)));
var dy = Math.round(d.y = Math.max(0, Math.min(height, evt.y)));
d3.select(this).attr("transform", "translate(" + [dx, dy] + ")")
// Now should redraw the links but
sankey.relayout(); // not a function anymore.
// path references sankey.link() that is also deprecated in v4 in
// favour of d3.sankeyLinkHorizontal() ? (I'm not sure)
// link references the g element containing the path elements
// classed .link
link.attr("d", path);
};
This is the final updated code. In a update function that takes a dataset object this code will render a real time legend. Other chart elements can be initialized when the script is loaded and the enter, update and exit selections for those belong in the update function as well. Maybe it is useful. Was for me. I think I can add this to any chart. Working fiddle
var legendG = svg.selectAll(".legend")
.data(donut(dataset), function (d) {
return d.data.label;
})
var legendEnter = legendG.enter();
legendEnter = legendEnter.append("g")
.attr("class", "legend");
//Now merge Enter and Update and adjust the location
legendEnter.merge(legendG)
.attr("transform", function (d, i) { return "translate(" + (width
- 350) + "," + (i * 15 + 20) + ")"; })
.attr("class", "legend");
legendG.exit().remove();
// Apend only to the enter selection
legendEnter.append("text")
.text(function (d) {
return d.data.label;
})
.style("font-size", 12)
.attr("y", 10)
.attr("x", 11);
//Now merge Enter and Update the legend color
legendEnter.merge(legendG)
.append("rect")
.attr("width", 10)
.attr("height", 10)
.attr("fill", function (d, i) {
console.log('index: ' + i + ' ' + d.data.label)
var c = color(i);
return c;
});
You should update the transform attribute of the update and enter selection together after merging the selection:
var legendG = svg.selectAll(".legend")
.data(donut(dataset), function (d) {
return d.data.label;
})
var legendEnter= legendG.enter().append("g")
.attr("class", "legend");
//Now merge Enter and Updatge and adjust the location
legendEnter.merge(legendG)
.attr("transform", function (d, i) { return "translate(" + (width - 500) + "," + (i * 15 + 20) + ")"; })
.attr("class", "legend");
legendG.exit().remove();
// Apend only to the enter selection
legendEnter.append("rect")
.attr("width", 10)
.attr("height", 10)
.attr("fill", function (d, i) {
return color(i);
});
...
Updated fiddle from previous post:fiddle
I've got a legend, with colored rectangles...
I'd like to replace the rectangles with symbols (i.e., circle, cross, diamond, square). I can't figure out how to do that.
I've been using variations of .attr("d", d3.svg.symbol().type('circle'). For instance, I tried:
legendRect
.attr("d", d3.svg.symbol().type(function (d) { return d[2] })
and I tried:
legendRect.append("svg:path")
.attr("d", d3.svg.symbol().type((d: any) => { return d[2] }))
d[2] is "supposed to be" pulling from legendData, as shown in the below code example...like it does with d[1] for the fill.
But I don't ever see anything change.
Here's the code I'm using for the legend, without the symbol stuff, below. What am I doing wrong and how can I change the rectangles to symbols? Where do I need to add what?
var legendData = [["OA", "yellow", "circle"], ["OI", "blue", "cross"], ["RARC", "green", "diamond"], ["CAPE", "red", "square"], ["Other", "black", "triangleDown"]];
var legend = this.svg.append("g")
.attr("class", "legend")
.attr("height", 0)
.attr("width", 0)
.attr('transform', 'translate(-20,250)');
var legendRect = legend.selectAll('rect').data(legendData);
legendRect.enter()
.append("rect")
.attr("x", width - 65)
.attr("width", 10)
.attr("height", 10)
;
legendRect
.attr("y", function (d, i) {
return i * 20;
})
.style("fill", function (d) {
return d[1];
})
var legendText = legend.selectAll('text').data(legendData);
legendText.enter()
.append("text")
.attr("x", width - 52);
legendText
.attr("y", function (d, i) {
return i * 20 + 9;
})
.text(function (d) {
return d[0];
});
Here's how I would code it. Notice, that I data-bind to a wrapper g element and then place the symbol and text into it for each legend item. You can then position the g instead of positioning the text and "symbol" separately. This also removes the need for double-binding the data.
var legendData = [["OA", "yellow", "circle"], ["OI", "blue", "cross"], ["RARC", "green", "diamond"], ["CAPE", "red", "square"], ["Other", "black", "triangleDown"]];
var svg = d3.select('body').append('svg').attr('width', 500).attr('height', 500);
var legend = svg.append('g')
.attr("class", "legend")
.attr("height", 0)
.attr("width", 0)
.attr('transform', 'translate(20,20)');
var legendRect = legend
.selectAll('g')
.data(legendData);
var legendRectE = legendRect.enter()
.append("g")
.attr("transform", function(d,i){
return 'translate(0, ' + (i * 20) + ')';
});
legendRectE
.append('path')
.attr("d", d3.svg.symbol().type((d) => { return d[2] }))
.style("fill", function (d) {
return d[1];
});
legendRectE
.append("text")
.attr("x", 10)
.attr("y", 5)
.text(function (d) {
return d[0];
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
This is a implementation which uses symbols for your legend. You can use the symbols like the following:
svg.selectAll('.symbol')
.data(legendData)
.enter()
.append('path')
.attr('transform', function(d, i) {
return 'translate(' + (20) + ',' + ((i * 20) + 10) + ')';
})
.attr('d', d3.symbol().type(function(d, i) {
if (d[2] === "circle") {
return d3.symbolCircle;
} else if (d[2] === "cross") {
return d3.symbolCross;
} else if (d[2] === "diamond") {
return d3.symbolDiamond;
} else if (d[2] === "square") {
return d3.symbolSquare;
} else {
return d3.symbolTriangle;
}
})
.size(100))
.style("fill", function(d) {
return d[1];
});
Then you can set your legend labels like the following:
svg.selectAll('.label')
.data(legendData)
.enter()
.append('text')
.attr("x", "40")
.attr("y", function(d, i){ return ((i * 20)+15);})
.text(function(d) {
return d[0];
});
Check fiddle here - https://jsfiddle.net/zoxckLe3/
P.S. - Above solution uses d3 v4. To achieve the same in v3, use the following line .attr('d', d3.svg.symbol().type(function(d){return d[2];})) instead of the part where I match d[2] to the symbol name.
For adding image icons, you can use below code.
legend.append("**image**")
.attr("x", 890)
.attr("y", 70)
.attr("width", 20)
.attr("height", 18)
.attr("xlink:href",function (d) {
**return "../assets/images/dev/"+d+".png";**
})
This works for me..