Commit ddb89c68 by Onlynagesha

init

parents
## Introduction
Visualization of C2IC algorithm.
## Usage
1. `cd current_directory`
2. `python -m http.server PORT-NUMBER` (e.g. 8888)
3. Open in web browser with URL 127.0.0.1:PORT-NUMBER (e.g. 127.0.0.1:8888)
4. Parameters can be adjusted via config.json
// const { default: G6 } = require("@antv/g6");
/**
* Requires: utils.js
*/
/**
* Helper class for processing node or edge states.
*/
class _StateItems {
/**
* Constructs a helper object.
* @param {String} namespace Namespace for AntV G6 state machanism.
* Each state in AntV G6 is named as `namespace:stateName`.
*/
constructor(namespace) {
this.dest = {};
this.namespace = namespace;
}
/**
* Adds a state as an object with the following attributes:
* * `category`: Category of the state
* * `name`: Name of the state
* * `fullName`: Full name used for AntV G6, `fullName = 'namespace:name'`
* * `model`: Model of node style parameters, including `size`, `style`
*
* Each state item added can be accessed via `states[name]` where `states` is the object
* obtained by `get()` method.
*
* Supports method chaining.
* @param {String} category See above
* @param {String} name See above
* @param {Object} model See above
* @returns this
*/
add(category, name, model) {
this.dest[name] = Object.freeze({
'category': category,
'name': name,
'fullName': `${this.namespace}:${name}`,
'model': model
});
return this;
}
/**
* Gets the state collection object.
* @returns A read-only object that contains all the states as attributes.
*/
get() {
return Object.freeze(this.dest);
}
}
/**
* Gets the coordinate by X axis of given node.
* @param {Object} node The node object
* @returns A floating point, X axis coordinate.
*/
function _getX(node) {
// A hack is used by accessing _cfg directly.
return node._cfg.model.x;
}
/**
* Gets the coordinate by Y axis of given node.
* @param {Object} node The node object
* @returns A floating point, Y axis coordinate.
*/
function _getY(node) {
// A hack is used by accessing _cfg directly.
return node._cfg.model.y;
}
/**
* Gets the coordinate by the specified axis of given node.
* @param {Object} node The node object
* @param {String} axis `x` or `y`
* @returns A floating point, coordinate of the corresponding axis.
*/
function _getPos(node, axis) {
return node._cfg.model[axis];
}
/**
* Gets the coordinate of given node as an array of numbers `[x, y]`.
* @param {Object} node The node object
* @returns An array of length 2, `[x, y]`
*/
function _getPos2D(node) {
return [_getX(node), _getY(node)];
}
/**
* Gets the identifier of animation node of given indices.
*
* Animation nodes are aggregated as G groups. Each group contains K nodes moving with different speed.
* @param {Number} i Group index, which group is this animation node in, ranged as 0, 1 ... G-1
* @param {Number} j Node index in the group, ranged as 0, 1 ... K-1
* @returns Identifier of the animation node.
*/
function _getAnimationNodeId(i, j) {
return `@Animation(${i}, ${j})`;
}
/**
* Checks whether the node identifier belongs to an animation node.
* @param {String} id Identifier of the node.
* @returns Whether the identifier is of an animation node.
*/
function _isAnimationNodeId(id) {
// Consistent with _getAnimationNodeId
return id.startsWith('@Animation');
}
/**
* Gets the opacity of the given node or edge.
* @param {Object} obj The node or edge object.
* @returns The opacity value
*/
function _getOpacity(obj) {
return obj._cfg.model.style.opacity;
}
/**
* Creates a table of node states.
* The states consist of the following categories:
* * `default`: contains state sub-names `default` and `boosted`;
* * `Ca`: contains state sub-names `default`, `source`, `boosted` and `animation`;
* * `Ca+`: Same as above;
* * `Cr`: Same as above;
* * `Cr-`: Same as above.
* Each state is named as the format `category,subName` (e.g. `Ca+,boosted`)
* @param {Object} styles Arguments of node styles obtained via `readConfig().nodeStyles`
* @param {String} namespace Which namespace is used in AntV G6 state mechanism, `nodeState` by default.
* @returns The object of node state table.
* @see _StateItems for details
*/
function createNodeStates(styles, namespace = 'nodeState') {
const items = new _StateItems(namespace);
['default', 'boosted'].forEach((t) => {
items.add('default', t, styles[t]);
});
['Ca', 'Ca+', 'Cr', 'Cr-'].forEach((s) => {
['default', 'boosted', 'source', 'animation'].forEach((t) => {
items.add(s, `${s},${t}`, styles[s][t]);
});
});
return items.get();
}
/**
* Creates a table of edge states.
* @param {Object} styles Arguments of node styles obtained via `readConfig().edgeStyles`
* @param {String} namespace Which namespace is used in AntV G6 state mechanism, `edgeState` by default.
* @returns The object of edge state table.
* @see _StateItems for details.
*/
function createEdgeStates(styles, namespace = 'edgeState') {
const items = new _StateItems(namespace);
['default', 'Ca', 'Ca+', 'Cr', 'Cr-'].forEach((t) => {
items.add(t, t, styles[t]);
});
return items.get();
}
/**
* Appends attributes to the canvas object and each node and edge inside the canvas.
* This is a helper function that shall be called by `createCanvas()` function,
* after adding all the nodes (including the animation nodes) and edges.
*
* The following attributed are added to the canvas object:
* * `n`: Number of nodes in the graph;
* * `m`: Number of edges in the graph;
* * `propData`: Propagation data transformed from the `data` object;
* * `nodeStates`: Collection of node states;
* * `edgeStates`: Collection of edge states;
* * `nNodes`: An object of node count, with the following attributes (consistent of node state categories):
* * `default`: Number of nodes without any state, i.e. no message received yet;
* * `Ca`: Number of nodes of state Ca, i.e. with non-boosted positive message;
* * `Ca+`: Number of nodes of state Ca+, i.e. with boosted positive message;
* * `Cr`: Number of nodes of state Cr, i.e. with non-boosted negative message;
* * `Cr-`: Number of nodes of state Cr-, i.e. with boosted negative message.
* * `nAnimationGroupsUsed`: How many groups of animation nodes are used currently;
* * `animationGroups`: Groups of animation nodes;
* * `staticNodes`: List of all the non-animation nodes, i.e. nodes originally in the graph.
*
* `propData` contains the attributes given in `createEventObject` function (in utils.js).
*
* Each object in `sourceEvents` 2D-array contains the following attributes:
* * `node`: A reference to the node object in the canvas as information source;
* * `state`: A reference to the state object that the current node shall be.
*
* Each object in `propagationEvents` 2D-array contains the following attributes:
* * `source`: A reference to the node object in the canvas which sends its information;
* * `target`: A reference to the node object in the canvas which receives the information;
* * `edge`: A reference to the corresponding edge object in the canvas;
* * `edgeState`: A reference to the edge state object that the edge shall be;
* * `animationState`: A reference to the node state object that the animation nodes along the edge shall be;
* * `targetState`: A reference to the node state object that the target node shall be.
*
* `animationGroups` is a list with length `G = propData.maxNPropagations`.
* Each group `animationGroups[i]` (i = 0...G-1) is a list of length `args.nAnimationNodes`,
* references to the animation nodes in the canvas, each assigned with different speed.
*
* Each node object in the canvas has the following attributes appended:
* * `id`: Identifier of the node;
* * `state`: State of the node, one of the objects in `canvas.nodeStates`;
* * `isBoosted`: Whether this node is a boosted node;
* * (Animation nodes only) `speed`: Speed of the animation node.
*
* Each edge object in the canvas has the following attributes appended:
* * `id`: Identifier of the edge;
* * `state`: State of the edge, one of the objects in `canvas.edgeStates`.
*
* @param {Object} canvas The canvas object obtained by calling `createCanvas()` function
* @param {Object} graph The graph object obtained via `readGraph()` function
* @param {Object} data The data object obtained via `readData()` function
* @param {Object} args Additional arguments obtained via `readConfig()` function
* @param {Object} nodeStates Node state collection obtained via `createNodeStates()` function
* @param {Object} edgeStates Edge state collection obtained via `createEdgeStates()` function
* @see createEventObject in input.js for details of attributes in `canvas.propData`.
*/
function _appendCanvasAttributes(canvas, graph, data, args, nodeStates, edgeStates) {
// Appends attributes for the canvas
// (1) data (details see createEventObject() in input.js)
createAttribute(canvas, 'propData', createEventObject(
data.label, data.nRounds, data.boostedNodes, data.maxNPropagations
));
// (2) Collection of node & edge states
createAttribute(canvas, 'nodeStates', nodeStates);
createAttribute(canvas, 'edgeStates', edgeStates);
// (3) Object of n, m and counting variables
createAttribute(canvas, 'n', graph.n);
createAttribute(canvas, 'm', graph.m);
createAttribute(canvas, 'nNodes', {
'default': graph.n, 'Ca': 0, 'Ca+': 0, 'Cr': 0, 'Cr-': 0
});
createAttribute(canvas, 'nAnimationGroupsUsed', 0);
// (4) Groups animation nodes as `G = maxNPropagation` groups, each group has `K = nAnimationNodes` nodes.
createAttribute(canvas, 'animationGroups', []);
d3.range(data.maxNPropagations).forEach((i) => {
canvas.animationGroups[i] = [];
d3.range(args.nAnimationNodes).forEach((j) => {
const node = canvas.findById(_getAnimationNodeId(i, j));
// Puts to i-th group
canvas.animationGroups[i].push(node);
// Additional parameters for current animation node
// Speed of animation nodes in a group are v, v+d, v+2d ... v+(K-1)d resp.
node.speed = args.initialSpeed + j * args.speedIncrement
});
});
// Appends attributes for each node
canvas.getNodes().forEach((node) => {
// (n-1) id: A hack is used here by accessing _cfg directly
createAttribute(node, 'id', node._cfg.id);
// (n-2) state: State of this node, one of attributes in canvas.nodeStates
createAttribute(node, 'state', canvas.nodeStates.default);
});
// (n-3) isBoosted: true/false for boosted/non-boosted nodes
data.boostedNodes.forEach((id) => {
createAttribute(canvas.findById(id), 'isBoosted', true);
});
// False for others
canvas.getNodes().forEach((node) => {
if (node.isBoosted == undefined) {
node.isBoosted = false;
}
})
// Appends attributes for each edge
canvas.getEdges().forEach((edge) => {
// (e-1) id: A hack is used here by accessing _cfg directly
edge.id = edge._cfg.id;
// (e-2) state: State of this edge, one of the attributes in canvas.edgeStates
edge.state = canvas.edgeStates.default;
});
// (Cont.) Appends attributes for the canvas
// (5) List of static nodes
createAttribute(canvas, 'staticNodes', canvas.findAll('node', (node) => {
return !_isAnimationNodeId(node.id);
}));
// (1) cont.
d3.range(data.nRounds).forEach((t) => {
// Transforms sourceEvents[t][]
data.sourceEvents[t].forEach((rawEvt) => {
const node = canvas.findById(rawEvt.id);
canvas.propData.sourceEvents[t].push({
'node': node,
// Takes the source state
'state': canvas.nodeStates[`${rawEvt.type},source`]
});
});
// Transforms propagationEvents[t][]
data.propagationEvents[t].forEach((rawEvt) => {
const u = canvas.findById(rawEvt.source);
const v = canvas.findById(rawEvt.target);
canvas.propData.propagationEvents[t].push({
'source': u,
'target': v,
'edge': canvas.findById(getEdgeId(u.id, v.id)),
'edgeState': canvas.edgeStates[rawEvt.edgeType],
// Animation node states are consistent of edge state (typically, the same color as the edge)
'animationState': canvas.nodeStates[`${rawEvt.edgeType},animation`],
// If v is a boosted node, then changes to the `'boosted'` alternative. Otherwise `'default'`.
'targetState': canvas.nodeStates[`${rawEvt.targetType},${v.isBoosted ? 'boosted' : 'default'}`]
});
})
});
}
/**
* Creates am AntV G6 canvas object and initializes with given graph and propagation data.
* @param {*} graph The graph object obtained via `readGraph()` function
* @param {*} data The data object obtained via `readData()` function
* @param {string} container Label of the container element
* @param {number} width Width of the canvas
* @param {number} height Height of the canvas
* @param {*} args Additional arguments obtained via `readConfig()` function
* @returns The canvas object created.
*/
function createCanvas(graph, data, container, width, height, args) {
function extractArgs(attrName, defaultValue, mapFn = null) {
let res = null;
if (args[attrName] == undefined) {
console.log(`'${attrName}' uses default value: `, defaultValue);
res = defaultValue;
} else {
res = args[attrName];
}
if (mapFn != null) {
res = mapFn(res);
console.log(`'${attrName}' maps to: `, res);
}
return res;
}
const nodeStates = createNodeStates(args.nodeStyles);
const edgeStates = createEdgeStates(args.edgeStyles);
// Transforms node/edge states to [node|edge]StateStyles parameter:
// namespace:state => model.style
function createStateStylesArg(states) {
const res = {};
Object.values(states).forEach((obj) => {
res[obj.fullName] = obj.model.style;
});
return res;
}
const fitViewPadding = extractArgs("fitViewPadding", 0, (r) => {
// Maps from ratio to size value
if (typeof(r) == "number") {
return [r * height, r * width, r * height, r * width];
} else {
return [r[0] * height, r[1] * width, r[2] * height, r[3] * width];
}
});
const canvas = new G6.Graph({
container: container,
width: width,
height: height,
// Default style of nodes
defaultNode: args.nodeStyles.default,
// Default style of edges
defaultEdge: args.edgeStyles.default,
// Let the graph fit the canvas,
fitView: true,
fitViewPadding: fitViewPadding,
// Node styles for each state (attribute 'size' etc. shall be set separately)
nodeStateStyles: createStateStylesArg(nodeStates),
// Graph layout style
layout: extractArgs("graphLayout", {
"type": "random",
// Prevents overlapping of nodes
"preventOverlap": true,
}),
animate: true,
animateCfg: {
duration: 1000,
},
});
// Initializes with the graph contents
canvas.read(graph);
canvas.updateLayout({
"type": "gForce",
// Prevents overlapping of nodes
"preventOverlap": true,
"nodeStrength": 20, // Default: 1000
"edgeStrength": 200, // Default: 200,
"onLayoutEnd": () => {
console.log(`animate = ${canvas.get('animate')}`);
console.log(`animateCfg = `, canvas.get('animateCfg'));
canvas.fitView(fitViewPadding);
}
});
// Adds animation nodes
d3.range(data.maxNPropagations).forEach((i) => {
d3.range(args.nAnimationNodes).forEach((j) => {
canvas.addItem('node', {
'id': _getAnimationNodeId(i, j)
});
});
});
canvas.refresh();
// Appends attributes.
_appendCanvasAttributes(canvas, graph, data, args, nodeStates, edgeStates);
return canvas;
}
/**
* Changes the state of the given node, with the following actions:
* * The new state is rendered to canvas immediately;
* * Count of nodes are updated;
* * The attribute `node.state` is updated as the current one.
*
* If `node` is provided as a string, the node object is searched by `canvas.findById(node)`.
*
* If `state` is provided as a string, the state is accessed via `canvas.nodeStates[state]`.
* @param {Object} canvas The canvas object;
* @param {Object | String} node The node object, or the identifier of the node;
* @param {Object | String} state The state object, or the name of the state.
*/
function setNodeState(canvas, node, state) {
// Transforms state to Object
if (typeof(state) == 'string' || state instanceof String) {
state = canvas.nodeStates[state];
}
// Transforms node to Object inside canvas
if (typeof(node) == 'string' || node instanceof String) {
node = canvas.findById(node);
}
function incCount(state, delta = 1) {
canvas.nNodes[state.category] += delta;
}
// No nothing if no change occurs
if (node.state == state) {
return;
}
// Otherwise, node states changes its category.
// Decreases count of old state by 1
incCount(node.state, -1);
// Sets to new state, and then adds count of new state by 1
node.state = state;
incCount(node.state, 1);
// Sets style
canvas.setItemState(node, 'nodeState', state.name);
// Sets size
canvas.updateItem(node, {size: state.model.size});
}
/**
* Changes the state of given edge, with the following actions:
* * The new state is rendered to canvas immediately;
* * The attribute `node.state` is updated as the current one.
*
* If `edge` is provided as a string, the edge is searched by `canvas.findById(edge)`.
*
* If `state` is provided as a string, the state object is accessed via `canvas.edgeStates`.
* @param {Object} canvas The canvas object;
* @param {Object | String} edge The edge object, or the identifier of the edge;
* @param {Object | String} state The state object, or the name of the state.
*/
function setEdgeState(canvas, edge, state) {
// Transforms state to Object
if (typeof(state) == 'string' || state instanceof String) {
state = canvas.edgeStates[state];
}
// Transforms node to Object inside canvas
if (typeof(edge) == 'string' || edge instanceof String) {
edge = canvas.findById(node);
}
edge.state = state;
// Sets style
canvas.updateItem(edge, {
'style': state.model.style
});
}
/**
* Initializes the first `g` groups of animation nodes in the canvas object, with the following actions:
* * Hides all the animation nodes.
*
* @param {Object} canvas The canvas object;
* @param {Number} nGroups `g` above, How many groups of animation nodes to initialize.
* Initializes all the groups if `nGroups` not provided.
*/
function initializeAnimationNodes(canvas, nGroups = null) {
if (nGroups == null) {
nGroups = canvas.animationGroups.length; // Takes all the groups by default.
}
// Clears animation nodes
d3.range(nGroups).forEach((i) => {
canvas.animationGroups[i].forEach((node) => {
canvas.hideItem(node); // Hides all the animation nodes requested before.
})
});
}
/**
* Initializes the canvas at the beginning of each round, with the following actions:
* * Resets all the animation nodes requested, including:
* * Hides all the animation nodes requested;
* * Resets count of used animation node groups to 0.
*
* @param {Object} canvas The canvas object.
* @see initializeAnimationNodes for details of resetting each animation node group.
*/
function initializeRound(canvas) {
// Clears animation nodes
initializeAnimationNodes(canvas, canvas.nAnimationGroupsUsed);
// Resets group count
canvas.nAnimationGroupsUsed = 0;
}
/**
* Initializes the canvas before starting the whole visualization process, with the following actions:
* * Sets each node state and edge state to default;
* * Resets all the animation nodes as hidden;
* * Resets all the counting variables in canvas.nNodes.
*
* @param {Object} canvas The canvas object.
*/
function initialize(canvas) {
// Resets states of static nodes and edges
canvas.staticNodes.forEach((node) => {
// For boosted nodes, sets to the `'boosted`' alternative. Otherwise `'default'`.
setNodeState(canvas, node, node.isBoosted ? canvas.nodeStates.boosted : canvas.nodeStates.default);
});
canvas.getEdges().forEach((edge) => {
setEdgeState(canvas, edge, canvas.edgeStates.default);
})
// Resets all the animation nodes
initializeAnimationNodes(canvas);
canvas.nAnimationGroupsUsed = 0;
function checkAndReset(obj, attr, value = 0) {
if (obj[attr] != value) {
console.warn(`WARNING: ${attr} = ${obj[attr]} (expected: ${value}) after resetting node states. ` +
`This inconsistency infers that bugs may exist in node state changing process. `);
obj[attr] = value;
}
}
// Resets counting variables
for (k in canvas.nNodes) {
checkAndReset(canvas.nNodes, k, k === "default" ? canvas.n : 0);
}
}
/**
* Dispatches a group of animation nodes for visualization of the propagation event.
*
* Each animation node is updated as the following:
* * Sets node size by `state.model.size`;
* * Sets node style by `state.model.style`.
*
* Besides, each animation node object in the requested group has the following attributes appended:
* * `sourcePosition`: An array of length 2 as `[x1, y1]` where (x1, y1) is the coordinate of the source position;
* * `targetPosition`: `[x2, y2]` where (x2, y2) is of the target position.
*
* Position of the requested nodes are not changed, which is typically initialized before as outside the canvas
* via `initialize()` or `initializeRound()` functions.
*
* @param {Object} canvas The canvas object;
* @param {Object | String} state The state object or its name;
* @param {Object} evt The propagation event object.
*/
function requestAnimationGroup(canvas, state, evt) {
// Transforms to state object.
if (typeof(state) == 'string' || state instanceof String) {
state = canvas.nodeStates[state];
}
const group = canvas.animationGroups[canvas.nAnimationGroupsUsed];
canvas.nAnimationGroupsUsed += 1;
group.forEach((node) => {
node.sourceNode = evt.source;
node.targetNode = evt.target;
const sourcePosition = _getPos2D(evt.source);
// Changes size and style.
canvas.updateItem(node, {
'x': sourcePosition[0], // Initializes the position the same as the source node
'y': sourcePosition[1],
'size': state.model.size,
'style': state.model.style
});
// Shows the current animation node group on the canvas
canvas.showItem(node);
});
}
/**
* Displays all the events in the t-th round.
*
* The event lists are accessed via `canvas.propData.sourceEvents[t]`
* and `canvas.propData.propagationEvents[t]` resp.
*
* State of propagation target node is delayed to display:
* its change is triggered when an animation node reaches from the corresponding source.
*
* The target node objects have the following attributes appended:
* * `delayedState`: a state object inside `canvas.nodeStates`, the state to be displayed later.
*
* @param {Object} canvas The canvas object;
* @param {Number} t The index of round.
*/
function displayEvents(canvas, t) {
// Source events
canvas.propData.sourceEvents[t].forEach((evt) => {
setNodeState(canvas, evt.node, evt.state);
});
// Propagation events
canvas.propData.propagationEvents[t].forEach((evt) => {
// Sets target node state: delaying machanism is applied.
// Target state is changed only after animation node reaches the target.
evt.target.delayedState = evt.targetState;
// Sets edge state according to source nodes
setEdgeState(canvas, evt.edge, evt.edgeState);
// Triggers an animation group along current edge.
requestAnimationGroup(canvas, evt.animationState, evt);
});
}
/**
* Performs a frame of animation by moving all the animation nodes requested.
*
* Each animation node will be moved linearly from source to target position by progreess `min(1.0, node.speed * ratio)`.
* Each node needs `1.0 / node.speed` of a round's duration to reach the destination,
* and current time point is `ratio * 100%` time elapsed of current round.
*
* @param {Object} canvas The canvas object;
* @param {Number} ratio A ratio value in the range [0, 1].
* @returns Whether some nodes arrives the target position.
*/
function doAnimation(canvas, ratio) {
// No animation nodes activated at all
if (canvas.nAnimationGroupsUsed == 0 || canvas.animationGroups.length == 0) {
return false;
}
let res = false;
// Each group moves separately
d3.range(canvas.nAnimationGroupsUsed).forEach((i) => {
const group = canvas.animationGroups[i];
group.forEach((node) => {
const lambda = Math.min(1.0, ratio * node.speed);
const u = _getPos2D(node.sourceNode);
const v = _getPos2D(node.targetNode);
// Changes position: u + lambda * (v - u)
canvas.updateItem(node, {
'x': (1.0 - lambda) * u[0] + lambda * v[0],
'y': (1.0 - lambda) * u[1] + lambda * v[1]
});
res |= (lambda == 1.0);
});
});
return res;
}
/**
* Fades all the edges of positive / negative states.
* @param {Object} canvas The canvas object;
* @param {Object} fadeArgs The parameters for edge fading, obtained as `args.edgeFading`
* where `args` is obtained via `readConfig()` function.
*/
function fadeEdges(canvas, fadeArgs) {
canvas.getEdges().forEach((edge) => {
// No change to default edges
if (edge.state.name == 'default') {
return;
}
const curArgs = fadeArgs[edge.state.name];
const curOpacity = _getOpacity(edge);
// No change if current opacity already reaches the minimal
if (curOpacity <= curArgs.finalOpacity) {
return;
}
// Decrements opacity by delta
const nextOpacity = Math.max(curOpacity - curArgs.delta, curArgs.finalOpacity);
// Refreshes on the canvas
canvas.updateItem(edge, {
'style': {
'opacity': nextOpacity
}
});
});
}
/**
* Main function to start the visualization loop process.
*
* At the beginning of each round, the `checkResumableCallback()` function will be called
* to check whether the display animation shall continue (returns `true`) or pause (returns `false`).
* An always-true function is used if not provided.
*
* After the first frame of each round, the `roundStartCallback()` function will be called
* to handle some other data processing (e.g. plots number of nodes of each state to a stacked bar, etc.).
* A no-op function is used if not provided.
*
* @param {Array<Object>} canvasList An array of canvases.
* @param {Object} args Arguments of the visualization process, obtained via `readConfig()` function.
* @param {() => boolean} checkResumableCallback Callback function to check whether the process shall be continued or paused.
* @param {() => void} roundStartCallback Callback function after the first frame of a round.
*/
function runVisualization(canvasList, args, checkResumableCallback = null, roundStartCallback = null) {
// For single canvas object, wraps as a list
if (!(canvasList instanceof Array)) {
canvasList = [canvasList];
}
// Always true by default
if (checkResumableCallback == null) {
checkResumableCallback = () => true;
}
// No-op by default
if (roundStartCallback == null) {
roundStartCallback = () => {};
}
// Total number of rounds. Rounds are indexed as R = 0, 1 ... nRounds-1
const nRounds = canvasList[0].propData.nRounds;
// How many frames for each iteration. For the R-th round, frames are indexed as i = 0, 1 ... nFramesPerRound-1
const nFramesPerRound = args.nFramesPerRound;
// Duration of each frame by milliseconds
const interval = 1000.0 / args.fps;
// Count of frames
let frameId = -1;
// How many ramaining frames before the next continue/pause check via checkResumableCallback()
let remainingNFrames = nFramesPerRound;
// A flag variable whether all delayed states of target nodes has been displayed.
let delayedTriggered = false;
// Start time
let roundStartTime = Date.now();
setInterval(() => {
if (remainingNFrames == 0) {
// If remaining frames run out, try to acquire more animation frames ...
if (checkResumableCallback()) {
remainingNFrames += nFramesPerRound;
} else {
// ... or pause the process
return;
}
}
frameId += 1;
remainingNFrames -= 1;
// Round index R
const round = Math.floor(frameId / nFramesPerRound) % nRounds;
// Frame index i in current round
const frame = frameId % nFramesPerRound;
if (frame == 0) {
// Resets start time of current round
roundStartTime = Date.now();
// Initializes globally for the first round
if (round == 0) {
canvasList.forEach((canvas) => initialize(canvas));
}
// For the first frame of each round, performs message propagation
canvasList.forEach((canvas) => {
initializeRound(canvas);
// Resets delay flag
delayedTriggered = false;
// Performs information source & propagation in current round
displayEvents(canvas, round);
});
// Performs callback function
roundStartCallback();
}
let hasArrived = false;
// Performs node animation and edge animation (edge fading) by 1 frame
canvasList.forEach((canvas) => {
hasArrived |= doAnimation(canvas, frame / (nFramesPerRound - 1));
fadeEdges(canvas, args.edgeFading);
});
// Triggers delayed states
if (hasArrived && !delayedTriggered) {
delayedTriggered = true;
canvasList.forEach((canvas) => {
canvas.staticNodes.forEach((node) => {
if (node.delayedState != undefined) {
// Delayed states are triggered here
setNodeState(canvas, node, node.delayedState);
// Removes the delayed state after triggered.
node.delayedState = undefined;
}
});
});
}
// Dumps real FPS to console
if (frame == nFramesPerRound - 1) {
const endTime = Date.now();
const realFps = 1000.0 / (endTime - roundStartTime) * nFramesPerRound;
console.log(`Real FPS = ${Math.round(realFps)}`);
}
}, interval);
}
\ No newline at end of file
{
"nodeStyles": {
"default": { "size": 6, "style": { "fill": "#444444", "stroke": "#000000", "lineWidth": 1 } },
"boosted": { "size": 10, "style": { "fill": "#444444", "stroke": "#000000", "lineWidth": 3 } },
"Ca": {
"default": { "size": 12, "style": { "fill": "#44BB44", "stroke": "#00FF00", "lineWidth": 1 } },
"source": { "size": 12, "style": { "fill": "#44BB44", "stroke": "#00FF00", "lineWidth": 1 } },
"boosted": { "size": 14, "style": { "fill": "#44BB44", "stroke": "#00FF00", "lineWidth": 5 } },
"animation": { "size": 6, "style": { "fill": "#44BB44", "lineWidth": 0 } }
},
"Ca+": {
"default": { "size": 12, "style": { "fill": "#4488BB", "stroke": "#0088FF", "lineWidth": 1 } },
"source": { "size": 12, "style": { "fill": "#4488BB", "stroke": "#0088FF", "lineWidth": 1 } },
"boosted": { "size": 14, "style": { "fill": "#4488BB", "stroke": "#0088FF", "lineWidth": 5 } },
"animation": { "size": 6, "style": { "fill": "#4488BB", "lineWidth": 0 } }
},
"Cr": {
"default": { "size": 12, "style": { "fill": "#BB4444", "stroke": "#FF0000", "lineWidth": 1 } },
"source": { "size": 12, "style": { "fill": "#BB4444", "stroke": "#FF0000", "lineWidth": 1 } },
"boosted": { "size": 14, "style": { "fill": "#BB4444", "stroke": "#FF0000", "lineWidth": 5 } },
"animation": { "size": 6, "style": { "fill": "#BB4444", "lineWidth": 0 } }
},
"Cr-": {
"default": { "size": 12, "style": { "fill": "#BB8844", "stroke": "#FF8800", "lineWidth": 1 } },
"source": { "size": 12, "style": { "fill": "#BB8844", "stroke": "#FF8800", "lineWidth": 1 } },
"boosted": { "size": 14, "style": { "fill": "#BB8844", "stroke": "#FF8800", "lineWidth": 5 } },
"animation": { "size": 6, "style": { "fill": "#BB8844", "lineWidth": 0 } }
}
},
"edgeStyles": {
"default": { "style": { "stroke": "#444444", "lineWidth": 2, "opacity": 0.1 } },
"Ca": { "style": { "stroke": "#44BB44", "lineWidth": 4, "opacity": 0.65 } },
"Ca+": { "style": { "stroke": "#4488BB", "lineWidth": 4, "opacity": 0.65 } },
"Cr": { "style": { "stroke": "#BB4444", "lineWidth": 4, "opacity": 0.65 } },
"Cr-": { "style": { "stroke": "#BB8844", "lineWidth": 4, "opacity": 0.65 } }
},
"edgeFading": {
"Ca": { "finalOpacity": 0.05, "delta": 0.01 },
"Ca+": { "finalOpacity": 0.05, "delta": 0.01 },
"Cr": { "finalOpacity": 0.05, "delta": 0.01 },
"Cr-": { "finalOpacity": 0.05, "delta": 0.01 }
},
"fitViewPadding": 0.025,
"nAnimationNodes": 5,
"initialSpeed": 1.2,
"speedIncrement": 0.025,
"nFramesPerRound": 30,
"fps": 60
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"boostedNodes": [
8,
9,
10,
11,
12,
13,
14,
15
],
"sourceEvents": [
{
"timestamp": 0,
"id": 2,
"type": "Ca"
},
{
"timestamp": 0,
"id": 3,
"type": "Cr"
}
],
"propagationEvents": [
{
"timestamp": 1,
"source": 2,
"target": 4,
"edgeType": "Ca",
"targetType": "Ca"
},
{
"timestamp": 2,
"source": 4,
"target": 8,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 3,
"source": 8,
"target": 16,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 16,
"target": 32,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 32,
"target": 64,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 64,
"target": 128,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 64,
"target": 129,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 32,
"target": 65,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 65,
"target": 130,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 65,
"target": 131,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 16,
"target": 33,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 33,
"target": 66,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 66,
"target": 132,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 66,
"target": 133,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 33,
"target": 67,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 67,
"target": 134,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 67,
"target": 135,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 3,
"source": 8,
"target": 17,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 17,
"target": 34,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 34,
"target": 68,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 68,
"target": 136,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 68,
"target": 137,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 34,
"target": 69,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 69,
"target": 138,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 69,
"target": 139,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 17,
"target": 35,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 35,
"target": 70,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 70,
"target": 140,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 70,
"target": 141,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 35,
"target": 71,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 71,
"target": 142,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 71,
"target": 143,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 2,
"source": 4,
"target": 9,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 3,
"source": 9,
"target": 18,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 18,
"target": 36,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 36,
"target": 72,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 72,
"target": 144,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 72,
"target": 145,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 36,
"target": 73,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 73,
"target": 146,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 73,
"target": 147,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 18,
"target": 37,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 37,
"target": 74,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 74,
"target": 148,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 74,
"target": 149,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 37,
"target": 75,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 75,
"target": 150,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 75,
"target": 151,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 3,
"source": 9,
"target": 19,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 19,
"target": 38,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 38,
"target": 76,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 76,
"target": 152,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 76,
"target": 153,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 38,
"target": 77,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 77,
"target": 154,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 77,
"target": 155,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 19,
"target": 39,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 39,
"target": 78,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 78,
"target": 156,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 78,
"target": 157,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 39,
"target": 79,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 79,
"target": 158,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 79,
"target": 159,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 1,
"source": 2,
"target": 5,
"edgeType": "Ca",
"targetType": "Ca"
},
{
"timestamp": 2,
"source": 5,
"target": 10,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 3,
"source": 10,
"target": 20,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 20,
"target": 40,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 40,
"target": 80,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 80,
"target": 160,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 80,
"target": 161,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 40,
"target": 81,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 81,
"target": 162,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 81,
"target": 163,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 20,
"target": 41,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 41,
"target": 82,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 82,
"target": 164,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 82,
"target": 165,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 41,
"target": 83,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 83,
"target": 166,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 83,
"target": 167,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 3,
"source": 10,
"target": 21,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 21,
"target": 42,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 42,
"target": 84,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 84,
"target": 168,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 84,
"target": 169,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 42,
"target": 85,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 85,
"target": 170,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 85,
"target": 171,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 21,
"target": 43,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 43,
"target": 86,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 86,
"target": 172,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 86,
"target": 173,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 43,
"target": 87,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 87,
"target": 174,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 87,
"target": 175,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 2,
"source": 5,
"target": 11,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 3,
"source": 11,
"target": 22,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 22,
"target": 44,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 44,
"target": 88,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 88,
"target": 176,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 88,
"target": 177,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 44,
"target": 89,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 89,
"target": 178,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 89,
"target": 179,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 22,
"target": 45,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 45,
"target": 90,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 90,
"target": 180,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 90,
"target": 181,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 45,
"target": 91,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 91,
"target": 182,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 91,
"target": 183,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 3,
"source": 11,
"target": 23,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 23,
"target": 46,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 46,
"target": 92,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 92,
"target": 184,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 92,
"target": 185,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 46,
"target": 93,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 93,
"target": 186,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 93,
"target": 187,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 4,
"source": 23,
"target": 47,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 47,
"target": 94,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 94,
"target": 188,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 94,
"target": 189,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 5,
"source": 47,
"target": 95,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 95,
"target": 190,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 6,
"source": 95,
"target": 191,
"edgeType": "Ca+",
"targetType": "Ca+"
},
{
"timestamp": 1,
"source": 3,
"target": 6,
"edgeType": "Cr",
"targetType": "Cr"
},
{
"timestamp": 2,
"source": 6,
"target": 12,
"edgeType": "Cr",
"targetType": "Cr-"
},
{
"timestamp": 3,
"source": 12,
"target": 24,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 24,
"target": 48,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 48,
"target": 96,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 96,
"target": 192,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 96,
"target": 193,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 48,
"target": 97,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 97,
"target": 194,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 97,
"target": 195,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 24,
"target": 49,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 49,
"target": 98,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 98,
"target": 196,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 98,
"target": 197,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 49,
"target": 99,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 99,
"target": 198,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 99,
"target": 199,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 3,
"source": 12,
"target": 25,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 25,
"target": 50,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 50,
"target": 100,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 100,
"target": 200,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 100,
"target": 201,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 50,
"target": 101,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 101,
"target": 202,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 101,
"target": 203,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 25,
"target": 51,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 51,
"target": 102,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 102,
"target": 204,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 102,
"target": 205,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 51,
"target": 103,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 103,
"target": 206,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 103,
"target": 207,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 2,
"source": 6,
"target": 13,
"edgeType": "Cr",
"targetType": "Cr-"
},
{
"timestamp": 3,
"source": 13,
"target": 26,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 26,
"target": 52,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 52,
"target": 104,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 104,
"target": 208,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 104,
"target": 209,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 52,
"target": 105,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 105,
"target": 210,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 105,
"target": 211,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 26,
"target": 53,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 53,
"target": 106,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 106,
"target": 212,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 106,
"target": 213,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 53,
"target": 107,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 107,
"target": 214,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 107,
"target": 215,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 3,
"source": 13,
"target": 27,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 27,
"target": 54,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 54,
"target": 108,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 108,
"target": 216,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 108,
"target": 217,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 54,
"target": 109,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 109,
"target": 218,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 109,
"target": 219,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 27,
"target": 55,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 55,
"target": 110,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 110,
"target": 220,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 110,
"target": 221,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 55,
"target": 111,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 111,
"target": 222,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 111,
"target": 223,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 1,
"source": 3,
"target": 7,
"edgeType": "Cr",
"targetType": "Cr"
},
{
"timestamp": 2,
"source": 7,
"target": 14,
"edgeType": "Cr",
"targetType": "Cr-"
},
{
"timestamp": 3,
"source": 14,
"target": 28,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 28,
"target": 56,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 56,
"target": 112,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 112,
"target": 224,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 112,
"target": 225,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 56,
"target": 113,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 113,
"target": 226,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 113,
"target": 227,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 28,
"target": 57,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 57,
"target": 114,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 114,
"target": 228,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 114,
"target": 229,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 57,
"target": 115,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 115,
"target": 230,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 115,
"target": 231,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 3,
"source": 14,
"target": 29,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 29,
"target": 58,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 58,
"target": 116,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 116,
"target": 232,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 116,
"target": 233,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 58,
"target": 117,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 117,
"target": 234,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 117,
"target": 235,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 29,
"target": 59,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 59,
"target": 118,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 118,
"target": 236,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 118,
"target": 237,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 59,
"target": 119,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 119,
"target": 238,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 119,
"target": 239,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 2,
"source": 7,
"target": 15,
"edgeType": "Cr",
"targetType": "Cr-"
},
{
"timestamp": 3,
"source": 15,
"target": 30,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 30,
"target": 60,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 60,
"target": 120,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 120,
"target": 240,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 120,
"target": 241,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 60,
"target": 121,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 121,
"target": 242,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 121,
"target": 243,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 30,
"target": 61,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 61,
"target": 122,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 122,
"target": 244,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 122,
"target": 245,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 61,
"target": 123,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 123,
"target": 246,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 123,
"target": 247,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 3,
"source": 15,
"target": 31,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 31,
"target": 62,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 62,
"target": 124,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 124,
"target": 248,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 124,
"target": 249,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 62,
"target": 125,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 125,
"target": 250,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 125,
"target": 251,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 4,
"source": 31,
"target": 63,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 63,
"target": 126,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 126,
"target": 252,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 126,
"target": 253,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 5,
"source": 63,
"target": 127,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 127,
"target": 254,
"edgeType": "Cr-",
"targetType": "Cr-"
},
{
"timestamp": 6,
"source": 127,
"target": 255,
"edgeType": "Cr-",
"targetType": "Cr-"
}
]
}
\ No newline at end of file
import json
from os import times
treeDepth = 8
boostedDepth = 4
nNodes = 2 ** treeDepth - 1
graph = {
'nodes': [{'id': i} for i in range(1, nNodes + 1)],
'edges': []
}
for i in range(1, 2 ** (treeDepth - 1)):
graph['edges'].append({'source': i, 'target': i * 2})
graph['edges'].append({'source': i, 'target': i * 2 + 1})
with open('graph.json', 'w') as file:
file.write(json.dumps(graph, indent=4))
boostedNodes = list(range(2 ** (boostedDepth - 1), 2 ** boostedDepth))
data = {
'boostedNodes': boostedNodes,
'sourceEvents': [
{'timestamp': 0, 'id': 2, 'type': 'Ca'},
{'timestamp': 0, 'id': 3, 'type': 'Cr'}
],
'propagationEvents': []
}
def makePropEvents(node, timestamp, isPositive, isBoosted=False):
if node * 2 > nNodes:
return
if node in boostedNodes:
isBoosted = True
for child in [node * 2, node * 2 + 1]:
childIsBoosted = (child in boostedNodes)
edgeType = ('Ca+' if isBoosted or childIsBoosted else 'Ca') if isPositive else ('Cr-' if isBoosted else 'Cr')
targetType = edgeType if isPositive else ('Cr-' if isBoosted or childIsBoosted else 'Cr')
data['propagationEvents'].append({
'timestamp': timestamp,
'source': node,
'target': child,
'edgeType': edgeType,
'targetType': targetType
})
makePropEvents(child, timestamp + 1, isPositive, isBoosted or childIsBoosted)
makePropEvents(2, 1, True)
makePropEvents(3, 1, False)
with open('data.json', 'w') as file:
file.write(json.dumps(data, indent=4))
\ No newline at end of file
{
"nodes": [
{
"id": 1
},
{
"id": 2
},
{
"id": 3
},
{
"id": 4
},
{
"id": 5
},
{
"id": 6
},
{
"id": 7
},
{
"id": 8
},
{
"id": 9
},
{
"id": 10
},
{
"id": 11
},
{
"id": 12
},
{
"id": 13
},
{
"id": 14
},
{
"id": 15
},
{
"id": 16
},
{
"id": 17
},
{
"id": 18
},
{
"id": 19
},
{
"id": 20
},
{
"id": 21
},
{
"id": 22
},
{
"id": 23
},
{
"id": 24
},
{
"id": 25
},
{
"id": 26
},
{
"id": 27
},
{
"id": 28
},
{
"id": 29
},
{
"id": 30
},
{
"id": 31
},
{
"id": 32
},
{
"id": 33
},
{
"id": 34
},
{
"id": 35
},
{
"id": 36
},
{
"id": 37
},
{
"id": 38
},
{
"id": 39
},
{
"id": 40
},
{
"id": 41
},
{
"id": 42
},
{
"id": 43
},
{
"id": 44
},
{
"id": 45
},
{
"id": 46
},
{
"id": 47
},
{
"id": 48
},
{
"id": 49
},
{
"id": 50
},
{
"id": 51
},
{
"id": 52
},
{
"id": 53
},
{
"id": 54
},
{
"id": 55
},
{
"id": 56
},
{
"id": 57
},
{
"id": 58
},
{
"id": 59
},
{
"id": 60
},
{
"id": 61
},
{
"id": 62
},
{
"id": 63
},
{
"id": 64
},
{
"id": 65
},
{
"id": 66
},
{
"id": 67
},
{
"id": 68
},
{
"id": 69
},
{
"id": 70
},
{
"id": 71
},
{
"id": 72
},
{
"id": 73
},
{
"id": 74
},
{
"id": 75
},
{
"id": 76
},
{
"id": 77
},
{
"id": 78
},
{
"id": 79
},
{
"id": 80
},
{
"id": 81
},
{
"id": 82
},
{
"id": 83
},
{
"id": 84
},
{
"id": 85
},
{
"id": 86
},
{
"id": 87
},
{
"id": 88
},
{
"id": 89
},
{
"id": 90
},
{
"id": 91
},
{
"id": 92
},
{
"id": 93
},
{
"id": 94
},
{
"id": 95
},
{
"id": 96
},
{
"id": 97
},
{
"id": 98
},
{
"id": 99
},
{
"id": 100
},
{
"id": 101
},
{
"id": 102
},
{
"id": 103
},
{
"id": 104
},
{
"id": 105
},
{
"id": 106
},
{
"id": 107
},
{
"id": 108
},
{
"id": 109
},
{
"id": 110
},
{
"id": 111
},
{
"id": 112
},
{
"id": 113
},
{
"id": 114
},
{
"id": 115
},
{
"id": 116
},
{
"id": 117
},
{
"id": 118
},
{
"id": 119
},
{
"id": 120
},
{
"id": 121
},
{
"id": 122
},
{
"id": 123
},
{
"id": 124
},
{
"id": 125
},
{
"id": 126
},
{
"id": 127
},
{
"id": 128
},
{
"id": 129
},
{
"id": 130
},
{
"id": 131
},
{
"id": 132
},
{
"id": 133
},
{
"id": 134
},
{
"id": 135
},
{
"id": 136
},
{
"id": 137
},
{
"id": 138
},
{
"id": 139
},
{
"id": 140
},
{
"id": 141
},
{
"id": 142
},
{
"id": 143
},
{
"id": 144
},
{
"id": 145
},
{
"id": 146
},
{
"id": 147
},
{
"id": 148
},
{
"id": 149
},
{
"id": 150
},
{
"id": 151
},
{
"id": 152
},
{
"id": 153
},
{
"id": 154
},
{
"id": 155
},
{
"id": 156
},
{
"id": 157
},
{
"id": 158
},
{
"id": 159
},
{
"id": 160
},
{
"id": 161
},
{
"id": 162
},
{
"id": 163
},
{
"id": 164
},
{
"id": 165
},
{
"id": 166
},
{
"id": 167
},
{
"id": 168
},
{
"id": 169
},
{
"id": 170
},
{
"id": 171
},
{
"id": 172
},
{
"id": 173
},
{
"id": 174
},
{
"id": 175
},
{
"id": 176
},
{
"id": 177
},
{
"id": 178
},
{
"id": 179
},
{
"id": 180
},
{
"id": 181
},
{
"id": 182
},
{
"id": 183
},
{
"id": 184
},
{
"id": 185
},
{
"id": 186
},
{
"id": 187
},
{
"id": 188
},
{
"id": 189
},
{
"id": 190
},
{
"id": 191
},
{
"id": 192
},
{
"id": 193
},
{
"id": 194
},
{
"id": 195
},
{
"id": 196
},
{
"id": 197
},
{
"id": 198
},
{
"id": 199
},
{
"id": 200
},
{
"id": 201
},
{
"id": 202
},
{
"id": 203
},
{
"id": 204
},
{
"id": 205
},
{
"id": 206
},
{
"id": 207
},
{
"id": 208
},
{
"id": 209
},
{
"id": 210
},
{
"id": 211
},
{
"id": 212
},
{
"id": 213
},
{
"id": 214
},
{
"id": 215
},
{
"id": 216
},
{
"id": 217
},
{
"id": 218
},
{
"id": 219
},
{
"id": 220
},
{
"id": 221
},
{
"id": 222
},
{
"id": 223
},
{
"id": 224
},
{
"id": 225
},
{
"id": 226
},
{
"id": 227
},
{
"id": 228
},
{
"id": 229
},
{
"id": 230
},
{
"id": 231
},
{
"id": 232
},
{
"id": 233
},
{
"id": 234
},
{
"id": 235
},
{
"id": 236
},
{
"id": 237
},
{
"id": 238
},
{
"id": 239
},
{
"id": 240
},
{
"id": 241
},
{
"id": 242
},
{
"id": 243
},
{
"id": 244
},
{
"id": 245
},
{
"id": 246
},
{
"id": 247
},
{
"id": 248
},
{
"id": 249
},
{
"id": 250
},
{
"id": 251
},
{
"id": 252
},
{
"id": 253
},
{
"id": 254
},
{
"id": 255
}
],
"edges": [
{
"source": 1,
"target": 2
},
{
"source": 1,
"target": 3
},
{
"source": 2,
"target": 4
},
{
"source": 2,
"target": 5
},
{
"source": 3,
"target": 6
},
{
"source": 3,
"target": 7
},
{
"source": 4,
"target": 8
},
{
"source": 4,
"target": 9
},
{
"source": 5,
"target": 10
},
{
"source": 5,
"target": 11
},
{
"source": 6,
"target": 12
},
{
"source": 6,
"target": 13
},
{
"source": 7,
"target": 14
},
{
"source": 7,
"target": 15
},
{
"source": 8,
"target": 16
},
{
"source": 8,
"target": 17
},
{
"source": 9,
"target": 18
},
{
"source": 9,
"target": 19
},
{
"source": 10,
"target": 20
},
{
"source": 10,
"target": 21
},
{
"source": 11,
"target": 22
},
{
"source": 11,
"target": 23
},
{
"source": 12,
"target": 24
},
{
"source": 12,
"target": 25
},
{
"source": 13,
"target": 26
},
{
"source": 13,
"target": 27
},
{
"source": 14,
"target": 28
},
{
"source": 14,
"target": 29
},
{
"source": 15,
"target": 30
},
{
"source": 15,
"target": 31
},
{
"source": 16,
"target": 32
},
{
"source": 16,
"target": 33
},
{
"source": 17,
"target": 34
},
{
"source": 17,
"target": 35
},
{
"source": 18,
"target": 36
},
{
"source": 18,
"target": 37
},
{
"source": 19,
"target": 38
},
{
"source": 19,
"target": 39
},
{
"source": 20,
"target": 40
},
{
"source": 20,
"target": 41
},
{
"source": 21,
"target": 42
},
{
"source": 21,
"target": 43
},
{
"source": 22,
"target": 44
},
{
"source": 22,
"target": 45
},
{
"source": 23,
"target": 46
},
{
"source": 23,
"target": 47
},
{
"source": 24,
"target": 48
},
{
"source": 24,
"target": 49
},
{
"source": 25,
"target": 50
},
{
"source": 25,
"target": 51
},
{
"source": 26,
"target": 52
},
{
"source": 26,
"target": 53
},
{
"source": 27,
"target": 54
},
{
"source": 27,
"target": 55
},
{
"source": 28,
"target": 56
},
{
"source": 28,
"target": 57
},
{
"source": 29,
"target": 58
},
{
"source": 29,
"target": 59
},
{
"source": 30,
"target": 60
},
{
"source": 30,
"target": 61
},
{
"source": 31,
"target": 62
},
{
"source": 31,
"target": 63
},
{
"source": 32,
"target": 64
},
{
"source": 32,
"target": 65
},
{
"source": 33,
"target": 66
},
{
"source": 33,
"target": 67
},
{
"source": 34,
"target": 68
},
{
"source": 34,
"target": 69
},
{
"source": 35,
"target": 70
},
{
"source": 35,
"target": 71
},
{
"source": 36,
"target": 72
},
{
"source": 36,
"target": 73
},
{
"source": 37,
"target": 74
},
{
"source": 37,
"target": 75
},
{
"source": 38,
"target": 76
},
{
"source": 38,
"target": 77
},
{
"source": 39,
"target": 78
},
{
"source": 39,
"target": 79
},
{
"source": 40,
"target": 80
},
{
"source": 40,
"target": 81
},
{
"source": 41,
"target": 82
},
{
"source": 41,
"target": 83
},
{
"source": 42,
"target": 84
},
{
"source": 42,
"target": 85
},
{
"source": 43,
"target": 86
},
{
"source": 43,
"target": 87
},
{
"source": 44,
"target": 88
},
{
"source": 44,
"target": 89
},
{
"source": 45,
"target": 90
},
{
"source": 45,
"target": 91
},
{
"source": 46,
"target": 92
},
{
"source": 46,
"target": 93
},
{
"source": 47,
"target": 94
},
{
"source": 47,
"target": 95
},
{
"source": 48,
"target": 96
},
{
"source": 48,
"target": 97
},
{
"source": 49,
"target": 98
},
{
"source": 49,
"target": 99
},
{
"source": 50,
"target": 100
},
{
"source": 50,
"target": 101
},
{
"source": 51,
"target": 102
},
{
"source": 51,
"target": 103
},
{
"source": 52,
"target": 104
},
{
"source": 52,
"target": 105
},
{
"source": 53,
"target": 106
},
{
"source": 53,
"target": 107
},
{
"source": 54,
"target": 108
},
{
"source": 54,
"target": 109
},
{
"source": 55,
"target": 110
},
{
"source": 55,
"target": 111
},
{
"source": 56,
"target": 112
},
{
"source": 56,
"target": 113
},
{
"source": 57,
"target": 114
},
{
"source": 57,
"target": 115
},
{
"source": 58,
"target": 116
},
{
"source": 58,
"target": 117
},
{
"source": 59,
"target": 118
},
{
"source": 59,
"target": 119
},
{
"source": 60,
"target": 120
},
{
"source": 60,
"target": 121
},
{
"source": 61,
"target": 122
},
{
"source": 61,
"target": 123
},
{
"source": 62,
"target": 124
},
{
"source": 62,
"target": 125
},
{
"source": 63,
"target": 126
},
{
"source": 63,
"target": 127
},
{
"source": 64,
"target": 128
},
{
"source": 64,
"target": 129
},
{
"source": 65,
"target": 130
},
{
"source": 65,
"target": 131
},
{
"source": 66,
"target": 132
},
{
"source": 66,
"target": 133
},
{
"source": 67,
"target": 134
},
{
"source": 67,
"target": 135
},
{
"source": 68,
"target": 136
},
{
"source": 68,
"target": 137
},
{
"source": 69,
"target": 138
},
{
"source": 69,
"target": 139
},
{
"source": 70,
"target": 140
},
{
"source": 70,
"target": 141
},
{
"source": 71,
"target": 142
},
{
"source": 71,
"target": 143
},
{
"source": 72,
"target": 144
},
{
"source": 72,
"target": 145
},
{
"source": 73,
"target": 146
},
{
"source": 73,
"target": 147
},
{
"source": 74,
"target": 148
},
{
"source": 74,
"target": 149
},
{
"source": 75,
"target": 150
},
{
"source": 75,
"target": 151
},
{
"source": 76,
"target": 152
},
{
"source": 76,
"target": 153
},
{
"source": 77,
"target": 154
},
{
"source": 77,
"target": 155
},
{
"source": 78,
"target": 156
},
{
"source": 78,
"target": 157
},
{
"source": 79,
"target": 158
},
{
"source": 79,
"target": 159
},
{
"source": 80,
"target": 160
},
{
"source": 80,
"target": 161
},
{
"source": 81,
"target": 162
},
{
"source": 81,
"target": 163
},
{
"source": 82,
"target": 164
},
{
"source": 82,
"target": 165
},
{
"source": 83,
"target": 166
},
{
"source": 83,
"target": 167
},
{
"source": 84,
"target": 168
},
{
"source": 84,
"target": 169
},
{
"source": 85,
"target": 170
},
{
"source": 85,
"target": 171
},
{
"source": 86,
"target": 172
},
{
"source": 86,
"target": 173
},
{
"source": 87,
"target": 174
},
{
"source": 87,
"target": 175
},
{
"source": 88,
"target": 176
},
{
"source": 88,
"target": 177
},
{
"source": 89,
"target": 178
},
{
"source": 89,
"target": 179
},
{
"source": 90,
"target": 180
},
{
"source": 90,
"target": 181
},
{
"source": 91,
"target": 182
},
{
"source": 91,
"target": 183
},
{
"source": 92,
"target": 184
},
{
"source": 92,
"target": 185
},
{
"source": 93,
"target": 186
},
{
"source": 93,
"target": 187
},
{
"source": 94,
"target": 188
},
{
"source": 94,
"target": 189
},
{
"source": 95,
"target": 190
},
{
"source": 95,
"target": 191
},
{
"source": 96,
"target": 192
},
{
"source": 96,
"target": 193
},
{
"source": 97,
"target": 194
},
{
"source": 97,
"target": 195
},
{
"source": 98,
"target": 196
},
{
"source": 98,
"target": 197
},
{
"source": 99,
"target": 198
},
{
"source": 99,
"target": 199
},
{
"source": 100,
"target": 200
},
{
"source": 100,
"target": 201
},
{
"source": 101,
"target": 202
},
{
"source": 101,
"target": 203
},
{
"source": 102,
"target": 204
},
{
"source": 102,
"target": 205
},
{
"source": 103,
"target": 206
},
{
"source": 103,
"target": 207
},
{
"source": 104,
"target": 208
},
{
"source": 104,
"target": 209
},
{
"source": 105,
"target": 210
},
{
"source": 105,
"target": 211
},
{
"source": 106,
"target": 212
},
{
"source": 106,
"target": 213
},
{
"source": 107,
"target": 214
},
{
"source": 107,
"target": 215
},
{
"source": 108,
"target": 216
},
{
"source": 108,
"target": 217
},
{
"source": 109,
"target": 218
},
{
"source": 109,
"target": 219
},
{
"source": 110,
"target": 220
},
{
"source": 110,
"target": 221
},
{
"source": 111,
"target": 222
},
{
"source": 111,
"target": 223
},
{
"source": 112,
"target": 224
},
{
"source": 112,
"target": 225
},
{
"source": 113,
"target": 226
},
{
"source": 113,
"target": 227
},
{
"source": 114,
"target": 228
},
{
"source": 114,
"target": 229
},
{
"source": 115,
"target": 230
},
{
"source": 115,
"target": 231
},
{
"source": 116,
"target": 232
},
{
"source": 116,
"target": 233
},
{
"source": 117,
"target": 234
},
{
"source": 117,
"target": 235
},
{
"source": 118,
"target": 236
},
{
"source": 118,
"target": 237
},
{
"source": 119,
"target": 238
},
{
"source": 119,
"target": 239
},
{
"source": 120,
"target": 240
},
{
"source": 120,
"target": 241
},
{
"source": 121,
"target": 242
},
{
"source": 121,
"target": 243
},
{
"source": 122,
"target": 244
},
{
"source": 122,
"target": 245
},
{
"source": 123,
"target": 246
},
{
"source": 123,
"target": 247
},
{
"source": 124,
"target": 248
},
{
"source": 124,
"target": 249
},
{
"source": 125,
"target": 250
},
{
"source": 125,
"target": 251
},
{
"source": 126,
"target": 252
},
{
"source": 126,
"target": 253
},
{
"source": 127,
"target": 254
},
{
"source": 127,
"target": 255
}
]
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
<!DOCTYPE html>
<html>
<head>
<title>Data Visualization</title>
</head>
<body>
<!-- todo: How to get their heights? -->
<div style="float: left; width: 90%; height: 90%;" id="bigCanvas">
<text style="position: absolute; top: 3%; left: 1%; font-family: sans-serif;
color: #ff4040" cols="1" id="description-infected">
Red: Infected (receives rumor)
</text>
<text style="position: absolute; top: 7%; left: 1%; font-family: sans-serif;
color: #40ff80" cols="1" id="description-protected">
Green: Protected (receives truth)
</text>
<input type="checkbox" style="position: absolute; top: 11%; left: 1%" id="checkbox-auto">
<text style="position: absolute; top: 11%; left: 3%; font-family: sans-serif;
color: black" cols="1" id="text-auto">
Auto play
</text>
<!-- <div style="position: absolute; left: 40%; width: 30%; top: 80%; height: 15%" id="stackedBar"></div> -->
</div>
<!-- d3.js -->
<script src="./d3.v7.min.js"></script>
<!-- AntV G6 for graph display -->
<script src="./g6.min.js"></script>
<!-- AntV G2Plot for Chart display -->
<script src="./g2plot.min.js"></script>
<!-- Common utility components -->
<script src="./utils.js"></script>
<!-- Components for handling input -->
<script src="./input.js"></script>
<!-- Components for handling graph display -->
<script src="./canvas.js"></script>
<!-- Components for handling stacked bar -->
<!-- <script src="./stackbar.js"></script> -->
<!-- Main function -->
<script>
async function main() {
// Config file
const config = await readConfig("./config.json");
// Futures of reading graph and data groups
const graphFut = readGraph("./data/graph.json");
const dataFut = readData("./data/data.json", "Default");
// Joins till all the reading processes finish.
const graph = await graphFut;
const data = await dataFut;
// Size of the big canvas (at left side)
const bigWidth = document.getElementById("bigCanvas").offsetWidth;
// todo: How to get the height of container element? offsetHeight does not work well.
const bigHeight = bigWidth * 0.5;
// // Size of the small canvases (at right side, devided by 3 blocks)
// const smallWidth = document.getElementById("smallCanvas1").offsetWidth;
// // todo: How to get the height of container element? offsetHeight does not work well.
// const smallHeight = smallWidth * 2.0 / 3;
const canvasList = [
createCanvas(graph, data, "bigCanvas", bigWidth, bigHeight, config)
];
// const stackedBarCanvasList = [canvasList[3], canvasList[2], canvasList[1]];
// const stackbar = createStackedBarPlot('stackedBar', stackedBarCanvasList, graph.n);
runVisualization(canvasList, config,
() => document.getElementById("checkbox-auto").checked,
// () => updateStackedBarData(stackbar, stackedBarCanvasList)
);
}
main();
</script>
</body>
</html>
/**
* Requires: utils.js
*/
/**
* Converts all the node indices (`id`, `source` and `target`) for each item in the given list.
* @param {Object | Array} itemList Object or list of objects for conversion
*/
function _indicesToString(itemList) {
attributesToString(itemList, "id", "source", "target");
}
/**
* Gets the edge identifier with node `u` and `v` as two ends.
*
* The edge identifier is represented as `(u', v')`, where `u' <= v'` (by lexicographical order) is always satisfied.
*
* @param {*} u Identifier of node `u`
* @param {*} v Identifier of node `v`
* @returns The identifier of the corresponding edge (u, v) with the format above.
*/
function getEdgeId(u, v) {
return `(${u <= v ? u : v}, ${u <= v ? v : u})`;
}
/**
* Reads the configuration parameters from a JSON file.
* @param {string} configPath Local path or remote URL of the config file
* @returns An object that contains all the configurations.
* @see `config-guide.md` for details of configuration file contents.
*/
async function readConfig(configPath) {
const conf = await fetch(configPath).then((t) => t.json());
// Required attributes:
// (1) nodeStyles and edgeStyles
checkAttributesAs(conf, ['nodeStyles', 'edgeStyles'], Object);
// (2) (node|edge)Styles contain 5 attributes: default (no message), Ca, Ca+, Cr, Cr-
checkAttributesAs([conf['nodeStyles'], conf['edgeStyles']], ['default', 'Ca', 'Ca+', 'Cr', 'Cr-'], Object);
['Ca', 'Ca+', 'Cr', 'Cr-'].forEach((s) => {
// (3) Static node & animation node
checkAttributesAs(conf['nodeStyles'][s], ['default', 'animation'], Object);
});
// Optional attributes:
// (1) nAnimationNodes: 10 by default
checkAttributes(conf, 'nAnimationNodes', (obj, attr) => {
obj[attr] = 10;
});
// (2) boosted or source node styles
function addNodeStyle(obj, name, defaultName = "default") {
if (obj[name] == undefined) {
obj[name] = obj[defaultName];
}
}
addNodeStyle(conf.nodeStyles, "boosted");
['Ca', 'Ca+', 'Cr', 'Cr-'].forEach((s) => {
['boosted', 'source'].forEach((t) => addNodeStyle(conf.nodeStyles[s], t));
});
return Object.freeze(conf);
}
/**
* Reads the graph from a file.
*
* The graph is given as a JSON object file including the following contents:
* ```
* "nodes": [
* { "id": 0 }, // Each node has an index
* { "id": 1 },
* ] // A list of all the nodes
* "edges": [
* { "source": 0, "target": 1 }, // Each edge (source, target) is bidirectional
* { "source": 1, "target": 2 }
* ], // A list of all the bidirectional edges
* ```
* The graph shall be **bidirectional** and satisfy the following criteria:
* * Connected: multiple connected components are disallowed;
* * Unique node: duplicated nodes (sharing the same id) are disallowed;
* * Unique edge: duplicated edges are disallowed (`{source: u, target: v}` and `{source: v, target: u}` are regarded the same);
* * Size constraints: Due to performance limit of rendering, too large graphs are not suggested.
*
* The graph object returned contains the following attributes:
* * `nodes`: A list of nodes, with id converted to string;
* * `edges`: A list of bidirectional edges, with node ids converted to string;
* * `n`: Number of nodes, equivalent to nodes.length;
* * `m`: Number of edges, equivalent to edges.length;
* * `nodeMap`: A map(string => Object) from index to the corresponding node object.
*
* Each node has the following attributes appended (if not provided in the input JSON file):
* * `degree`: Degree of the node
*
* Each link has the following attributes appended (if not provided in the input JSON file):
* * `id`: Identifier of the edge
*
* **Warning: If the additional attributes are provided via input, it's your own duty to
* ensure their correctness.**
*
* @param {string} path Local path or remote URL of the graph file
* @returns The graph object
* @see getEdgeId for details of format of edge identifier.
*/
async function readGraph(path) {
const graph = await fetch(path).then((t) => t.json());
// 'id' attribute should be transformed to String
_indicesToString(graph.nodes);
_indicesToString(graph.edges);
// Number of nodes in the graph
graph.n = graph.nodes.length;
// Number of edges in the graph
graph.m = graph.edges.length;
console.log(`${path}: |V| = ${graph.n}, |E| = ${graph.m}`);
// Builds the node map: id => node
graph.nodeMap = new Map();
graph.nodes.forEach((node) => {
graph.nodeMap.set(node.id, node);
});
// Degree is calculated if not provided by input
if (graph.nodes.length > 0 && graph.nodes[0].degree == undefined) {
graph.nodes.forEach((node) => {
// Initializes degrees to 0
node.degree = 0;
});
graph.edges.forEach((edge) => {
for (const id of [edge.source, edge.target]) {
graph.nodeMap.get(id).degree += 1;
}
});
}
// Identifiers of each edge is assigned if not provided by input
if (graph.edges.length > 0 && graph.edges[0].id == undefined) {
graph.edges.forEach((edge) => {
const u = edge.source;
const v = edge.target;
edge.id = getEdgeId(u, v);
})
}
return graph;
}
/**
* Creates an object as a collection of all the events, including the following attributes:
* * `label`: Label of current group of data;
* * `nRounds`: Total number of rounds (such that each timestamp falls in range [0, nRounds-1]);
* * `boostedNodes': List of boosted nodes;
* * `sourceEvents`: A 2D-array of all the events of source events;
* * `propagationEvents`: A 2D-array of all the events of propagation events;
* * `maxNPropagations`: Maximum number of propagation events in each timestamp.
*
* For each timestamp *t*, `sourceEvents[t]` and `propagationEvents[t]` contain all the corresponding events
* at timestamp *t*.
*
* `sourceEvents` and `propagationEvents` are initialized as an array of length `nRounds`, with each element inside
* initialized as en empty array.
*
* @param {String} label Label of current group of data;
* @param {Number} nRounds Number of rounds;
* @param {Array<String>} boostedNodes List of boosted nodes, empty by default;
* @param {Number} maxNPropagations Maximum number of propagation events in a round. `NaN` as default if not provided.
* @returns An event collection object.
* @see getEventListsObject
*/
function createEventObject(label, nRounds, boostedNodes = [], maxNPropagations = NaN) {
const res = {
label: label,
nRounds: nRounds,
boostedNodes: boostedNodes,
sourceEvents: d3.range(nRounds).map((_) => []),
propagationEvents: d3.range(nRounds).map((_) => []),
maxNPropagations: maxNPropagations
};
return res;
}
/**
* Reads the data about propagation information from a file.
*
* The data are given as a JSON list which may include the following types of objects:
* ```
* {
* "boostedNodes": [a, b, c],
* "sourceEvents": [
* {
* "timestamp": t,
* "id": u,
* "type": "Ca" | "Ca+" | "Cr" | "Cr-"
* }
* ],
* "propagationEvents": [
* {
* "timestamp": t,
* "source": u,
* "target": v,
* "edgeType": "Ca" | "Ca+" | "Cr" | "Cr-",
* "targetType": "Ca" | "Ca+" | "Cr" | "Cr-"
* }
* ]
* }
* ```
* The result object is initialized via `createEventObject` function.
*
* Each item in the event lists has all the node indices converted to string.
*
* @param {string} path Local path or remote URL of the input file
* @param {string} label Identifier of current data file.
* @returns Object of the data collection, categorized by timestamp.
* @see createEventObject for details of each attribute in the object returned.
*/
async function readData(path, label) {
const data = await fetch(path).then((t) => t.json());
// Converts all the node indices to string
[data.sourceEvents, data.propagationEvents].forEach((evtList) => _indicesToString(evtList));
// Converts the boosted node list to string
for (i in data.boostedNodes) {
data.boostedNodes[i] = data.boostedNodes[i].toString();
}
// The number of rounds
const nRounds = Math.max(
d3.max(data["sourceEvents"].map((obj) => obj.timestamp)),
d3.max(data["propagationEvents"].map((obj) => obj.timestamp))
) + 1;
const validTypeValues = ['Ca', 'Ca+', 'Cr', 'Cr-'];
const errorOnTypeValue = (item, attr) => {
const errInfo = `attribute "${attr}" is not one of ${validTypeValues}`;
console.error(errInfo + " in object: ", item);
throw errInfo;
};
// Checks source event lists
data.sourceEvents.forEach((item) => {
checkAttributes(item, ["id", "type"]);
if (!validTypeValues.includes(item.type)) {
errorOnTypeValue(item, "type");
}
});
// Checks propagation event lists
data.propagationEvents.forEach((item) => {
checkAttributes(item, ["source", "target", "edgeType", "targetType"]);
["edgeType", "targetType"].forEach((attr) => {
if (!validTypeValues.includes(item[attr])) {
errorOnTypeValue(item, attr);
}
});
});
// maxNPropagations is initialized later.
const res = createEventObject(label, nRounds, data.boostedNodes);
// Categorizes each item by timestamp
["sourceEvents", "propagationEvents"].forEach((evtListName) => {
data[evtListName].forEach((item) => {
res[evtListName][item.timestamp].push(item)
});
});
// Calculates maximum of propagation events in each timestamp
res.maxNPropagations = d3.max(res.propagationEvents.map((arr) => arr.length));
return res;
}
/**
* Reads multiple groups of data with given label and file path resp.
*
* The input `group` is an array of objects with the following attributes:
* * `label`: Label of current group of data;
* * `path`: Path of the JSON file (local file path or remote URL).
*
* For each group of data, `nRounds`, along with the length of arrays `positiveAt` and `negativeAt`,
* will be aligned to the maximum.
*
* @param {Array<Object>} groups Information of data groups (see above).
* @returns An object that maps each label to the corresponding data object.
* @see readData for details of processing with each data group.
*/
async function readDataGroup(groups) {
// Starts all the tasks as future
const futures = {};
for (const g of groups) {
futures[g.label] = readData(g.path, g.label);
}
// Joins until all the futures finish
const res = {};
for (const [label, fut] of Object.entries(futures)) {
res[label] = await fut;
}
// Takes the maximum rounds.
const maxNRounds = d3.max(Object.values(res).map((data) => data.nRounds));
Object.values(res).forEach((data) => {
['sourceEvents', 'propagationEvents'].forEach((evtListName) => {
// Aligns array length with empty event lists
d3.range(data.nRounds, maxNRounds).forEach((_) => {
data[evtListName].push([]);
});
});
data.nRounds = maxNRounds;
});
return res;
}
\ No newline at end of file
/**
* Checks whether the given attribute(s) exists in the given object(s).
*
* The callback function `callback(obj, attr)` is invoked if the attribute does not exist in the object.
* If not provided by user, the default callback raises an error log in the console, and then
* an exception will be thrown.
* @param {Object | Array<Object>} obj The object, or the list of objects to be checked
* @param {string | Array<string>} attr Attribute, or the list of the attributes to check
* @param {function(Object, string)} callback The callback function if given attribute does not exist
* @throws For the default callback, throws an error message if the attribute does not exist.
*/
function checkAttributes(obj, attr, callback = null) {
const objList = obj instanceof Array ? obj : [obj];
const attrList = attr instanceof Array ? attr : [attr];
attrList.forEach((attr) => {
objList.forEach((obj) => {
if (obj[attr] == null) {
if (callback == null) {
errInfo = `Missing config attribute '${attr}'`;
console.error(errInfo + " in object: ", obj);
throw errInfo;
} else {
callback(obj, attr);
}
}
});
});
}
/**
* Checks whether the given attribute(s) exists and matches the specified type in the given object(s).
*
* Each attribute is checked (if exists):
* * If type is a string **literal**, then `typeof obj[attr] == type` is checked;
* * Otherwise, `obj[attr] instanceof type` is checked.
*
* The callback function `callback(obj, attr)` is invoked if the attribute does not exist or has type mismatched in the object.
* If not provided by user, the default callback raises an error log in the console, and then
* an exception will be thrown.
*
* @param {Object | Array<Object>} obj The object, or the list of objects to be checked
* @param {string | Array<string>} attr Attribute, or the list of the attributes to check
* @param {*} type The expected type of each object
* @param {*} callback The callback function if given attribute does not exist
* @throws For the default callback, throws an error message if the attribute does not exist.
* @see checkAttributes
*/
function checkAttributesAs(obj, attr, type, callback = null) {
const objList = obj instanceof Array ? obj : [obj];
const attrList = attr instanceof Array ? attr : [attr];
attrList.forEach((attr) => {
objList.forEach((obj) => {
if (obj[attr] == null) {
if (callback == null) {
errInfo = `Missing attribute '${attr}'`;
console.error(errInfo + " in object: ", obj);
throw errInfo;
} else {
callback(obj, attr);
}
}
else if (typeof type == 'string' && typeof obj[attr] != type) {
if (callback == null) {
errInfo = `Wrong type of attribute '${attr}': Expected typeof '${type}' instead of '${typeof obj[attr]}'`;
console.error(errInfo + " in object: ", obj);
throw errInfo;
} else {
callback(obj, attr);
}
}
else if (typeof type == 'object' && !(obj[attr] instanceof type)) {
if (callback == null) {
errInfo = `Wrong type of attribute '${attr}'`;
console.error(errInfo + " in object: ", obj);
throw errInfo;
} else {
callback(obj, attr);
}
}
});
});
}
/**
* Converts all the given attributes (if exist) to string for each item in the given object list.
* @param {Object | Array} itemList Object, or list of objects for conversion
* @param {...string} attributes Attributes to convert.
*/
function attributesToString(itemList, ...attributes) {
function doConvert(item) {
attributes.forEach((attrName) => {
if (item[attrName] != undefined) {
item[attrName] = item[attrName].toString();
}
});
}
if (itemList instanceof Array) {
itemList.forEach((item) => doConvert(item));
} else {
doConvert(item);
}
}
/**
* Adds an attribute to `dest` as `dest[name] = value`.
*
* Checking is performed before assigning whether another attribute with the same identifier already exists.
* A warning message to console is triggered and the old value is replaced
* if duplicated attribute identifiers are detected.
* @param {Object} dest The object where the new attribute is appended
* @param {String} name Identifier of the new attribute
* @param {any} value Value of the new attribute
*/
function createAttribute(dest, name, value) {
if (dest[name] != undefined) {
console.warn(`WARNING: rewriting attribute '${name}' that already exists.`);
}
dest[name] = value;
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment