Zoom on multiple areas in d3.js - svg

I'm planning to have a geoJSON map inside my svg alongside other svg elements. I would like to be able to zoom (zoom+pan) in the map and keep the map in the same location with a bounding box. I can accomplish this by using a clipPath to keep the map within a rectangular area. The problem is that I also want to enable zooming and panning on my entire svg. If I do d3.select("svg").call(myzoom); this overrides any zoom I applied to my map. How can I apply zoom to both my entire svg and to my map? That is, I want to be able to zoom+pan on my map when my mouse is in the map's bounding box, and when the mouse is outside the bounding box, zoom+pan on the entire svg.
Here's example code: http://bl.ocks.org/nuernber/aeaac0e8edcf7ca93ade.
<svg id="svg" width="640" height="480" xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
<clipPath id="rectClip">
<rect x="150" y="25" width="400" height="400" style="stroke: gray; fill: none;"/>
</clipPath>
</defs>
<g id="outer_group">
<circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red" />
<g id="svg_map" style="clip-path: url(#rectClip);">
</g>
</g>
</svg><br/>
<script type="text/javascript">
var svg = d3.select("#svg_map");
var mapGroup = svg.append("g");
var projection = d3.geo.mercator();
var path = d3.geo.path().projection(projection);
var zoom = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.on("zoom", zoomed);
mapGroup.call(zoom);
var pan = d3.behavior.zoom()
.on("zoom", panned);
d3.select("svg").call(pan);
mapGroup.attr("transform", "translate(200,0) scale(2,2)");
d3.json("ne_110m_admin_0_countries/ne_110m_admin_0_countries.geojson", function(collection) {
mapGroup.selectAll("path").data(collection.features)
.enter().append("path")
.attr("d", path)
.attr("id", function(d) { return d.properties.name.replace(/\s+/g, "")})
.style("fill", "gray").style("stroke", "white").style("stroke-width",1);
}
);
function panned() {
var x = d3.event.translate[0];
var y = d3.event.translate[1];
d3.select("#outer_group").attr("transform", "translate("+x+","+y+") scale(" + d3.event.scale + ")");
}
function zoomed() {
previousScale = d3.event.scale;
projection.translate(d3.event.translate).scale(d3.event.scale);
translationOffset = d3.event.translate;
mapGroup.selectAll("path").attr("d", path);
}
</script>

You need two zoom behaviours for that. The first one would be attached to the SVG and the second one to the map. In the zoom handlers you would have to take care of taking the appropriate action for each.

Related

SVG elements to zoom whole SVG group on click or mouseover

I would like to use the circles within my SVG file to trigger a zoom in centred on the circle. I have got it working with a div acting as the trigger for the zoom but if I instead apply id="pin" to one of the circle elements within the SVG it no longer zooms in. Can anyone tell me why this is?
Is there a better way for me to achieve what I am trying to do? Ideally, I would like it to be possible to click to zoom and then to access other interactivity within the SVG while zoomed in.
If this is not possible is there a simple way to zoom and pan an SVG and to be able to access SVG interactivity while zoomed?
If I have missed something obvious please forgive me, I’m very much still learning the basics!
Rough example:
CodePen link
<div id="pin">click to trigger zoom</div>
<div class="map" id="mapFrame">
<svg class="image" id="mapSVG" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1920 1442.5"" style="
enable-background:new 0 0 1920 924.9;" xml:space="preserve">
<g id="Layer_7" data-name="Layer 7">
<image width="1800" height="1350" transform="translate(0) scale(1.069)" opacity="0.3"
xlink:href="https://media.npr.org/assets/img/2020/07/04/seamus-coronavirus-d3-world-map-20200323_wide-a3888a851b91a905e9ad054ea03e177e23620015.png" />
</g>
<g id="one">
<circle cx="929.664" cy="944.287" r="81.191"/>
</g>
<g id="two">
<circle cx="638.164" cy="456.863" r="81.191" />
</g>
<g id="three">
<circle cx="1266.164" cy="498.868" r="81.191" />
</g>
</svg>
</div>
<script src="app.js"></script>
svg {
width: 100%;
height: auto;
}
#pin {
position: absolute;
height: 65px;
width: 75px;
top: 300px;
left: 550px;
padding: 10px;
background-color: yellow;
}
let imgElement = document.querySelector('#mapFrame');
let pinElement = document.querySelector('#pin');
pinElement.addEventListener('click', function (e) {
imgElement.style.transform = 'translate(-' + 0 + 'px,-' + 0 + 'px) scale(2)';
pinElement.style.display = 'none';
});
imgElement.addEventListener('click', function (e) {
imgElement.style.transform = null;
pinElement.style.display = 'block';
});
When you click on the circle, you are also clicking on the background image as well, triggering two events which is essentially cancelling the zoom. You can see this if you place alert('click 1'); and alert('click 2'); in your listeners.
This doesn't happen on the #pin element because it's outside background div and avoids the event bubbling up. This is solved by adding event.stopPropagation();
Code from your CodePen:
let imgElement = document.querySelector('#mapFrame');
let pinElement = document.querySelector('#one'); //changed to #one
pinElement.addEventListener('click', function (e) {
imgElement.style.transform = 'translate(-' + 0 + 'px,-' + 0 + 'px) scale(2)';
pinElement.style.display = 'none';
event.stopPropagation(); //added to prevent bubbling
});
imgElement.addEventListener('click', function (e) {
imgElement.style.transform = null;
pinElement.style.display = 'block';
});

