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';
});
It is necessary that the text fills the svg as much as possible as with bacground-size: contain. The svg parent can be resized dynamically. The text is always single line. Now such a solution is used using js, but it is desirable to do it using the svg itself.
const $svg = document.querySelector(`svg`);
const $text = $svg.querySelector('text');
const $input = document.querySelector('input');
$input.addEventListener('input', e => {
$text.textContent = e.target.value;
const box = $text.getBBox();
$svg.setAttribute('viewBox', `${box.x} ${box.y} ${box.width} ${box.height}`);
});
$input.dispatchEvent(new Event('input'))
svg {
border: 1px solid red;
height: 10em;
width: 100%;
}
<svg preserveAspectRatio="xMaxYMid meet">
<text alignment-baseline="baseline" dominant-baseline="middle" text-anchor="middle" fill="black"></text>
</svg>
<input type='text' value='Random single line text'>
Can you not just use a parent div? Add a no wrap attribute (word-break or flex-wrap for a flex container) to the text and that will stretch the div to full text width. Then make the svg width: 100% within the div.
I'm trying to make 2 html objects in SVGs and further use them inside Vis.js graphs. My first svg (Button) works as intended and looks good. My problem is that when I try to insert the table div the width/height are not what I have set them to be.
Here's what I get:
As you can see the button is larger than the red box even though the red box has a larger width and height (1000px x 800px versus 220px x 68px)!
Here's my JavaScript:
// THE RED BOX
const tableComponent =
`<svg xmlns="http://www.w3.org/2000/svg" width="1000px" height="800px">
<foreignObject x="0" y="0" width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml" style="height: 100%; width: 100%; background-color: red;">
<p>Here is a paragraph that requires word wrap</p>
</div>
</foreignObject>
</svg>`;
// THE ORANGE BUTTON
const buttonComponent =
`<svg xmlns="http://www.w3.org/2000/svg" width="220px" height="68px">
<foreignObject x="0" y="0" width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml" style="height: 100%; width: 100%;">
<button style="width: 100%; height: 100%; font-size: 20px; color: white; background-color: #FF9700; border-radius: 8px; border: none;">Order Filling</button>
</div>
</foreignObject>
</svg>`;
const tableComponentUrl = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(tableComponent);
const buttonComponentUrl = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(buttonComponent);
const nodes = new DataSet([
{ id: 1, image: buttonComponentUrl, shape: 'image' },
{ id: 2, image: tableComponentUrl, shape: 'image' },
{ id: 3, label: 'Node 3', shape: 'box' }
]);
// create an array with edges
const edges = new DataSet([
{ from: 1, to: 2, arrows: 'to' },
{ from: 1, to: 3, arrows: 'to' }
]);
// create a network
const container = document.querySelector('.data-source-container');
const data = {
nodes: nodes,
edges: edges
};
const options = {
edges: { smooth: false }
};
const network = new Network(container, data, options);
Well, SVG is scalable vector graphics, the inner sizes has nothing to do with the sizes a user sees (more accurately: x dimensions of objects that user sees are x_orig*scale where x_orig is the x dimension inside SVG and scale is a factor set by the x dimension of the whole SVG element).
In other words (assuming the other parts work and if they do, you have invented an interesting hack to extend vis.js' possibilities of inserting html) you have to set the dimensions of your pictures. Try to use size, scale or shapeProperties.useImageSize in corresponding nodes' options. I can't see an option to adjust aspect ratio, though, so this may require additional tweaking inside SVG itself (like setting its dimensions even).
Let me know how it goes, that's quite an interesting approach that you have.
I need to output the result of a fabric canvas drawing to an SVG file.
I'm using fabric.js version 1.7.6 and when I have a path drawn to a canvas with an rgba fill like rgba(255,0,0,.15) the resulting SVG has a fill of rgb(0,0,0). Is there some setting I need to enable to make it output the alpha chanel?
In my sample code the purple circle converts to SVG properly, but the rectangle just shows up as black.
Sample HTML:
<html>
<head>
<script src="fabric.js"></script>
</head>
<body>
<div id="canvasHolder" style="border: 3px solid black;">
<canvas id="canvasElement" width="400" height="400" />
</div>
<div id="svgHolder" style="border: 3px solid blue;">
</div>
</body>
<script>
var canvas = new fabric.Canvas('canvasElement');
var rect = new fabric.Path('M,0,0,h,100,v,100,h,-100,z',{
top:100,
left:100,
stroke: 'green',
fill: 'rgba(255,0,0,.15)'
});
canvas.add(rect);
var circ = new fabric.Circle({
radius: 30,
top:30,
left:30,
stroke: 'blue',
fill: 'purple'
});
canvas.add(circ);
canvas.renderAll();
// Make an SVG object out of the fabric canvas
var SVG = canvas.toSVG();
document.getElementById('svgHolder').innerHTML = SVG;
</script>
</html>
Output SVG:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="400" height="400" viewBox="0 0 400 400" xml:space="preserve">
<desc>Created with Fabric.js 1.7.6</desc>
<defs>
</defs>
<path d="M 0 0 h 100 v 100 h -100 z" style="stroke: rgb(0,128,0); stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform="translate(150.5 150.5) translate(-50, -50) " stroke-linecap="round"></path>
</svg>
As I said in comment, this looks like a bug, that you should report on the project's issue tracker.
Colors are all converted to rgb() (rgba, hsl, hsla, hex, keywords) and thus don't support alpha channel...
For the time being, here is an heavy workaround :
toSVG accepts an reviver function, which will receive all the svg nodes markups. From there, you can reapply the correct styles, but not so easily.
The only parameter I could find allowing us to identify which object corresponds to the svg markup we get, is the id one.
So first, we will construct a dictionary, which will store our colors, by id.
Then, we will assign the id and colors to our fabric's objects.
Finally, in the reviver, we will parse our markup to convert it to an svg node, check its id atribute, and then change its style.fill and style.stroke properties, before returning the serialization of this modified node.
var canvas = new fabric.Canvas('canvasElement');
var colors_dict = {};
// an helper function to generate and store our colors objects
function getColorId(fill, stroke) {
var id = 'c_' + Math.random() * 10e16;
return colors_dict[id] = {
id: id,
fill: fill || 'black',
stroke: stroke || 'black' // weirdly fabric doesn't support 'none'
}
}
// first ask for the color object of the rectangle
var rect_color = getColorId('hsla(120, 50%, 50%, .5)', 'rgba(0,0,255, .25)');
var rect = new fabric.Path('M,0,0,h,100,v,100,h,-100,z', {
top: 60,
left: 60,
stroke: rect_color.stroke, // set the required stroke
fill: rect_color.fill, // fill
id: rect_color.id // and most importantly, the id
});
canvas.add(rect);
var circ_color = getColorId('rgba(200, 0,200, .7)');
var circ = new fabric.Circle({
radius: 30,
top: 30,
left: 30,
stroke: circ_color.stroke,
fill: circ_color.fill,
id: circ_color.id
});
canvas.add(circ);
canvas.renderAll();
var parser = new DOMParser();
var serializer = new XMLSerializer();
function reviveColors(svg){
// first we parse the markup we get, and extract the node we're interested in
var svg_doc = parser.parseFromString('<svg xmlns="http://www.w3.org/2000/svg">' + svg + '</svg>', 'image/svg+xml');
var svg_node = svg_doc.documentElement.firstElementChild;
var id = svg_node.getAttribute('id');
if (id && id in colors_dict) { // is this one of the colored nodes
var col = colors_dict[id]; // get back our color object
svg_node.style.fill = col.fill; // reapply the correct styles
svg_node.style.stroke = col.stroke;
// return the new markup
return serializer.serializeToString(svg_node).replace('xmlns="http://www.w3.org/2000/svg', '');
}
return svg;
}
// Make an SVG object out of the fabric canvas
var SVG = canvas.toSVG(null, reviveColors);
document.getElementById('svgHolder').innerHTML = SVG;
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.12/fabric.js"></script>
<div id="canvasHolder" style="border: 3px solid black;">
<!-- beware canvas tag can't be self-closing -->
<canvas id="canvasElement" width="400" height="200"></canvas>
</div>
<div id="svgHolder" style="border: 3px solid blue;">
The colors are converted to RGB because svg specs wants color in CSS2 format and so rgba is unsupported.
cit: https://www.w3.org/TR/2008/REC-CSS2-20080411/syndata.html#value-def-color
Fabric fulfill the transparency with the fill-opacity rull. The point is that fabric.Color color parser looks like is choking over the .15 notation for the alpha channel.
Please use 0.15 and it will work.
i agree that fabric.Color could be fixed for this.
I'm trying to add padding around SVG figures that are drawn via Raphael, and am seeing the following from wkhtmltopdf. Has anyone else dealt with this issue? Is padding supported on SVG elements?
Of course you can add padding and other styling rules to your Raphael-created SVG elements. It's just another element in the DOM -- there's no need for a workaround. #BigBadaboom's assertion is only true for elements inside the SVG element, not the SVG element itself.
<style type="text/css">
svg.padding-please
{
padding: 20px;
background-color: rgb(128,128,128);
border: 1px solid black;
margin: 20px;
}
</style>
And when you create your Raphael paper objects:
var paper = Raphael( 'container', width, height );
jQuery( paper.canvas ).attr( "class", "padding-please" ); // `canvas` gives us direct access to the DOM node
Alternative Approach
You could always whip up a simple Raphael extension to add a specified amount of margin to a given SVG by manipulating the viewbox. For instance:
Raphael.fn.addMargin = function addMargin( ratio )
{
var contentBox = { x: 100000, y: 100000, width: 0, height: 0 };
this.forEach( function( element )
{
var elemBox = element.getBBox();
contentBox.x = Math.min( contentBox.x, elemBox.x );
contentBox.y = Math.min( contentBox.y, elemBox.y );
contentBox.width = Math.max( contentBox.width, elemBox.width );
contentBox.height = Math.max( contentBox.height, elemBox.height );
}, this );
if ( contentBox.x == 100000 ) // No elements? Whatevs
return;
var xMargin = contentBox.width * ratio;
var yMargin = contentBox.height * ratio;
this.setViewBox( contentBox.x - xMargin, contentBox.y - yMargin, contentBox.width + xMargin * 2, contentBox.height + xMargin * 2 );
}
Then if you wanted to add a 10% margin to a given SVG, you would just call
paper.addMargin( 0.1 );
There are so many ways to skin cats, it's amazing Felis catus isn't extinct =)