cytoscape / cytoscape.js

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

SVG node background tutorial #1802

Closed jri closed 5 years ago

jri commented 7 years ago

I have the demand for individual top/right/bottom/left padding when styling a node. At the moment only padding is supported which affects all 4 sides at once.

My application needs to decorate each node with an icon (additionally to the node label). To make room for the icon I need extra padding just on one side.

maxkfranz commented 7 years ago

As it stands, it's not possible to have separate paddings and it will probably not be added. We tried it at one point, and it conflicts with several important features.

For your usecase, it doesn't sound like you really need separate paddings. Have you tried the background positioning properties and label margin properties?

jri commented 7 years ago

I want to achieve this node rendering:

node-with-icon

Yes, I can use text-margin-x to move the label to the left. But still, the horizontal padding needs to be larger than the vertical padding.

What approach would you generally suggest to customize individual node rendering beyond style properties? I mean node renderings and interactions programmatically created by the user? Like a customizable per-node render function. I guess the DOM would be more appropriate here but is in contrast to Cytoscape's canvas based approach.

I'm sure you (and others) already have considered a hybrid approach: using the canvas for edge rendering and superimpose it with a DOM layer for the node rendering. (I did that in my former application and it works very well, but it lacks parallel edges, and self-edges.) Are you aware of a Cytoscape plugin that goes for this hybrid approach?

I've evaluated several graph libraries, and Cytoscape seems to be the only one which supports both out-of-the-box, interactive parallel edges (multigraphs), and interactive self-edges. To me Cytoscape is very strong on edge rendering, but falls back when it comes to node rendering flexibility.

Would Cytoscape's render event be the basis for customizing the node rendering?

I like Cytoscape's clear concepts and API, mature implementation, and excellent documentation very much. Thank you for your effort!

maxkfranz commented 7 years ago

I'm sure you (and others) already have considered a hybrid approach: using the canvas for edge rendering and superimpose it with a DOM layer for the node rendering. (I did that in my former application and it works very well, but it lacks parallel edges, and self-edges.) Are you aware of a Cytoscape plugin that goes for this hybrid approach?

That approach wouldn't work very well in the case of this lib. Even if it did, it would be very slow as (iirc) there's no way to get a bitmap of the dom.

I've evaluated several graph libraries, and Cytoscape seems to be the only one which supports both out-of-the-box, interactive parallel edges (multigraphs), and interactive self-edges. To me Cytoscape is very strong on edge rendering, but falls back when it comes to node rendering flexibility.

You can do all the rendering for nodes yourself if you want as an svg background image. An svg is pretty well as customisable as a dom node and it's drawable for canvas (and cacheable as a bitmap).

Just make the background shape transparent and choose the node shape that best approximates your node for hit tests etc.

You can alternatively use multiple background images on the node shape.

In either case, you'll need to figure out dimensions for the node yourself --- probably based on the svg dimensions.

@josephst It might be a good idea to have a tutorial on this topic, along with best practices --- e.g. cache the svg generation function.

Would Cytoscape's render event be the basis for customizing the node rendering?

No, that's just to let you know when a new frame is drawn.

josephst commented 7 years ago

@maxkfranz I'm open to writing a tutorial on this after I finish the contributing guide but need to make sure I understand the content well enough. As I understand it:

