graphology-canvas
Advanced tools
Comparing version 0.2.0 to 0.3.0
@@ -0,2 +1,5 @@ | ||
var merge = require('lodash/merge'); | ||
var DEFAULTS = { | ||
batchSize: 500, | ||
margin: 20, | ||
@@ -17,2 +20,23 @@ width: 2048, | ||
exports.refineSettings = function refineSettings(settings) { | ||
var dimensions = { | ||
width: settings.width, | ||
height: settings.height | ||
}; | ||
if (!dimensions.width && !dimensions.height) | ||
throw new Error('graphology-canvas: need at least a valid width or height!'); | ||
if (dimensions.width && !dimensions.height) | ||
dimensions.height = dimensions.width; | ||
if (dimensions.height && !dimensions.width) | ||
dimensions.width = dimensions.height; | ||
settings = merge({}, DEFAULTS, settings, dimensions); | ||
return settings; | ||
}; | ||
exports.DEFAULT_NODE_REDUCER = function(settings, node, attr) { | ||
@@ -19,0 +43,0 @@ var reduced = { |
@@ -9,10 +9,17 @@ /** | ||
// Taken from @jacomyma (graph-recipes) | ||
var CAMERA = { | ||
x: 0.5, | ||
y: 0.5, | ||
angle: 0, | ||
ratio: 1 | ||
}; | ||
function reduceNodes(graph, settings) { | ||
var width = settings.width, | ||
height = settings.height; | ||
var containerWidth = settings.width, | ||
containerHeight = settings.height; | ||
var xBarycenter = 0, | ||
yBarycenter = 0, | ||
totalWeight = 0; | ||
var xMin = Infinity, | ||
xMax = -Infinity, | ||
yMin = Infinity, | ||
yMax = -Infinity; | ||
@@ -30,39 +37,50 @@ var data = {}; | ||
// Computing rescaling items | ||
xBarycenter += attr.size * attr.x; | ||
yBarycenter += attr.size * attr.y; | ||
totalWeight += attr.size; | ||
// Finding bounds | ||
if (attr.x < xMin) | ||
xMin = attr.x; | ||
if (attr.x > xMax) | ||
xMax = attr.x; | ||
if (attr.y < yMin) | ||
yMin = attr.y; | ||
if (attr.y > yMax) | ||
yMax = attr.y; | ||
}); | ||
xBarycenter /= totalWeight; | ||
yBarycenter /= totalWeight; | ||
var graphWidth = xMax - xMin, | ||
graphHeight = yMax - yMin; | ||
var d, ratio, n; | ||
var dMax = -Infinity; | ||
var ratio = Math.max(graphWidth, graphHeight) || 1; | ||
var k; | ||
var dX = (xMax + xMin) / 2; | ||
var dY = (yMax + yMin) / 2; | ||
for (k in data) { | ||
n = data[k]; | ||
d = Math.pow(n.x - xBarycenter, 2) + Math.pow(n.y - yBarycenter, 2); | ||
containerWidth -= settings.margin * 2; | ||
containerHeight -= settings.margin * 2; | ||
if (d > dMax) | ||
dMax = d; | ||
} | ||
var smallest = Math.min(containerWidth, containerHeight); | ||
ratio = (Math.min(width, height) - 2 * settings.margin) / (2 * Math.sqrt(dMax)); | ||
var dpX = smallest / containerWidth; | ||
var dpY = smallest / containerHeight; | ||
var dpRatio = CAMERA.ratio / smallest; | ||
var k, n; | ||
var x, y; | ||
for (k in data) { | ||
n = data[k]; | ||
n.x = width / 2 + (n.x - xBarycenter) * ratio; | ||
n.y = height / 2 + (n.y - yBarycenter) * ratio; | ||
// Normalize | ||
x = 0.5 + (n.x - dX) / ratio; | ||
y = 0.5 + (n.y - dY) / ratio; | ||
// Conserving original orientation | ||
if (xBarycenter < 0) | ||
n.x = width - n.x; | ||
if (yBarycenter < 0) | ||
n.y = height - n.y; | ||
// Align | ||
x = (x - CAMERA.x) / dpRatio; | ||
y = (CAMERA.y - y) / dpRatio; | ||
n.size *= ratio; // TODO: keep? | ||
// Rotate | ||
x = x * Math.cos(CAMERA.angle) - y * Math.sin(CAMERA.angle); | ||
y = y * Math.cos(CAMERA.angle) + x * Math.sin(CAMERA.angle); | ||
n.x = settings.margin + x + smallest / 2 / dpX; | ||
n.y = settings.margin + y + smallest / 2 / dpY; | ||
} | ||
@@ -69,0 +87,0 @@ |
@@ -1,4 +0,47 @@ | ||
import Graph from 'graphology-types'; | ||
import Graph, {Attributes, NodeKey, EdgeKey} from 'graphology-types'; | ||
export default function render(graph: Graph, outputPath: string, callback: () => void): void; | ||
export default function render(graph: Graph, outputPath: string, settings: any, callback: () => void): void; | ||
export type CanvasRendererSettings< | ||
NodeAttributes extends Attributes = Attributes, | ||
EdgeAttributes extends Attributes = Attributes | ||
> = { | ||
batchSize?: number; | ||
margin?: number; | ||
width?: number; | ||
height?: number; | ||
nodes?: { | ||
defaultColor?: string; | ||
reducer?: (settings: CanvasRendererSettings, node: NodeKey, attributes: NodeAttributes) => Attributes; | ||
}, | ||
edges?: { | ||
defaultColor?: string; | ||
reducer?: (settings: CanvasRendererSettings, edge: EdgeKey, attributes: EdgeAttributes) => Attributes; | ||
} | ||
}; | ||
export function render< | ||
NodeAttributes extends Attributes = Attributes, | ||
EdgeAttributes extends Attributes = Attributes | ||
>( | ||
graph: Graph, | ||
context: CanvasRenderingContext2D, | ||
settings?: CanvasRendererSettings<NodeAttributes, EdgeAttributes> | ||
): void; | ||
export function renderAsync< | ||
NodeAttributes extends Attributes = Attributes, | ||
EdgeAttributes extends Attributes = Attributes | ||
>( | ||
graph: Graph, | ||
context: CanvasRenderingContext2D, | ||
settings: CanvasRendererSettings<NodeAttributes, EdgeAttributes>, | ||
callback: () => void | ||
): void; | ||
export function renderAsync< | ||
NodeAttributes extends Attributes = Attributes, | ||
EdgeAttributes extends Attributes = Attributes | ||
>( | ||
graph: Graph, | ||
context: CanvasRenderingContext2D, | ||
callback: () => void | ||
): void; |
37
index.js
@@ -5,12 +5,19 @@ /** | ||
* | ||
* Node.js API relying on `node-canvas` and Cairo. | ||
* Publicly-exposed routine used to render the given graph into an arbitrary | ||
* canvas context. | ||
*/ | ||
var fs = require('fs'); | ||
var canvasApi = require('canvas'); | ||
var defaultsDeep = require('lodash/defaultsDeep'); | ||
var renderer = require('./renderer.js'); | ||
var isGraph = require('graphology-utils/is-graph'); | ||
var lib = require('./renderer.js'); | ||
var refineSettings = require('./defaults.js').refineSettings; | ||
var DEFAULTS = require('./defaults.js').DEFAULTS; | ||
exports.render = function render(graph, context, settings) { | ||
if (!isGraph(graph)) | ||
throw new Error('graphology-canvas/render: expecting a valid graphology instance.'); | ||
module.exports = function render(graph, outputPath, settings, callback) { | ||
settings = refineSettings(settings); | ||
lib.renderSync(graph, context, settings); | ||
}; | ||
exports.renderAsync = function renderAsync(graph, context, settings, callback) { | ||
if (arguments.length === 3) { | ||
@@ -21,17 +28,5 @@ callback = settings; | ||
settings = defaultsDeep({}, DEFAULTS, settings); | ||
settings = refineSettings(settings); | ||
var canvas = canvasApi.createCanvas(settings.width, settings.height); | ||
var context = canvas.getContext('2d'); | ||
renderer(graph, context, settings); | ||
var out = fs.createWriteStream(outputPath); | ||
var pngStream = canvas.createPNGStream(); | ||
pngStream.pipe(out); | ||
out.once('finish', function() { | ||
callback(); | ||
}); | ||
lib.renderAsync(graph, context, settings, callback); | ||
}; |
{ | ||
"name": "graphology-canvas", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "Canvas rendering routines for graphology.", | ||
@@ -9,7 +9,9 @@ "main": "index.js", | ||
"*.d.ts", | ||
"browser.js", | ||
"components", | ||
"defaults.js", | ||
"helpers.js", | ||
"index.js", | ||
"renderer.js", | ||
"helpers.js" | ||
"node.js", | ||
"renderer.js" | ||
], | ||
@@ -45,7 +47,8 @@ "scripts": { | ||
"@yomguithereal/eslint-config": "^4.0.0", | ||
"eslint": "^7.13.0", | ||
"canvas": "^2.6.1", | ||
"eslint": "^7.17.0", | ||
"fs-extra": "^9.0.1", | ||
"graphology": "^0.19.1", | ||
"graphology-gexf": "^0.6.0", | ||
"graphology-types": "^0.19.0", | ||
"graphology": "^0.19.3", | ||
"graphology-gexf": "^0.7.2", | ||
"graphology-types": "^0.19.2", | ||
"mocha": "^8.2.1" | ||
@@ -60,9 +63,14 @@ }, | ||
"dependencies": { | ||
"canvas": "^2.6.1", | ||
"graphology-utils": "^1.8.0", | ||
"graphology-utils": "^2.0.0", | ||
"lodash": "^4.17.20" | ||
}, | ||
"peerDependencies": { | ||
"canvas": "^2.6.1", | ||
"graphology-types": ">=0.19.0" | ||
}, | ||
"peerDependenciesMeta": { | ||
"canvas": { | ||
"optional": true | ||
} | ||
} | ||
} |
@@ -13,11 +13,53 @@ [![Build Status](https://travis-ci.org/graphology/graphology-canvas.svg)](https://travis-ci.org/graphology/graphology-canvas) | ||
Note that `graphology-canvas` relies on [`node-canvas`](https://www.npmjs.com/package/canvas). As such, if you experience issues when installing the libray check that you have the required dependencies as listed [here](https://www.npmjs.com/package/canvas#compiling). | ||
If you need to use this package's functions in node, you will also need to install [`node-canvas`](https://www.npmjs.com/package/canvas) thusly: | ||
``` | ||
npm install canvas | ||
``` | ||
If you experience any issue when installing the libray check that you have the required dependencies as listed [here](https://www.npmjs.com/package/canvas#compiling). | ||
## Usage | ||
### Rendering a graph in an arbitrary canvas context | ||
```js | ||
var render = require('graphology-canvas'); | ||
import {render} from 'graphology-canvas'; | ||
render(graph, './graph.png', () => console.log('Done!')); | ||
render(graph, './graph.png', settings, () => console.log('Done!')); | ||
render(graph, context, settings); | ||
``` | ||
### Rendering asynchronously to avoid freezing main thread | ||
```js | ||
import {renderAsync} from 'graphology-canvas'; | ||
renderAsync(graph, context, settings, function() { | ||
console.log('Done!'); | ||
}); | ||
``` | ||
### Rendering a graph to PNG in node | ||
```js | ||
import {renderToPNG} from 'graphology-canvas/node'; | ||
renderToPNG(graph, './graph.png', () => console.log('Done!')); | ||
renderToPNG(graph, './graph.png', settings, () => console.log('Done!')); | ||
``` | ||
### Settings | ||
* **width** *?number* [`2048`]: width of the canvas. Will be the same as `height` if not provided. | ||
* **height** *?number* [`2048`]: height of the canvas. Will be the same as `width` if not provided. | ||
* **margin** *?number* [`20`]: margin to keep around the drawing. | ||
* **nodes** *?object*: node-related settings: | ||
* **defaultColor** *?string* [`#999`]: default color for nodes. | ||
* **reducer** *?function*: reducer fonction for nodes taking the rendering settings, the node key and its attributes and tasked to return rendering info such as `color`, `size` etc. | ||
* **edges** *?object*: node-related settings: | ||
* **defaultColor** *?string* [`#999`]: default color for edges. | ||
* **reducer** *?function*: reducer fonction for edges taking the rendering settings, the node key and its attributes and tasked to return rendering info such as `color`, `size` etc. | ||
### Async Settings | ||
* **batchSize** *?number* [`500`]: number of items to render on canvas on each animation frame, increase or decrease to tweak performance vs. UI availability. |
100
renderer.js
@@ -7,3 +7,2 @@ /** | ||
*/ | ||
var isGraph = require('graphology-utils/is-graph'); | ||
var helpers = require('./helpers.js'); | ||
@@ -22,5 +21,3 @@ var defaults = require('./defaults.js'); | ||
function renderer(graph, context, settings) { | ||
if (!isGraph(graph)) | ||
throw new Error('graphology-canvas/renderer: expecting a valid graphology instance.'); | ||
exports.renderSync = function renderSync(graph, context, settings) { | ||
@@ -50,3 +47,2 @@ // Reducing nodes | ||
// Drawing nodes | ||
// TODO: should we draw in size order to avoid weird overlaps? Should we run noverlap? | ||
var k, d; | ||
@@ -58,4 +54,96 @@ | ||
} | ||
}; | ||
var raf = function(fn) { | ||
return setTimeout(fn, 16); | ||
}; | ||
if (typeof requestAnimationFrame !== 'undefined') | ||
raf = requestAnimationFrame; | ||
function asyncWhile(condition, task, callback) { | ||
if (condition()) { | ||
task(); | ||
// Early termination to avoid delay | ||
if (!condition()) | ||
return callback(); | ||
return raf(function() { | ||
asyncWhile(condition, task, callback); | ||
}); | ||
} | ||
return callback(); | ||
} | ||
module.exports = renderer; | ||
exports.renderAsync = function renderAsync(graph, context, settings, callback) { | ||
// Reducing nodes | ||
var nodeData = helpers.reduceNodes(graph, settings); | ||
// Filling background | ||
fillBackground(context, 'white', settings.width, settings.height); | ||
// Drawing edges asynchronously | ||
var edges = graph.edges(); | ||
var edgesDone = 0; | ||
function renderEdgeBatch() { | ||
var l; | ||
var edge, attr, extremities, source, target, sourceData, targetData; | ||
for (l = Math.min(edgesDone + settings.batchSize, edges.length); edgesDone < l; edgesDone++) { | ||
edge = edges[edgesDone]; | ||
attr = graph.getEdgeAttributes(edge); | ||
extremities = graph.extremities(edge); | ||
source = extremities[0]; | ||
target = extremities[1]; | ||
// Reducing edge | ||
if (typeof settings.edges.reducer === 'function') | ||
attr = settings.edges.reducer(settings, edge, attr); | ||
attr = defaults.DEFAULT_EDGE_REDUCER(settings, edge, attr); | ||
sourceData = nodeData[source]; | ||
targetData = nodeData[target]; | ||
components.edges[attr.type](settings, context, attr, sourceData, targetData); | ||
} | ||
} | ||
var nodes = new Array(graph.order); | ||
var step = 0; | ||
for (var k in nodeData) | ||
nodes[step++] = nodeData[k]; | ||
var nodesDone = 0; | ||
function renderNodeBatch() { | ||
var l; | ||
var node; | ||
for (l = Math.min(nodesDone + settings.batchSize, nodes.length); nodesDone < l; nodesDone++) { | ||
node = nodes[nodesDone]; | ||
components.nodes[node.type](settings, context, node); | ||
} | ||
} | ||
// Async logic | ||
function edgesCondition() { | ||
return edgesDone < edges.length; | ||
} | ||
function nodesCondition() { | ||
return nodesDone < nodes.length; | ||
} | ||
asyncWhile(edgesCondition, renderEdgeBatch, function() { | ||
asyncWhile(nodesCondition, renderNodeBatch, function() { | ||
return callback(); | ||
}); | ||
}); | ||
}; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
17568
14
414
65
8
1
+ Addedgraphology-utils@2.5.2(transitive)
- Removedcanvas@^2.6.1
- Removedgraphology-utils@1.8.0(transitive)
Updatedgraphology-utils@^2.0.0