How can i make a svg <rect> box dynamic with changeable <text>?

How can i make a svg rect box dynamic with changeable text ? Like in my code, if the text "Hello" will more than 30 character ?
<svg version="1.1" viewBox="0 0 500 500" preserveAspectRatio="xMinYMin meet" class="svg-content">
<g>
<rect x="0" y="0" width="100" height="100" stroke-width="5" stroke="#000000" fill="none"></rect> ?
<text x="0" y="50" font-family="Verdana" font-size="35" fill="blue">Hello</text>
</g>
</svg>
Probably the best approach would be to have the rect preset at specific width. Then create tspans to fill the text element, and dynamically resize the rect height as the characters exceed the preset width.
Below is an example:
<!DOCTYPE HTML>
<html>
<head>
<title>Wrap Text Rectangle</title>
</head>
<body onload=wrapTextRect()>
Place this text:<br>
<textarea id="myTextValue" style='width:400px;height:60px;'>
Hello!
</textarea><br>
<button onClick=wrapTextRect()> Wrap text in rect</button>
<div id="svgDiv" style='background-color:lightgreen;width:400px;height:400px;'>
<svg id="mySVG" width="400" height="400">
<rect id=myRect x=50 y=50 rx=10 ry=10 width=100 fill="#4682b4" stroke='black' stroke-width=5 opacity=.5 />
<text id=myText font-size=14 font-family="arial" fill="white" />
</svg>
</div>
SVG Source:<br>
<textarea id=sourceValue style=width:500px;height:300px></textarea>
<script>
var NS="http://www.w3.org/2000/svg"
//---onload and button---
function wrapTextRect()
{
//---clear previous---
for(var k=myText.childNodes.length-1;k>=0;k--)
myText.removeChild(myText.childNodes.item(k))
var padding=10
var width=+myRect.getAttribute("width")-padding
var x=+myRect.getAttribute("x")
var y=+myRect.getAttribute("y")
var fontSize=+myText.getAttribute("font-size")
var text=myTextValue.value
var words = text.split(' ');
var text_element = document.getElementById('myText');
var tspan_element = document.createElementNS(NS, "tspan"); // Create first tspan element
var text_node = document.createTextNode(words[0]); // Create text in tspan element
tspan_element.setAttribute("x", x+padding);
tspan_element.setAttribute("y", y+padding+fontSize);
tspan_element.appendChild(text_node); // Add tspan element to DOM
text_element.appendChild(tspan_element); // Add text to tspan element
//---[EDIT] a single word that exceeds preset rect with---
if(words.length==1)
{
var len = tspan_element.getComputedTextLength()
if(len>+myRect.getAttribute("width"))
myRect.setAttribute("width", len+2*padding)
}
//---end [EDIT]------------------
for(var i=1; i<words.length; i++)
{
var len = tspan_element.firstChild.data.length // Find number of letters in string
tspan_element.firstChild.data += " " + words[i]; // Add next word
if (tspan_element.getComputedTextLength() > width-padding)
{
tspan_element.firstChild.data = tspan_element.firstChild.data.slice(0, len); // Remove added word
var tspan_element = document.createElementNS(NS, "tspan"); // Create new tspan element
tspan_element.setAttribute("x", x+padding);
tspan_element.setAttribute("dy", fontSize);
text_node = document.createTextNode(words[i]);
tspan_element.appendChild(text_node);
text_element.appendChild(tspan_element);
}
}
var height = text_element.getBBox().height +2*padding; //-- get height plus padding
myRect.setAttribute('height', height); //-- change rect height
//---show svg source---
sourceValue.value=svgDiv.innerHTML
}
</script>
</body>
</html>

