Published
Edited
Oct 21, 2020
1 fork
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
fetchData = graph(dataQuery)
Insert cell
Insert cell
data = fetchData()
Insert cell
Insert cell
Insert cell
prepareGraphData = () => {
/**
* We will use JSDoc format to descrive whats going on in our code
*/

// For example, below you see the definition of the type,
// so that you better understand what a data structure is

/**
* @typedef Link - link between person and location
* @property {string} source - person id
* @property {string} target - location id
*/

/**
* @typedef Person - data about person in the Node
* @property {string} id - person id
* @property {string} lastName - person's last name
* @property {string} firstName - person's first name
* @property {string} patronymic - person's patronymic
* @property {'person'} type - Node type to distinguish them
* @property {number} weight - a number that represents the total number of relationships that have been made with this person
*/

/**
* @typedef Location - data about location in the Node
* @property {string} id - location id
* @property {string} name - location name
* @property {'location'} type - Node type to distinguish them
* @property {number} weight - a number that represents the total number of relationships that have been made with this location
*/

/**
* Array of links
* @type {Link[]}
*/
const links = [];

/**
* Map of the persons - bind person id with its data
* @type {Record<string, Person>}
*/
const persons = {};

/**
* Map of the locations - bind location id with its data
* @type {Record<string, Location>}
*/
const locations = {};

/**
* Special data structure for optimization
* Binds Node's id with an array of other Nodes id that connected to the this Node
* @type {Record<string, string[]>}
*/
const relations = {};

/**
* Loop through each relation to extract necessary data
*/
data.relations.edges.forEach(relationEdge => {
const relation = relationEdge.node;

if (!relation.person || !relation.locationInstance) {
return;
}

const person = relation.person;
const location = relation.locationInstance;

/**
* Add connection between person and location to relations map
*/
if (relations[person.id]) {
relations[person.id].push(location.id);
} else {
relations[person.id] = [location.id];
}

/**
* Add connection between location and person to relations map
*/
if (relations[location.id]) {
relations[location.id].push(person.id);
} else {
relations[location.id] = [person.id];
}

/**
* Add person to persons array and increment weight counter
*/
if (!persons[person.id]) {
persons[person.id] = {
...person,
type: 'person',
weight: 1
};
} else {
persons[person.id].weight++;
}

/**
* Add location to locations array and increment weight counter
*/
if (!locations[location.id]) {
locations[location.id] = {
...location,
type: 'location',
weight: 1
};
} else {
locations[location.id].weight++;
}

/**
* Add new link between person and location
*/
links.push({
source: person.id,
target: location.id
});
});

/**
* Merge locations and persons into single array
*/
const nodes = [...Object.values(persons), ...Object.values(locations)];

return {
nodes,
links,
relations
};
}
Insert cell
Insert cell
chart = {
/**
* Graph dimensions
*/
const width = 1000;
const height = 600;

const viewBoxWidth = width * 3;
const viewBoxHeight = height * 3;

/**
* Create svg element and set its viewBox
*/
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, viewBoxWidth, viewBoxHeight]);

/**
* Create group element that will contain all our elements
*/
const g = svg.append('g');

/**
* Add ability to zoom and drag our graph with mouse (you can try it!)
*/
svg.call(
d3
.zoom()
.extent([[0, 0], [width, height]])
.scaleExtent([0, 8])
.on('zoom', event => g.attr('transform', event.transform))
);

/**
* Get our prepared data
*/
const { nodes, links, relations } = prepareGraphData();

