D3js v5 tree draggable - d3.js

I want to create drag tree using D3js V5.
But when I choose dragging start it jump to other coordinate.
How can I fix this?
var dragStartX = null,
dragStartY = null;
var svg = d3.select($container).append("svg")
.call(d3.drag()
.on("start", function() {
dragStartX = parseFloat(d3.event.sourceEvent.offsetX);
dragStartY = parseFloat(d3.event.sourceEvent.offsetY);
console.log("start", dragStartX, dragStartY);
})
.on("drag", function() {
var g = d3.select(this).selectAll("g.containerG");
dragStartX += parseFloat(d3.event.dx);
dragStartY += parseFloat(d3.event.dy);
g.attr("transform", "translate(" +
dragStartX + "," + dragStartY + ")");
})
.on("end", function() {
console.log("end", dragStartX, dragStartY);
dragStartX = null;
dragStartY = null;
console.log("null", dragStartX, dragStartY);
}))

I used d3.zoom for this.
var zoom = d3.zoom()
.extent([[margin.left, margin.top], [width, height]])
.scaleExtent([1, 1])
.on("zoom", zoomed);
Then call the zoom on svg selection, and change transform attribute on zoom event.
Demo: http://jsfiddle.net/b4xcg2sj/35/

Related

d3 combine zoom and drag calls?

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);

Updating zoom behavior from v3 to v5

