I have some circles made in d3. I need to add more circles when these circles are clicked and make the existing circles disappear(they should not be removed as I will use them again).
The way I'm doing this right now is through a listener to a click event on the original circles. (these are created with .selectAll('circle.nodes')
.on("click",function(d){
//do stuff}).duration(1000);
populateSubCircles);
I want the sub circles to appear around a center (I looked at my calculations and they seem to be correct).
var populateSubCircles = function(){
var subCircles = nodesG.selectAll("circles.subNodes").data(....
This correctly adds the secondary circles, and cx and cy seem to be correct(not too far off from the center circle). However, they don't seem to appear on the page(they appear at the top left at 0,0). Why is that happening? How do I fix that?
Thanks.
EDIT-
A picture or two may help.
It's likely that you don't have the radius set on the circles (it must be set explicitly on the circle element, and cannot be set via CSS, as only style properties such as fill, stroke, etc., can be set) .
As you can see, one svg circle shows normally when set with a radius (r) of 25, and the other does not, and its size is correctly reported as 0px x 0px. (The tooltip reports size, not position). Here's the SVG I used:
<svg>
<circle cx="100" cy="100" fill="#ffdf00"
stroke="#222222" stroke-width="2px" />
<circle cx="50" cy="50" fill="#ffdf00" r="25"
stroke="#222222" stroke-width="2px" />
</svg>
This image shows the second of two circles, using inspector in Chrome web tools, it correctly reports the size:
Again, using inspector in Chrome, highlighted the first SVG element where no radius was set:
It shows the size as 0px x 0px and shows the circle as if it were in the upper left corner of the SVG document.
http://jsfiddle.net/wiredprairie/gNrZ3/
Related
I am attempting to make a world map made from an svg comprising many circles. I based this from a codepen I found here: https://codepen.io/mvaneijgen/pen/NRzENO
E.g.
<svg viewBox="0 0 845.2 458">
<circle class="st0" cx="826.1" cy="110.3" r="1.9"/>
<circle class="st0" cx="819.3" cy="110.3" r="1.9"/>
<circle class="st0" cx="819.3" cy="117.1" r="1.9"/>
<circle class="st0" cx="812.6" cy="90" r="1.9"/>
The map is great. I have been dividing it up into coloured continent regions using classes. These change colour when hovered over. All good so far. Most of my functionality is there.
The issue is that you have to be hovering directly on a circle to make the colour change happen. I am using a javascript mouseover event to change the colour.
Is there any way of increasing the area of effect around the circle elements? Maybe putting an invisible square either behind or in front? I am still getting to grips with front-end stuff and any pointers here would be great.
That’s the right idea: transparent rects behind each circle. (Or transparent continent-shaped paths based on geo data, depending on what you’re going for.)
The trick is to use the SVG CSS property pointer-events. Setting it to fill or all should do the trick.
I have multiple existing SVG circles exported from CorelDraw and would like each to have unique text appear in a tooltip on hover.
Inside g element where are circles I added text elements. I located each text next to corresponding circle and with corresponding text.
<g id="cities" class="gradici">
<circle class="first" r="7" />
<circle class="second" r="7 />
</g>
var Citytooltip = svg.selectAll("g.gradici").selectAll("text")
.data(naziviGradova)
.enter()
.append("text")
.style("visibility", "hidden")
.attr("x", function(d,i){return graddx[i]})
.attr("y",function(d,i){return graddy[i]})
.text(function(d) {return d;})
.attr("font-size", "10px")
.attr("fill", "#black");
I menage to get when I hover over any circle that all text is visible/hidden next to all circles.
var city= svg.selectAll("#cities circle");
city
.on("mouseover", (function(){Citytooltip.style("visibility",
"visible");}))
.on("mouseout", (function(){Citytooltip.style("visibility",
"hidden");}));
But I am straggling how to get text to be visible/hidden just over the circle I am hovering. I suppose I should somehow iterate trough city but I am stuck how to do that. Any ideas?
Add a title, description or metadata elements as content for a circle element in order for the user agent to provide tooltips (depending on the user agent):
<g id="cities" class="gradici">
<desc>A group of circles</desc>
<circle class="first" r="7">
<desc>First circle</desc>
</circle>
<circle class="second" r="7>
<desc>Second circle</desc>
</circle>
</g>
This is specified by SVG 1.1.
For modern desktop and mobile Web browsers, the provided element descriptions are typically rendered as you describe and expect -- as tooltips appearing when the user "hovers their pointer device" over the circle element that e.g. contains the desc element.
My advice would be to not reinvent the wheel with elaborate and complicated script-based solutions that always carry the risk of breaking for some of your users, not when something like the above is part of SVG already and suffices for you.
If user agent tooltips aren't going to cut it, one will have to implement some of the functionality oneself. I decide to still rely on the declarative desc elements and we can trivially use these even with rendering tooltips ourselves.
In the following SVG document, a tooltip definition is used as a template, and whenever the "mouse" pointer (anything that can generate "mouse*" events, really) enters an element, we extract the document fragment (Range) that is the contents of its desc element and copy these contents into the "contents" group/graphics of the tooltip. On top of that, we calculate the position where the tooltip should be shown -- at the tip of the mouse pointer -- and we resize the background "panel" so that it actually resembles what most people accept as tooltips.
You can add your own styling and even animation to further refine desired result.
More explanation is in the comments in the code below:
<?xml version="2.0" encoding="utf-8" ?>
<!DOCTYPE svg SYSTEM "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100">
<style>
#rich-tooltip {
pointer-events: none;
}
#rich-tooltip .panel {
fill: silver;
}
</style>
<defs>
<!-- the actual template, will be removed from the context and always shown as appended at the end of the document body, so that it is rendered above everything else. -->
<g id="rich-tooltip">
<rect class="panel" /><!-- just some decorative background -->
<g class="contents" /><!-- contents of an element's description will be added as child nodes of this element -->
</g>
</defs>
<circle cx="50" cy="50" r="25" fill="yellow">
<desc><circle cx="10" cy="10" r="5" /><text dominant-baseline="hanging" fill="red">First circle</text></desc>
</circle>
<circle cx="70" cy="50" r="40" fill="green">
<desc><circle cx="10" cy="10" r="5" /><text dominant-baseline="hanging" fill="red">Second circle</text></desc>
</circle>
<script>
const tooltip = document.getElementById("rich-tooltip");
tooltip.remove(); /// Initial state of the tooltip is "not shown" (removed from DOM tree)
var timeout; /// We only show the tooltip once the pointer settles and some time passes
const tooltip_delay = 1000; /// Delay before showing the tooltip once pointer settles
var last_tooltip_ev; /// Auxilary variable to be able to calculate movement after showing the tooltip, so we don't remove it immediately but only once the pointer actually moved substantially, this is just a nice touch, not otherwise crucial
const remove_tooltip_move_threshold = 10; /// How many user units (pixels, normall) the cursor may move before tooltip is hidden
function on_mouse_move_event(ev) {
if(document.contains(tooltip)) { /// Is the tooltip being shown?
if(last_tooltip_ev) {
if(((x, y) => Math.sqrt(x * x + y * y))(ev.clientX - last_tooltip_ev.clientX, ev.clientY - last_tooltip_ev.clientY) >= remove_tooltip_move_threshold) { /// has the pointer moved far enough from where the tooltip was originally shown?
tooltip.remove(); /// Hide the tooltip
}
}
} else {
if(timeout) clearTimeout(timeout);
timeout = setTimeout(show_tooltip, tooltip_delay, ev);
}
}
function show_tooltip(ev) {
const desc = ev.target.querySelector(":scope > desc");
if(!desc) { /// Does the element that is under the pointer even have a description?
tooltip.remove(); /// Hide the tooltip (ignoring the fact it may not be showing in the first place)
return;
}
document.documentElement.appendChild(tooltip);
const desc_range = document.createRange();
desc_range.selectNodeContents(desc); /// Select all children of the description element, as `desc_range`
const contents = tooltip.querySelector(".contents");
const contents_range = document.createRange();
contents_range.selectNodeContents(contents); /// Select all children of the tooltip contents element, as `contents_range`
contents_range.extractContents(); /// Empty tooltip contents
contents.appendChild(desc_range.cloneContents()); /// Fill contents with previously selected description. We _copy_ the description -- the original should legitimately stay where it was
const panel = tooltip.querySelector("rect.panel");
panel.setAttribute("width", contents.getBBox().width);
panel.setAttribute("height", contents.getBBox().height); /// "Stretch" the panel so that it covers the tooltip contents
const pt = document.documentElement.createSVGPoint();
pt.x = ev.clientX;
pt.y = ev.clientY;
const view_pt = pt.matrixTransform(document.documentElement.getScreenCTM().inverse()); /// We need to convert mouse pointer coordinates to the SVG viewbox coordinates
tooltip.setAttribute("transform", `translate(${view_pt.x} ${view_pt.y})`); /// Move the tooltip to appropriate position
last_tooltip_ev = ev; /// Save the event to be able to calculate distance later (above)
}
addEventListener("mouseover", function(ev) { /// When the pointer gets over an element...
ev.target.addEventListener("mousemove", on_mouse_move_event); /// We will be tracking pointer movements to trigger timeout for showing the tooltip
ev.target.addEventListener("mouseout", function() { /// Once the pointer gets anywhere but the element itself -- like over its children or other elements...
ev.target.removeEventListener("mouseout", on_mouse_move_event); /// Cancel the whole mousemove business, the behavior will be setup by whatever element the mouse pointer gets over next anyway
}, { once: true }); /// Once, for this element, everything else will be setup by another call for "mouseover" listener
});
</script>
</svg>
The code would be simpler without timeout triggering etc, but chances are a well thought out and user friendly tooltip implementation would be using delays and compensate for stray pointer movements, so I thought I'd make sense to keep some skeletal framework in place for these and also demonstrate how one would go about implementing them.
This is anyway fairly optimal in the sense that you only use one set of listeners at each time -- you don't need to assign listeners to each and all elements you want to track. If an element has a description, this script will make sure a tooltip is shown, is all. Temporarily, we do assign mouseout listener to an element but it's typically only one listener assigned to only one element at any point in time -- as soon as the pointer gets out of an element, the listener is removed (and something else reassigns another instance of it, but that's perfectly fine).
I menage to solve my problem, so I will leave the answer if someone get stuck like me.
var titleCreate = svg.selectAll("g.gradici circle").append("title").text("tooltip");
for (var i =0; i<naziviGradova.length; i++){
var textNazivaGradova = naziviGradova[i];
var title = svg.getElementsByTagName("title");
title[i].innerHTML = textNazivaGradova;
}
I am working on a JavaScript project, and as part of it, trying to rotate an svg path element around a given point.
Here is a simplified example of the js file:
var angle=0;
d3.select("#canvas")
.append("path")
.attr("id", "sector")
.attr("fill", "red")
.attr("d", "M150,150 L150,50 A100,100 0 0,1 236.6,200 Z")
.on("click", function(){
console.log("click");
angle=(angle+120)%360;
d3.select("#sector")
.transition().duration(2500) //removing this line leads to a nice transform attribute in the resulting html
.attr("transform","rotate("+angle+",150,150)");
})
And the html is just:
<svg id="canvas" width="300" height="300">
<circle cx="150" cy="150" r="100" stroke="blue" fill="none"/>
</svg>
Here you can find it on JSfiddle.
As the comment in the above excerpt suggests, it all works fine with the 3-argument version of the rotate function, where I can specify the x and y coordinates of the point which I want to use as the center of rotation. The resulting path element gets a transform attribute with the value of "rotate(120,150,150)" Unless I want to use a transition.
When I insert the line about the transition, the transformation gets some weird extra things added, it looks like "translate(354.9038105676658, 95.09618943233417) rotate(119.99999999999999) skewX(-3.1805546814635176e-15) scale(0.9999999999999999,0.9999999999999999)"
I guess in the background the non-(0,0)-centered rotation gets replaced with some translations and a (0,0)-centered rotation, just as you can do it in geometry. The position and orientation after the transition is fine indeed. However, during the transformation the element is moving on a funny path, in the example the sector leaves the circle.
Is there a way I can suppress the transition doing all these transformations and just apply a single non-(0,0)-based rotation?
Are there any other workarounds?
Changing the transform-origin for the path element attribute did not seem to work, but maybe I was doing it wrong.
I am looking for a CSS-free solution. It is an architectural decision which I cannot overrule in the project.
As you see, D3 is already involved in the project, but I would like to use as few additional external libraries as possible.
Thanks in advance!
create your arc with its center at (0,0). Then translate it to the center of the circle.
Then the d3 transition will work nicely as follows:
d3.select("#sector")
.transition().duration(2500)
.attr("transform","translate(150,150)rotate("+angle+")");
I have an SVG-based graph. On top is a scrollable area, where the user can navigate around the graph, as shown in the image:
By dragging either of the tabs on the left/right, the user can zoom in on a particular part of the plot, as shown in the image:
Notice that only the selected portion of the graph is blue, and has a grey highlight. Both the blue and the highlight are unchanged, but are clipped using a clipping mask:
<clippath id="clip_path_scroll">
<rect id="clip_path_scroll_rect" x="x" y="y" width="w" height="h" stroke-width="0" />
</clippath>
and
<g clip-path="url(#clip_path_scroll)">
<rect id="highlighted" width="w" height="h" x="x" y="y" fill="rgba(0, 0, 0, .03)" stroke="none" />
<path id="blue_plot" d="..." stroke="rgba(26, 171, 219, 1)" stroke-width="2" fill="none"/>
</g>
And this all works fine and dandy. The clip path works flawlessly, when dragging the tabs. In that situation, it is changed incrementally, as the tab(s) slide.
But I have a reset button, that calls the following:
$('#left_scroll_group').attr('transform', 'translate(0, 0)');
$('#right_scroll_group').attr('transform', 'translate(0, 0)');
$('#clip_path_scroll_rect').attr('width', width).attr('x', x);
and when this runs, the outcome is the following:
Notice that the tab groups snap back to exactly where they should be, but it appears the clip path has only partially gone back to its original width and x-position.
BUT THIS ISN'T THE CASE
If I view the page source, and hover over the clippath rect object, it shows the following:
That is to say, it shows the rect object DID resize and move to its original position, but is not displaying properly. Moving either of the tabs immediately snaps the clippath back to working properly, as in the first two images.
Why is this happening? It happens whether the right tab is moved, or the left, or both. If the tabs are moved only a little, there doesn't seem to be much issue, but the obvious glitch/error increases as the tabs are moved further.
A similar function performs the same attr changes to the clippath, but it always shortens the path length. This glitch only seems to be happening when lengthening the length.
I am using the same types of operations on many other elements in the graph, and all work fine. All are referenced with jQuery notation (i.e. $('#element')).
I am not interested in using d3 or other libraries. I am looking for jQuery or pure JS solutions only.
EDIT 1
I've also tried
var steps = Math.floor(scrollPlotWidth - $('#clip_path_scroll_rect').attr('width'));
var curWidth = Math.ceil($('#clip_path_scroll_rect').attr('width'));
var curX = $('#clip_path_scroll_rect').attr('x');
for(var i = 0; i < steps; i++){
$('#clip_path_scroll_rect').attr('x', curX).attr('width', curWidth);
curX--;
curWidth++;
}
With no success. The changes are happening incrementally, but still no dice - it looks the same as before. This clipping issue is only happening in Safari, and seems to be fine in FireFox. Haven't tested others.
WORKING SOLUTION (HACKED)
When defining the rect within the clippath, I changed the width property. I've changed it from what is shown above to the following:
<rect id="highlighted" style="width:w;" height="h" x="x" y="y" fill="rgba(0, 0, 0, .03)" stroke="none" />
And then, the clippath functionality works properly IFF I apply the two operations:
$('#clip_path_scroll_rect').attr('x', x).css('width', w);
$('#clip_path_scroll_rect').animate(
{"width": w},
{duration: 1,
step: function(){
this.setAttribute("x", x);
}
});
Removing either of these will prevent it from working. I'm not sure why, but it will have to be a temporary fix until something better comes along. Answers are still very much welcome!
I'm using a circular clip path for my nodes (in d3.js) as follows:
<g class="node" id="140" transform="translate(392.3261241288836,64.3699106556645)">
<image class="mainImage" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="images/manual-story-140.jpg" width="100" height="116.66705555685185" x="-50" y="-50" clip-path="url(#140)">
</image>
<clipPath class="clipPath" id="140">
<circle class="clipPathCircle" stroke-width="1" r="42"></circle>
</clipPath>
<circle class="outlinecircle" stroke="#0099FF" fill="url(#myLinearGradient1)" stroke-width="3" r="42"></circle>
</g>
But in Firefox the images don't load because the circle element within the clipPath element doesn't inherit the position of the node (i.e. from the g element).
In Chrome/Safari, everything works great and when I open up the console and hover over the circle element that's within the clipPath element it clearly shows the circle in the correct place (with dimensions of 84x84 since the radius is 42).
But in Firefox I see no images, and when I hover over the circle using the console I see it's positioned at the top left of the screen with dimensions 0x0.
Any ideas what I'm doing wrong here? Do I have to give an absolute position of the circle for firefox or should it already understand from the g-element it's in?
Apologies for the false alarm, but the problem here (as you can see in my original code) was that I was using the same id on my parent g element as I was to reference my clipPath! I changed the "id" attribute for my clip path to start with the string "clipPath-" and now it works on Firefox. Not sure why that would affect different browsers differently (which is why I kinda went 'round the houses trying to troubleshoot the bug), but thankfully enough it's quite a trivial fix!