How can I flip a zoomable svg vertically in d3.js? - javascript

I'm trying to visualize a geometric dataset as an SVG using d3 whose y axis goes in the inverse direction's of d3 (in my dataset, up is positive whereas in d3 down is positive). As a result, my svg currently is mirrored upside down from the way I would like it to appear.
I want users to be able pan, zoom, and draw on the canvas, and so am trying to flip the image in the most minimal way -- ideally only in one location rather than applying a scale every time data is handled. Is there a way to do so?
const svg = d3.select<SVGSVGElement, unknown>(d3Container.current);
const g = svg.append("g");
const zoom = d3.zoom<SVGSVGElement, unknown>().on("zoom", (event) => {
const { transform } = event;
g.attr("transform", transform);
});
const filledRegions = g.selectAll(".filledRegions");
filledRegions.data(data).join(
(enter) =>
enter
.append("path")
.attr("class", "filledRegions")
.attr("d", (d) => datumToPath(d))
.attr("fill", (d) => d.fill.color)
);
svg.call(zoom);
const boundingBox = g?.node()?.getBBox();
if (boundingBox) {
const { x: x0, y: y0, width: bbWidth, height: bbHeight } = boundingBox;
const x1 = x0 + bbWidth;
const y1 = y0 + bbHeight;
const { clientHeight: frameHeight, clientWidth: frameWidth } =
d3Container.current;
svg.call(
zoom.transform,
d3.zoomIdentity
.translate(frameWidth / 2, frameHeight / 2)
.scale(
Math.min(
8,
0.9 / Math.max((x1 - x0) / frameWidth, (y1 - y0) / frameHeight)
)
)
.translate(-(x0 + x1) / 2, -(y0 + y1) / 2)
);

Related

Graphing a tan wave with x and y co-ordinates in d3

When I created this sine wave, I created the following d3 scales:
const xScale = scaleLinear()
.domain([0, Math.PI * 2])
.range([0, width]);
const yScale = scaleLinear()
.domain([-1.0, 1.0])
.range([height - 100, 0]);
I can then add numbers to an array that starts at 0 and increase by (Math.PI * 2) / 360 on a timer that animates the sine wave.
const nextTime = sineData[sineData.length - 1] + (Math.PI * 2) / 360;
sineData.push(nextTime);
I then create a d3 line function for the sine wave:
const sineCurve = line<number>()
.curve(curveMonotoneX)
.x((d) => xScale(d))
.y((d) => yScale(Math.sin(d) + 1));
<path d={sineCurve(state.sineData) />
I want to create a tan wave like this but I'm not sure how as the wave is not continuous and there are multiple at different points.
First of all, if you're using D3 for plotting math functions, you're using the wrong tool. I wrote an explanation here and here.
That said, the important thing here is realising that the tangents go from minus infinity to infinity, and that's clearly something that you don't want. So, what you can do is using the defined method for skipping some values.
Here, I'm setting a maximum and minimum tangent value you want to plot in the y axis (named maxTan) in the line generator:
const lineGenerator = d3.line()
.defined(d => Math.tan(d) < maxTan && Math.tan(d) > (-maxTan))
That way the chart will stop at the maxTan y value, and will resume from minus maxTan at the bottom, using a single <path>.
And here is a demo (using radians):
const svg = d3.select("svg"),
margin = 20,
width = 500,
height = 300,
maxTan = 5,
circles = 1;
const xScale = d3.scaleLinear()
.domain([-circles * Math.PI * 2, circles * Math.PI * 2])
.range([margin, width - margin]);
const yScale = d3.scaleLinear()
.domain([-maxTan, maxTan])
.range([height - margin, margin]);
d3.axisBottom(xScale)(svg.append("g").attr("transform", `translate(0,${height/2})`));
d3.axisLeft(yScale)(svg.append("g").attr("transform", `translate(${width/2},0)`));
const data = d3.range(xScale.domain()[0], xScale.domain()[1], 0.025);
const lineGenerator = d3.line()
.defined(d => Math.tan(d) < maxTan && Math.tan(d) > (-maxTan))
.x(d => xScale(d))
.y(d => yScale(Math.tan(d)));
const tan = svg.append("path")
.attr("class", "tanPath")
.datum(data)
.attr("d", d => lineGenerator(d));
.tanPath {
fill: none;
stroke: blue;
stroke-width: 2;
}
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg width="500" height="300"></svg>

Change zooming from geometric to sementic ( zooming only on x-axis )

I have a graph of lines paths .
Actually when I zoom it applies a transform attribute.
I would like to make a semantic zoom by zooming only on the x-axis.
Here's the code for zooming
private manageZoom(svgs: AllSvg, allAxis: AllAxis, dimension: Dimension): D3ZoomBehavior {
const zoom: D3ZoomBehavior = d3
.zoom()
.scaleExtent([1, 40])
.translateExtent([
[0, 0],
[dimension.width, dimension.height]
])
.on('zoom', zoomed.bind(null, allAxis));
svgs.svgContainer.call(zoom);
return zoom;
function zoomed({ xAxis, xAxisBottom, yAxis }: AllAxis, { transform }: any) {
svgs.sillons.attr('transform', transform);
xAxisBottom.axisContainer.call(xAxisBottom.axis.scale(transform.rescaleX(xAxisBottom.scale)) as any);
}
}
the sillons object is an array of paths + text + circles
I would that the lines get re-drawed in the right position as the x-axis get larger, but not zoom sillons on y-axis.
I have checked many posts but can't repoduce them to solve my issue. for example
When you set up something along the lines of
svg.call(
d3.zoom()
.on("zoom", zoom)
)
the zoom function can be just about anything that you want. The first argument of zoom is the zoom event itself. Let's denote it by evt. Then
evt.transform.k tells you the scale factor,
evt.transform.x tells you the horizontal translation, and
evt.transform.y tells you the vertical translation.
You don't have to use all of those, though. Rather, you can redraw your image however you want.
Here's a slightly cute example that rescales the image only horizontally.
let w = 500;
let h = 100;
let svg = d3
.select("#container")
.append("svg")
.attr("width", w)
.attr("height", h)
.style("border", "solid 1px black");
let n = 500;
let pts0 = d3.range(n).map((_) => [d3.randomNormal(w / 2, w / 20)(), 0]);
let pts1 = pts0.map((pt) => [w - pt[0], h]);
let g = svg.append("g");
let link_group = g.append("g");
link_group
.selectAll("path")
.data(d3.range(n))
.join("path")
.attr("d", (i) => d3.linkVertical()({ source: pts0[i], target: pts1[i] }))
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.1)
.attr("stroke-width", 1.5);
let all_pts = pts0.concat(pts1);
let circle_group = g.append("g");
let circles = circle_group
.attr("fill", "black")
.attr("fill-opacity", 0.2)
.selectAll("circle")
.data(all_pts)
.join("circle")
.attr("cx", (d) => d[0])
.attr("cy", (d) => d[1])
.attr("data-x", (d) => d[0])
.attr("r", 4);
svg.call(
d3
.zoom()
.scaleExtent([1 / 4, 20])
.duration(500)
.on("zoom", function (evt) {
let k = evt.transform.k;
link_group.selectAll("path").attr("d", function (i) {
let x00 = pts0[i][0];
let x01 = k * (x00 - w / 2) + w / 2;
let x10 = pts1[i][0];
let x11 = k * (x10 - w / 2) + w / 2;
return d3.linkVertical()({ source: [x01, 0], target: [x11, h] });
});
circle_group
.selectAll("circle")
.nodes()
.forEach(function (c) {
let x0 = c.getAttribute("data-x");
let k = evt.transform.k;
let x1 = k * (x0 - w / 2) + w / 2;
c.setAttribute("cx", x1);
});
})
);
<script src="https://d3js.org/d3.v7.min.js"></script>
<div id="container"></div>

d3.js visibility-zone calculations or how to draw geo rectangle

i want to figure out how to properly calculate vizibility zone and draw it using d3.geo projections. visibility zone in my case is optical camera frustum
for now, i have a two plots, both represent azimuth and elevation from view point, one in gnomonic (according to wiki) projection:
// this magic number is experimentally found
//pixels in one degree in gnomonic projection chart with scale 1500
var px = 26.8;
Width and height below is a optical camera view angles in degrees by azimuth and elevation axes
var w = px * viewport.width;
var h = px * viewport.height;
d3.geoGnomonic()
.translate([w / 2, h / 2])
.scale(1500)
on gnomonic plot i've placed points by its border, then reproject these points using d3.projection.invert method and used resulting angles on d3.geoEquirectangular projection plot to draw areas(like here), with following results:
viewport here is a size of frustum in angles
current method is wrong, but gives me approximate result
i want to figure out what is wrong in my scenario..
ps: i've extracted minimum example, it differs from original code but has same bug: here you can see that size by horizontal axis differs from input size (must be 10, 20, 30, 40 degrees)
Suggestions and comments are appriciated. Thanks for reading!
var d3 = window.d3;
var colorGenerator = d3.scaleOrdinal(d3.schemeCategory10);
var bounds = [650, 500];
var projection = d3.geoEquirectangular().translate([bounds[0]/2, bounds[1]/2]);
var geoPath = d3.geoPath().projection(projection);
var zoom = d3.zoom()
.scaleExtent([1, 1000])
.translateExtent([[0, 0], bounds])
.on("zoom", zoomed);
var svg = d3.select('body')
.append('svg')
.attr("width", bounds[0])
.attr("height", bounds[1])
.attr("viewbox", "0 0 " + bounds[0] + " " + bounds[1])
.call(zoom)
.append('g');
svg.append("g")
.append("path")
.datum(d3.geoGraticule())
.attr("stroke", "gray")
.attr("d", geoPath);
d3.range(0, 4).forEach(function (i) {
var size = (i + 1) * 10;
addVisibilityZone([-130 + size * 5, 50],
colorGenerator(i), [size, size]);
});
function zoomed() {
var t = d3.event.transform;
svg.attr("transform", t);
d3.selectAll("path").attr('stroke-width', 1/t.k);
}
function addVisibilityZone(angles, color, size) {
var xy = projection(angles);
var points = generateRect(100, 0, 0, size[0], size[1]);
var gnomonicProjection = d3.geoGnomonic().clipAngle(180)
.translate([size[0]/2, size[1]/2])
.scale(57); // this magic number is experimentally found
var g = svg.append("g");
var drag = d3.drag()
.on("start", dragged)
.on("drag", dragged);
var path = g.append("path")
.datum({
type: "Polygon",
coordinates: [[]],
})
.classed("zone", "true")
.attr("fill", color)
.attr("stroke", color)
.attr("fill-opacity", 0.3)
.call(drag);
update();
function dragged() {
g.raise();
xy = [d3.event.x, d3.event.y];
update()
}
function update() {
angles = projection.invert(xy);
gnomonicProjection.rotate([-angles[0], -angles[1]]);
path.datum().coordinates[0] = points.map(gnomonicProjection.invert);
path.attr('d', geoPath);
}
}
function generateRect(num, x, y, width, height) {
var count = Math.floor(num / 4) + 1;
var range = d3.range(count);
return range.map(function (i) { // top
return pt(i * width / count, 0);
}).concat(range.map(function (i) { // right
return pt(width, i * height / count);
})).concat(range.map(function (i) { // bottom
return pt(width - i * width / count, height);
})).concat(range.map(function (i) { // left
return pt(0, height - i * height / count);
}));
function pt(dx, dy) {
return [x + dx, y + dy];
}
}
* {
margin: 0;
overflow: hidden;
}
<script src="//d3js.org/d3.v5.min.js"></script>
Your approach looks correct for FOV on sphere visualization. It shouldn't be a rectangle in the result.
Here is an example:
As you can see the distorsion looks correct. It shouldn't be a rectangle.
Same for non equatorial target:

rotate and zoom svg with d3 javascript

I want to rotate and zoom graphic around its center with D3.js. When I zoom graphic I want to zoom it with current aspect ratio and vice versa when I rotate graphic I want to zoom it to the current point that my mouse points. For zooming I use wheel of the mouse and for rotation I use the button of the mouse.
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
transform = d3.zoomIdentity;
var points = d3.range(2000).map(phyllotaxis(10));
var g = svg.append("g");
g.append("line")
.attr("x1", "20")
.attr("y1", "20")
.attr("x2", "60")
.attr("y2", "60")
.attr("stroke", "black")
.attr("stroke-width", "10");
svg.call(d3.drag()
.on("drag",onDrag)
)
// ##########################
var boxCenter = [100, 100];
// #############################
function onDrag(){
var x = d3.event.sourceEvent.pageX,
y = d3.event.sourceEvent.pageY;
var angle = Math.atan2(x - boxCenter[0],
- (y - boxCenter[1]) )*(180/Math.PI);
g.attr("transform", "rotate("+angle+")");
}
svg.call(d3.zoom()
.scaleExtent([1 / 2, 8])
.on("zoom", zoomed));
function zoomed() {
g.attr("transform", d3.event.transform);
}
function phyllotaxis(radius) {
var theta = Math.PI * (3 - Math.sqrt(5));
return function(i) {
var r = radius * Math.sqrt(i), a = theta * i;
return {
x: width / 2 + r * Math.cos(a),
y: height / 2 + r * Math.sin(a)
};
};
}
Here is my example:
https://jsfiddle.net/6Lyjz35L/
For the rotation around center to be correct at the initial zoom you need to add a 'transform-origin' attribute to 'g'.
g.attr("transform-origin", "50% 50%");
The other problems you're having stem from assigning the 'transform' attribute in two separate places. An element ('g') can only have one 'transform' attribute applied at a time, so you're overwriting one or the other each time you rotate or zoom. To fix this you can create a helper method which will append both of the transforms you want in a single string.
var currentAngle = 0;
var currentZoom = '';
function getTransform(p_angle, p_zoom) {
return `${p_zoom} rotate(${p_angle})`;
// return p_zoom + " rotate(" + p_angle + ")";
}
// In the rotate:
currentAngle = angle;
g.attr("transform", getTransform(currentAngle, currentZoom));
// In the zoom:
currentZoom = d3.event.transform;
g.attr("transform", getTransform(currentAngle, currentZoom));
There is one more issue which is introduced by the zoom, and that is that you'll have to calculate a new transform-origin at different zoom levels.
The issue I said was introduced by the zoom was actually the result of applying the operations in the incorrect order. Originally I applied the rotation and THEN then translation. It actually needs to be reversed, translation and THEN rotation. This will keep the correct transform-origin.
Here's a fiddle with those changes: https://jsfiddle.net/scmxcszz/1/

D3: finding graph y-coordinate with mouseover

I am making an interactive area chart using D3. While mousing over the area-chart, I'd like to have a dot rolling along the top of the chart, as in the following example:
http://hci.stanford.edu/jheer/files/zoo/ex/time/multiples.html
Once I get the mouse position (using d3.mouse), how do I translate these coordinates to the corresponding data? The x-axis is straightforward using the inverse of the x-scale (e.g. x.invert). However, I can't find the corresponding y-coordinate for my graph. Ideally I could "look up" the x-coordinate in my data and find the corresponding y-coordinate, but not sure how to do this with D3. Thanks!
It's actually relatively easy to create your own lookup table:
/* Create the lookup table */
var table = {};
data.forEach(function(d) {
table[d.x] = d.y;
});
This is a viable solution if you have enough data points, but it's likely that you will probably need to use some sort of rounding or interpolator to fill in the intermediate x-values. For instance, if there is a fixed spacing between your points, you can use a linear interpolator and do the following calculations to get the coordinates for your circle on mouseover:
var x = d3.mouse(this)[0];
var y;
if ( table[x] === undefined ) {
var lower = x - (x % SPACING);
var upper = lower + SPACING;
var between = d3.interpolateNumber(table[lower], table[upper]);
y = between( (x % SPACING) / SPACING );
} else {
y = table[x];
}
Here is the code in action: http://jsfiddle.net/Wexcode/KGxHF/
Here is another example of how you could do this from Mike Bostock: http://bl.ocks.org/3025699
mbostock (D3.js author) implement this here
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", function() { focus.style("display", null); })
.on("mouseout", function() { focus.style("display", "none"); })
.on("mousemove", mousemove);
function mousemove() {
var x0 = x.invert(d3.mouse(this)[0]),
i = bisectDate(data, x0, 1),
d0 = data[i - 1],
d1 = data[i],
d = x0 - d0.date > d1.date - x0 ? d1 : d0;
focus.attr("transform", "translate(" + x(d.date) + "," + y(d.close) + ")");
focus.select("text").text(formatCurrency(d.close));
}
I use this code to see value X and Y of each point and draw a circle on my curve on mouse event :
exemple on jsFiddle here
Y_value is a global !
var Y_value;
i define my axes rage
x = d3.time.scale().range([0, w]);
y = d3.scale.linear().range([h, 0]);
i define the circle cursor
var circle = svg.append("circle")
.attr("r", 8)
.attr("cx", 0)
.attr("cy", 0)
.style({fill: '#fff', 'fill-opacity': .2, stroke: '#000', "stroke-width": '1px'})
.attr("opacity", 0);
i add a tooltip on my circle
var tooltip = circle.append("svg:title");
and i have my event code
mySensitiveArea.on("mousemove", function() {
var X_pixel = d3.mouse(this)[0],
X_date = x.invert(X_pixel);
var Y_pixel = y(Y_value);
var pathData = curve1.data()[0]; // recupere donnée de la courbe
pathData.forEach(function(element, index, array) {
if ((index+1 < array.length) && (array[index].date <= X_date) && (array[index+1].date >= X_date)) {
if (X_date-array[index].date < array[index+1].date-X_date) Y_value = array[index].val;
else Y_value = array[index+1].val;
}
});
circle.attr("opacity", 1)
.attr("cx", X_px)
.attr("cy", Math.round(y(Y_value)));
tooltip.text("X = " + (X_date) + "\nY = " + (Y_value));
});

Categories