Closed acardona closed 5 years ago
Yes, this has been considered. The reason it's not there currently is that we didn't want to maintain a separate SVG renderer that only gets used for export.
We initially considered using a library for abstracting rendering so it could be output to canvas, WebGL, SVG, etc. However, those don't give us enough control for the kind of interactivity and performance we want.
This could be done as an extension with an external library dependency if there is a library that creates a canvas 2D context API that renders to SVG properly.
If you're interested in experimenting right now, you could try something like SVGCanvas with the undocumented/private cy.renderer().renderTo( cxt, zoom, pan, pxRatio )
function.
I'm trying to use SVGCanvas and the renderTo function. I'm running into an issue where cytoscape.js calls
context.translate(centerX, centerY);
and SVGKit implements:
SVGKit.prototype.translate = function(elem, tx, ty) {
/***
SVGKit.prototype.:
translate(' translate( 1 ,2 ) ', -10,-20)
translate(' translate(1) ', -10,-20)
translate(' translate(10,20) ', 0, -20)
translate('translate(10,10) rotate(20)', 10, 10) == 'translate(10,10) rotate(20)translate(10,10)'
translate('translate(10,10)', -10, -10) == ''
translate('translate(10)', -10) == ''
***/
var element = MochiKit.DOM.getElement(elem);
if (MochiKit.Base.isUndefinedOrNull(element)) {
return this._twoParameter(elem, tx, ty,
SVGKit.translateRE, 'translate')
}
var old_transform = element.getAttribute('transform')
var new_transform = this._twoParameter(old_transform, tx, ty,
SVGKit.translateRE,'translate');
element.setAttribute('transform', new_transform);
return new_transform;
}
where elem becomes centerX and tx becomes centerY. What would be the proper modification on the SVGKit side to properly pass the context?
Also, SVGKit does not implement setTransform, but has its own currentTransformationMatrix and transformations attribute. I'm trying to understand SVGKit's transformation handling better.
Maybe a different SVG library would be worth exploring?
If I recall correctly, SVG expects transforms to be on groups (<g>
), so elem
is probably expected to be a <g>
element. Because transforms in canvas stack, you could try embedding a group after each new set of transforms. It sounds like SVGKit/SVGCanvas isn't very complete then...
Do you know another SVG library that could do the trick?
It was my mistake that I instantiated an SVGKit instead of an SVGCanvas. The translate/scale transformations worked then fine. However, the library does not implement e.g. the fillText method to draw text . Adding a dummy fillText method to SVGCanvas let me create an SVG with the correct transformation, but other attributes (e.g. stroke width of cytoscape nodes) were not rendered correctly out of the box.
I don't know of any libraries off the top of my head, so it would probably require more research.
Another alternative in the short term is to use a high-res PNG. Maybe we could add/document some options for cy.png()
so you can specify the dimensions or pixel ratio.
Indeed, options to specify dimensions with cy.png()
would come in handy. It would be nice if you could add that. And thanks for this great library!
Hi all, just to comment that I managed to successfully export with SVGCanvas, with proper transformations and text. It required manglying two API mismatches:
SVGCanvas.prototype.transform = SVGCanvas.prototype.translate;
SVGCanvas.prototype.fillText = SVGCanvas.prototype.text;
... and filtering the resulting SVG DOM to fix up small issues, for example:
These were easy to fix, fortunately, with a bit of ad-hoc post-processing.
The code is here: https://github.com/acardona/CATMAID/commit/13d5a30b72de83bb773f265c60f64c409d529c4c
Hope it helps others out there.
Best,
Albert
@acardona You may want to package your code as a reusable SVG extension
@unidesigner I've added a ticket for specifying zoom in cy.png()
: https://github.com/cytoscape/cytoscape.js/issues/659
Hi @maxkfranz , my workaround stopped working. Now, all paths rendered are like arrowhead paths--with the properties still correct as if they were the expected edges, arrowheads and node circles.
Any ideas what could be going wrong?
We use cytoscapejs 2.2.8.
I think that with the new performance enhancements and new features, we use APIs that are not supported in those canvas-to-svg libs.
Probably, the most stable and least invasive way to export to SVG would be to add a SVG option to the canvas renderer. This would involve adding a SVG option to the canvas renderer and adding SVG generating code to each of the low-level draw functions:
(1) node (2) edge (3) edge arrows
Then the renderer would be renamed to something other than "canvas". Because the canvas renderer handles interaction on a low level, interaction with the SVG graphs would be free and probably much more performant than adding listeners on SVG elements. The main tricky part is reusing SVG elements for performance.
I don't have time to look at this now, but I could fork the unstable branch if someone is interested in trying to get SVG output from the renderer. And I would be able to help out a bit if needed.
Extending the canvas renderer would be nice indeed. If time permits I might have a look at it.
The linked commit was in response to @acardona, because I fixed the workaround to work again. Like described in the commit message, it was Path2D which suddenly became a problem: The Chrome browser made it available by default. Cytoscape uses it for path caching as it seems, but SVGKit (which we use) can't deal with it. So I monkey-patched Cytoscape for the export to not use Path2D for the export and it works.
I noticed that in the recent versions, an option for JPG export has been added in. Is there any hope that an SVG option will also get implemented soon considering this issue was created several months ago?
Unfortunately, it would be a very large undertaking and I'm not sure whether it will fit into the next release -- given the extensive changes needed in the renderer. It also would double maintenance costs of the renderer for little pragmatic benefit over high-res PNGs.
You could use @tomka's method and indeed his change to disable Path2D
for export could make it into a minor, patch release to make things more straightforward.
If you're interested in native support in the renderer sooner, I can review a PR that covers the changes I've mentioned earlier.
Is it possible to have an example code that uses the SVGcanvas approach as developed by acardona and tomka? I am a rookie in javascript and had difficulty in implementing the svg export functionality. Thanks a lot in advance.
Hi @miskar, the commit linked right before my last comment in this issue contains the changes needed to make it work (at least for us). You basically need to do the following:
// Create a new SVGKit SVG canvas of desired size, it will be used to render from Cytoscape.
var svg = new SVGCanvas(width, height);
// Cytoscape uses Path2D if it is available. Unfortunately, SVGKit isn't able
// to make use of this as well and silently fails to draw paths. We therefore
// have to monkey-patch Cytoscape to not use Path2D by overriding its test.
// We reset to the original function after the graph has been rendered.
var CanvasRenderer = cytoscape('renderer', 'canvas');
var orignalUsePaths = CanvasRenderer.usePaths;
CanvasRenderer.usePaths = function() { return false; };
// Assuming your cytoscape instance is available as 'cy', render it to the SVG canvas
// created earlier.
cy.renderer().renderTo( svg, 1.0, {x: 0, y: 0}, 1.0 );
// Reset Path2D test of Cytoscape
CanvasRenderer.usePaths = orignalUsePaths;
I've tried the solution from @tomka , but ran into an error with setTransform (which isn't implemented in SVGKit as mentioned by @unidesigner). How did you get around this?
I've tried SVGCanvas
and it works only in very, very simple cases. It's not a reliable solution...
I'm also getting some error's when trying to convert canvas to svg with canvas2svg.js
canvas2svg.js:515 Error: Invalid value for <path> attribute d="L 54.1865707939973 -189.17190648836421 L 60.196982187342144 -196.81961561481185 L 50.47256805017713 -197.03931925361914"
So i added this piece of code to make sure a path always starts with an "M". on line 515 of canvas2svg.js
d=d.replace(/^L/, 'M');
i'm using Canvas 2 Svg v1.0.6
When I use canvs2svg I do not get errors. The first tests however give me a svg that is filled with PNG objects for each node (our nodes have SVG backgrounds). Is cytoscapeJS rendering background SVG as PNG? In that case there is no use for exporting to SVG, because it will always contain bitmaps.
This is my code, which does not yet deliver a good SVG anyway.
var ctx = new C2S(1000,1000);
cy.renderer().renderTo( ctx);
var mySerializedSVG = ctx.getSerializedSvg();
Yeah we noticed the same. Cytoscape caches previously rendered elements as images. We had some success with overriding the caches during SVG export so that cache look-ups would always fail, which in turn causes the actual drawing routines to be called. However, this also didn't allow previous hack to work completely due to some problems with SVGCanvas.
Since we only need to export relatively simple nodes and edges and their labels, we wrote our own SVG exporter in the end:
On the calling side, all Cytoscape nodes and edges are walked and rendered one by one. Luckily Cytoscape caches some rendering information, so we barely need to do any math ourselves (for now):
Depending on how complex your graph is and the features you use, a similar approach might work for you.
https://github.com/nrnb/GoogleSummerOfCode/issues/61
This project will use a similar approach with new calculated rendered values in 3.1:
https://github.com/cytoscape/cytoscape.js/issues/1552
https://github.com/cytoscape/cytoscape.js/issues/1551
The GSOC project (if accepted) will give a good initial version of a SVG export extension. It will probably support all rendering features in 3.1. For core versions beyond that, the extension will probably require community PRs for new rendering features added to the core.
Is there any update on svg export? My apology if it is already supported through an extension.
As far as I know there is no update.
There are two roads that people seem to follow:
I tried the first route, because the second one is like building a new graph library for svg, including styling etc. I found out the cytoscape uses a lot of bitmap caching for speed, and as you can read above, people have tried to hack the renderer to forget the caches. What I did was just create a new cytoscape instance, fill this with the same elements - cy2.json(cy1.json()); - and render this to the pseudocanvas.
This kind of worked, but not enough. The real problem in our project was, that we use SVG background images for nodes. These are rendered by CytoscapeJs using canvas.drawImage, with as argument an . At this point, any SVG paths are already turned into bitmaps, so the final SVG will contain a bitmapped image in the right dimensions for the current zoom. Not scalable at all...
So I am ready to give up on this. The alternative might be finding a good way to export a PNG in a given resolution.
The only thing I can think of right now (using route 1) is make the cytoscape renderer render svg images not through an image tag, but by converting the SVG into canvas. Maybe through Path2D (https://developer.mozilla.org/en-US/docs/Web/API/Path2D/Path2D) or some library?
This library seems to have this purpose: https://code.google.com/archive/p/canvas-svg/source/default/source.
@jelmerjellema I try your way to use your code
var ctx = new C2S(1000,1000);
cy.renderer().renderTo( ctx);
var mySerializedSVG = ctx.getSerializedSvg();
it has no error happend, but it just output and svg with base64 encode image
if I use ctx.getSvg();
it has an error say Uncaught Error: Attempted to apply path command to node g
I just want to know if I can output an svg with nodes
Trying to use any of the automatic canvas-to-svg conversion libs isn't going to work. It was only semi-working when cyjs just directly drew everything with the canvas fill/stroke apis. Now cyjs is more advanced. In future, cyjs will use webgl in places.
At this point, using a canvas-to-svg is unrealistic and hacky.
The only way to have this working is to build a svg exporter extension that builds up svg objects based on the stylesheet and the other computed style values (like bezier points).
Please keep further discussion in this thread on the topic of an exporter extension for svg.
@maxkfranz I just downloaded cytoscape and it seems like this is a feature on the current release. Might be suitable to close this issue. Thanks for providing this feature!
@zfrenchee I don't see any mention of this in the release notes or other documentation of cytoscape.js. Are you maybe confusing cytoscape.js and cytoscape? The former is a JavaScript library while the latter is a standalone desktop app. See also http://manual.cytoscape.org/en/stable/Cytoscape.js_and_Cytoscape.html
My mistake @VGSebastian!
Hi, I just arrived to the conversation, but I would also in investigate using dagre graphlib / dot file format .(https://github.com/dagrejs) file format. Since the library already renders to dagre, would not be possible to extract the .dot file or graphlib representation ? . If so then it could be translated to a .doc file or to a .good quality svg.
Dagre itself won't render but this project http://viz-js.com/, (emsripten port of graphbi) will render graphlib/.dot to svg
Again, I'm new on these libraries and don't know the status of the project although the demo looks nice, but it could help people that just need to transform graphs offline. Regarding this, my advice would be not to invest trying to transform canvas/bitmaps to svg..My two cents. will try to investigate more, thanks
Write an extension that builds up svg objects based on the computed style of each of the elements and returns a root svg element.
Using an automatic canvas-to-svg library will not work.
Original content follows:
Would be wonderful to be able to export to SVG, completing this fantastic library that is cytoscapejs. Is an export to SVG perhaps in the roadmap?