I have a svg element ; the nodes, links, labels etc. are appended to it. I got the zoom-to-particular-node-by-name functionality running but the issue is after zooming automatically to the respective node , whenever I try to pan svg (by clicking and dragging it around), it resets the zoom and the coordinates to how it was before I zoomed to a particular node. I think it has to do with the way d3.event.transform works but I am not able to fix it. I want to be able to continue panning and zooming from the node I zoomed to without resetting any values.
(Also, from a bit of debugging , I observed that the cx and cy coordinates for the nodes did not change by zooming and panning from the code, but If I were to zoom and pan to a node manually , then it would. I guess that is the problem)
var svg1 = d3.select("svg");
var width = +screen.width;
var height = +screen.height - 500;
svg1.attr("width", width).attr("height", height);
var zoom = d3.zoom();
var svg = svg1
.call(
zoom.on("zoom", function() {
svg.attr("transform", d3.event.transform);
})
)
.on("dblclick.zoom", null)
.append("g");
function highlightNode() {
var userInput = document.getElementById("targetNode");
theNode = d3.select("#" + userInput.value);
const isEmpty = theNode.empty();
if (isEmpty) {
document.getElementById("output").innerHTML = "Given node doesn't exist";
} else {
document.getElementById("output").innerHTML = "";
}
svg
.transition()
.duration(750)
.attr(
"transform",
"translate(" +
-(theNode.attr("cx") - screen.width / 2) +
"," +
-(theNode.attr("cy") - screen.height / 4) +
")"
// This works correctly
);
}
Related
I'm currently working on a circular progress bar. I've been able to create the progress bar but I need to at border-radius to the end and start of the progress and also change de font-size of the % number. I've searched around and in theory, the border-radius is added with stroke-linecap="round", but that doesn't seem to work for me. I haven't been able to find anything regarding the font-size.Adding a shadow to the bar would also be great, but that's not truly necessary.
I have already looked at this answer but I can't seem to get it right.
function drawProgress(percentage, element, svg) {
if (svg) {
svg.selectAll("*").remove();
}
var wrapper = element;
var start = 0;
var colours = {
fill: '#3F88FB',
track: '#DDDDDD',
text: '#444444',
}
var radius = 34;
var border = 8;
var strokeSpacing = 4;
var endAngle = Math.PI * 2;
var formatText = d3.format('.0%');
var boxSize = radius * 2;
var count = percentage;
var progress = start;
var step = percentage < start ? -0.01 : 0.01;
//Define the circle
var circle = d3.svg.arc()
.startAngle(0)
.innerRadius(radius)
.outerRadius(radius - border);
//setup SVG wrapper
svg = d3.select(wrapper)
.append('svg')
.attr('width', boxSize)
.attr('height', boxSize);
// ADD Group container
var g = svg.append('g')
.attr('transform', 'translate(' + boxSize / 2 + ',' + boxSize / 2 + ')');
//Setup track
var track = g.append('g').attr('class', 'radial-progress');
track.append('path')
.attr('fill', colours.track)
.attr('stroke-width', strokeSpacing + 'px')
.attr('d', circle.endAngle(endAngle));
//Add colour fill
var value = track.append('path')
.attr('fill', colours.fill)
.attr('stroke-width', strokeSpacing + 'px')
.attr('stroke-linecap', 'round');
//Add text value
var numberText = track.append('text')
.attr('fill', colours.text)
.attr('text-anchor', 'middle')
.attr('dy', '0.5rem');
//update position of endAngle
value.attr('d', circle.endAngle(endAngle * percentage));
//update text value
numberText.text(formatText(percentage));}
var svgVisitas;
drawProgress(50 / 100, document.getElementById('radialprogressVisitas'), svgVisitas);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="radialprogressVisitas"></div>
Thank you. If you need anything else, please specify it in the comments and I'll edit the question. The project is run on Visual Basic.
I have a problem with a panel development. The panel generates a bar graph for the current reading and draws lines for the lower and upper limits. This works great. Now I want to draw a sparkline to it. I first draw this sparkline in a separate div element below the actual graph.
In the graph I have two rect elements, one for the background and one for the actual bar. My goal now is to determine the size of the background rect element, then assign this size to my div for the sparkline and then place the sparkline div over the graph.
But the problem is that I can't access the rect element (it's just not found).
Hopefully my question is understandable. What am I doing wrong?
Here is the code snippet:
...
var panelID = "dd-multistat-panel-" + id;
var tooltipDivID = "dd-multistat-panel-tooltip-" + id;
var RectBackID = "dd-multistat-panel-back-" + id;
var RectBackClass = "dd-multistat-panel-back-" + id;
var RectBarID = "dd-multistat-panel-bar-" + id;
...
// draw the background
svg
.append("g")
//.attr("id", RectBackID)
//.attr("class", RectBackClass)
.selectAll("rect")
.data(stripedata)
.enter()
.append("rect")
.attr("id", RectBackID)
.attr("class", RectBackClass)
.attr("width", w)
.attr("height", stripeScale.step())
.attr("x", left)
.attr("y", function (d) {
return stripeScale(d);
})
.attr("fill", "rgba(0,0,0,0)")
.attr("stroke", OutlineColor)
.on("mouseover", function (d) {
if (showTooltips || Links.length /* && i < data.length*/)
tooltipShow(d);
})
.on("mouseleave", function () {
if (!isInTooltip) {
tooltipHide(false);
}
});
...
//my seach code
var BarClassID = "." + panelID;
var PanelBackID = "#" + RectBackID;
var PanelBackClass = "." + RectBackClass;
console.log("var findBar = d3.select(" + BarClassID + ")");
var findBar = d3.select(BarClassID);
console.log(findBar.nodes()); // --> finds 1 svg
console.log("var findRect = findBar.selectAll(" + PanelBackID + ")");
var findRect = findBar.selectAll(PanelBackID);
console.log(findRect.nodes()); // --> finds 0 elements
console.log("var findRect2 = d3.selectAll(" + PanelBackID + ")");
var findRect2 = d3.selectAll(PanelBackID);
console.log(findRect2.nodes()); // --> finds 0 elements
console.log("var findRect3 = d3.selectAll(" + PanelBackClass + ")");
var findRect3 = d3.selectAll(PanelBackClass);
console.log(findRect3.nodes()); // --> finds 0 elements
console.log("var findRect4 = d3.selectAll(svg)");
var findRect4 = d3.selectAll("svg");
console.log(findRect4.nodes()); // --> finds 55 svg
console.log("var findRect5 = d3.selectAll(g)");
var findRect5 = d3.selectAll("g");
console.log(findRect5.nodes()); // --> finds 0 elements
console.log("var findRect6 = d3.selectAll(rect)");
var findRect6 = d3.selectAll("rect");
console.log(findRect6.nodes()); // --> finds 0 elements
Instead of creating multiple svgs, you should create one svg and append your bar graph and spakrline to that.
In case you stick to same logic, try using d3-selection to select your rect.
import * as d3Select from 'd3-selection';
d3Select.select('#your-rect-id');
Here are some helpful links -
ngx-charts
Simple Bar Graph
Graph Gallery
I'm using D3 to zoom onto an image on click and on Mousewheel. Everything is working fine but the first zoom glitches a lot.
Here is the demo of the app.
This is how I'm zooming towards the objects:
const star = "https://gmg-world-media.github.io/skymap-v1dev/static/media/star.19b34dbf.svg";
const galaxy = "https://gmg-world-media.github.io/skymap-v1dev/static/media/galaxy.c5e7b011.svg";
const nebula = "https://gmg-world-media.github.io/skymap-v1dev/static/media/nebula.d65f45e5.svg";
const exotic = "https://gmg-world-media.github.io/skymap-v1dev/static/media/exotic.21ad5d39.svg";
const sWidth = window.innerWidth;
const sHeight = window.innerHeight;
const x = d3.scaleLinear().range([0, sWidth]).domain([-180, 180]);
const y = d3.scaleLinear().range([0, sHeight]).domain([-90, 90]);
const svg = d3.select("#render_map").append("svg").attr("width", sWidth).attr("height", sHeight);
const node = svg.append("g").attr('class', 'scale-holder');
const zoom = d3
.zoom()
.scaleExtent([1, 30])
.translateExtent([
[0, 0],
[sWidth, sHeight]
])
svg.call(zoom);
const imgG = node.append("g");
imgG
.insert("svg:image")
.attr("preserveAspectRatio", "none")
.attr("x", 0)
.attr("y", 0)
.attr("width", sWidth)
.attr("height", sHeight)
.attr("xlink:href", "https://gmg-world-media.github.io/skymap-v1dev/img-set/image-1.jpg");
imgG
.insert("svg:image")
.attr("preserveAspectRatio", "none")
.attr("x", 0)
.attr("y", 0)
.attr("width", sWidth)
.attr("height", sHeight)
.attr("xlink:href", "https://gmg-world-media.github.io/skymap-v1dev/img-set/image.jpg");
// Draw objects on map with icon size 8
drawObjects(8)
function drawObjects(size) {
const dataArray = [];
const to = -180;
const from = 180;
const fixed = 3;
const objectType = ["ST", "G", "N", "E"];
// Following loop is just for demo.
// Actual data comes from a JSON file.
for (let i = 0; i < 350; i++) {
const latitude = (Math.random() * (to - from) + from).toFixed(fixed) * 1;
const longitude = (Math.random() * (to - from) + from).toFixed(fixed) * 1;
const random = Math.floor(Math.random() * objectType.length);
dataArray.push({
"Longitude": longitude,
"Latitude": latitude,
"Category": objectType[random]
})
}
for (let index = 0; index < dataArray.length; index++) {
// Loop over the data
const item = dataArray[index]
const mY = y(Number(item.Latitude))
const mX = x(Number(item.Longitude))
if (node.select(".coords[index='" + index + "']").size() === 0) {
let shape = star;
// Plot various icons based on Category
switch (item.Category) {
case "ST":
shape = star;
break;
case "G":
shape = galaxy;
break;
case "N":
shape = nebula;
break;
case "E":
shape = exotic;
break;
}
const rect = node
.insert("svg:image")
.attr("class", "coords")
.attr("preserveAspectRatio", "none")
.attr("x", mX)
.attr("y", mY)
.attr("width", size)
.attr("height", size)
.attr("cursor", "pointer")
.attr("index", index)
.attr("xlink:href", shape)
.attr("opacity", "0")
.on("click", function() {
handleObjectClick(index, mX, mY)
})
// Add the objects on the map
rect.transition().duration(Math.random() * (2000 - 500) + 500).attr("opacity", "1")
}
}
}
function boxZoom(x, y) {
// Zoom towards the selected object
// This is the part responsible for zooming
svg
.transition()
.duration(1000)
.call(
zoom.transform,
d3.zoomIdentity
.translate(sWidth / 2, sHeight / 2)
.scale(6)
.translate(-x, -y)
);
}
function handleObjectClick(currentSelect, x, y) {
// Appending some thumbnails to the clicked object here...
//Call the zoom function
boxZoom(x, y)
}
#render_map {
width: 100vw;
height: 100vh;
margin: 0 auto;
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="render_map">
</div>
This zoom doesn't seem to be working here. But it does definitely work in the app. I've not modified the piece of code responsible for zooming. (See this demo instead.)
The problem is that zoom jumps when you do it for the first time after a page load, and then it fixes itself.
I don't understand what I'm doing wrong here. Any hints would be lovely.
TIA!
The issue seems caused by a very expensive CSS repaint cycle. I tested this in Firefox by going to Performance in the DEV tools and starting a recording, then zooming for the first time.
I saw the fps drop enormously, and that the repaint took as much as 250ms. Normally, that is 10-50ms.
I have some pointers:
Why do you have two images behind each other? Big images are definitely the reason why repainting takes this long, and your image is 8000x4000 pixels! Start by removing the image that we're not even seeing;
Try adding an initial value of transform="translate(0, 0) scale(1)" to .scale-holder. I have a feeling that adding this the first time is what forces the entire screen to be repainted. Maybe changing an existing scale value is an easier mathematical operation than applying a scale value to something that was not scaled before;
If that doesn't help, compress the image to at most 1600 or even 1080 pixels wide. Us mortals should not even be able to see the difference.
I'm having trouble translating a D3 example with a zoom behavior from v3 to v5. My code is based on this example: https://bl.ocks.org/mbostock/2206340 by Mike Bostock. I use react and I get these errors "d3.zoom(...).translate is not a function" and "d3.zoom(...).scale is not a function". I looked in the documentation, but could not find scale or translate just scaleBy and translateTo and translateBy. I can't figure out how to do it either way.
componentDidMount() {
this.drawChart();
}
drawChart = () => {
var width = window.innerWidth * 0.66,
height = window.innerHeight * 0.7,
centered,
world_id;
window.addEventListener("resize", function() {
width = window.innerWidth * 0.66;
height = window.innerHeight * 0.7;
});
var tooltip = d3
.select("#container")
.append("div")
.attr("class", "tooltip hidden");
var projection = d3
.geoMercator()
.scale(100)
.translate([width / 2, height / 1.5]);
var path = d3.geoPath().projection(projection);
var zoom = d3
.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([height * 0.197, 3 * height])
.on("zoom", zoomed);
var svg = d3
.select("#container")
.append("svg")
.attr("width", width)
.attr("class", "map card shadow")
.attr("height", height);
var g = svg.append("g").call(zoom);
g.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
var world_id = data2;
var world = data;
console.log(world);
var rawCountries = topojson.feature(world, world.objects.countries)
.features,
neighbors = topojson.neighbors(world.objects.countries.geometries);
console.log(rawCountries);
console.log(neighbors);
var countries = [];
// Splice(remove) random pieces
rawCountries.splice(145, 1);
rawCountries.splice(38, 1);
rawCountries.map(country => {
//console.log(parseInt(country.id) !== 010)
// Filter out Antartica and Kosovo
if (parseInt(country.id) !== parseInt("010")) {
countries.push(country);
} else {
console.log(country.id);
}
});
console.log(countries);
g.append("g")
.attr("id", "countries")
.selectAll(".country")
.data(countries)
.enter()
.insert("path", ".graticule")
.attr("class", "country")
.attr("d", path)
.attr("data-name", function(d) {
return d.id;
})
.on("click", clicked)
.on("mousemove", function(d, i) {
var mouse = d3.mouse(svg.node()).map(function(d) {
return parseInt(d);
});
tooltip
.classed("hidden", false)
.attr(
"style",
"left:" + mouse[0] + "px;top:" + (mouse[1] - 50) + "px"
)
.html(getCountryName(d.id));
})
.on("mouseout", function(d, i) {
tooltip.classed("hidden", true);
});
function getCountryName(id) {
var country = world_id.filter(
country => parseInt(country.iso_n3) == parseInt(id)
);
console.log(country[0].name);
console.log(id);
return country[0].name;
}
function updateCountry(d) {
console.log(world_id);
var country = world_id.filter(
country => parseInt(country.iso_n3) == parseInt(d.id)
);
console.log(country[0].name);
var iso_a2;
if (country[0].name === "Kosovo") {
iso_a2 = "xk";
} else {
iso_a2 = country[0].iso_a2.toLowerCase();
}
// Remove any current data
$("#countryName").empty();
$("#countryFlag").empty();
$("#countryName").text(country[0].name);
var src = "svg/" + iso_a2 + ".svg";
var img = "<img id='flag' class='flag' src=" + src + " />";
$("#countryFlag").append(img);
}
// Remove country when deselected
function removeCountry() {
$("#countryName").empty();
$("#countryFlag").empty();
}
// When clicked on a country
function clicked(d) {
if (d && centered !== d) {
centered = d;
updateCountry(d);
} else {
centered = null;
removeCountry();
}
g.selectAll("path").classed(
"active",
centered &&
function(d) {
return d === centered;
}
);
console.log("Clicked");
console.log(d);
console.log(d);
var centroid = path.centroid(d),
translate = projection.translate();
console.log(translate);
console.log(centroid);
projection.translate([
translate[0] - centroid[0] + width / 2,
translate[1] - centroid[1] + height / 2
]);
zoom.translate(projection.translate());
g.selectAll("path")
.transition()
.duration(700)
.attr("d", path);
}
// D3 zoomed
function zoomed() {
console.log("zoomed");
projection.translate(d3.event.translate).scale(d3.event.scale);
g.selectAll("path").attr("d", path);
}
};
render() {
return (
<div className="container-fluid bg">
<div class="row">
<div className="col-12">
<h2 className="header text-center p-3 mb-5">
Project 2 - World value survey
</h2>
</div>
</div>
<div className="row mx-auto">
<div className="col-md-8">
<div id="container" class="mx-auto" />
</div>
<div className="col-md-4">
<div id="countryInfo" className="card">
<h2 id="countryName" className="p-3 text-center" />
<div id="countryFlag" className="mx-auto" />
</div>
</div>
</div>
</div>
);
}
I won't go into the differences between v3 and v5 partly because it has been long enough that I have forgotten much of the specifics and details as to how v3 was different. Instead I'll just look at how to implement that example with v5. This answer would require adaptation for non-geographic cases - the geographic projection is doing the visual zooming in this case.
In your example, the zoom keeps track of the zoom state in order to set the projection properly. The zoom does not set a transform to any SVG element, instead the projection reprojects the features each zoom (or click).
So, to get started, with d3v5, after we call the zoom on our selection, we can set the zoom on a selected element with:
selection.call(zoom.transform, transformObject);
Where the base transform object is:
d3.zoomIdentity
d3.zoomIdentity has scale (k) of 1, translate x (x) and y (y) values of 0. There are some methods built into the identity prototype, so a plain object won't do, but we can use the identity to set new values for k, x, and y:
var transform = d3.zoomIdentity;
transform.x = projection.translate()[0]
transform.y = projection.translate()[1]
transform.k = projection.scale()
This is very similar to the example, but rather than providing the values to the zoom behavior itself, we are building an object that describes the zoom state. Now we can use selection.call(zoom.transform, transform) to apply the transform. This will:
set the zoom's transform to the provided values
trigger a zoom event
In our zoom function we want to take the updated zoom transform, apply it to the projection and then redraw our paths:
function zoomed() {
// Get the new zoom transform
transform = d3.event.transform;
// Apply the new transform to the projection
projection.translate([transform.x,transform.y]).scale(transform.k);
// Redraw the features based on the updaed projection:
g.selectAll("path").attr("d", path);
}
Note - d3.event.translate and d3.event.scale won't return anything in d3v5 - these are now the x,y,k properties of d3.event.transform
Without a click function, we might have this, which is directly adapted from the example in the question. The click function is not included, but you can still pan.
If we want to include a click to center function like the original, we can update our transform object with the new translate and call the zoom:
function clicked(d) {
var centroid = path.centroid(d),
translate = projection.translate();
// Update the translate as before:
projection.translate([
translate[0] - centroid[0] + width / 2,
translate[1] - centroid[1] + height / 2
]);
// Update the transform object:
transform.x = projection.translate()[0];
transform.y = projection.translate()[1];
// Apply the transform object:
g.call(zoom.transform, transform);
}
Similar to the v3 version - but by applying the zoom transform (just as we did initially) we trigger a zoom event, so we don't need to update the path as part of the click function.
All together that might look like this.
There is on detail I didn't include, the transition on click. As we triggering the zoomed function on both click and zoom, if we included a transition, panning would also transition - and panning triggers too many zoom events for transitions to perform as desired. One option we have is to trigger a transition only if the source event was a click. This modification might look like:
function zoomed() {
// Was the event a click?
var event = d3.event.sourceEvent ? d3.event.sourceEvent.type : null;
// Get the new zoom transform
transform = d3.event.transform;
// Apply the new transform to the projection
projection.translate([transform.x,transform.y]).scale(transform.k);
// Redraw the features based on the updaed projection:
(event == "click") ? g.selectAll("path").transition().attr("d",path) : g.selectAll("path").attr("d", path);
}
In my index.html I have an svg viewbox:
<svg viewBox = "0 0 2000 2000" version = "1.1">
</svg>
I wish to animate an svg ellipse I have created such that when the ellipse is clicked it moves vertically to a certain y point we'll call TOP, and if clicked again moves back to its original position called BOTTOM. Currently I am using the following code which works to an extent.
var testFlag = 0;
d3.select("#ellipseTest").on("click", function(){
if (testFlag == 0){
d3.select(this)
.attr("transform", "translate(0,0)")
.transition()
.duration(450)
.ease("in-out")
.attr("transform", "translate(0,-650)")
testFlag = 1;
}else{
d3.select(this)
.attr("transform", "translate(0,-650)")
.transition()
.duration(450)
.ease("in-out")
.attr("transform", "translate(0,0)")
testFlag = 0;
}
});
The issue however, is that I have also made the ellipse drag-able up to the point TOP and down to the point BOTTOM. So if I drag the ellipse halfway in between TOP and BOTTOM, then click the ellipse, it animates vertically above TOP, instead of stopping when it reaches TOP (and like wise for BOTTOM when animating down). This seems to be a result of how the transform translate method works.
I believe I can resolve this if I create a function that dynamically returns the amount the ellipse should translate relative to where the mouse clicks (or better yet, to where the ellipse is currently position) . The problem is that I can't figure out how to get the current y position of the element relative to the viewbox, rather I can only get the position with respect to the entire page.
Here is the faulty code I am using to get the position of my click:
var svg2 = document.getElementsByTagName('svg')[0];
var pt = svg2.createSVGPoint();
document.documentElement.addEventListener('click',function(evt){
pt.x = evt.clientX;
pt.y = evt.clientY;
console.log('Position is.... ' + pt.y);
},false);
Here is my working code to make the ellipse draggable:
//These points are all relative to the viewbox
var lx1 = -500;
var ly1 = 1450;
var lx2 = -500;
var ly2 = 800;
function restrict(mouse){ //Y position of the mouse
var x;
if ( (mouse < ly1) && (mouse > ly2) ) {
x = mouse;
}else if (mouse > ly1){
x = ly1;
}else if (mouse < ly2) {
x = ly2;
}
return x;
}
var drag = d3.behavior.drag()
.on("drag", function(d) {
var mx = d3.mouse(this)[0];
var my = d3.mouse(this)[1];
d3.select("#ellipseTest")
.attr({
cx: lx2,
cy: restrict(d3.mouse(this)[1])
});
})
Animate cy instead of the transformation...
var testFlag = 0;
d3.select("#ellipseTest").on("click", function(){
if (testFlag == 0){
d3.select(this)
.transition()
.duration(450)
.ease("in-out")
.attr("cy", 0)
testFlag = 1;
}else{
d3.select(this)
.transition()
.duration(450)
.ease("in-out")
.attr("cy", 650)
testFlag = 0;
}
});