Sketch Plugin Development: Export Symbol as SVG Text

I have a working example of exporting a selected symbol to an SVG string in Sketch 3. (Based on this code from Sketch's GitHub)
The issue is the output is distorted and I'm unable to see the next logical step in troubleshooting.
The Code
// Get all symbols
var symbols = context.document.documentData().allSymbols();
// For purpose of this example, only get the first one for testing.
var symbol = symbols[0];
// Set a temporary file to save to. Necessary for generating the SVG
var tempPath = '/tmp/com.symbolui.sketch-commands/';
var guid = [[NSProcessInfo processInfo] globallyUniqueString];
var path = tempPath + guid;
[[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:true attributes:nil error:nil]
var export_path = path;
var export_filename = export_path + '/' + 'test.svg';
// do export
[doc saveArtboardOrSlice:frame toFile:export_filename];
var file_url = [NSURL fileURLWithPath:export_filename];
var str = [[NSString alloc] initWithContentsOfURL:file_url];
At the final point in the code, str is a text value of the SVG generated from the symbol.
The Output
Input Symbol:
Generated preview of SVG:
Generated SVG text:
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200px" height="65px" viewBox="0 0 1200 65" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
<title></title>
<desc>Created with Sketch.</desc>
<defs>
<rect id="path-1" x="0" y="0" width="49" height="20" rx="3"></rect>
<mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="49" height="20" fill="white">
<use xlink:href="#path-1"></use>
</mask>
</defs>
<g id="Symbols:-Labels" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Buttons" transform="translate(-422.000000, 31.000000)"></g>
<g id="Label-/-Default" transform="translate(670.000000, 60.000000)">
<use id="background" stroke="#979797" mask="url(#mask-2)" stroke-width="2" fill="#777777" xlink:href="#path-1"></use>
<text id="-Default" font-family="HelveticaNeue-Bold, Helvetica Neue" font-size="10" font-weight="bold" fill="#FFFFFF">
<tspan x="7" y="14">Default</tspan>
</text>
</g>
</g>
Thoughts
As you can see the output is quite large in addition to being distorted. At the very least I believe I need to trim or rescale the symbol somehow. Where the distortion is coming through I have no idea.
Fixing the SVG markup itself isn't a solution - I'd like to see the solution in the Sketch plugin code. I'm finding documentation for internal Sketch code very difficult to work with and has been the big blocker for resolving this.
you can convert the MSSymbolInstance to normal group, then export it.
if (layer.symbolMaster().children().count() > 1) {
var tempSymbol = layer.duplicate(),
tempGroup = tempSymbol.detachByReplacingWithGroup();
tempGroup.resizeToFitChildrenWithOption(0);
layer.setIsVisible(false);
tempGroup.exportOptions().removeAllExportFormats();
var format = tempGroup.exportOptions().addExportFormat();
format.setFileFormat("SVG");
var slice = MSExportRequest.exportRequestsFromExportableLayer(tempGroup).firstObject();
var filePath = export_path + '/test.svg';
doc.saveArtboardOrSlice_toFile(slice, filePath);
}

Maintain orientation of some elements in dynamic rotation?

I'm animating rotations of groups of SVG elements using d3.js. However, I want to preserve the orientation of some elements. For example, in this fiddle (code below), the blue dot is the center of the blue circle. The blue dot is displaced vertically from the black dot, which rotates around the yellow center. I want to maintain the vertical relationship between these two dots.
In the fiddle, I maintain the vertical orientation by rotating the "shift" <g> group backwards the same amount that its enclosing group is rotating forwards. This is the same method given in cmonkey's answer here. That works, but I'm wondering whether there are other methods. Is there any way to preserve orientation without an inverse rotation?
Why?
The inverse rotation strategy means that one has to carefully keep the inverse rotations in sync with changes to rotations of outer groups. In full-fledged versions of this code, I use rotations within rotations (within rotations), as in this example. That means summing up all of the outer groups' rotations in order to determine what the inverse rotation should be. I also want to add text labels to SVG elements. Since different text labels will fall within different numbers of rotation groups, each text label will need its own customized rotation if I want to keep the text upright.
Feel free to suggest more D3. I only hand-coded the SVG in these versions in order to get clarity about how I would dynamically generate the SVG with D3 in a later version.
<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v3.min.js"></script>
<style>
.cycle {
stroke : #000000;
fill : none;
}
.movingPointOnCycle {
stroke : #000000;
fill : #000000;
}
#pointB {
stroke : #0000FF;
fill : #0000FF;
}
#epicycle2 {
stroke : #0000FF;
}
.centerOfUniverse {
stroke : #000000;
fill : #000000;
}
.sun {
stroke : #000000;
fill : #F0F000;
}
.mars {
stroke : #000000;
fill : #A00000;
}
.earth {
stroke : #000000;
fill : #00A000;
}
</style>
</head>
<body>
<svg width="400" height="400">
<g transform="translate(200,200) scale(1.3)">
<circle class="sun" r="10"></circle>
<g class="cycle"speed="0.01">
<circle id="deferent" class="cycle" r="60"></circle>
<g class="epicycleCenter" transform="translate(60,0)">
<circle id="pointD" class="movingPointOnCycle" r="2"></circle>
<g class="shift" speed="-0.01" displacement="-25">
<circle id="pointB" class="movingPointOnCycle" r="2"></circle>
<line x1="0" y1="0" x2="0" y2="25" stroke-dasharray="1,2"></line>
<g class="cycle"speed="0.01">
<circle id="epicycle2" class="cycle" r="75"></circle>
</g>
</g>
</g>
</g>
</g>
</svg>
<script type="text/javascript">
var t0 = Date.now();
var svg = d3.select("svg");
d3.timer(function() {
var delta = (Date.now() - t0);
svg.selectAll(".cycle").attr("transform", function(d) {
return "rotate(" + delta * d3.select(this).attr("speed") + ")";
});
svg.selectAll(".shift").attr("transform", function(d) {
return "rotate(" + delta * d3.select(this).attr("speed") + ")"
+
"translate(0," + d3.select(this).attr("displacement") + ")"
;
});
});
</script>
</body>
</html>

