Repository: goldsborough/ig
Branch: master
Commit: b7f9d88c7d1a
Files: 23
Total size: 39.1 KB
Directory structure:
gitextract_jv0db8cj/
├── .gitignore
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── ig/
│ ├── __init__.py
│ ├── colors.py
│ ├── graph.py
│ ├── main.py
│ ├── paths.py
│ ├── serve.py
│ └── walk.py
├── scripts/
│ ├── __init__.py
│ └── bump.py
├── setup.cfg
├── setup.py
└── www/
├── graph.html
├── graph.js
├── sigma/
│ ├── LICENSE.txt
│ ├── README.md
│ ├── sigma.canvas.edges.curvedArrow.js
│ └── sigma.canvas.labels.def.js
└── style.css
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Own
*.json
# Created by https://www.gitignore.io/api/python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# dotenv
.env
# virtualenv
.venv
venv/
ENV/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# End of https://www.gitignore.io/api/python
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2017 Peter Goldsborough
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: MANIFEST.in
================================================
include *.md
include LICENSE
recursive-include www *
recursive-include * Makefile
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
recursive-exclude www/sigma/sigma.js/ *
================================================
FILE: Makefile
================================================
.PHONY: clean-pyc clean-build docs clean
help:
@echo "clean - remove all build, test, coverage and Python artifacts."
@echo "clean-build - remove build artifacts."
@echo "clean-pyc - remove Python file artifacts."
@echo "bump - bumps the version."
@echo "test-register - register the project at TestPyPI."
@echo "register - register the project at PyPI."
@echo "test-upload - package and upload a releaes to TestPyPI."
@echo "upload - package and upload a upload to PyPI."
@echo "dist - package."
@echo "install - install the package to the active Python's site-packages."
clean: clean-build clean-pyc
clean-build:
rm -rf build/
rm -rf dist/
rm -rf .eggs/
find . -name '*.egg-info' -exec rm -rf {} +
find . -name '*.egg' -exec rm -f {} +
clean-pyc:
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -rf {} +
bump:
python -m scripts.bump
dist: clean
python setup.py sdist
python setup.py bdist_wheel
test-register:
python setup.py register -r test
register:
python setup.py register
test-upload: dist
twine upload -r test $(wildcard dist/*)
upload: dist
twine upload $(wildcard dist/*)
install:
python setup.py install
================================================
FILE: README.md
================================================
# :fireworks: ig
ig is a tool to interactively visualize include graphs for C++ projects
## Overview
Point `ig` at any directory containing C++ source or header files and it will
construct a full graph of all includes, serve you a local website and visualize
the graph interactively with [sigma.js](http://sigmajs.org), for you to admire.
Usage is very easy:
```sh
$ ig -o include
```
will inspect the folder `include`, serve a website on `localhost:8080` and even
open your browser for you. The full set of options currently include:
```sh
usage: ig [-h] [--pattern PATTERNS] [-i PREFIXES] [-v] [-p PORT] [-o] [-j]
[-d DIRECTORY] [--relation {includes,included-by}]
[--min-degree MIN_DEGREE] [--group-granularity GROUP_GRANULARITY]
[--full-path] [--colors COLORS] [--color-variation COLOR_VARIATION]
[--color-alpha-min COLOR_ALPHA_MIN]
directories [directories ...]
Visualize C++ include graphs
positional arguments:
directories The directories to inspect
optional arguments:
-h, --help show this help message and exit
--pattern PATTERNS The file (glob) patterns to look for
-i PREFIXES, -I PREFIXES, --prefix PREFIXES
An include path for headers to recognize
-v, --verbose Turn on verbose output
-p PORT, --port PORT The port to serve the visualization on
-o, --open Open the webpage immediately
-j, --json Print the graph JSON and instead of serving it
-d DIRECTORY, --dir DIRECTORY
The directory to store the served files in. If not
supplied, a temporary directory is created.
--relation {includes,included-by}
The relation of edges in the graph
--min-degree MIN_DEGREE
The initial minimum degree nodes should have to be
displayed
--group-granularity GROUP_GRANULARITY
How coarse to group nodes (by folder)
--full-path If set, shows the full path for nodes
--colors COLORS The base RGB colors separated by commas
--color-variation COLOR_VARIATION
The variation in RGB around the base colors
--color-alpha-min COLOR_ALPHA_MIN
The minimum alpha value for colors
```
But does it scale? It scales quite well. The graph you see above is the include
graph for the entire LLVM and clang codebase, which spans more than 5,000 files
and 1.5M LOC. Note that the visualization also includes sliders to group nodes
by folder and filter out low-degree nodes.
## Installation
Get it with pip:
```sh
$ pip install ig-cpp
```
Works with Python 2 and 3.
## Examples
Who ever said C++ was an ugly language?
LLVM/ADT
TensorFlow
libc++ (the standard library)
## Authors
[Peter Goldsborough](http://goldsborough.me) + [cat](https://goo.gl/IpUmJn)
:heart:
================================================
FILE: ig/__init__.py
================================================
from datetime import date
__title__ = 'ig'
__url__ = "https://github.com/goldsborough/ig"
__version__ = '0.1.9'
__author__ = 'Peter Goldsborough'
__license__ = 'MIT'
__copyright__ = 'Copyright {0} Peter Goldsborough'.format(date.today().year)
================================================
FILE: ig/colors.py
================================================
import random
def random_color(base, variation):
'''
Returns a random, bounded color value.
Args:
base: Some base color component (between 0 and 255)
variation: The degree of variation (around the color)
Returns:
A random color.
'''
color = base + (2 * random.random() - 1) * variation
return max(8, min(int(color), 256))
class Colors(object):
'''
Aggregates information about the color scheme of the visualization.
'''
def __init__(self, base_colors):
'''
Constructor.
Args:
base_colors: The base colors around which to vary
'''
self.base = list(base_colors)
self.variation = None
self.alpha_min = None
def generate(self):
'''
Generates a color.
Returns:
A new RGBA color value.
'''
rgba = [random_color(color, self.variation) for color in self.base]
rgba.append(max(self.alpha_min, random.random()))
return 'rgba({0})'.format(','.join(map(str, rgba)))
================================================
FILE: ig/graph.py
================================================
''' Defines the graph structure that stores the include relationships. '''
import logging
import os
import random
log = logging.getLogger(__name__)
class Graph(object):
'''
Stores nodes (files) and edges (includes).
Nodes are stored in a map from absolute filename to a dictionary, which
holds all the information required by sigma.js (e.g. ID and label). Edges
are stored as a list of (id, source, target) objects, along with some
additional information. The representation is not very compact or optimal,
but processing even very large projects still seems instant.
'''
def __init__(self,
relation,
full_path,
colors,
group_granularity):
'''
Constructor.
Args:
relation: One of {'includes', 'included-by'}
full_path: Whether to print node labels with their full path
colors: A `Colors` object storing information about the color scheme
group_granularity: The granularity setting for node groups
directory: The directory from which to serve the visualization.
'''
assert relation in ('includes', 'included-by')
self.edges = []
self.nodes = {}
self.is_included_by_relation = (relation == 'included-by')
self.use_full_path = full_path
self.colors = colors
self.group_granularity = group_granularity
def add(self, node_name, neighbors):
'''
Adds a new node to the graph, along with its adjacent neighbors.
Args:
node_name: The name of the node (i.e. current file)
neighbors: A list of names of neighbors (included files)
'''
node = self._get_or_add_node(node_name)
if not self.is_included_by_relation:
node['size'] = len(neighbors)
for neighbor_name in neighbors:
neighbor = self._get_or_add_node(neighbor_name)
if self.is_included_by_relation:
neighbor['size'] += 1
self._add_edge(node, neighbor)
def to_json(self):
'''
Turns the graph into a dictionary (a.k.a. JSON).
The format is {"nodes": list of node objects, "edges": array of edge
objects}.
Returns:
A JSON (dictionary) representation of the graph.
'''
nodes = list(self.nodes.values())
return dict(nodes=nodes, edges=self.edges)
@property
def is_empty(self):
'''
Returns:
True if the graph has no nodes at all, else False.
'''
return len(self.nodes) == 0
def _get_or_add_node(self, node_name):
'''
Returns a node and possibly adds it to the graph.
Args:
node_name: The name of the node to fetch
Returns:
The node entry for the given name.
'''
node = self.nodes.get(node_name)
if node is None:
node = self._add_node(node_name)
return node
def _add_node(self, node_name):
'''
Adds a node to the graph.
Args:
node_name: The name of the node to add
Returns:
The newly created node object.
'''
assert node_name not in self.nodes
node = {}
node['id'] = len(self.nodes)
node['size'] = 1
node['color'] = self.colors.generate()
if self.use_full_path:
node['label'] = node_name
else:
node['label'] = os.path.basename(node_name)
# Take up to the last two directory names as the group
directories = os.path.dirname(node_name).split(os.sep)
begin = len(directories) - self.group_granularity
node['group'] = os.sep.join(directories[begin:begin + 2])
# Make the initial starting point random, but very small, so we get
# an "explosion"/"expansion" effect.
node['x'] = random.random() * 0.01
node['y'] = random.random() * 0.01
self.nodes[node_name] = node
return node
def _add_edge(self, source, target):
'''
Adds an edge to the graph.
Args:
source: The entry of the source node
target: The entry of the target node
Returns:
The newly created edge object
'''
edge = {}
edge['id'] = len(self.edges)
edge['size'] = 10 # Make the arrows larger?
edge['type'] = 'curvedArrow'
# The natural direction is "includes", so swap if we want "included-by"
if self.is_included_by_relation:
source, target = target, source
edge['source'] = source['id']
edge['target'] = target['id']
self.edges.append(edge)
return edge
def __repr__(self):
'''
Returns:
A string representation of the graph.
'''
nodes = len(self.nodes)
edges = len(self.edges)
return ''.format(nodes, edges)
================================================
FILE: ig/main.py
================================================
'''Entry point and command line parsing for ig.'''
from __future__ import print_function
import argparse
import logging
import os
import sys
from ig import colors, graph, serve, walk
def setup_logging():
'''Sets up the root logger.'''
handler = logging.StreamHandler(sys.stderr)
formatter = logging.Formatter('[%(levelname)s] %(message)s')
handler.setFormatter(formatter)
log = logging.getLogger(__package__)
log.addHandler(handler)
log.setLevel(logging.INFO)
return log
def parse_arguments(args):
'''
Sets up the command line argument parser and parses arguments.
Args:
args: The list of argumnets passed to the command line
Returns:
The parsed arguments.
'''
parser = argparse.ArgumentParser(description='Visualize C++ include graphs')
parser.add_argument('directories',
nargs='+',
help='The directories to inspect')
parser.add_argument('--pattern',
action='append',
default=['*.[ch]pp', '*.[ch]'],
dest='patterns',
help='The file (glob) patterns to look for')
parser.add_argument('-i', '-I', '--prefix',
action='append',
dest='prefixes',
default=[os.getcwd()],
help='An include path for headers to recognize')
parser.add_argument('-v', '--verbose',
action='store_true',
help='Turn on verbose output')
parser.add_argument('-p', '--port',
type=int,
default=8080,
help='The port to serve the visualization on')
parser.add_argument('-o', '--open',
action='store_true',
help='Open the webpage immediately')
parser.add_argument('-j', '--json',
action='store_true',
help='Print the graph JSON instead of serving it')
parser.add_argument('-d', '--dir',
dest='directory',
help='The directory to store the served files in. If '
'not supplied, a temporary directory is created.')
parser.add_argument('--relation',
choices=['includes', 'included-by'],
default='included-by',
help='The relation of edges in the graph')
parser.add_argument('--min-degree',
type=float,
default=0.1,
help='The initial minimum degree nodes should have to '
'be displayed')
parser.add_argument('--group-granularity',
type=int,
default=2,
help='How coarse to group nodes (by folder)')
parser.add_argument('--full-path',
action='store_true',
help='If set, shows the full path for nodes')
parser.add_argument('--colors',
type=lambda p: colors.Colors(map(int, p.split(','))),
default='234, 82, 77',
help='The base RGB colors separated by commas')
parser.add_argument('--color-variation',
type=int,
default=200,
help='The variation in RGB around the base colors')
parser.add_argument('--color-alpha-min',
type=float,
default=0.7,
help='The minimum alpha value for colors')
args = parser.parse_args(args)
# Necessary for standard includes
args.prefixes.append('')
if not (0 <= args.color_alpha_min <= 1):
raise RuntimeError('--color-alpha-min must be in interval [0, 1]')
args.colors.variation = args.color_variation
args.colors.alpha_min = args.color_alpha_min
return args
def make_json(args, graph_json):
'''
Creates the JSON payload for the visualization.
Args:
args: The command line arguments.
graph_json: The JSON dict from the graph.
Returns:
The payload.
'''
if args.json:
print(graph_json)
sys.exit(0)
# Additional settings to configure the visualization
settings = dict(initialDegree=args.min_degree)
return dict(settings=settings, graph=graph_json)
def main():
log = setup_logging()
args = parse_arguments(sys.argv[1:])
if args.verbose:
log.setLevel(logging.DEBUG)
log.debug('Received arguments: %s', args)
include_graph = graph.Graph(args.relation,
args.full_path,
args.colors,
args.group_granularity)
walk.walk(include_graph, args)
if include_graph.is_empty:
log.debug('Could not find a single node, exiting')
sys.exit(-1)
json = make_json(args, include_graph.to_json())
with serve.Server(args.directory) as server:
server.write(json)
server.run(args.open, args.port)
log.info('Shutting down')
if __name__ == '__main__':
main()
================================================
FILE: ig/paths.py
================================================
'''Handles searching for the WWW path and copying operations.'''
import logging
import os
import shutil
import tempfile
log = logging.getLogger(__name__)
WWW = os.path.join(os.path.dirname(os.path.realpath(__file__)),
os.pardir,
'www')
if not os.path.exists(WWW):
message = 'Could not find www directory for ig: {0}'
raise EnvironmentError(message.format(WWW))
def create_directory(directory):
'''
(Maybe) creates a directory and copies the `www` folder to it.
Args:
directory: Optionally, an existing directory to copy to.
Returns:
The path of the possibly created directory.
'''
if directory is None:
directory = tempfile.mkdtemp(prefix='ig-')
log.debug('Created temporary directory %s', directory)
# Has to not exist for copytree
if os.path.exists(directory):
shutil.rmtree(directory)
shutil.copytree(WWW, directory)
log.debug('Copied contents of www folder to %s', directory)
return directory
================================================
FILE: ig/serve.py
================================================
'''The server that serves the web visualization.'''
import json
import logging
import os
import shutil
import socket
import webbrowser
from ig import paths
try:
import socketserver
import http.server as http
except ImportError:
import SocketServer as socketserver
import SimpleHTTPServer as http
log = logging.getLogger(__name__)
class Server(object):
def __init__(self, directory):
'''
Constructor.
Args:
directory: The directory to serve from.
'''
self.delete_directory = directory is None
self.directory = paths.create_directory(directory)
def run(self, open_immediately, port):
'''
Serves the `www` directory.
Args:
open_immediately: Whether to open the web browser immediately
port: The port at which to serve the graph
'''
os.chdir(self.directory)
handler = http.SimpleHTTPRequestHandler
handler.extensions_map.update({
'.webapp': 'application/x-web-app-manifest+json',
})
server = socketserver.TCPServer(('', port), handler)
server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
address = 'http://localhost:{0}/graph.html'.format(port)
log.info('Serving at %s', address)
if open_immediately:
log.debug('Opening webbrowser')
webbrowser.open(address)
server.serve_forever()
def write(self, payload):
'''
Writes the given JSON representation to the served location.
Args:
payload: The playlod to JSONify and store.
'''
path = os.path.join(self.directory, 'graph.json')
with open(path, 'w') as graph_file:
graph_file.write(json.dumps(payload, indent=4))
log.debug('Wrote graph file to {0}'.format(path))
def cleanup(self):
if self.delete_directory:
assert self.directory is not None
shutil.rmtree(self.directory, ignore_errors=True)
log.debug('Deleted directory %s', self.directory)
def __enter__(self):
return self
def __exit__(self, error_type, error_value, traceback):
self.cleanup()
if error_type == KeyboardInterrupt:
return True # Supresses the exception
# Any other exception is propagated up
================================================
FILE: ig/walk.py
================================================
from __future__ import print_function
import fnmatch
import logging
import os
import re
import sys
log = logging.getLogger(__name__)
INCLUDE_PATTERN = re.compile(r'^#include ["<](.*)[">]$')
def try_prefixes(path, prefixes):
'''
Tries to prepend a list of prefixes to a path to see if any exists.
This is necessary to ensure that we have unique file paths, even if the same
file is specified differently (sometimes relative to one directory, then to
another etc.).
Args:
path: The path to append to the prefixes
perfixes: The prefixes to prepend to the path
Returns:
The best possible path.
'''
for prefix in prefixes:
full_path = os.path.realpath(os.path.join(prefix, path))
if os.path.exists(full_path):
return full_path
return path
def get_includes(filename, prefixes):
'''
Parses out the includes from a file.
Args:
filename: The name of the file to get includes for
prefixes: The prefixes under which to search for includes
Returns:
A list of includes for the file.
'''
includes = set()
with open(filename) as source:
for line in source:
match = INCLUDE_PATTERN.match(line)
if match is not None:
full_path = try_prefixes(match.group(1), prefixes)
includes.add(full_path)
return includes
def glob(directory, pattern):
'''
Globs for files patterns under a directory.
There is a `glob` module, but its recursive variant only works in Python3.
This is a short DIY version of recursive globbing.
Args:
directory: The root directory
pattern: The pattern to glob for
Yields:
Any matching files (with absolute paths).
'''
for root, _, filenames in os.walk(directory):
for filename in fnmatch.filter(filenames, pattern):
yield os.path.join(root, filename)
def walk(graph, args):
'''
Walks the file tree, populating the graph.
Args:
graph: The empty graph to populate
args: The arguments passed to the command line
Returns:
The (possibly) populated graph.
'''
for directory in args.directories:
# Swap pattern <-> filename loops if too inefficient
for pattern in args.patterns:
path = os.path.realpath(directory)
for filename in glob(path, pattern):
if os.path.isdir(filename):
log.debug('%s is a directory, skipping', filename)
continue
includes = get_includes(filename, [path] + args.prefixes)
graph.add(filename, includes)
log.debug('Resulting graph: %s', repr(graph))
================================================
FILE: scripts/__init__.py
================================================
================================================
FILE: scripts/bump.py
================================================
"""Version-bumping script."""
from __future__ import print_function
import os.path
import re
def bump(match):
"""Bumps the version"""
before, old_version, after = match.groups()
major, minor, patch = map(int, old_version.split('.'))
patch += 1
if patch == 10:
patch = 0
minor += 1
if minor == 10:
minor = 0
major += 1
new_version = '{0}.{1}.{2}'.format(major, minor, patch)
print('{0} => {1}'.format(old_version, new_version))
return before + new_version + after
def main():
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
project = os.path.split(root)[-1]
init_path = os.path.join(root, project, '__init__.py')
with open(init_path) as source:
original = source.read()
pattern = re.compile(r'^(\s*__version__\s*=\s*[\'"])([^\'"]*)([\'"])', re.M)
replaced = re.sub(pattern, bump, original)
if replaced == original:
raise RuntimeError('Could not find version!')
with open(init_path, 'w') as destination:
destination.write(replaced)
if __name__ == '__main__':
main()
================================================
FILE: setup.cfg
================================================
[wheel]
universal=1
================================================
FILE: setup.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
setup.py script for setuptools.
"""
import re
from setuptools import setup, find_packages
with open('ig/__init__.py') as init:
text = init.read()
match = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', text, re.M)
version = match.group(1)
with open('README.md') as readme:
long_description = readme.read()
setup(
name='ig-cpp',
version=version,
description='A tool to visualize include graphs for C++ projects',
long_description=long_description,
url="https://github.com/goldsborough/ig",
license='MIT',
author='Peter Goldsborough',
author_email='peter@goldsborough.me',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Topic :: Software Development',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5'
],
keywords='visualization C++ tool',
packages=find_packages(exclude=['www']),
include_package_data=True,
package_data=dict(ig=[
'../README.md',
'../Makefile',
'../www/*',
'../www/sigma/*'
]),
entry_points=dict(console_scripts=['ig = ig.main:main'])
)
================================================
FILE: www/graph.html
================================================
filters
degree 0
0
0
groups
================================================
FILE: www/graph.js
================================================
'use strict';
const $ = id => document.getElementById(id);
function createFilter(instance, settings) {
// Initialize the Filter API
const filter = new sigma.plugins.filter(instance);
const maximumDegree = visualizePane(instance.graph, filter);
function applyMinDegreeFilter(value) {
if (typeof value === 'object') {
value = value.target.value;
}
$('min-degree').value = value;
$('min-degree-val').textContent = value;
filter
.undo('min-degree')
.nodesBy(node => instance.graph.degree(node.id) >= value, 'min-degree')
.apply();
}
function applyGroupFilter(element) {
let group = element.target[element.target.selectedIndex].value;
filter
.undo('node-group')
.nodesBy(node => !group || node.group === group, 'node-group')
.apply();
}
// for Chrome and FF
$('min-degree').addEventListener('input', applyMinDegreeFilter);
// for IE10+, that sucks
$('min-degree').addEventListener('change', applyMinDegreeFilter);
$('node-group').addEventListener('change', applyGroupFilter);
let degree = settings.initialDegree;
if (degree < 1) {
// Assume it's a fraction.
degree = Math.ceil(maximumDegree * degree);
}
applyMinDegreeFilter(Math.min(maximumDegree, degree));
}
function visualizePane(graph, filter) {
let maximumDegree = 0, categories = {};
// Collect the maximum degree and categories.
graph.nodes().forEach(node => {
maximumDegree = Math.max(maximumDegree, graph.degree(node.id));
categories[node.group] = true;
})
// Set the slider values.
$('min-degree').max = maximumDegree;
$('max-degree-value').textContent = maximumDegree;
// Set up the node group combo box.
const nodeGroup = $('node-group');
Object.keys(categories).forEach(function(group) {
if (group.length === 0) return;
let option = document.createElement('option');
option.text = group;
nodeGroup.add(option);
});
return maximumDegree;
}
function visualize(json) {
console.log(json);
const instance = new sigma({
graph: json.graph,
renderer: {
container: 'graph-container',
type: 'canvas',
skipErrors: true,
labelThreshold: 0,
labelSize: 'proportional'
}
});
instance.startForceAtlas2({
worker: true,
barnesHutOptimize: true,
adjustSizes: true,
slowDown: 20,
strongGravityMode: true
});
createFilter(instance, json.settings);
const drag = sigma.plugins.dragNodes(instance, instance.renderers[0]);
drag.bind('startdrag', event => {
if (instance.isForceAtlas2Running()) {
instance.killForceAtlas2()
}
});
}
const xhr = new XMLHttpRequest();
xhr.open('GET', 'graph.json');
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
visualize(JSON.parse(xhr.responseText));
}
}
xhr.send();
================================================
FILE: www/sigma/LICENSE.txt
================================================
Copyright (C) 2013-2014, Alexis Jacomy, http://sigmajs.org
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
================================================
FILE: www/sigma/README.md
================================================
[](https://travis-ci.org/jacomyal/sigma.js)
sigma.js - v1.2.0
=================
Sigma is a JavaScript library dedicated to graph drawing, mainly developed by [@jacomyal](https://github.com/jacomyal) and [@Yomguithereal](https://github.com/Yomguithereal).
### Resources
[The website](http://sigmajs.org) provides a global overview of the project, and the documentation is available in the [Github Wiki](https://github.com/jacomyal/sigma.js/wiki).
Also, the `plugins` and `examples` directories contain various use-cases that might help you understand how to use sigma.
### How to use it
To use it, clone the repository:
```
git clone git@github.com:jacomyal/sigma.js.git
```
To build the code:
- Install [Node.js](http://nodejs.org/).
- Install [gjslint](https://developers.google.com/closure/utilities/docs/linter_howto?hl=en).
- Use `npm install` to install sigma development dependencies.
- Use `npm run build` to minify the code with [Uglify](https://github.com/mishoo/UglifyJS). The minified file `sigma.min.js` will then be accessible in the `build/` folder.
Also, you can customize the build by adding or removing files from the `coreJsFiles` array in `Gruntfile.js` before applying the grunt task.
### Contributing
You can contribute by submitting [issues tickets](http://github.com/jacomyal/sigma.js/issues) and proposing [pull requests](http://github.com/jacomyal/sigma.js/pulls). Make sure that tests and linting pass before submitting any pull request by running the command `grunt`.
The whole source code is validated by the [Google Closure Linter](https://developers.google.com/closure/utilities/) and [JSHint](http://www.jshint.com/), and the comments are written in [JSDoc](http://en.wikipedia.org/wiki/JSDoc) (tags description is available [here](https://developers.google.com/closure/compiler/docs/js-for-compiler)).
================================================
FILE: www/sigma/sigma.canvas.edges.curvedArrow.js
================================================
;(function() {
'use strict';
sigma.utils.pkg('sigma.canvas.edges');
/**
* This edge renderer will display edges as curves with arrow heading.
*
* @param {object} edge The edge object.
* @param {object} source node The edge source node.
* @param {object} target node The edge target node.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.edges.curvedArrow =
function(edge, source, target, context, settings) {
var color = edge.color,
prefix = settings('prefix') || '',
edgeColor = settings('edgeColor'),
defaultNodeColor = settings('defaultNodeColor'),
defaultEdgeColor = settings('defaultEdgeColor'),
cp = {},
size = edge[prefix + 'size'] || 1,
tSize = target[prefix + 'size'],
sX = source[prefix + 'x'],
sY = source[prefix + 'y'],
tX = target[prefix + 'x'],
tY = target[prefix + 'y'],
aSize = Math.max(size * 2.5, settings('minArrowSize')),
d,
aX,
aY,
vX,
vY;
cp = (source.id === target.id) ?
sigma.utils.getSelfLoopControlPoints(sX, sY, tSize) :
sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);
if (source.id === target.id) {
d = Math.sqrt(Math.pow(tX - cp.x1, 2) + Math.pow(tY - cp.y1, 2));
aX = cp.x1 + (tX - cp.x1) * (d - aSize - tSize) / d;
aY = cp.y1 + (tY - cp.y1) * (d - aSize - tSize) / d;
vX = (tX - cp.x1) * aSize / d;
vY = (tY - cp.y1) * aSize / d;
}
else {
d = Math.sqrt(Math.pow(tX - cp.x, 2) + Math.pow(tY - cp.y, 2));
aX = cp.x + (tX - cp.x) * (d - aSize - tSize) / d;
aY = cp.y + (tY - cp.y) * (d - aSize - tSize) / d;
vX = (tX - cp.x) * aSize / d;
vY = (tY - cp.y) * aSize / d;
}
if (!color)
switch (edgeColor) {
case 'source':
color = source.color || defaultNodeColor;
break;
case 'target':
color = target.color || defaultNodeColor;
break;
default:
color = defaultEdgeColor;
break;
}
context.strokeStyle = color;
context.lineWidth = size;
context.beginPath();
context.moveTo(sX, sY);
if (source.id === target.id) {
context.bezierCurveTo(cp.x2, cp.y2, cp.x1, cp.y1, aX, aY);
} else {
context.quadraticCurveTo(cp.x, cp.y, aX, aY);
}
context.stroke();
context.fillStyle = color;
context.beginPath();
context.moveTo(aX + vX, aY + vY);
context.lineTo(aX + vY * 0.6, aY - vX * 0.6);
context.lineTo(aX - vY * 0.6, aY + vX * 0.6);
context.lineTo(aX + vX, aY + vY);
context.closePath();
context.fill();
};
})();
================================================
FILE: www/sigma/sigma.canvas.labels.def.js
================================================
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.canvas.labels');
/**
* This label renderer will just display the label on the right of the node.
*
* @param {object} node The node object.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.labels.def = function(node, context, settings) {
var fontSize,
prefix = settings('prefix') || '',
size = node[prefix + 'size'];
// Hardcoded: always show labels
// if (size < settings('labelThreshold'))
// return;
if (!node.label || typeof node.label !== 'string')
return;
// Hardcoded: Make labels proportionally sized
fontSize = settings('labelSizeRatio') * size;
// fontSize = (settings('labelSize') === 'fixed') ?
// settings('defaultLabelSize') :
// settings('labelSizeRatio') * size;
context.font = (settings('fontStyle') ? settings('fontStyle') + ' ' : '') +
fontSize + 'px ' + settings('font');
context.fillStyle = (settings('labelColor') === 'node') ?
(node.color || settings('defaultNodeColor')) :
settings('defaultLabelColor');
context.fillText(
node.label,
Math.round(node[prefix + 'x'] + size + 3),
Math.round(node[prefix + 'y'] + fontSize / 3)
);
};
}).call(this);
================================================
FILE: www/style.css
================================================
body {
color: #333;
font-size: 14px;
font-family: Lato, sans-serif;
}
#graph-container {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
background: rgb(249, 247, 237); /* A light beige */
}
#control-pane {
top: 10px;
right: 10px;
position: absolute;
width: 230px;
background-color: rgb(249, 247, 237);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
#control-pane > div {
margin: 10px;
overflow-x: auto;
}
.line {
clear: both;
display: block;
width: 100%;
margin: 0;
padding: 12px 0 0 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
background: transparent;
}
h2, h3 {
padding: 0;
text-transform: uppercase;
}
h2.underline {
background: #f4f0e4;
margin: 0;
border-radius: 2px;
padding: 8px 12px;
font-weight: 700;
}
input[type=range] {
width: 160px;
}