From 701ee4461864503b60dbde893b6fb1f0488797b2 Mon Sep 17 00:00:00 2001 From: simon Date: Sat, 25 May 2019 08:45:20 -0400 Subject: [PATCH] Graph improvements, temporary menu actions --- music_graph/src/MusicGraph.js | 133 +++++++++++++++++----- music_graph/src/MusicGraphApi.js | 109 ++++++++++++++++-- music_graph/src/components/HelloWorld.vue | 8 +- music_graph/src/icons.js | 4 + 4 files changed, 213 insertions(+), 41 deletions(-) diff --git a/music_graph/src/MusicGraph.js b/music_graph/src/MusicGraph.js index 770da47..948cccd 100644 --- a/music_graph/src/MusicGraph.js +++ b/music_graph/src/MusicGraph.js @@ -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 () { diff --git a/music_graph/src/MusicGraphApi.js b/music_graph/src/MusicGraphApi.js index 9bb96cb..b14d71e 100644 --- a/music_graph/src/MusicGraphApi.js +++ b/music_graph/src/MusicGraphApi.js @@ -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 } diff --git a/music_graph/src/components/HelloWorld.vue b/music_graph/src/components/HelloWorld.vue index 50883ee..363d518 100644 --- a/music_graph/src/components/HelloWorld.vue +++ b/music_graph/src/components/HelloWorld.vue @@ -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 { diff --git a/music_graph/src/icons.js b/music_graph/src/icons.js index 3ef15e2..55d58b7 100644 --- a/music_graph/src/icons.js +++ b/music_graph/src/icons.js @@ -40,6 +40,10 @@ const icons = { '\n' + + '', + delete: '\n' + + '\t\t\n' + '' }