Closed jri closed 5 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?
I want to achieve this node rendering:
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!
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.
@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?
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).
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:
(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?
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 .
OK. Thank you very much!
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.
@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
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.
Is there any update on this? I have been trying to use SVGs for node backgrounds without much luck.
With a little work I got d3 + cola.js to do everything I need...
@jesuspc Eventually I got SVG node background images working, in all major browsers, including european <text>
chars, and FontAwesome icons.
Some gotchas:
Don't use Base64 encoding in SVG data URLs. Use URL-encoding instead. See https://github.com/cytoscape/cytoscape.js/issues/1873
The node render function has to return an object containing the SVG (e.g. as data URL) and its dimensions. See https://github.com/cytoscape/cytoscape.js/issues/1802#issuecomment-310386514
You can't SVG draw beyond a node's bounding box. See https://github.com/cytoscape/cytoscape.js/issues/1865
Webfonts (e.g. FontAwesome) do not work in SVG <text>
elements. As fas as I understand this is due to browser oddities (Canvas/SVG/Webfonts), and is not a Cytoscape problem. A workaround is to render the plain SVG glyphs as <path>
elements instead. See https://github.com/cytoscape/cytoscape.js/issues/1867
The XML namespace (<svg>
element's xmlns
attribute) is required. A XML declaration (<?xml version="1.0" encoding="..."?>
) is not required. See https://github.com/cytoscape/cytoscape.js/issues/1873
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
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):
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.
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 ?
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.
@sveinp's approach could use something like canvg to convert SVG to a bitmap.
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.
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.