/**
* Create Simulation instance for calculating node's position
* See https://github.com/d3/d3-force
*/
const simulation = d3
.forceSimulation(nodes) // pass our nodes array
/**
* Setup link force
* The link force pushes linked nodes together or apart according to the desired link distance
* Check out https://github.com/d3/d3-force#links
*/
.force(
'link',
d3
.forceLink(links)
.id(d => d.id)
.distance(100)
)
/**
* Setup charge force
* The many-body (or n-body) force applies mutually amongst all nodes.
* It can be used to simulate gravity (attraction) if the strength is positive,
* or electrostatic charge (repulsion) if the strength is negative.
* Check out https://github.com/d3/d3-force#many-body
*/
.force('charge', d3.forceManyBody())
/**
* Setup collide force
* The collision force treats nodes as circles with a given radius,
* rather than points, and prevents nodes from overlapping.
* Check out https://github.com/d3/d3-force#collision
*/
.force(
'collide',
d3
.forceCollide()
.radius(d => 10 + d.weight * 0.6)
.iterations(2)
)
/**
* Setup center force
* The centering force translates nodes uniformly so that the mean position of all nodes
* (the center of mass if all nodes have equal weight) is at the given position ⟨x,y⟩.
*/
.force('center', d3.forceCenter((width * 3) / 2, (height * 3) / 2));

/**
* Create group that will be contain all links element
*/
let link = g
.append('g')
.style('stroke', '#aaa') // set line color
.selectAll('line') // each link is just a line
.data(links) // pass data for rendering
.join(enter => enter.append('line'));

/**
* Create group that will be contain all Nodes element
*/
let node = g
.append('g')
.selectAll('circle') // each Node is just a circle
.data(nodes)
.join(
enter =>
enter // append some styles to each Node
.append('circle')
.attr('r', d => 10 + d.weight * 0.6) // set Node radius
.style('fill', d => (d.type === 'location' ? '#ffd248' : '#90a2fc')) // set Node color
.style('stroke', '#424242') // set color of the Node border
.style('stroke-width', '1px') // set Node's border width
.style('cursor', 'pointer') // add pointer cursor to the Node to indicate that it is clickable

/**
* On-click event handler
* Highlights other connected elements of the clicked Node
*
* @param event - mouse click event
* @param d - Node data
*/
.on('click', function(event, d) {
const currentDatum = d.id;

/**
* Iterates over all Links and set red color if that Link connects clicked Node
*/
link.style('stroke', _d =>
_d.source.id === currentDatum || _d.target.id === currentDatum
? '#ff0000'
: '#aaa'
);

/**
* Iterates over all Nodes and set red border color if that Node connected with clicked Node
*/
node.style('stroke', _d =>
relations[currentDatum].findIndex(rel => rel === _d.id) >= 0
? '#ff0000'
: '#424242'
);

/**
* Set red border color to the clicked item
*/
d3.select(this).style('stroke', '#ff0000');
})
.call(drag(simulation)) // add ability to drag Nodes (you can try it!)
);

/**
* Adds title to the Node to see it's name when hover
*/
node
.append("title")
.text(d =>
d.type === 'location'
? d.name || ''
: `${d.lastName} ${d.firstName} ${d.patronymic}`
);

/**
* On each simulation update we need to update position of our Nodes and Links
*/
simulation.on('tick', () => {
link // set link start and end coordinates
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);

node.attr('cx', d => d.x).attr('cy', d => d.y); // set Nodes center coordinates
});

return svg.node();
}
Insert cell
Insert cell
/**
* Handler for node's drag events
*
* @param sim - force simulation that calculates nodes position
*/
drag = sim => {
/**
* Handler for started drag event
*
* @param event - drag event
* @param d - node data
*/
function dragstarted(event, d) {
if (!event.active) {
sim.alphaTarget(0.3).restart();
}
d.fx = d.x;
d.fy = d.y;
}

/**
* Handler for started dragging
*
* @param event - drag event
* @param d - node data
*/
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}

/**
* Handler for ending drag event
*
* @param event - drag event
* @param d - node data
*/
function dragended(event, d) {
if (!event.active) {
sim.alphaTarget(0);
}
d.fx = null;
d.fy = null;
}

return d3
.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
}
Insert cell
Insert cell
Insert cell
Insert cell
graphql = require("graphql.js")
Insert cell
Insert cell
graph = graphql(graphServerEndpoint)
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more