Skip to content

Commit

Permalink
[utils] Drafts @sigma/utils
Browse files Browse the repository at this point in the history
Details:
- Drafts new package @sigma/utils
- Adds `fitNodesToViewport` and `getCameraStateToFitNodesToViewport`
- Adds a new story to showcase `fitNodesToViewport`
  • Loading branch information
jacomyal committed Sep 20, 2024
1 parent 4ac2c8d commit 38455e7
Show file tree
Hide file tree
Showing 13 changed files with 331 additions and 13 deletions.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"packages/edge-curve",
"packages/layer-leaflet",
"packages/layer-maplibre",
"packages/layer-webgl"
"packages/layer-webgl",
"packages/utils"
],
"exports": {
"importConditionDefaultExport": "default"
Expand Down
64 changes: 64 additions & 0 deletions packages/storybook/stories/utils/fit-nodes-to-viewport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { fitNodesToViewport } from "@sigma/utils";
import Graph from "graphology";
import louvain from "graphology-communities-louvain";
import iwanthue from "iwanthue";
import Sigma from "sigma";

import data from "../_data/data.json";
import { onStoryDown } from "../utils";

export default () => {
const graph = new Graph();
graph.import(data);

// Detect communities
louvain.assign(graph, { nodeCommunityAttribute: "community" });
const communities = new Set<string>();
graph.forEachNode((_, attrs) => communities.add(attrs.community));
const communitiesArray = Array.from(communities);

// Determine colors, and color each node accordingly
const palette: Record<string, string> = iwanthue(communities.size).reduce(
(iter, color, i) => ({
...iter,
[communitiesArray[i]]: color,
}),
{},
);
graph.forEachNode((node, attr) => graph.setNodeAttribute(node, "color", palette[attr.community]));

// Retrieve some useful DOM elements
const container = document.getElementById("sigma-container") as HTMLElement;

// Instantiate sigma
const renderer = new Sigma(graph, container);

// Add buttons
const buttonsContainer = document.createElement("div");
buttonsContainer.style.position = "absolute";
buttonsContainer.style.right = "10px";
buttonsContainer.style.bottom = "10px";
document.body.append(buttonsContainer);

communitiesArray.forEach((community) => {
const id = `cb-${community}`;
const buttonContainer = document.createElement("div");
buttonContainer.innerHTML += `
<button id="${id}" style="color:${palette[community]};margin-top:3px">Community n°${community + 1}</button>
`;
buttonsContainer.append(buttonContainer);
const button = buttonsContainer.querySelector(`#${id}`) as HTMLButtonElement;

button.addEventListener("click", () => {
fitNodesToViewport(
renderer,
graph.filterNodes((_, attr) => attr.community === community),
{ animate: true },
);
});
});

onStoryDown(() => {
renderer?.kill();
});
};
14 changes: 14 additions & 0 deletions packages/storybook/stories/utils/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<style>
html,
body,
#storybook-root,
#sigma-container {
width: 100%;
height: 100%;
margin: 0 !important;
padding: 0 !important;
overflow: hidden;
font-family: sans-serif;
}
</style>
<div id="sigma-container"></div>
25 changes: 25 additions & 0 deletions packages/storybook/stories/utils/stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Meta, StoryObj } from "@storybook/web-components";

import FitNodesToViewportPlay from "./fit-nodes-to-viewport";
import FitNodesToViewportSource from "./fit-nodes-to-viewport?raw";
import template from "./index.html?raw";

const meta: Meta = {
id: "utils",
title: "utils",
};
export default meta;

type Story = StoryObj;

export const FitNodesToViewport: Story = {
name: "Fit nodes to viewport",
render: () => template,
play: FitNodesToViewportPlay,
args: {},
parameters: {
storySource: {
source: FitNodesToViewportSource,
},
},
};
2 changes: 2 additions & 0 deletions packages/utils/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
4 changes: 4 additions & 0 deletions packages/utils/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.gitignore
node_modules
src
tsconfig.json
7 changes: 7 additions & 0 deletions packages/utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Sigma.js - Utils

This package contains various utils functions to ease the use of [sigma.js](https://www.sigmajs.org/).

### `fitNodesToViewport` and `getCameraStateToFitNodesToViewport`

These functions allow moving a sigma instance's camera so that it fits to a given group of nodes.
34 changes: 34 additions & 0 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@sigma/utils",
"version": "3.0.0-beta.0",
"description": "A set of utils functions to ease the use of sigma.js",
"main": "dist/sigma-utils.cjs.js",
"module": "dist/sigma-utils.esm.js",
"files": [
"/dist"
],
"sideEffects": false,
"homepage": "https://www.sigmajs.org",
"bugs": "http://github.com/jacomyal/sigma.js/issues",
"repository": {
"type": "git",
"url": "http://github.com/jacomyal/sigma.js.git"
},
"preconstruct": {
"entrypoints": [
"index.ts"
]
},
"peerDependencies": {
"sigma": ">=3.0.0-beta.29"
},
"license": "MIT",
"exports": {
".": {
"module": "./dist/sigma-utils.esm.js",
"import": "./dist/sigma-utils.cjs.mjs",
"default": "./dist/sigma-utils.cjs.js"
},
"./package.json": "./package.json"
}
}
104 changes: 104 additions & 0 deletions packages/utils/src/fitNodesToViewport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import Sigma from "sigma";
import type { CameraState } from "sigma/types";
import { getCorrectionRatio } from "sigma/utils";

