Skip to content

Commit

Permalink
updated modular code example
Browse files Browse the repository at this point in the history
  • Loading branch information
rofrischmann committed Feb 12, 2017
1 parent 0e04611 commit 1ec0ddc
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 0 deletions.
Empty file removed code/1-displaying-nodes.js
Empty file.
Empty file removed code/2-relations.js
Empty file.
Empty file removed code/3-user-interaction.js
Empty file.
Empty file removed code/4-dynamic-updates.js
Empty file.
16 changes: 16 additions & 0 deletions code/data/links.js
@@ -0,0 +1,16 @@
export default [
{ target: 'mammal', source: 'dog', strength: 0.7 },
{ target: 'mammal', source: 'cat', strength: 0.7 },
{ target: 'mammal', source: 'fox', strength: 0.7 },
{ target: 'mammal', source: 'elk', strength: 0.7 },
{ target: 'insect', source: 'ant', strength: 0.7 },
{ target: 'insect', source: 'bee', strength: 0.7 },
{ target: 'fish', source: 'carp', strength: 0.7 },
{ target: 'fish', source: 'pike', strength: 0.7 },
{ target: 'cat', source: 'elk', strength: 0.1 },
{ target: 'carp', source: 'ant', strength: 0.1 },
{ target: 'elk', source: 'bee', strength: 0.1 },
{ target: 'dog', source: 'cat', strength: 0.1 },
{ target: 'fox', source: 'ant', strength: 0.1 },
{ target: 'pike', source: 'cat', strength: 0.1 }
]
13 changes: 13 additions & 0 deletions code/data/nodes.js
@@ -0,0 +1,13 @@
export default [
{ id: 'mammal', group: 0, label: 'Mammals', level: 1 },
{ id: 'dog', group: 0, label: 'Dogs', level: 2 },
{ id: 'cat', group: 0, label: 'Cats', level: 2 },
{ id: 'fox', group: 0, label: 'Foxes', level: 2 },
{ id: 'elk', group: 0, label: 'Elk', level: 2 },
{ id: 'insect', group: 1, label: 'Insects', level: 1 },
{ id: 'ant', group: 1, label: 'Ants', level: 2 },
{ id: 'bee', group: 1, label: 'Bees', level: 2 },
{ id: 'fish', group: 2, label: 'Fish', level: 1 },
{ id: 'carp', group: 2, label: 'Carp', level: 2 },
{ id: 'pike', group: 2, label: 'Pikes', level: 2 }
]
175 changes: 175 additions & 0 deletions code/index.js
@@ -0,0 +1,175 @@

import getLinkColor from './utils/getLinkColor'
import getNodeColor from './utils/getNodeColor'
import getTextColor from './utils/getTextColor'
import getNeighbors from './utils/getNeighbors'
import isNeighborLink from './utils/isNeighborLink'

import baseNodes from './data/nodes'
import baseLink from './data/links'

const nodes = [...baseNodes]
const links = [...baseLinks]

const width = window.innerWidth
const height = window.innerHeight

const svg = d3.select('svg')
svg.attr('width', width).attr('height', height)

let linkElements,
nodeElements,
textElements

// we use svg groups to logically group the elements together
const linkGroup = svg.append('g').attr('class', 'links')
const nodeGroup = svg.append('g').attr('class', 'nodes')
const textGroup = svg.append('g').attr('class', 'texts')

// we use this reference to select/deselect
// after clicking the same element twice
let selectedId

// simulation setup with all forces
const linkForce = d3
.forceLink()
.id(link => link.id)
.strength(link => link.strength)

const simulation = d3
.forceSimulation()
.force('link', linkForce)
.force('charge', d3.forceManyBody().strength(-120))
.force('center', d3.forceCenter(width / 2, height / 2))

const dragDrop = d3.drag().on('start', (node) => {
node.fx = node.x
node.fy = node.y
}).on('drag', (node) => {
simulation.alphaTarget(0.7).restart()
node.fx = d3.event.x
node.fy = d3.event.y
}).on('end', (node) => {
if (!d3.event.active) {
simulation.alphaTarget(0)
}
node.fx = null
node.fy = null
})

/**---------------------------
--- UPDATE & INTERACTION ---
---------------------------**/

