Is it possible to conditionally ignore the drag function from eating an event? I have a pan/zoom canvas (as seen here: www.proofapp.io/workspace) and I'm trying to implement a shift+drag multi-selection lasso. The svg object already has the zoom function registered so when I put a drag function above it the zoom never gets called. Since call only runs once at the beginning I'm not sure how I can make this work. Any suggestions?
const zoom = d3.zoom()
.scaleExtent([0.25 ,5])
.on("zoom", function() {
root.attr('transform', d3.event.transform);
});
// THIS DOESNT WORK BECAUSE IT ONLY RUNS ONCE AT THE BEGINNING
const lasso = function() {
if (d3.event.sourceEvent) {
if (d3.event.sourceEvent.shiftKey) {
d3.drag()
.dragDisable() // maybe the answer is with this?
.on("start", function() { console.log('lasso-start') })
.on("drag", function() { console.log('lasso-drag') })
.on("end", function() { console.log('lasso-end') });
}
}
}
var svg = d3.select("div#nodegraph")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.on('click', Graph.setNodeInactive)
.call(lasso)
.call(zoom);
UPDATE
Trying to just use the mousedown.drag event instead so that I can control the event bubbling. Not quite there yet, but the behaviour is correct (only blocks zoom when shift is pressed).
const zoom = d3.zoom()
.scaleExtent([0.25 ,5])
.on("zoom", function() {
root.attr('transform', d3.event.transform);
});
function lasso() {
if (d3.event.shiftKey) {
// do stuff
d3.event.stopImmediatePropagation();
}
}
var svg = d3.select("div#nodegraph")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.on('click', Graph.setNodeInactive)
.on('mousedown.drag', lasso)
.call(zoom);
If there's a d3 way to do this I'd still love to know how it works to take advantage of all the extra goodness d3.drag does. Until that happens, here is a fairly complete example that takes into account the zoomed canvas.
Selection = {};
Selection.DragLasso = {};
Selection.DragLasso.__lasso = null;
Selection.DragLasso.handler = function() {
if (d3.event.shiftKey) {
d3.event.stopImmediatePropagation();
if (Selection.DragLasso.__lasso) {
Selection.DragLasso.__lasso.remove();
Selection.DragLasso.__lasso = null;
}
var m = d3.mouse(this);
svg
.on('mousemove.drag', Selection.DragLasso.drag)
.on('mouseup.drag', Selection.DragLasso.end);
var z = d3.zoomTransform(svg.node());
var x = (z.x / z.k * -1) + (m[0] / z.k);
var y = (z.y / z.k * -1) + (m[1] / z.k);
Selection.DragLasso.__lasso = noderoot.append('rect')
.attr("fill", 'red')
.attr('x', x)
.attr('y', y)
.attr('width', 0)
.attr('height', 0)
.classed('selection-lasso', true);
}
}
Selection.DragLasso.drag = function(e) {
var m = d3.mouse(this);
var z = d3.zoomTransform(svg.node());
var x = (z.x / z.k * -1) + (m[0] / z.k);
var y = (z.y / z.k * -1) + (m[1] / z.k);
Selection.DragLasso.__lasso
.attr("width", Math.max(0, x - +Selection.DragLasso.__lasso.attr("x")))
.attr("height", Math.max(0, y - +Selection.DragLasso.__lasso.attr("y")));
}
Selection.DragLasso.end = function() {
svg.on('mousemove.drag', null).on('mouseup.drag', null);
Selection.DragLasso.__lasso.remove();
Selection.DragLasso.__lasso = null;
}
var svg = d3.select("div#nodegraph")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.on('mousedown.drag', Selection.DragLasso.handler)
.call(zoom);
Related
I am very new to D3 and as you can see in the image above there are tiny lines/gaps between each rectangle that I would love to get rid of, this is drawn on a canvas element with each rectangle starting where the last one ends using D3.js following this tutorial almost exactly minus adding the gaps between each square.
I've tried
this.canvas.imageSmoothingQuality = 'low';
draw() {
const canvas = d3
.select(this.chartContainer.nativeElement)
.append('canvas')
.attr('width', this.width)
.attr('height', this.height)
.attr(
'transform',
'translate(' + this.margin.left + ',' + this.margin.top + ')'
);
this.canvas = canvas.node().getContext('2d');
this.clearCanvas();
this.canvas.imageSmoothingQuality = 'low';
const elements = this.shadowContainer.selectAll('custom.rect');
const _this = this;
elements.each(function(d, i) {
const node = d3.select(this);
// Here you retrieve the colour from the individual in-memory node and set the fillStyle for the canvas paint
_this.canvas.fillStyle = node.attr('color');
// Here you retrieve the position of the node and apply it to the fillRect context function which will fill and paint the square.
_this.canvas.fillRect(
Number(node.attr('x')),
Number(node.attr('y')),
Number(node.attr('width')),
Number(node.attr('height'))
);
});
}
private dataBind(value) {
const customBase = document.createElement('custom');
this.shadowContainer = d3.select(customBase);
const {
viewModes: {
heatMap: {
data,
chartOptions: { engagementStatus, xAxis, yAxis }
}
}
} = value;
const x = this.d3
.scaleBand()
.range([0, this.width])
.domain(xAxis.categories);
this.shadowContainer
.append('g')
.style('font-size', 11)
.attr('class', 'x-axis')
.call(this.d3.axisTop(x).tickSize(0))
.select('.domain')
.remove();
this.shadowContainer
.selectAll('.x-axis text')
.style('text-anchor', 'start')
.attr('transform', function(d) {
return `translate(8, -8)rotate(-90)`;
});
const y = this.d3
.scaleBand()
.domain(d3.reverse(yAxis.categories))
.range([this.height, 0]);
const color = this.d3
.scaleLinear()
.domain([-2, -1, 0, 1])
// #ts-ignore
.range(['#5b717d', '#ffb957', '#ee6b56', '#40a050']);
const join = this.shadowContainer
.selectAll('custom.rect')
.data(data, function(d) {
return `${d.Date}:${d.Member}`;
});
const enterSelection = join
.enter()
.append('custom')
.attr('class', 'rect')
.attr('x', d =>
this.getCorrectDatePosition(
d.Date,
x,
xAxis.categories[0].split('/').length
)
)
.attr('y', function(d) {
return y(d.Member);
})
.attr('width', 24)
.attr('height', 24);
join
.merge(enterSelection)
.attr('width', x.bandwidth())
.attr('height', y.bandwidth())
.attr('color', function(d) {
return color(d.score);
});
const exitSelection = join
.exit()
.transition()
.attr('width', 0)
.attr('height', 0)
.remove();
}
This is likely an issue stemming from your scales. It can occur with either SVG or canvas and occurs when dealing with coordinates that require plotting at fractions of a pixel.
Here's a demonstration with SVG:
var data = d3.range(20);
var x = d3.scaleBand()
.range([10,250])
.domain(data)
var svg = d3.select("body")
.append("svg")
.attr("width", 500);
var rect = svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d=>x(d) )
.attr("y", 50)
.attr("width", x.bandwidth())
.attr("height",100)
.attr("fill","crimson")
svg.transition()
.attrTween("tween", function() {
var i = d3.interpolate(250,480)
return function(t) {
x.range([50,i(t)])
rect.attr("x",d=>x(d))
.attr("width", x.bandwidth());
return "";
}
})
.duration(10000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
And one with Canvas:
var data = d3.range(20);
var x = d3.scaleBand()
.range([10,250])
.domain(data)
var canvas = d3.select("body")
.append("canvas")
.attr("width", 500);
var rect = d3.create("div").selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d=>x(d) )
.attr("y", 50)
.attr("width", x.bandwidth())
.attr("height",100)
.attr("fill","crimson")
canvas.transition()
.attrTween("tween", function() {
var i = d3.interpolate(250,480)
var context = canvas.node().getContext("2d");
return function(t) {
x.range([50,i(t)])
context.fillStyle = "#fff";
context.fillRect(0,0,550,300);
rect.attr("x",d=>x(d))
.attr("width", x.bandwidth())
.each(function() {
var node = d3.select(this);
context.fillStyle = "crimson"
context.fillRect(
+node.attr("x"),
+node.attr("y"),
+node.attr("width"),
+node.attr("height"))
})
return "";
}
})
.duration(10000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
The solution is to be a bit more involved in setting the scale's domain and range. Start with the desired bandwidth, a whole number in pixels, and set the range so that the difference between the minimum and maximum values is equal to the number of values in the domain * the bandwidth.
So instead of:
const x = this.d3
.scaleBand()
.range([0, this.width])
.domain(xAxis.categories);
You'd have:
const length = 10; // length of a box side
const x = this.d3
.scaleBand()
.domain(xAxis.categories)
.range([0,xAxis.categories * length])
You could also calculate length above dynamically, say by using: Math.floor(width/xAxis.categories)
Using the above approach and a slightly contrived example to accommodate the transition, we remove the aliasing/moire pattern. Because we use only full pixels, the transition jumps as each bar increases in width by a full pixel at the same time, as space becomes available in the range:
var data = d3.range(20);
var length = 30;
var x = d3.scaleBand()
.range([10,data.length*length])
.domain(data)
var canvas = d3.select("body")
.append("canvas")
.attr("width", 500);
var rect = d3.create("div").selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d=>x(d) )
.attr("y", 50)
.attr("width", x.bandwidth())
.attr("height",100)
.attr("fill","crimson")
canvas.transition()
.attrTween("tween", function() {
var i = d3.interpolate(250,480)
var context = canvas.node().getContext("2d");
return function(t) {
length = Math.floor(i(t)/data.length)
x.range([10,length*data.length+10])
context.fillStyle = "#fff";
context.fillRect(0,0,550,300);
rect.attr("x",d=>x(d))
.attr("width", x.bandwidth())
.each(function(d,i) {
var node = d3.select(this);
context.fillStyle = d3.schemeCategory10[i%10];
context.fillRect(
+node.attr("x"),
+node.attr("y"),
+node.attr("width"),
+node.attr("height"))
})
return "";
}
})
.duration(10000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
So I have a piechart that all transitions will not work on with the message that they're not a function. Which is true when I dig in the console. The window.d3 har a transition function, but not d3.selectAll('path').transition
I'm a bit of a loss as to why this does not work. Obviously my selection to do the transition is wrong, but how?
(function(d3) {
'use strict';
var tooltip = d3.select('body')
.append('div')
.attr('class', 'pie-tooltip')
.style("opacity", 0);
/**
* Width and height has to be the same for a circle, the variable is in pixels.
*/
var width = 350;
var height = 350;
var radius = Math.min(width, height) / 2;
/**
* D3 allows colours to be defined as a range, beneath is input the ranges in same order as our data set above. /Nicklas
*/
var color = d3.scaleOrdinal()
.range(['#ff875e', '#f6bc58', '#eae860', '#85d280']);
var svg = d3.select('#piechart')
.append('svg')
.attr('width', width+20)
.attr('height', height+20)
.append('g')
.attr('transform', 'translate(' + ((width+20) / 2) +
',' + ((height+20) / 2) + ')');
var arc = d3.arc()
.innerRadius(0)
.outerRadius(radius);
/**
* bArc = biggerArc, this is the arc with a bigger outerRadius thats used when a user mouseovers.
*/
var bArc = d3.arc()
.innerRadius(0)
.outerRadius(radius*1.05);
var pie = d3.pie()
.value(function(d){
return d.value;
})
.sort(null);
var path = svg.selectAll('path')
.data(pie(data))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d) {
return color(d.data.color);
});
path.transition()
.duration(600)
.attrTween("d", makePieAnimation);
path.on("mouseover", function(d){
d3.select(this)
.attr("width", width+10)
.attr("height", height+10);
tooltip.transition()
.duration(200)
.style("opacity", .9)
.style("display", null)
.text(d.data.label + ": " + d.data.value);
d3.select(this).transition()
.duration(300)
.style('fill', d.data.highlight).attr("d", bArc);
});
path.on("mousemove", function(){
tooltip.style("top", (event.pageY-10)+"px")
.style("left",(event.pageX+10)+"px");
});
path.on("mouseout", function(d){
d3.select(this).style('fill', d.data.color);
tooltip.transition()
.duration(300)
.style("opacity", 0);
d3.select(this).transition()
.duration(300)
.attr("d", arc);
});
/**
* makePieAnimation() animates the creation of the pie, setting startangles to 0, interpolating to full circle on creation in path.transition. D3 magic.
* b is an array of arc objects.
*/
function makePieAnimation(b) {
b.innerRadius = 0;
var angles = d3.interpolate({startAngle: 0, endAngle: 0}, b);
return function(t) {
return arc(angles(t));
};
}
})(window.d3);
$.each(data, function (index, value) {
$('#legend').append('<span class="label label-legend" style="background-color: ' + value['color'] + '">' + value['label'] + ': ' + value['value'] + '</span>');
});
EDIT:
After digging around Ive found that the d3 file used by typo3 is manually edited: https://forge.typo3.org/issues/83741
I cannot see how this impacts this issue, but it does. When using a CDN with d3 v4.12.2 the error disappears.
Based on the code here(http://zeroviscosity.com/d3-js-step-by-step/step-3-adding-a-legend), I have created a horizontal legend..
code below..jsfiddle
(function(d3) {
'use strict';
var dataset = [{
"colorName":"red",
"hexValue":"#f00"
},
{
"colorName":"green",
"hexValue":"#0f0"
},
{
"colorName":"blue",
"hexValue":"#00f"
},
{
"colorName":"cyan",
"hexValue":"#0ff"
},
{
"colorName":"magenta",
"hexValue":"#f0f"
},
{
"colorName":"yellow",
"hexValue":"#ff0"
},
{
"colorName":"black",
"hexValue":"#000"
}
]
var width = 360;
var height = 360;
var legendRectSize = 18; // NEW
var legendSpacing = 4; // NEW
var svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
var legend = svg.selectAll('.legend') // NEW
.data(dataset) // NEW
.enter() // NEW
.append('g') // NEW
.attr('class', 'legend') // NEW
.attr('transform', function(d, i) { // NEW
// var height = 0; // NEW
var horz = 100*i; // NEW
var vert = 6; // NEW
return 'translate(' + horz + ',' + vert + ')'; // NEW
}); // NEW
legend.append('rect') // NEW
.attr('width', legendRectSize) // NEW
.attr('height', legendRectSize) // NEW
.style('fill', function (d, i) {
return d.hexValue;
}) // NEW
.style('stroke', function (d, i) {
return d.hexValue;
}); // NEW
legend.append('text') // NEW
.attr('x', legendRectSize + legendSpacing) // NEW
.attr('y', legendRectSize - legendSpacing) // NEW
.text(function(d) {return d.colorName; }); // NEW
})(window.d3);
But it gets cut off on the right size. How can I ensure that it goes to the next line if the width available is less than legend width.
Also, I have viewed this question (How to create a horizontal legend with d3.js) but was unable to figure how to use the same in my case.
It would be great if someone could show me how to ensure not to hard code width (colorbox + text) as in my code.
Add the following if inside your translate function
.attr('transform', function(d, i) {
var horz = 100*i; // NEW
var vert = 6;
if (horz >= width) {
horz = 100 * (i - 4);
vert = 40;
}
return 'translate(' + horz + ',' + vert + ')'; // NEW
});
This should solve your current issue however you should do it more automated in terms that if you have a legend that need to be 3 rows this may not work for you.
Hope this helps.
I am creating pie chart using d3.js. I would like to create 3 pies with single svg element with animation.
This is working fine for me. But do creating different I am reducing the radius each time using a loop. But the radius not getting changed.
How to solve this?
my code (sample) :
var array1 = [
0,200
]
window.onload = function () {
var width = 660,
height = 200,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var arc = null;
var pie = d3.layout.pie()
.value(function(d) {
return d; })
.sort(null);
function tweenPie(finish) {
var start = {
startAngle: 0,
endAngle: 0
};
var i = d3.interpolate(start, finish);
return function(d) { return arc(i(d)); };
}
var svg1 = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
for( var i = 0; i < 3; i++) {
arc = d3.svg.arc()
.innerRadius(radius - (5*i)) //each time size differs
.outerRadius(radius - (6)*i); //each time size differs
svg1.append('g')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.datum(array1).selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.transition()
.duration(5000)
.attrTween('d', tweenPie)
}
}
Live Demo
There is a single arc variable that is being used in the tweenPie method and in the for loop. Each time through the for loop, the arc variable is set to a new value. The tweenPie method is called for each pie chart after the for loop exits. As a result, all the pie charts are using the same tweenPie method which is using the arc created in the last for loop.
For each pie chart, you need to create a separate tweenPie method with its own arc. For example...
var array1 = [ 0, 200 ]
window.onload = function () {
var width = 660,
height = 200,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var arc = null;
var pie = d3.layout.pie()
.value(function(d) {
return d; })
.sort(null);
function getTweenPie(arc) {
return function (finish) {
var start = {
startAngle: 0,
endAngle: 0
};
var i = d3.interpolate(start, finish);
return function(d) { return arc(i(d)); };
}
}
var svg1 = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
for( var i = 0; i < 3; i++) {
arc = d3.svg.arc()
.innerRadius(radius - (5*i)) //each time size differs
.outerRadius(radius - (6)*i); //each time size differs
svg1.append('g')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.datum(array1).selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.transition()
.duration(5000)
.attrTween('d', getTweenPie(arc))
}
}
I'm trying to create a reusable pie chart with dynamic transitions as a learning task. I'm working off of the d3.js resuable components e-book by Chris Viau.
The problem I'm having is basically its not updating, but creating multiple pie charts. I'm wondering if I'm not understanding how d3.dispatch works or whether I've messed something up in the way the pie char should work. It creates multiple circles instead of dynamically updating a single pie chart with random values.
here is my jsfiddle.
http://jsfiddle.net/seoulbrother/Upcr5/
thanks!
js code below:
d3.edge = {};
d3.edge.donut = function module() {
var width = 460,
height = 300,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var dispatch = d3.dispatch("customHover");
function graph(_selection) {
_selection.each(function(_data) {
var pie = d3.layout.pie()
.value(function(_data) { return _data; })
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 50);
if (!svg){
var svg = d3.select(this).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
}
var path = svg.selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) {this._current = d;} );
path.transition()
.ease("elastic")
.duration(750)
.attrTween("d", arcTween);
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
});
}
d3.rebind(graph, dispatch, "on");
return graph;
}
donut = d3.edge.donut();
var data = [1, 2, 3, 4];
var container = d3.select("#viz").datum(data).call(donut);
function update(_data) {
data = d3.range(~~(Math.random() * 20)).map(function(d, i) {
return ~~(Math.random() * 100);
});
container.datum(data).transition().ease("linear").call(donut);
}
update();
setTimeout( update, 1000);
The main reason for multiple SVGs appearing is that you're not checking if there is one already correctly. You're relying on the variable svg being defined, but define it only after checking whether it is defined.
The better way is to select the element you're looking for and check whether that selection is empty:
var svg = d3.select(this).select("svg > g");
if (svg.empty()){ // etc
In addition, you need to handle the update and exit selections in your code in addition to the enter selection. Complete jsfiddle here.