The problem i am facing is that i am not able to write the function following in my code because of version mismatch.
Code is
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1, 10])
.on("zoom", zoomed);
I have tried by this way
const zoom = d3.zoom()
.scaleExtent([1, 4])
.x(this.xScale)
.on('zoom', () => {})
But it does not work for me.
How to write same function in d3 version 5? I want to make line chart scrollable in x axis with y axis as fixed position using d3 version 5
This is my implementation Basic Code
private createLineChart() {
this.width = 2000 - this.margin.left - this.margin.right;
this.height = 600 - this.margin.top - this.margin.bottom;
// X AXIS
this.xScale = d3.scaleBand()
.domain(this.dataset[0].fluencyData.map((data) => {
return new Date(data.date);
}))
.range([0, this.width]);
// Y AXIS
this.yScale = d3.scaleLinear()
.domain([0, 110])
.range([this.height, 0]);
// Line Generator
this.line = d3.line()
.x((data) => this.xScale(new Date(data.date)))
.y((data) => this.yScale(data.wcpm));
// .curve(d3.curveMonotoneX);
// Add SVG to Div
this.svg = d3.select('#displayChart').append('svg')
.attr('preserveAspectRatio', 'xMinYMin meet')
.attr(
'viewBox',
'0 0 ' +
(this.width + this.margin.left + this.margin.right) +
' ' +
(this.height + this.margin.top + this.margin.bottom))
// .attr('width', this.width + this.margin.left + this.margin.right)
// .attr('height', this.height + this.margin.top + this.margin.bottom)
.append('g')
.attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
// Define the div for the tooltip
this.toolTipDiv = d3.select('#displayChart').append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
// Append XAXIS to the SVG
this.svg.append('g')
.attr('class', 'xAxis')
.attr('transform', 'translate(0,' + this.height + ')')
.call(d3.axisBottom(this.xScale).tickSizeOuter(0).tickFormat(d3.timeFormat('%b %d')));
const zoom = d3.zoom()
.scaleExtent([1, 4])
.extent([100, 100], [this.width - 100, this.height - 100])
.x(this.xScale)
.on('zoom', () => {
console.log(d3.event.transform);
// this.svg.select('#displayChart').attr('d', this.line);
});
this.svg.call(zoom);
// Append YAXIS to SVG
this.svg.append('g')
.attr('class', 'yAxis')
.call(d3.axisLeft(this.yScale).tickSize(-this.width)
);
// Make a Path for Dataset
this.svg.append('path')
.datum(this.dataset[0].fluencyData)
.attr('class', 'line')
.attr('d', this.line)
.attr('transform', 'translate(' + this.margin.left + ',0)');
// Text Heading of DATE in chart
this.svg.append('text')
.attr('transform', 'translate(' + (-20) + ',' + (this.height + 13) + ')')
.attr('dy', '.35em')
.attr('class', ' xAxis')
.text('Date');
}
}
Error I am getting is
LineChartComponent_Host.ngfactory.js? [sm]:1 ERROR TypeError: d3__WEBPACK_IMPORTED_MODULE_2__.zoom(...).scaleExtent(...).x is not a function
at LineChartComponent.push../src/app/line-chart/line-chart.component.ts
With d3v3 and before, the zoom could track a scale's state. From the documentation, scale.x(): "Specifies an x-scale whose domain should be automatically adjusted when zooming." (docs). This modifies the original scale.
D3v4+ does not have zoom.x or zoom.y methods.
With d3v4+, the zoom does not track or modifiy a d3 scale's state. Infact, for d3v4+, the zoom behavior doesn't even track the current zoom state: "Zoom behaviors no longer store the active zoom transform (i.e., the visible region; the scale and translate) internally. The zoom transform is now stored on any elements to which the zoom behavior has been applied.(change log)".
As part of this, and more importantly, "Zoom behaviors are no longer dependent on scales, but you can use transform.rescaleX, transform.rescaleY, transform.invertX or transform.invertY to transform a scale’s domain(change log)".
So rather than have the zoom update the d3 scale, we need to do this ourselves. The most common way this is done is through a reference scale, which remains unchanged, and a scale to which we apply the zoom transform:
var zoom = d3.zoom()
.on("zoom",zoomed)
var x = d3.scaleLinear().... // working scale
var x2 = x.copy(); // reference scale.
function zoomed() {
x = d3.event.transform.rescaleX(x2) // update the working scale.
// do something...
}
So, something like this:
var x = d3.scale.linear()
.domain([0,1])
.range([0,500]);
var zoom = d3.behavior.zoom()
.x(x)
.scaleExtent([1, 10])
.on("zoom", zoomed);
var svg = d3.select("svg")
.call(zoom);
var axis = d3.svg.axis()
.orient("bottom")
.scale(x);
var axisG = svg.append("g")
.attr("transform", "translate(0,30)")
.call(axis);
function zoomed() {
axisG.call(axis);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<svg width="500" height="200"></svg>
Becomes something like that:
var x = d3.scaleLinear()
.domain([0,1])
.range([0,500]);
var x2 = x.copy(); // reference.
var zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var svg = d3.select("svg")
.call(zoom);
var axis = d3.axisBottom().scale(x)
var axisG = svg.append("g")
.attr("transform", "translate(0,30)")
.call(axis);
function zoomed() {
x = d3.event.transform.rescaleX(x2)
axis.scale(x);
axisG.call(axis);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="200"></svg>
Note that d3.event.transform.rescaleX is for continuous scales - you have an ordinal band scale, so we'll need to use a slightly modified approach for band and/or point scales:
var x = d3.scaleBand()
.domain(d3.range(10).map(function(d) { return d/10; }))
.range([0,500]);
var x2 = x.copy(); // reference.
var zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var svg = d3.select("svg")
.call(zoom);
var axis = d3.axisBottom().scale(x)
var axisG = svg.append("g")
.attr("transform", "translate(0,30)")
.call(axis);
function zoomed() {
// Rescale the range of x using the reference range of x2.
x.range(x2.range().map(function(d) {
return d3.event.transform.applyX(d);
}))
axisG.call(axis);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="200"></svg>
This is band/point scale solution is based on this issue and Bostock's proposed solution to it

How to draw vertical line on mouse over displaying data with d3.js

How to append a vertical line to a graph and display on a tooltip the data focused?
Something like this:
TASK:
Add line indicator and tooltip
Append an invisible div to the vis container, set its class to "tooltip" and use the index.css to define necessary styles (e.g. position)
Append an indicator line to the viewport
Append a rectangle and set its class to "interaction-rect" (see index.css). We will use this rectangle to capture the mouse-events
Whenever there is a mousemove, update the tooltip to show the correct dates and values
Whenever the mouse leaves the viewport, make the indicator and tooltip disappear
CODE:
/* Retrieve the node of the div element declared within the index.html by its identifier */
var visContainerNode = d3.select("#vis-container");
// Specify margins such that the visualization is clearly visible and no elements are invisible due to the svg border
var margins = {
top: 20,
right: 25,
bottom: 20,
left: 50
};
// Specify the width and height of the svg as well as the width height of the viewport of the visualization.
var width = 1200;
var height = 800;
var gapY = 50;
var focusAreaHeight = 600 - margins.top;
var contextAreaHeight = 200 - margins.bottom - gapY;
var visWidth = width - margins.left - margins.right;
var visHeight = focusAreaHeight + contextAreaHeight;
/* Appending an svg element to the vis-container, set its width and height (in pixels), and add it to the vis-container */
var svg = visContainerNode.append("svg").attr("width", width).attr("height", height);
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", visWidth)
.attr("height", visHeight);
// Adding a group element to the svg to realize the margin by translating the group.
var viewport = svg.append("g").attr("transform", "translate(" + margins.left + "," + margins.top + ")");
var dateParser = d3.timeParse('%m %Y');
var dateFormat = d3.timeFormat('%m / %Y');
var curve = d3.curveMonotoneX;
// We use the d3.dsv method, which uses the fetchAPI internally, to retrieve the data
d3.dsv(";", "pr_1991_2015.csv", function (d) {
return {
date: dateParser(d.Month + " " + d.Year),
rain: parseFloat(d.pr),
temperature: parseFloat(d.tas)
};
}).then(function (data) {
console.log("Raw Data:", data);
// Init Scales
var xFocus = d3.scaleTime().domain(d3.extent(data, function (d) {
return d.date;
})).range([0, visWidth]);
var yRainFocus = d3.scaleLinear().domain([0, d3.max(data.map(function (d) {
return d.rain
}))]).range([focusAreaHeight, 0]);
var yTempFocus = d3.scaleLinear().domain(d3.extent(data.map(function (d) {
return d.temperature
}))).range([focusAreaHeight, 0]);
// In order to organize our code, we add one group for the focus visualization (the large lien chart)
var focusVis = viewport.append("g");
// Initialize a line generator for each line
var rainLine = d3.line()
.x(function (d) {
return xFocus(d.date);
})
.y(function (d) {
return yRainFocus(d.rain);
})
.curve(curve);
var tempLine = d3.line()
.x(function (d) {
return xFocus(d.date);
})
.y(function (d) {
return yTempFocus(d.temperature);
})
.curve(curve);
// Append two path elements
focusVis.append("path")
.datum(data)
.attr("class", "line line-rain")
.attr("d", rainLine);
focusVis.append("path")
.datum(data)
.attr("class", "line line-temp")
.attr("d", tempLine);
// Lets add some axis
var axisG = focusVis.append("g");
var xAxisFocus = d3.axisBottom(xFocus);
axisG.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + focusAreaHeight + ")")
.call(xAxisFocus);
axisG.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yTempFocus));
axisG.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + visWidth + ", 0)")
.call(d3.axisRight(yRainFocus));
// Append three text elements to the axisG group and label the axes respectively
axisG.append("text").text("Temperature").attr("x", -50).attr("y", -5).attr("fill", "red");
axisG.append("text").text("Rain").attr("x", visWidth - 10).attr("y", -5).attr("fill", "blue");
axisG.append("text").text("Years").attr("x", visWidth / 2).attr("y", focusAreaHeight - 10);
// Create the context visualization (small line chart) directly below the focus vis
// Init scales since range differs
var xContext = d3.scaleTime().domain(d3.extent(data, function (d) {
return d.date;
})).range([0, visWidth]);
var yContextRain = d3.scaleLinear().domain([0, d3.max(data.map(function (d) {
return d.rain
}))]).range([contextAreaHeight, 0]);
var yContexttemp = d3.scaleLinear().domain(d3.extent(data.map(function (d) {
return d.temperature
}))).range([contextAreaHeight, 0]);
// To organize our code, we add one group for the context visualization
var contextVis = viewport.append("g").attr("transform", "translate(0," + (focusAreaHeight + gapY) + ")");
var xAxisContext = d3.axisBottom(xContext);
contextVis.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + contextAreaHeight + ")")
.call(xAxisContext);
// Init two line generators
var rainLineContext = d3.line()
.x(function (d) {
return xContext(d.date);
})
.y(function (d) {
return yContextRain(d.rain);
})
.curve(curve);
var tempLineContext = d3.line()
.x(function (d) {
return xContext(d.date);
})
.y(function (d) {
return yContexttemp(d.temperature);
})
.curve(curve);
// Add the two lines for rain and temperature
contextVis.append("path")
.datum(data)
.attr("class", "line line-rain")
.attr("d", rainLineContext);
contextVis.append("path")
.datum(data)
.attr("class", "line line-temp")
.attr("d", tempLineContext);
/*
* Add Interactive Features here
*/
/*
TASK: Add the brush using the d3.brush function, define the extent and the necessary event functions
Append a new group element and apply the brush on it using the "call" function
During the brush and on brush end you want to make sure that the lines are redrawn correctly by setting their "d" attribute
*/
//
var brush = d3.brushX()
.extent([[-10, -10], [width+10, height+10]])
.on("brush end", brushed);
var zoom = d3.zoom()
.scaleExtent([1, Infinity])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", zoomed);
contextVis.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, xContext.range());
function brushed() {
if (d3.event || d3.event.selection)
var s = d3.event.selection || xContext.range();
xFocus.domain(s.map(xContext.invert, xContext));
focusVis.select(".line-rain").attr("d", rainLine);
focusVis.select(".line-temp").attr("d", tempLine);
focusVis.select(".x axis").call(xAxisFocus);
focusVis.select(".zoom").call(zoom.transform, d3.zoomIdentity
.scale(visWidth / (s[1] - s[0]))
.translate(-s[0], 0));
}
function zoomed() {
if (d3.event || d3.event.selection)
var t = d3.event.transform;
xFocus.domain(t.rescaleX(xContext).domain());
focusVis.select(".line-rain").attr("d", rainLine);
focusVis.select(".line-temp").attr("d", tempLine);
axisG.select(".x axis").call(xAxisFocus);
contextVis.select(".brush").call(brush.move, xContext.range().map(t.invertX, t));
}
})

