I do not understand why d3.event.x == NaN and not the x cursor's position when I use d3.behavior.drag().
I used d3.event.sourceEvent.offsetX instead, but it works only with Chrome (e.g. not with Firefox).
The side effect can be seen here :
http://gloumouth1.free.fr/test/Nature
The user can move the "frequency cursors" only with Chrome.
Any idea ?
G.
From my point of view, d3.js is terrible documented library. Sorry Mike Bostock, but personally for me it's sometimes absolutely impossible to find "why", without debugging the source and examples.
Now, the answer:
THE FIRST
d3 can create and recognize own events like d3.event.x properly in any browser, BUT you should draw draggable interface elements INSIDE <g></g>! You should not place circles, rectangles or whatever you going to drag later precisely in <svg></svg>. This is not documented.
Example:
<svg>
<rect></rect> // you can not use 'd3.event.x' or 'd3.event.y' for this element.
// for this element is possible to use 'd3.event.sourceEvent.x' in Chrome
// and 'd3.event.sourceEvent.offsetX' in Firefox.
// d3.event.x === NaN, d3.event.y === NaN in any browser
<g>
<rect></rect> // 'd3.event.x' and 'd3.event.y' will work for this element
</g>
<svg>
THE SECOND
The <g></g> wrapper should store the data([{x:..., y:...}]) even you not need it. Without the data in <g></g>, the .origin() method will not work properly and element will jump unpredictably under cursor over the window.
FINALLY
Lets combine all knowledge about dragging in one example:
var draggable = d3.behavior.drag()
.origin(Object) // the sequence function(d){return(d);} in not necessary
.on("drag", drg);
var board = d3.select("body").append("svg:svg").attr({width: 500, height: 500});
var wrap = board.append("svg:g")
.data([{x: 100, y: 100}]); // this is the coordinates of draggable element
// they should be stored in <g></g> wrapper as the data
// IMPORTANT! There is no wrap.data().enter() sequence
// You should not call .enter() method for <g></g> data,
// but nested elements will read the data() freely
var handle = wrap.append("svg:circle")
.attr({
cx: function(d){return(d.x);}, // position of circle will be stored in the data property of <g> element
cy: function(d){return(d.x);},
r: 30,
fill: "gray",
stroke: "black"
}).call(draggable);
function drg(d){
// now d3.event returns not a mouse event,
// but Object {type: "drag", x: ..., y: ..., dx: ..., dy: ...}
d3.select(this).attr({
// reposition the circle and
// update of d.x, d.y
cx: d.x = Math.max(0, Math.min(500 - 60, d3.event.x)), // 500 - width of svg, 60 - width of circle
cy: d.y = Math.max(0, Math.min(500 - 60, d3.event.y)) // 500 - height of svg, 60 - height of circle
});
}
DEMO: http://jsfiddle.net/c8e2Lj9d/4/
Related
When I use d3-zoom and programatically call the scaleTo function using zoomIdentity I cannot zoom using the mouse wheel anymore.
How do I fix this issue?
https://observablehq.com/d/8a5dfbc7d858a16b
// mouse wheel zoom not working because use of zoomIdentity
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("display", "block");
const zoom = d3Zoom.zoom()
svg.call(zoom);
const zoomArea = svg.append('g');
zoom.on('zoom', (e) => {
zoomArea.attr("transform", e.transform)
})
zoomArea.append('circle')
.attr("cx", width/2)
.attr("cy", height/2)
.attr("r", 20)
zoom.scaleTo(svg, d3Zoom.zoomIdentity)
return svg.node();
}
The second parameter of zoom.scaleTo(svg, d3Zoom.zoomIdentity) accepts a k scaling factor (e.g., 2 for 2x zoom). The method zoom.scaleTo is intended to be used when you want to set the zoom level, but not the translation (x and y positions).
If you want to set the whole transform to the zoom identity (which resets both the zoom level and the x and y positions), the method is zoom.transform(svg, d3Zoom.zoomIdentity).
If you indeed just want to reset the scale, you can use zoom.scaleTo(svg, d3Zoom.zoomIdentity.k), or simply zoom.scaleTo(svg, 1).
I am using the same chart as below. I want to push the x-axis headers i.e. Regular, Premium, Budget little bit below i.e. top padding or margin. Give some styling to it like give background color and change text color. I tried using fill and it does not work as desired. I would like to hide Price Tier/Channel also
http://dimplejs.org/examples_viewer.html?id=bars_vertical_grouped
These are SVG text elements so there is no top-padding or margin. You can move them down a bit by increasing the y property though, running the following after you call the chart.draw method will move the labels down 5 pixels:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.attr("y", function (d) {
// Get the y property of the current shape and add 5 pixels
return parseFloat(d3.select(this).attr("y")) + 5;
});
To change the text colour you need to use the fill property (again that's an svg text thing):
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.style("fill", "red");
To colour the background of the text is a little less trivial, there actually isn't a thing for that in SVG, however you can insert a rectangle behind the text and do what you like with it:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
// Iterate each shape matching the selector above (all the x axis labels)
.each(function () {
// Select the shape in the current iteration
var shape = d3.select(this);
// Get the bounds of the text (accounting for font-size, alignment etc)
var bounds = shape.node().getBBox();
// Get the parent group (this the target for the rectangle to make sure all its transformations etc are applied)
var parent = d3.select(this.parentNode);
// This is just the number of extra pixels to add around each edge as the bounding box is tight fitting.
var padding = 2;
// Insert a rectangle before the text element in the DOM (SVG z-position is entirely determined by DOM position)
parent.insert("rect", ".dimple-custom-axis-label")
// Set the bounds using the bounding box +- padding
.attr("x", bounds.x - padding)
.attr("y", bounds.y - padding)
.attr("width", bounds.width + 2 * padding)
.attr("height", bounds.height + 2 * padding)
// Do whatever styling you want - or set a class and use CSS.
.style("fill", "pink");
});
These three statements can all be chained together so the final code will look a bit like this:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.attr("y", function (d) { return parseFloat(d3.select(this).attr("y")) + 5; })
.style("fill", "red")
.each(function () {
var shape = d3.select(this);
var bounds = shape.node().getBBox();
var parent = d3.select(this.parentNode);
var padding = 2;
parent.insert("rect", ".dimple-custom-axis-label")
.attr("x", bounds.x - padding)
.attr("y", bounds.y - padding)
.attr("width", bounds.width + 2 * padding)
.attr("height", bounds.height + 2 * padding)
.style("fill", "pink");
});
FYI the dimple-custom-axis-label class was added in a recent release of dimple so please make sure you are using the latest version. Otherwise you'll have to find an alternative selector
I'm very new to d3 and trying to learn by building a visualization.
My goal right now is to make a circle and color the circle based on some temporal data. I've made the circle, and want to add a timescale to it. The circle I have created fine using d3.arc() on an svg element. I have also created a time scale (seen below). My question is, how can I "attach" this time scale to the circle? I want to be able to say that at xyz point in time, my data holds this value, so now color the circle based on a color scale.
Or...am I going about this wrong?
var time = d3.scale.ordinal()
.domain(d3.extent(data, function(d) {
return d.date;
}))
I think you may need to use a quantitative scale instead of ordinal.
https://github.com/mbostock/d3/wiki/Ordinal-Scales says -
Ordinal scales have a discrete domain, such as a set of names or categories
and in your code, you use the "extent" of the date property, which only gives you 2 values - the earliest and most recent date in your data. That is a discrete domain, but a very limited one, and wouldn't represent your data very well. The scale will only output at most 2 values.
var now = Date.now();
var then = now - 1000;
var colors = d3.scale.ordinal()
.domain([then, now])
.range(['#ff0000','#0000ff']);
colors(then); // red
colors(now); // blue
colors(now - 500); // red... expecting violet
change 'ordinal' to 'linear' and leave the rest as is.
var now = Date.now();
var then = now - 1000;
var colors = d3.scale.linear()
.domain([then, now])
.range(['#ff0000','#0000ff']);
colors(then); // red
colors(now); // blue
colors(now - 500); // violet
The tricky part (at least for me) was remembering that the output of d3.scale.linear() (the 'colors' variable above) is a function. It can be called just like any other function.
var fakeData = d3.range(then, now, 10);
var svg = d3.select('body')
.append('svg')
.attr({ height: 500, width: 500 });
var circle = svg.append('circle')
.attr({ r: 100, cx: 250, cy: 250 });
function changeTime(time){
circle.attr('fill', colors(time));
}
I'm trying to get the screen position of a node after the layout has been transformed by d3.behavior.zoom() but I'm not having much luck. How might I go about getting a node's actual position in the window after translating and scaling the layout?
mouseOver = function(node) {
screenX = magic(node.x); // Need a magic function to transform node
screenY = magic(node.y); // positions into screen coordinates.
};
Any guidance would be appreciated.
EDIT: 'node' above is a force layout node, so it's x and y properties are set by the simulation and remain constant after the simulation comes to rest, regardless of what type of transform is applied.
EDIT: The strategy I'm using to transform the SVG comes from d3's zoom behavior, which is outlined here: SVG Geometric Zooming.
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.call(d3.behavior.zoom().scaleExtent([1, 8]).on("zoom", zoom))
.append("g");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height);
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 2.5)
.attr("transform", function(d) { return "translate(" + d + ")"; });
function zoom() {
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
It's pretty straightforward. d3's zoom behavior delivers pan and zoom events to a handler, which applies the transforms to the container element by way of the transform attribute.
EDIT: I'm working around the issue by using mouse coordinates instead of node coordinates, since I'm interested in the node position when the node is hovered over with the mouse pointer. It's not exactly the behavior I'm after, but it works for the most part, and is better than nothing.
EDIT: The solution was to get the current transformation matrix of the svg element with element.getCTM() and then use it to offset the x and y coordinates to a screen-relative state. See below.
It appears the solution to my original question looks something like this:
(Updated to support rotation transforms.)
// The magic function.
function getScreenCoords(x, y, ctm) {
var xn = ctm.e + x*ctm.a + y*ctm.c;
var yn = ctm.f + x*ctm.b + y*ctm.d;
return { x: xn, y: yn };
}
var circle = document.getElementById('svgCircle'),
cx = +circle.getAttribute('cx'),
cy = +circle.getAttribute('cy'),
ctm = circle.getCTM(),
coords = getScreenCoords(cx, cy, ctm);
console.log(coords.x, coords.y); // shows coords relative to my svg container
Alternately, this can also be done using the translate and scale properties from d3.event (if rotation transforms are not needed):
// This function is called by d3's zoom event.
function zoom() {
// The magic function - converts node positions into positions on screen.
function getScreenCoords(x, y, translate, scale) {
var xn = translate[0] + x*scale;
var yn = translate[1] + y*scale;
return { x: xn, y: yn };
}
// Get element coordinates and transform them to screen coordinates.
var circle = document.getElementById('svgCircle');
cx = +circle.getAttribute('cx'),
cy = +circle.getAttribute('cy'),
coords = getScreenCoords(cx, cy, d3.event.translate, d3.event.scale);
console.log(coords.x, coords.y); // shows coords relative to my svg container
// ...
}
EDIT: I found the below form of the function to be the most useful and generic, and it seems to stand up where getBoundingClientRect falls down. More specifically, when I was trying to get accurate SVG node positions in a D3 force layout project, getBoundingClientRect produced inaccurate results while the below method returned the circle element's exact center coordinates across multiple browsers.
(Updated to support rotation transforms.)
// Pass in the element and its pre-transform coords
function getElementCoords(element, coords) {
var ctm = element.getCTM(),
x = ctm.e + coords.x*ctm.a + coords.y*ctm.c,
y = ctm.f + coords.x*ctm.b + coords.y*ctm.d;
return {x: x, y: y};
};
// Get post-transform coords from the element.
var circle = document.getElementById('svgCircle'),
x = +circle.getAttribute('cx'),
y = +circle.getAttribute('cy'),
coords = getElementCoords(circle, {x:x, y:y});
// Get post-transform coords using a 'node' object.
// Any object with x,y properties will do.
var node = ..., // some D3 node or object with x,y properties.
circle = document.getElementById('svgCircle'),
coords = getElementCoords(circle, node);
The function works by getting the transform matrix of the DOM element, and then using the matrix rotation, scale, and translate information to return the post-transform coordinates of the given node object.
You can try node.getBBox() to get the pixel positions of a tight bounding box around the node shapes after any transform has been applied. See here for more: link.
EDIT:
getBBox doesn't work quite the way I thought. Since the rectangle is defined in terms of the transformed coordinate space it is always relative to the parent <g> and will therefore always be the same for contained shapes.
There is another function called element.getBoundingClientRect that appears to be quite widely supported and it returns its rectangle in pixel position relative to the top left of the browser view port. That might get you closer to what you want without needing to mess with the transform matrix directly.
I'm trying to wrap my head around the log scales provided by D3.js. It should be noted that as of yesterday, I had no idea what a logarithmic scale was.
For practice, I made a column chart displaying a dataset with four values: [100, 200, 300, 500]. I used a log scale to determine their height.
var y = d3.scale.log()
.domain([1, 500])
.range([height, 1]);
This scale doesn't work (at least not when applied to the y-axis as well). The bar representing the value 500 does not reach the top of the svg container as it should. If I change the domain to [100, 500] that bar does reach the top but the axis ticks does not correspond to the proper values of the bars. Because 4e+2 is 4*10^2, right?
What am I not getting here? Here is a fiddle.
Your scale already reverses the range to account for the SVG y-coordinates starting at the top of the screen -- ie, you have domain([min, max]) and range([max, min]). This means your calcs for the y position and height should be reversed because your scale already calculated y directly:
bars.append("rect")
.attr("x", function (d, i) { return i * 20 + 20; })
.attr("y", function (d) { return y(d); })
.attr("width", 15)
.attr("height", function (d) { return height - y(d); });
Here's an updated Fiddle: http://jsfiddle.net/findango/VeNYj/2/