export type FitNodesToScreenOptions = {
animate: boolean;
};
export const DEFAULT_FIT_NODES_TO_SCREEN_OPTIONS: FitNodesToScreenOptions = {
animate: true,
};

/**
* This function takes a Sigma instance and a list of nodes as input, and returns a CameraState so that the camera fits
* the best to the given groups of nodes (i.e. the camera is as zoomed as possible while keeping all nodes on screen).
*
* @param sigma A Sigma instance
* @param nodes A list of nodes IDs
* @param opts An optional and partial FitNodesToScreenOptions object
*/
export function getCameraStateToFitNodesToViewport(
sigma: Sigma,
nodes: string[],
_opts: Partial<Omit<FitNodesToScreenOptions, "animate">> = {},
): CameraState {
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
let groupMinX = Infinity;
let groupMaxX = -Infinity;
let groupMinY = Infinity;
let groupMaxY = -Infinity;
let groupFramedMinX = Infinity;
let groupFramedMaxX = -Infinity;
let groupFramedMinY = Infinity;
let groupFramedMaxY = -Infinity;

const group = new Set(nodes);
const graph = sigma.getGraph();
graph.forEachNode((node, { x, y }) => {
const data = sigma.getNodeDisplayData(node);
if (!data) throw new Error(`getCameraStateToFitNodesToViewport: Node ${node} not found in sigma's graph.`);
const { x: framedX, y: framedY } = data;

minX = Math.min(minX, x);
maxX = Math.max(maxX, x);
minY = Math.min(minY, y);
maxY = Math.max(maxY, y);
if (group.has(node)) {
groupMinX = Math.min(groupMinX, x);
groupMaxX = Math.max(groupMaxX, x);
groupMinY = Math.min(groupMinY, y);
groupMaxY = Math.max(groupMaxY, y);
groupFramedMinX = Math.min(groupFramedMinX, framedX);
groupFramedMaxX = Math.max(groupFramedMaxX, framedX);
groupFramedMinY = Math.min(groupFramedMinY, framedY);
groupFramedMaxY = Math.max(groupFramedMaxY, framedY);
}
});

const groupCenterX = (groupFramedMinX + groupFramedMaxX) / 2;
const groupCenterY = (groupFramedMinY + groupFramedMaxY) / 2;
const groupWidth = groupMaxX - groupMinX || 1;
const groupHeight = groupMaxY - groupMinY || 1;
const graphWidth = maxX - minX || 1;
const graphHeight = maxY - minY || 1;

const { width, height } = sigma.getDimensions();
const correction = getCorrectionRatio({ width, height }, { width: graphWidth, height: graphHeight });
const ratio =
((groupHeight / groupWidth < height / width ? groupWidth : groupHeight) / Math.max(graphWidth, graphHeight)) *
correction;

const camera = sigma.getCamera();
return {
...camera.getState(),
x: groupCenterX,
y: groupCenterY,
ratio,
};
}

/**
* This function takes a Sigma instance and a list of nodes as input, and updates the camera so that the camera fits the
* best to the given groups of nodes (i.e. the camera is as zoomed as possible while keeping all nodes on screen).
*
* @param sigma A Sigma instance
* @param nodes A list of nodes IDs
* @param opts An optional and partial FitNodesToScreenOptions object
*/
export function fitNodesToViewport(sigma: Sigma, nodes: string[], opts: Partial<FitNodesToScreenOptions> = {}): void {
const { animate } = {
...DEFAULT_FIT_NODES_TO_SCREEN_OPTIONS,
...opts,
};

const camera = sigma.getCamera();
const newCameraState = getCameraStateToFitNodesToViewport(sigma, nodes, opts);
if (animate) {
camera.animate(newCameraState);
} else {
camera.setState(newCameraState);
}
}
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./fitNodesToViewport";
23 changes: 23 additions & 0 deletions packages/utils/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"declaration": true
},
"include": ["src"],
"exclude": ["src/**/__docs__", "src/**/__test__"]
}
51 changes: 39 additions & 12 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,45 @@
"extends": "./tsconfig.base.json",
"files": [],
"references": [
{ "path": "./packages/demo" },
{ "path": "./packages/test" },
{ "path": "./packages/storybook" },
{ "path": "./packages/sigma" },
{ "path": "./packages/layer-leaflet" },
{ "path": "./packages/layer-maplibre" },
{ "path": "./packages/layer-webgl" },
{ "path": "./packages/node-border" },
{ "path": "./packages/node-image" },
{ "path": "./packages/node-piechart" },
{ "path": "./packages/node-square" },
{ "path": "./packages/edge-curve" }
{
"path": "./packages/demo"
},
{
"path": "./packages/test"
},
{
"path": "./packages/storybook"
},
{
"path": "./packages/sigma"
},
{
"path": "./packages/layer-leaflet"
},
{
"path": "./packages/layer-maplibre"
},
{
"path": "./packages/layer-webgl"
},
{
"path": "./packages/node-border"
},
{
"path": "./packages/node-image"
},
{
"path": "./packages/node-piechart"
},
{
"path": "./packages/node-square"
},
{
"path": "./packages/edge-curve"
},
{
"path": "./packages/utils"
}
],
"watchOptions": {
"excludeDirectories": ["**/node_modules"]
Expand Down

0 comments on commit 38455e7

Please sign in to comment.