Add Zoom to a Grouped Bar Chart that uses a scaleBand

I'm trying to use rescaleX on a scaleBand to add zoom functionality to a Grouped Bar Chart.
var x0 = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(0.1);
var x1 = d3.scaleBand()
.padding(0.05);
var zoom = d3.zoom()
.scaleExtent([1, 8])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", () => { zoomed() });
...
function zoomed() {
var t = d3.event.transform;
var x0t = t.rescaleX(this.x0);
var x1t = t.rescaleX(this.x1);
...
}
But t.rescaleX(this.x0) doesn't work on a scaleBand, how can I apply zoom functionallity to a scaleBand?
Here is a full codepen Grouped Bar Chart
I was able to achieve this by changing the zoomed function to:
function zoomed() {
var t = d3.event.transform;
// redefine the x0 domain range with the event transform scale (k)
x0.range([0, width * t.k]);
// transform .barGroup using redefined domain range and event transform params
g.selectAll(".barGroup")
.attr("transform", function(d) { return "translate(" + (x0(d.State) + t.x) + ",0)scale(" + t.k + ",1)"; });
// apply transform to .axis--x and call xAxis
g.select(".axis--x")
.attr("transform", "translate(" + t.x + "," + (height) + ")")
.call(xAxis);
}
Full codepen here
Update:
A better method of zooming a scaleBand bar chart by Mike Bostock (non-grouped)
https://beta.observablehq.com/#mbostock/d3-zoomable-bar-chart
Update 2:
Updated Grouped bar chart zoom function:
function zoomed() {
x0.range([0, width].map(d => d3.event.transform.applyX(d)));
x1.rangeRound([0, x0.bandwidth()]);
g.selectAll(".barGroup").attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; });
g.selectAll(".bar").attr("x", function(d) { return x1(d.key); }).attr("width", x1.bandwidth());
g.select(".axis--x").call(xAxis);
}
Full Codepen: here