// select node is called on every click
// we either update the data according to the selection
// or reset the data if the same node is clicked twice
function selectNode(selectedNode) {
if (selectedId === selectedNode.id) {
selectedId = undefined
resetData()
updateSimulation()
} else {
selectedId = selectedNode.id
updateData(selectedNode)
updateSimulation()
}

const neighbors = getNeighbors(selectedNode, baseLinks)

// we modify the styles to highlight selected nodes
nodeElements.attr('fill', node => getNodeColor(node, neighbors))
textElements.attr('fill', node => getTextColor(node, neighbors))
linkElements.attr('stroke', link => getLinkColor(selectedNode, link))
}

// this helper simple adds all nodes and links
// that are missing, to recreate the initial state
function resetData() {
const nodeIds = nodes.map(node => node.id)

baseNodes.forEach((node) => {
if (nodeIds.indexOf(node.id) === -1) {
nodes.push(node)
}
})

links = baseLinks
}

// diffing and mutating the data
function updateData(selectedNode) {
const neighbors = getNeighbors(selectedNode, baseLinks)
const newNodes = baseNodes.filter(node => neighbors.indexOf(node.id) > -1 || node.level === 1)

const diff = {
removed: nodes.filter(node => newNodes.indexOf(node) === -1),
added: newNodes.filter(node => nodes.indexOf(node) === -1)
}

diff.removed.forEach(node => nodes.splice(nodes.indexOf(node), 1))
diff.added.forEach(node => nodes.push(node))

links = baseLinks.filter(link => link.target.id === selectedNode.id || link.source.id === selectedNode.id)
}

function updateGraph() {
// links
linkElements = linkGroup.selectAll('line').data(links, link => link.target.id + link.source.id)
linkElements.exit().remove()

const linkEnter = linkElements.enter().append('line').attr('stroke-width', 1).attr('stroke', 'rgba(50, 50, 50, 0.2)')

linkElements = linkEnter.merge(linkElements)

// nodes
nodeElements = nodeGroup.selectAll('circle').data(nodes, node => node.id)
nodeElements.exit().remove()

const nodeEnter = nodeElements
.enter()
.append('circle')
.attr('r', 10)
.attr('fill', node => node.level === 1 ? 'red' : 'gray')
.call(dragDrop)
// we link the selectNode method here
// to update the graph on every click
.on('click', selectNode)

nodeElements = nodeEnter.merge(nodeElements)

// texts
textElements = textGroup.selectAll('text').data(nodes, node => node.id)
textElements.exit().remove()

const textEnter = textElements
.enter()
.append('text')
.text(node => node.label)
.attr('font-size', 15)
.attr('dx', 15)
.attr('dy', 4)

textElements = textEnter.merge(textElements)
}

function updateSimulation() {
updateGraph()

simulation.nodes(nodes).on('tick', () => {
nodeElements.attr('cx', node => node.x).attr('cy', node => node.y)
textElements.attr('x', node => node.x).attr('y', node => node.y)
linkElements
.attr('x1', link => link.source.x)
.attr('y1', link => link.source.y)
.attr('x2', link => link.target.x)
.attr('y2', link => link.target.y)
})

simulation.force('link').links(links)
simulation.restart()
}

// last but not least, we call updateSimulation
// to trigger the initial render
updateSimulation()
3 changes: 3 additions & 0 deletions code/utils/getLinkColor.js
@@ -0,0 +1,3 @@
export default function getLinkColor(node, link) {
return isNeighborLink(node, link) ? 'green' : '#E5E5E5'
}
13 changes: 13 additions & 0 deletions code/utils/getNeighbors.js
@@ -0,0 +1,13 @@
export default function getNeighbors(node, links) {
return links.reduce(
(neighbors, link) => {
if (link.target.id === node.id) {
neighbors.push(link.source.id)
} else if (link.source.id === node.id) {
neighbors.push(link.target.id)
}
return neighbors
},
[node.id]
)
}
6 changes: 6 additions & 0 deletions code/utils/getNodeColor.js
@@ -0,0 +1,6 @@
export default function getNodeColor(node, neighbors) {
if (neighbors.indexOf(node.id)) {
return node.level === 1 ? 'blue' : 'green'
}
return node.level === 1 ? 'red' : 'gray'
}
3 changes: 3 additions & 0 deletions code/utils/getTextColor.js
@@ -0,0 +1,3 @@
export default function getTextColor(node, neighbors) {
return neighbors.indexOf(node.id) ? 'green' : 'black'
}
3 changes: 3 additions & 0 deletions code/utils/isNeighborLink.js
@@ -0,0 +1,3 @@
export default function isNeighborLink(node, link) {
return link.target.id === node.id || link.source.id === node.id
}

0 comments on commit 1ec0ddc

Please sign in to comment.