Pure SVG way to fit text to a box

Box size known. Text string length unknown. Fit text to box without ruining its aspect ratio.
After an evening of googling and reading the SVG spec, I'm pretty sure this isn't possible without JavaScript. The closest I could get was using the textLength and lengthAdjust text attributes, but that stretches the text along one axis only.
<svg width="436" height="180"
style="border:solid 6px"
xmlns="http://www.w3.org/2000/svg">
<text y="50%" textLength="436" lengthAdjust="spacingAndGlyphs">UGLY TEXT</text>
</svg>
I am aware of SVG Scaling Text to fit container and fitting text into the box
I didn't find a way to do it directly without Javascript, but I found a JS quite easy solution, without for loops and without modify the font-size and fits well in all dimensions, that is, the text grows until the limit of the shortest side.
Basically, I use the transform property, calculating the right proportion between the desired size and the current one.
This is the code:
<?xml version="1.0" encoding="UTF-8" ?>
<svg version="1.2" viewBox="0 0 1000 1000" width="1000" height="1000" xmlns="http://www.w3.org/2000/svg" >
<text id="t1" y="50" >MY UGLY TEXT</text>
<script type="application/ecmascript">
var width=500, height=500;
var textNode = document.getElementById("t1");
var bb = textNode.getBBox();
var widthTransform = width / bb.width;
var heightTransform = height / bb.height;
var value = widthTransform < heightTransform ? widthTransform : heightTransform;
textNode.setAttribute("transform", "matrix("+value+", 0, 0, "+value+", 0,0)");
</script>
</svg>
In the previous example the text grows until the width == 500, but if I use a box size of width = 500 and height = 30, then the text grows until height == 30.
first of all: just saw that the answer doesn't precisely address your need - it might still be an option, so here we go:
you are rightly observing that svg doesn't support word-wrapping directly. however, you might benefit from foreignObject elements serving as a wrapper for xhtml fragments where word-wrapping is available.
have a look at this self-contained demo (available online):
<?xml version="1.0" encoding="utf-8"?>
<!-- SO: http://stackoverflow.com/questions/15430189/pure-svg-way-to-fit-text-to-a-box -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
width="20cm" height="20cm"
viewBox="0 0 500 500"
preserveAspectRatio="xMinYMin"
style="background-color:white; border: solid 1px black;"
>
<title>simulated wrapping in svg</title>
<desc>A foreignObject container</desc>
<!-- Text-Elemente -->
<foreignObject
x="100" y="100" width="200" height="150"
transform="translate(0,0)"
>
<xhtml:div style="display: table; height: 150px; overflow: hidden;">
<xhtml:div style="display: table-cell; vertical-align: middle;">
<xhtml:div style="color:black; text-align:center;">Demo test that is supposed to be word-wrapped somewhere along the line to show that it is indeed possible to simulate ordinary text containers in svg.</xhtml:div>
</xhtml:div>
</xhtml:div>
</foreignObject>
<rect x="100" y="100" width="200" height="150" fill="transparent" stroke="red" stroke-width="3"/>
</svg>
I've developed #Roberto answer, but instead of transforming (scaling) the textNode, we simply:
give it font-size of 1em to begin with
calculate the scale based on getBBox
set the font-size to that scale
(You can also use 1px etc.)
Here's the React HOC that does this:
import React from 'react';
import TextBox from './TextBox';
const AutoFitTextBox = TextBoxComponent =>
class extends React.Component {
constructor(props) {
super(props);
this.svgTextNode = React.createRef();
this.state = { scale: 1 };
}
componentDidMount() {
const { width, height } = this.props;
const textBBox = this.getTextBBox();
const widthScale = width / textBBox.width;
const heightScale = height / textBBox.height;
const scale = Math.min(widthScale, heightScale);
this.setState({ scale });
}
getTextBBox() {
const svgTextNode = this.svgTextNode.current;
return svgTextNode.getBBox();
}
render() {
const { scale } = this.state;
return (
<TextBoxComponent
forwardRef={this.svgTextNode}
fontSize={`${scale}em`}
{...this.props}
/>
);
}
};
export default AutoFitTextBox(TextBox);
This is still an issue in 2022. There is no way to define bounds and get text to scale in a pure scalable vector graphic. Adjusting the font size manually is still the only solution it seems, and the examples given are quite buggy. Has anybody figured out a clean solution that works? Judging by the svg spec it looks like a pure solution doesn't exist.
And to provide some sort of answer myself, this resource is the best I've found, is hacky, but works much more robustly: fitrsvgtext - storybook | fitrsvgtext - GitHub
I don't think its the solution for what you want to do but you can use textLength
with percentage ="100%" for full width.
<svg width="436" height="180"
style="border:solid 6px"
xmlns="http://www.w3.org/2000/svg">
<text x="0%" y="50%" textLength="100%">blabla</text>
</svg>
you can also add text-anchor="middle" and change the x position to center perfectly your text
this will not change the fontsize and you will have weird space letterspacing...
JSFIDDLE DEMO

Resources