Use a background SVG image so that scaling is not an issue. Chose a node shape that is similar to the background content so that hit tests for the node serves as a proxy for hitting the background SVG. Made the node entirely transparent to see background SVG. Cache the SVG-generation function (something like Lodash's memoize?) so that (re)rendering many identical nodes can reuse previously-calculated results.

The one part of this I'm not clear on is how to generate this SVG on the fly. Would the recommended way be to use a tool such as snap.svg to create a new SVG element, cache it, call it for the various graph elements, convert the generated SVG to a string, convert XML string to URI, and pass the data URI to Cytoscape.js?

maxkfranz commented 7 years ago

Use a background SVG image so that scaling is not an issue. Chose a node shape that is similar to the background content so that hit tests for the node serves as a proxy for hitting the background SVG. Made the node entirely transparent to see background SVG. Cache the SVG-generation function (something like Lodash's memoize?) so that (re)rendering many identical nodes can reuse previously-calculated results.

This is the first case. The second is where multiple images are used on a normal node shape to add markings, decorations, etc.

The one part of this I'm not clear on is how to generate this SVG on the fly. Would the recommended way be to use a tool such as snap.svg to create a new SVG element, cache it, call it for the various graph elements, convert the generated SVG to a string, convert XML string to URI, and pass the data URI to Cytoscape.js?

Yes, you need to create a data URI at the moment. You could use the SVG API manually or any lib that you prefer. Whatever makes it easiest to do and whatever makes the code easier to read.

In future we could autogenerate a URI if passed a <canvas>, <img>, or <svg> reference. That would be a good PR for 3.2. If you're interested in the PR, then you could mention this approach in your tutorial as an alternative for >=3.2.0 users.

Internally, we have to use an Image/<img>, so the <img> case is simple. A <canvas> can be exported to a URI directly. A <svg> needs to be done more manually. Since you'd either have to have that code in the tutorial itself or the lib, maybe it makes sense to just put it in a PR to the lib and then elide the URI creation from the tutorial (or just link to where's it's done in the lib code so someone could do it manually if they like).

jri commented 7 years ago

You can do all the rendering for nodes yourself if you want as an svg background image. An svg is pretty well as customisable as a dom node and it's drawable for canvas (and cacheable as a bitmap).

OK, very good. Rendering the entire node as an SVG background image is the approach I'm trying now.

Still I want to achieve this node rendering:

node-with-icon

(Note the extra "right padding" to make room for the icon. The icon is meant to be a fontawsome text character, with the color set programmatically.)

In either case, you'll need to figure out dimensions for the node yourself --- probably based on the svg dimensions.

OK. But once I've figured out the node's dimensions how tell I Cytoscape about it?

What I have at the moment:

style: {
  'background-image': renderNode,
  ...
}

The function to render the node's background image:

function renderNode (ele) {
  var label     = ele.data('label')
  var icon      = ele.data('icon')
  var iconColor = ele.data('iconColor')
  var width = 100    // TODO: calculate based on label, add padding
  var height = 25    // TODO: calculate based on label
  var svg = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
    <g>
      <rect x="0" y="0" width="${width}" height="${height}" fill="#d0e0f0"></rect>
      <text x="${width - 20}" y="${height}" fill="${iconColor}">${icon}</text>
    </g>
  </svg>`
  return 'data:image/svg+xml;base64,' + btoa(svg)
}

How can the render function tell Cytoscape the calculated node dimensions? Cytoscape needs to know the node's bounding box for hit tests and border drawing, right?

maxkfranz commented 7 years ago

Your function should return { img, dims } and possibly be memoized for performance.

'background-image': node => fn( node ).img,
'width': node => fn( node ).dims.w
// etc

On Wed, Jun 21, 2017 at 7:02 PM, Jörg Richter notifications@github.com wrote:

You can do all the rendering for nodes yourself if you want as an svg background image. An svg is pretty well as customisable as a dom node and it's drawable for canvas (and cacheable as a bitmap).

OK, very good. Rendering the entire node as an SVG background image is the approach I'm trying now.

Still I want to achieve this node rendering:

[image: node-with-icon] https://user-images.githubusercontent.com/120337/27409714-36e37f5c-56e3-11e7-80b0-792ac93193b9.png

(Note the extra "right padding" to make room for the icon. The icon is meant to be a fontawsome text character, with the color set programmatically.)

In either case, you'll need to figure out dimensions for the node yourself --- probably based on the svg dimensions.

OK. But once I've figured out the node's dimensions how tell I Cytoscape about it?

What I have at the moment:

style: { 'background-image': renderNode, ... }

The function to render the node's background image:

function renderNode (ele) { var label = ele.data('label') var icon = ele.data('icon') var iconColor = ele.data('iconColor') var width = 100 // TODO: calculate based on label, add padding var height = 25 // TODO: calculate based on label var svg = `

${icon}

` return 'data:image/svg+xml;base64,' + btoa(svg) }

How can the render function tell Cytoscape the calculated node dimensions? Cytoscape needs to know the node's bounding box for hit tests and border drawing, right?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/cytoscape/cytoscape.js/issues/1802#issuecomment-310228895, or mute the thread https://github.com/notifications/unsubscribe-auth/AA8Xc_oCAChY1h_8R3NCeG2Y6CFKl9pTks5sGaDugaJpZM4NMkqI .

jri commented 7 years ago

OK. Thank you very much!

winni2k commented 7 years ago

I would be very interested in an example of this in the cytoscape.js gallery.

The problem I'm trying to solve is to embed histograms in nodes. I'm super new to javascript, so I'm not even sure that'll work.

maxkfranz commented 7 years ago

@winni2k It will work.

Joseph has gone on to med school, so he won't be doing the tutorial for this. I'm not sure when we'll be able to do it. We've noted all of the important factors in the docs already. It's just not as step-by-step. http://js.cytoscape.org/#style/background-image

winni2k commented 7 years ago

Thanks @maxkfranz.

I saw those notes as well, but I could not figure out how to translate them into code that would work.

For now I am exploring d3, which has native support for SVG tags and lots of compatible charting libraries.

jesuspc commented 6 years ago

Is there any update on this? I have been trying to use SVGs for node backgrounds without much luck.

winni2k commented 6 years ago

With a little work I got d3 + cola.js to do everything I need...

jri commented 6 years ago

@jesuspc Eventually I got SVG node background images working, in all major browsers, including european <text> chars, and FontAwesome icons.

Some gotchas:

As an example see the Cytoscape code in my project, in particular the renderNode function: https://github.com/jri/dm5-topicmap-panel/blob/master/src/cytoscape-renderer.js#L350

sveinp commented 5 years ago

In case someone drops by this thread:

We have been using SVG background images with cytoscape for a couple of years with great success. We draw shareholder maps, and need to put in a lot of information in the nodes. A couple of things to be aware of: Earlier we to some extend modified the node data within the rendering functions, and also changed classes for the node in question. In later editions of cytoscape, this causes a racing condition.

I'm attaching a sample of our maps. Works well in all major browsers, including IE9+ (for IE, we convert the SVG's to PNG):

sp 4

maxkfranz commented 5 years ago

Earlier we to some extend modified the node data within the rendering functions, and also changed classes for the node in question. In later editions of cytoscape, this causes a racing condition.

Mapper functions should be pure functions w.r.t. element state. Basically, read-only.

canbax commented 5 years ago

I have to keep styles inside a file so I cytoscape.js style has to be a string. Yet I want to use SVG as background-image for nodes. I still can't do that. When I zoom in/out, images got changed.

How can I use SVG background-image ?

maxkfranz commented 5 years ago

The documentation says it's just an address. That could be a traditional URL or it could be a data URI. The library is agnostic, and you are free to choose which approach works best for your app. Either case works for string stylesheets.

If you have a requirement to use a string stylesheet, then you can't do anything procedural by definition. A string stylesheet is inherently declarative.

You have to be careful when you use SVG images. Despite SVG being old technology, there are still a lot of quirks and issues with SVG in different browsers. If you want everything to be easy and simple, stick to the built-in styles to customise the look of nodes. If you want to do complex custom drawing with SVG, you're going to have to do a lot of testing and tweaking. That's one of several reasons why the library does not use SVG for rendering.

The documentation already includes a lot of tips, but a tutorial could be more in-depth. Unfortunately, I don't have time to write tutorials. It would be great if someone in the community would contribute an SVG tutorial. We could post the tutorial on the Cytoscape.js blog or we could link to something like Medium in the documentation.

maxkfranz commented 5 years ago

@sveinp's approach could use something like canvg to convert SVG to a bitmap.

lukethacoder commented 4 years ago

Leaving this here incase anyone else runs into the same issue I did. SVG's fill the node by default, which isn't the most beautiful thing, so lets add some padding* (not actual padding, just scaling down the icon within the node).

function renderNode(ele) {
  // Icon path is assumed to be of 32x32 in this example. You may auto calculate this if you wish.
  const iconPath = 'M31.4,16.5c0.8,0.8,0.8,2,0,2.8l-6,6c-0.8,0.8-2,0.8-2.8,0l-2.9-2.9c0.6,3,0.2,6.2-1.3,9 c-0.2,0.3-0.5,0.5-0.9,0.5c-0.3,0-0.5-0.1-0.7-0.3l-7.6-7.5l-1.4,1.4C7.9,25.7,8,25.8,8,26c0,1.1-0.9,2-2,2s-2-0.9-2-2 c0-1.1,0.9-2,2-2c0.2,0,0.3,0.1,0.5,0.1l1.4-1.4l-7.5-7.5c-0.5-0.5-0.4-1.3,0.2-1.6c2.7-1.5,5.9-1.9,8.9-1.3L6.6,9.4 c-0.8-0.8-0.8-2,0-2.8l6-6C13,0.2,13.5,0,14,0c0.5,0,1,0.2,1.4,0.6L19.9,5l4.4-4.4c0.8-0.8,2-0.8,2.8,0c0,0,0,0,0,0l4.3,4.3 c0.8,0.8,0.8,2,0,2.8L27,12.1L31.4,16.5z M14,18c-2.5-2.5-6-3.5-9.4-2.8l12.2,12.2C17.5,24.1,16.5,20.5,14,18z M9.4,8l3.7,3.7 l4.6-4.6L14,3.4L9.4,8z M14.6,14.6c0.6,0.4,1.1,0.8,1.6,1.3c0.5,0.5,0.9,1,1.3,1.6L28.6,6.3l-2.9-2.9L14.6,14.6z M28.6,18l-3.7-3.7 l-4.6,4.6l3.7,3.7L28.6,18z';
  const iconColor = '#ffffff';
  const size = 32; // may need to calculate this yourself
  const iconResize = 22; // adjust this for more "padding" (bigger number = more smaller icon)

  const width = size;
  const height = size;
  const scale = (size - iconResize) / size;
  const iconTranslate = iconResize / 2 / scale;
  const backgroundColor = `#33362F`;

  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
      <rect x="0" y="0" width="${width}" height="${height}" fill="${backgroundColor}"></rect>
      <path d="${iconPath}" fill="${iconColor}" transform="scale(${scale}) translate(${iconTranslate}, ${iconTranslate}) "></path>
    </svg>`;
  return {
    svg: 'data:image/svg+xml;base64,' + btoa(svg),
    width,
    height,
  };
}

let nodes = [
  {
    data: {
      id: 'id',
    },
    style: {
      shape: 'circle',
      'background-image': (ele) => renderNode(ele).svg,
      'background-opacity': 0,
      width: (ele) => renderNode(ele).width,
      height: (ele) => renderNode(ele).height,
      'border-width': 1,
      'border-color': `#E0749F`,
      'border-opacity': 1,
    },
  },
];

Small snippet to calculate the width/height/scale/position of the <path/> within the <svg/> based on a few params. Still requires you to know the width/height of the <path d=""/> value.

If you're lucky enough to find an svg icon lib that has all the same size svg icons, even better. My case I have the icon set to 32x32, but you could calculate the size if you had varying <path d=""/> sizes.

image