Graph improvements, temporary menu actions

This commit is contained in:
simon 2019-05-25 08:45:20 -04:00
parent 6558cf5a22
commit 701ee44618
4 changed files with 213 additions and 41 deletions

View File

@ -42,11 +42,14 @@ export function MusicGraph(data) {
this.dismiss = () => {
this.menu.remove()
this.nodes.forEach(d => {
d.fx = null
d.fy = null
d.menu = null
})
const menuNode = this.nodes.find(d => d.menu)
if (menuNode !== undefined) {
menuNode.menu = null
setTimeout(() => {
menuNode.fx = null
menuNode.fy = null
}, 600)
}
this.svg.classed('menu-mode', false)
}
@ -153,33 +156,105 @@ export function MusicGraph(data) {
this.makeMenu = function (d) {
// Todo global const?
const items = [
{idx: 0, icon: icons.expand, title: 'Related'},
{idx: 1, icon: icons.release, title: 'Releases'},
{idx: 2, icon: icons.hash, title: 'Tags'},
{idx: 3, icon: icons.guitar, title: 'Members'}
]
const items = []
if (!d.membersExpanded) {
items.push({
idx: 3,
icon: icons.guitar,
title: 'Members',
fn: (d) => {
this.api.getGroupMembers(d.mbid, d.id)
.then(data => {
d.membersExpanded = true
this.addNodes(data.newNodes, data.relations, d.id)
})
}
})
}
if (!d.relatedExpanded) {
items.push({
idx: 0,
icon: icons.expand,
title: 'Related',
fn: (d) => {
if (d.relatedExpanded) {
return
}
this.api.getRelatedByMbid(d.mbid)
.then(data => {
this.addNodes(data.newNodes, data.relations, d.id)
this.expandedNodes.add(d.id)
d.relatedExpanded = true
})
}
})
}
if (!d.releasesExpanded) {
items.push({
idx: 1,
icon: icons.release,
title: 'Releases',
fn: (d) => {
if (d.releasesExpanded) {
return
}
this.api.getArtistReleases(d.mbid, d.id)
.then(data => {
this.addNodes(data.newNodes, data.relations, d.id)
d.releasesExpanded = true
})
}
})
}
if (!d.tagsExpanded) {
items.push({
idx: 2,
icon: icons.hash,
title: 'Tags',
fn: (d) => {
if (d.tagsExpanded) {
return
}
this.api.getArtistTags(d.mbid, d.id)
.then(data => {
this.addNodes(data.newNodes, data.relations, d.id)
d.tagsExpanded = true
})
}
})
}
items.push({
idx: 4,
icon: icons.delete,
title: 'Remove from graph',
fn: (d) => {
this.removeNodes([d.id])
}
})
const tr = `translate(${d.x},${d.y})`
this.menu = this.container.select('#menu')
.selectAll('g')
.data(items)
.enter()
.append('g')
.classed('menu-item', true)
.attr('transform', tr)
.attr('transform', `translate(${d.x},${d.y})`)
const path = this.menu
.append('path')
.attr('d', item => arc(35, item.idx, items.length, 1)())
.attr('d', item => arc(d.radius, item.idx, items.length, 1)())
path
.on('mouseover', d => {
this.menu.classed('hover', item => item.idx === d.idx)
})
.on('mouseout', () => this.menu.classed('hover', false))
.on('mousedown', tab => {
this.dismiss()
return tab.fn(d)
})
.transition()
.duration(200)
.attr('d', item => arc(35, item.idx, items.length, 30)())
.attr('d', item => arc(d.radius, item.idx, items.length, 30)())
path
.append('title')
.text(item => item.title)
@ -192,7 +267,7 @@ export function MusicGraph(data) {
.attr('transform', d => `translate(${centroid(d.idx, 1)[0] - 13}, ${centroid(d.idx, 1)[1] - 13})`)
.transition()
.duration(250)
.attr('transform', d => `translate(${centroid(d.idx, 35)[0] - 10}, ${centroid(d.idx, 35)[1] - 10})`)
.attr('transform', item => `translate(${centroid(item.idx, d.radius)[0] - 10}, ${centroid(item.idx, d.radius)[1] - 10})`)
}
this.nodeDbClick = (d) => {
@ -203,7 +278,6 @@ export function MusicGraph(data) {
this.svg.classed('menu-mode', true)
d.menu = true
// todo: unfreeze node on dismiss
d.fx = d.x
d.fy = d.y
@ -300,7 +374,11 @@ export function MusicGraph(data) {
.forEach(target => {
target.targetLinks.delete(id)
})
Array.from(this.nodeById.get(id).targetLinks)
.map(srcId => this.nodeById.get(srcId))
.forEach(target => {
target.sourceLinks.delete(id)
})
this.nodeById.delete(id)
})
@ -323,7 +401,7 @@ export function MusicGraph(data) {
.id(d => d.id)
.strength(l => l.weight)
.distance(d => Math.min(
(1.2 / d.weight) * (94 * this.expandedNodes.size))
(1.2 / d.weight) * (94 * (this.expandedNodes.size + 1)))
)
)
@ -333,22 +411,24 @@ export function MusicGraph(data) {
this.link = this.container.select('#links')
.selectAll('.link')
.data(this.links)
let linkEnter = this.link
this.link.exit().remove()
this.link = this.link
.enter()
.append('line')
.classed('link', true)
this.link = linkEnter.merge(this.link)
.merge(this.link)
// Add new nodes
this.node = this.container.select('#nodes')
.selectAll('.node')
.attr('stroke', d => this._getNodeColor(d))
.data(this.nodes)
let nodeEnter = this.node
this.node.exit().remove()
this.node = this.node
.enter()
.append('circle')
.classed('node', true)
.attr('r', 35)
.attr('r', d => d.radius)
.attr('stroke', d => this._getNodeColor(d))
.call(d3.drag()
.on('start', this.dragStarted)
@ -358,18 +438,19 @@ export function MusicGraph(data) {
.on('mouseout', this.nodeOut)
.on('dblclick', this.nodeDbClick)
.on('contextmenu', this.nodeDbClick)
this.node = nodeEnter.merge(this.node)
.merge(this.node)
// Add new labels
this.label = this.container.select('#labels')
.selectAll('.label')
.data(this.nodes)
let labelEnter = this.label
this.label.exit().remove(0)
this.label = this.label
.enter()
.append('text')
.text(d => d.name)
.classed('label', true)
this.label = labelEnter.merge(this.label)
.merge(this.label)
}
this.setupKeyBindings = function () {

View File

@ -8,18 +8,51 @@ const nodeUtils = {
return 'Group'
} else if (labels.find(l => l === 'Artist')) {
return 'Artist'
} else if (labels.find(l => l === 'Album')) {
return 'Album'
} else if (labels.find(l => l === 'Single')) {
return 'Single'
} else if (labels.find(l => l === 'EP')) {
return 'EP'
}
return undefined
},
fromRawDict: function (data) {
return {
id: data.id,
mbid: data.mbid,
name: data.name,
listeners: data.listeners,
type: nodeUtils.getNodeType(data.labels),
sourceLinks: new Set(),
targetLinks: new Set()
const type = nodeUtils.getNodeType(data.labels)
if (type === 'Group' || type === 'Artist') {
return {
id: data.id,
mbid: data.mbid,
name: data.name,
listeners: data.listeners,
type: type,
sourceLinks: new Set(),
targetLinks: new Set(),
radius: nodeUtils.radius(type)
}
} else {
return {
id: data.id,
name: data.name,
type: type,
sourceLinks: new Set(),
targetLinks: new Set(),
radius: nodeUtils.radius(type)
}
}
},
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
}
}
}
@ -29,11 +62,11 @@ export function MusicGraphApi() {
// TODO: rmv
this.url = 'http://localhost:3030'
this.resolveCoverUrl = function(mbid) {
this.resolveCoverUrl = function (mbid) {
return this.url + '/cover/' + mbid
}
this.getArtistDetails = function(mbid) {
this.getArtistDetails = function (mbid) {
return d3.json(this.url + '/artist/details/' + mbid)
}
@ -49,11 +82,65 @@ export function MusicGraphApi() {
})
}
this.getGroupMembers = function (mbid, originId) {
return d3.json(this.url + '/artist/members/' + mbid)
.then((r) => {
return {
newNodes: r.artists.map(nodeUtils.fromRawDict),
relations: r.artists.map(a => {
return {
source: a.id,
target: originId,
weight: 0.8
}
})
}
})
}
this.getArtistReleases = function (mbid, originId) {
return d3.json(this.url + '/artist/details/' + mbid)
.then((r) => {
const newNodes = r.releases
.map(nodeUtils.fromRawDict)
.filter(release => release.type === 'Album')
return {
newNodes: newNodes,
relations: newNodes.map(t => {
return {
source: originId,
target: t.id,
weight: 0.8
}
})
}
})
}
this.getArtistTags = function (mbid, originId) {
return d3.json(this.url + '/artist/details/' + mbid)
.then((r) => {
return {
newNodes: r.tags.map(tag => {
tag.labels = ['Tag']
return tag
}).map(nodeUtils.fromRawDict),
relations: r.tags.map(t => {
return {
source: originId,
target: t.id,
weight: t.weight
}
})
}
})
}
this.getRelatedByMbid = function (mbid) {
return d3.json(this.url + '/artist/related/' + mbid)
.then((r) => {
return {
node: nodeUtils.fromRawDict(r.artists.find(a => a.mbid === mbid)),
newNodes: r.artists.map(nodeUtils.fromRawDict),
relations: r.relations
}

View File

@ -80,13 +80,13 @@ export default {
svg .link {
stroke: orange;
pointer-events: none;
stroke-opacity: 0.7;
stroke-opacity: 1;
stroke-width: 1;
}
svg.hover .link:not(.selected) {
stroke-opacity: 0.2;
stroke-width: 0.1;
stroke-opacity: 0.5;
stroke-width: 0.2;
}
/* Node */
@ -112,7 +112,7 @@ export default {
}
svg.hover .label:not(.selected) {
display: none;
fill-opacity: 0.2;
}
body {

View File

@ -40,6 +40,10 @@ const icons = {
'<path d="M273.661,114.318V67.035h-45.558L236.886,0h-47.69l-8.783,67.035h-60.084L129.113,0H81.425L72.64,67.035H7.804v47.283\n' +
'\th58.649l-6.904,52.791H7.804v47.289h45.559l-8.784,67.066h47.687l8.787-67.066h60.083l-8.786,67.066h47.691l8.783-67.066h64.836\n' +
'\tv-47.289h-58.647l6.901-52.791H273.661z M167.326,167.109h-60.084l6.9-52.791h60.082L167.326,167.109z"/>\n' +
'</svg>',
delete: '<svg width="28px" height="28px" viewBox="0 0 510 510" >\n' +
'\t\t<path d="M255,0C114.75,0,0,114.75,0,255s114.75,255,255,255s255-114.75,255-255S395.25,0,255,0z M382.5,346.8l-35.7,35.7\n' +
'\t\t\tL255,290.7l-91.8,91.8l-35.7-35.7l91.8-91.8l-91.8-91.8l35.7-35.7l91.8,91.8l91.8-91.8l35.7,35.7L290.7,255L382.5,346.8z"/>\n' +
'</svg>'
}