d3 topojson zoom on map with cities

I'm working on a d3 map with topojson format data.
I can draw the country shapes and the zoom and pane works fine.
The problem is when I try to plot cities on the map.
I can not figure out how to manage the zoom with those points: the point size must be the same, but the points must translate right.
this is an example, when I zoom the map, the points translate out of the map:
var width = 724;
var height = 768;
var objMap = null;
var x, y;
//Projection
projection = d3.geo.transverseMercator()
.center([2.5, -38.5])
.rotate([66, 0])
.scale((height * 56.5) / 33)
.translate([(width / 2), (height / 2)]);
//Path
path = d3.geo.path()
.projection(projection);
x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
y = d3.scale.linear()
.domain([0, height])
.range([height, 0]);
svg = d3.select("#div_map").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
g = svg.append("g");
// Zoom behavior
var zoom = d3.behavior.zoom()
.scaleExtent([1,15])
.on("zoom",function() {
g.selectAll("path.zoomable").attr("transform","translate("+d3.event.translate.join(",")+")scale("+d3.event.scale+")")
g.selectAll(".place").attr("transform", function(d) { p = projection(d.geometry.coordinates); return "translate(" + x(p[0]) + "," + y(p[1]) + ")"; });
}
);
svg.call(zoom);
d3.json("datos/ARcompleto.json.txt", function (error, ar) {
objMap = ar;
//Draw the map
provs = g.append("g")
.attr("id", "g_provincias")
.selectAll("path")
.data(topojson.feature(ar, ar.objects.provincias).features)
.enter().append("path")
.classed("zoomable", true)
.attr("d", path)
g.append("g")
.attr("id", "g_localidades")
.selectAll("path")
.data(topojson.feature(objMap, objMap.objects.localidades).features.filter(function (d) { return d.properties.LPROVINCIA == 'MENDOZA'; }))
.enter().append("path")
.attr("d", path)
.attr("class", "localidad")
.classed("place", true)
//.attr("transform", function(d) {return "translate(" + projection(d.geometry.coordinates.reverse()) + ")"; });
});
I can manage it with the Lars help. this is the new zoom function:
var zoom = d3.behavior.zoom()
.translate(projection.translate())
.scaleExtent([height, Infinity])
.scale(projection.scale())
.on("zoom", function() {
projection.translate(d3.event.translate).scale(d3.event.scale)
g.selectAll("path.zoomable").attr("d", path);
projection.translate(d3.event.translate).scale(d3.event.scale)
svg.selectAll(".place").attr("d", path);
});
Thanks a lot!

Resources