diff --git a/music_graph/src/MusicGraph.js b/music_graph/src/MusicGraph.js
index 3272f45..77bc1db 100644
--- a/music_graph/src/MusicGraph.js
+++ b/music_graph/src/MusicGraph.js
@@ -1,6 +1,7 @@
import * as d3 from 'd3'
import icons from './icons'
import {MusicGraphApi} from './MusicGraphApi'
+import {fitCaptionIntoCircle} from './graphGeometry'
// TODO: export somewhere else
const arc = function (radius, itemNumber, itemCount, width) {
@@ -32,7 +33,7 @@ export function MusicGraph(data) {
this.simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody())
.force('collide', d3.forceCollide()
- .radius(50)
+ .radius(40)
.strength(1))
.force('center', d3.forceCenter(width / 2, height / 2))
@@ -138,9 +139,9 @@ export function MusicGraph(data) {
n.targetLinks.has(d.id))
this.label.classed('selected', n =>
- n.sourceLinks.has(d.id) ||
- n.targetLinks.has(d.id) ||
- n.id === d.id)
+ n.node.sourceLinks.has(d.id) ||
+ n.node.targetLinks.has(d.id) ||
+ n.node.id === d.id)
this.node.classed('hover', n => n.id === d.id)
@@ -182,8 +183,8 @@ export function MusicGraph(data) {
fn: (d) => {
this.api.getRelatedByMbid(d.mbid)
.then(data => {
- this.addNodes(data.newNodes, data.relations, d.id)
this.expandedNodes.add(d.id)
+ this.addNodes(data.newNodes, data.relations, d.id)
d.relatedExpanded = true
})
}
@@ -408,12 +409,10 @@ export function MusicGraph(data) {
.force('link', d3.forceLink(this.links)
.id(d => d.id)
.strength(l => l.weight)
- .distance(d => Math.min(
- (1.2 / d.weight) * (94 * (this.expandedNodes.size + 1)))
- )
+ .distance(d => (1.15 / d.weight) * (82 * (this.expandedNodes.size + 1)))
)
- this.simulation.alphaTarget(0.01).restart()
+ this.simulation.alphaTarget(0.03).restart()
// Add new links
this.link = this.container.select('#links')
@@ -451,14 +450,16 @@ export function MusicGraph(data) {
// Add new labels
this.label = this.container.select('#labels')
.selectAll('.label')
- .data(this.nodes)
+ .data([].concat(...this.nodes.map(d => fitCaptionIntoCircle(d.name, d))))
this.label.exit().remove()
this.label = this.label
.enter()
.append('text')
.merge(this.label)
- .text(d => d.name)
.classed('label', true)
+ .classed('release', d => d.node.type === 'Album' || d.node.type === 'EP' || d.node.type === 'Single')
+ .classed('tag', d => d.node.type === 'Tag')
+ .text(d => d.text)
}
this.setupKeyBindings = function () {
@@ -514,8 +515,8 @@ export function MusicGraph(data) {
.attr('cx', d => d.x)
.attr('cy', d => d.y)
this.label
- .attr('x', d => d.x)
- .attr('y', d => d.y + 5)
+ .attr('x', d => Math.round(d.node.x))
+ .attr('y', d => d.node.y + d.baseline)
})
this._update()
diff --git a/music_graph/src/MusicGraphApi.js b/music_graph/src/MusicGraphApi.js
index 209a47e..c385a96 100644
--- a/music_graph/src/MusicGraphApi.js
+++ b/music_graph/src/MusicGraphApi.js
@@ -48,17 +48,7 @@ const nodeUtils = {
}
},
radius: function (type) {
- if (type === 'Group') {
- return 35
- } else if (type === 'Artist') {
- return 25
- } else if (type === 'Tag') {
- return 20
- } else if (type === 'Album') {
- return 20
- } else if (type === 'EP' || type === 'Single') {
- return 15
- }
+ return 35
}
}
diff --git a/music_graph/src/components/HelloWorld.vue b/music_graph/src/components/HelloWorld.vue
index 363d518..22f7f5a 100644
--- a/music_graph/src/components/HelloWorld.vue
+++ b/music_graph/src/components/HelloWorld.vue
@@ -3,6 +3,7 @@
+
@@ -74,13 +75,12 @@ export default {
/* Link */
svg .link.selected {
stroke-width: 2;
- stroke-opacity: 1;
}
svg .link {
- stroke: orange;
+ stroke: #FFE082;
pointer-events: none;
- stroke-opacity: 1;
+ stroke-opacity: 0.3;
stroke-width: 1;
}
@@ -91,11 +91,11 @@ export default {
/* Node */
svg .node.selected {
- stroke: red;
+ stroke: #7C4DFF;
}
svg .node.hover {
- stroke: red;
+ stroke: #7C4DFF;
}
svg.hover .node:not(.selected):not(.hover) {
@@ -104,11 +104,22 @@ export default {
svg .node {
fill: transparent;
+ stroke-width: 2;
}
/* Label */
svg .label {
text-anchor: middle;
+ font-family: Tahoma, sans-serif;
+ font-size: 11px;
+ }
+
+ svg .label.release {
+ fill: darkgrey;
+ }
+
+ svg .label.tag {
+ fill: darkgrey;
}
svg.hover .label:not(.selected) {
@@ -116,27 +127,18 @@ export default {
}
body {
- background: #E7EDEB;
+ background: #ffffff;
}
/* test */
#menu .menu-item {
cursor: pointer;
- fill: orange;
+ fill: #FFB300;
}
#menu .menu-item.hover {
- fill: darkorange;
- }
-
- #menu .menu-item.hover text {
- font-weight: bold;
- }
-
- #menu .menu-item text {
- text-anchor: middle;
- fill: white;
+ fill: #FF8F00;
}
#menu .menu-icon {
diff --git a/music_graph/src/graphGeometry.js b/music_graph/src/graphGeometry.js
new file mode 100644
index 0000000..123cffd
--- /dev/null
+++ b/music_graph/src/graphGeometry.js
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2002-2019 "Neo4j,"
+ * Neo4j Sweden AB [http://neo4j.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+import measureText from './textMeasurement'
+
+let addShortenedNextWord = (line, word, measure) => {
+ const result = []
+ while (!(word.length <= 2)) {
+ word = word.substr(0, word.length - 2) + '\u2026'
+ if (measure(word) < line.remainingWidth) {
+ line.text += ` ${word}`
+ break
+ } else {
+ result.push(undefined)
+ }
+ }
+ return result
+}
+
+let noEmptyLines = function (lines) {
+ for (let line of Array.from(lines)) {
+ if (line.text.length === 0) {
+ return false
+ }
+ }
+ return true
+}
+
+export let fitCaptionIntoCircle = function (captionText, node) {
+ const fontFamily = 'Tahoma'
+ const fontSize = 11
+ const lineHeight = fontSize
+ const measure = text => measureText(text, fontFamily, fontSize)
+
+ const words = captionText.split(' ')
+
+ const emptyLine = function (lineCount, iLine) {
+ let baseline = (1 + iLine - lineCount / 2) * lineHeight
+ const containingHeight = iLine < lineCount / 2 ? baseline - lineHeight : baseline
+ const lineWidth =
+ Math.sqrt(node.radius * node.radius - containingHeight * containingHeight) * 2
+ return {
+ node,
+ text: '',
+ baseline,
+ remainingWidth: lineWidth
+ }
+ }
+
+ const fitOnFixedNumberOfLines = function (lineCount) {
+ const lines = []
+ let iWord = 0
+ for (
+ let iLine = 0, end = lineCount - 1, asc = end >= 0;
+ asc ? iLine <= end : iLine >= end;
+ asc ? iLine++ : iLine--
+ ) {
+ const line = emptyLine(lineCount, iLine)
+ while (
+ iWord < words.length &&
+ measure(` ${words[iWord]}`) < line.remainingWidth) {
+ line.text += ` ${words[iWord]}`
+ line.remainingWidth -= measure(` ${words[iWord]}`)
+ iWord++
+ }
+ lines.push(line)
+ }
+ if (iWord < words.length) {
+ addShortenedNextWord(lines[lineCount - 1], words[iWord], measure)
+ }
+ return [lines, iWord]
+ }
+
+ let consumedWords = 0
+ const maxLines = (node.radius * 2) / fontSize
+
+ let lines = [emptyLine(1, 0)]
+ for (
+ let lineCount = 1, end = maxLines, asc = end >= 1;
+ asc ? lineCount <= end : lineCount >= end;
+ asc ? lineCount++ : lineCount--
+ ) {
+ const [candidateLines, candidateWords] = Array.from(
+ fitOnFixedNumberOfLines(lineCount)
+ )
+ if (noEmptyLines(candidateLines)) {
+ [lines, consumedWords] = Array.from([candidateLines, candidateWords])
+ }
+ if (consumedWords >= words.length) {
+ return lines
+ }
+ }
+ return lines
+}
diff --git a/music_graph/src/icons.js b/music_graph/src/icons.js
index 55d58b7..31d9285 100644
--- a/music_graph/src/icons.js
+++ b/music_graph/src/icons.js
@@ -1,50 +1,41 @@
const icons = {
- expand: '',
- release: '',
- guitar: '',
- hash: '',
- delete: ''
+ expand: '',
+ release: '',
+ guitar: '',
+ hash: '',
+ delete: ''
}
diff --git a/music_graph/src/textMeasurement.js b/music_graph/src/textMeasurement.js
new file mode 100644
index 0000000..b4532f3
--- /dev/null
+++ b/music_graph/src/textMeasurement.js
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2002-2019 "Neo4j,"
+ * Neo4j Sweden AB [http://neo4j.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+import * as d3 from 'd3'
+
+const measureUsingCanvas = function (text, font) {
+ const canvasSelection = d3.select('canvas#textMeasurementCanvas').data([this])
+ canvasSelection
+ .enter()
+ .append('canvas')
+ .attr('id', 'textMeasurementCanvas')
+ .style('display', 'none')
+
+ const canvas = canvasSelection.node()
+ const context = canvas.getContext('2d')
+ context.font = font
+ return context.measureText(text).width
+}
+
+export default function (text, fontFamily, fontSize) {
+ const font = `normal normal normal ${fontSize}px/normal ${fontFamily}`
+ return measureUsingCanvas(text, font)
+}