cytoscape / cytoscape.js

Graph theory (network) library for visualisation and analysis
https://js.cytoscape.org
MIT License
10.09k stars 1.64k forks source link

Export to SVG extension #639

Closed acardona closed 5 years ago

acardona commented 10 years ago

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?

maxkfranz commented 10 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.

unidesigner commented 10 years ago

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.

maxkfranz commented 10 years ago

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...

unidesigner commented 10 years ago

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.

maxkfranz commented 10 years ago

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.

unidesigner commented 10 years ago

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!

acardona commented 10 years ago

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:

  1. The text of a node is rendered not centered in SVGCanvas. Had to add text-anchor: middle.
  2. Nodes are rendered with the wrong M starting point of the path, almost 0.00 ... that creates an artificial line to 0,0. And with two paths rather than one: one for the contour and one for the fill.
  3. 'triangle' arrowheads have a supernumerary point that makes the 4-point polygon fold over itself, which, in combination with a stroke of 'none', makes them invisible: no area.

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

maxkfranz commented 10 years ago

@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

acardona commented 10 years ago

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.

maxkfranz commented 10 years ago

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.

tomka commented 10 years ago

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.

DSin52 commented 9 years ago

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?

maxkfranz commented 9 years 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.

miskar commented 9 years ago

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.

tomka commented 9 years ago

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;
sepro commented 8 years ago

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?

maxkfranz commented 8 years ago

I've tried SVGCanvas and it works only in very, very simple cases. It's not a reliable solution...

eelco2k commented 8 years ago

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

jelmerjellema commented 7 years ago

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();
tomka commented 7 years ago

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:

https://github.com/catmaid/CATMAID/blob/dev/django/applications/catmaid/static/libs/catmaid/svg-factory.js

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):

https://github.com/catmaid/CATMAID/blob/dev/django/applications/catmaid/static/js/widgets/compartment_graph_widget.js#L2364

Depending on how complex your graph is and the features you use, a similar approach might work for you.

maxkfranz commented 7 years ago

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.

tfjmp commented 7 years ago

Is there any update on svg export? My apology if it is already supported through an extension.

jelmerjellema commented 7 years ago

As far as I know there is no update.

There are two roads that people seem to follow:

  1. Use a pseudocanvas library like Canvas2Svg (the one I tried) and make the native cytoscape.js code render to this canvas. (using cy.renderer().renderTo(...))
  2. Create special code to directly create a svg from the information in cytoscape.

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.

gexiaowei commented 7 years ago

@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

maxkfranz commented 7 years ago

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.

alexlenail commented 6 years ago

@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!

VGSebastian commented 6 years ago

@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

alexlenail commented 6 years ago

My mistake @VGSebastian!

kinimesi commented 5 years ago

I have created an extension that exports current graph as an SVG image. It works for most of the simple graphs. Here is a simple demo.

It uses canvas2svg library. I know it is hacky, but until we have something better, we can make use of it.

cancerberoSgx commented 5 years ago

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