mirror of
https://github.com/simon987/music-graph-ui.git
synced 2025-04-10 14:06:41 +00:00
Graph improvements, temporary menu actions
This commit is contained in:
parent
6558cf5a22
commit
701ee44618
@ -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 () {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>'
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user