mirror of
https://github.com/simon987/music-graph-ui.git
synced 2025-04-18 01:26:43 +00:00
Bug fixes, labels, updated deps, tags in search bar
This commit is contained in:
parent
55b47450df
commit
f2760d8f3e
@ -3,10 +3,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<title>music_graph</title>
|
<title>music-graph v1.0</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
6
music_graph/package-lock.json
generated
6
music_graph/package-lock.json
generated
@ -3372,9 +3372,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"element-ui": {
|
"element-ui": {
|
||||||
"version": "2.7.2",
|
"version": "2.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/element-ui/-/element-ui-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/element-ui/-/element-ui-2.9.2.tgz",
|
||||||
"integrity": "sha512-Exh9QTkm9gwMMPzg1TyaTlBKyr3k4K9XcC5vl0A/mneDvJX//RsURGuOWsCNDVQMdhh5h9e+W5icosh+pKfbCg==",
|
"integrity": "sha512-HU5DDKivv2UFZghVWiP3I74IbTzSW8VXc070fM787i1X/u9f43olDuu3S6Pe9z87Z1oNjXt06ZmBlLYtySJMCw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"async-validator": "~1.8.1",
|
"async-validator": "~1.8.1",
|
||||||
"babel-helper-vue-jsx-merge-props": "^2.0.0",
|
"babel-helper-vue-jsx-merge-props": "^2.0.0",
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
"d3": "^5.9.2",
|
"d3": "^5.9.2",
|
||||||
"d3-force": "^2.0.1",
|
"d3-force": "^2.0.1",
|
||||||
"d3-path": "^1.0.7",
|
"d3-path": "^1.0.7",
|
||||||
"element-ui": "^2.7.2",
|
"element-ui": "^2.9.2",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.11",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-resource": "^1.5.1",
|
"vue-resource": "^1.5.1",
|
||||||
|
@ -33,12 +33,13 @@ export function MusicGraph(data) {
|
|||||||
this.simulation = d3.forceSimulation()
|
this.simulation = d3.forceSimulation()
|
||||||
.force('charge', d3.forceManyBody())
|
.force('charge', d3.forceManyBody())
|
||||||
.force('collide', d3.forceCollide()
|
.force('collide', d3.forceCollide()
|
||||||
.radius(40)
|
.radius(35)
|
||||||
.strength(1))
|
.strength(1))
|
||||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||||
|
|
||||||
this.zoomed = () => {
|
this.zoomed = () => {
|
||||||
this.container.attr('transform', d3.event.transform)
|
this.container.attr('transform', d3.event.transform)
|
||||||
|
this.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dismiss = () => {
|
this.dismiss = () => {
|
||||||
@ -157,7 +158,7 @@ export function MusicGraph(data) {
|
|||||||
this.makeMenu = function (d) {
|
this.makeMenu = function (d) {
|
||||||
let items = []
|
let items = []
|
||||||
let i = 0
|
let i = 0
|
||||||
if (d.type === 'Group' && !d.membersExpanded) {
|
if ((d.type === 'Group' || d.type === 'Artist')) {
|
||||||
items.push({
|
items.push({
|
||||||
idx: i++,
|
idx: i++,
|
||||||
icon: icons.guitar,
|
icon: icons.guitar,
|
||||||
@ -165,13 +166,12 @@ export function MusicGraph(data) {
|
|||||||
fn: (d) => {
|
fn: (d) => {
|
||||||
this.api.getGroupMembers(d.mbid, d.id)
|
this.api.getGroupMembers(d.mbid, d.id)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
d.membersExpanded = true
|
|
||||||
this.addNodes(data.newNodes, data.relations, d.id)
|
this.addNodes(data.newNodes, data.relations, d.id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if ((d.type === 'Group' || d.type === 'Artist') && !d.relatedExpanded) {
|
if ((d.type === 'Group' || d.type === 'Artist')) {
|
||||||
items.push({
|
items.push({
|
||||||
idx: i++,
|
idx: i++,
|
||||||
icon: icons.expand,
|
icon: icons.expand,
|
||||||
@ -183,34 +183,43 @@ export function MusicGraph(data) {
|
|||||||
this.expandedNodes.add(d.id)
|
this.expandedNodes.add(d.id)
|
||||||
this.addNodes(data.newNodes, data.relations, d.id)
|
this.addNodes(data.newNodes, data.relations, d.id)
|
||||||
}
|
}
|
||||||
d.relatedExpanded = true
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if ((d.type === 'Artist' || d.type === 'Group') && !d.releasesExpanded) {
|
if (d.type === 'Tag') {
|
||||||
items.push({
|
items.push({
|
||||||
idx: i++,
|
idx: i++,
|
||||||
icon: icons.release,
|
icon: icons.label,
|
||||||
title: 'Releases',
|
title: 'Related',
|
||||||
fn: (d) => {
|
fn: (d) => {
|
||||||
this.api.getArtistReleases(d.mbid, d.id)
|
this.api.getRelatedTags(d.id)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.addNodes(data.newNodes, data.relations, d.id)
|
this.addNodes(data.newNodes, data.relations, d.id)
|
||||||
d.releasesExpanded = true
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if ((d.type === 'Album' || d.type === 'EP' || d.type === 'Single' || d.type === 'Group' || d.type === 'Artist') &&
|
if ((d.type === 'Artist' || d.type === 'Group')) {
|
||||||
!d.tagsExpanded) {
|
items.push({
|
||||||
|
idx: i++,
|
||||||
|
icon: icons.label,
|
||||||
|
title: 'Label',
|
||||||
|
fn: (d) => {
|
||||||
|
this.api.getArtistLabels(d.mbid, d.id)
|
||||||
|
.then(data => {
|
||||||
|
this.addNodes(data.newNodes, data.relations, d.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (d.type === 'Album' || d.type === 'EP' || d.type === 'Single' || d.type === 'Group' || d.type === 'Artist') {
|
||||||
let fn
|
let fn
|
||||||
if (d.type === 'Group' || d.type === 'Artist') {
|
if (d.type === 'Group' || d.type === 'Artist') {
|
||||||
fn = (d) => {
|
fn = (d) => {
|
||||||
this.api.getArtistTags(d.mbid, d.id)
|
this.api.getArtistTags(d.mbid, d.id)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.addNodes(data.newNodes, data.relations, d.id)
|
this.addNodes(data.newNodes, data.relations, d.id)
|
||||||
d.tagsExpanded = true
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (d.type === 'Album' || d.type === 'EP' || d.type === 'Single') {
|
} else if (d.type === 'Album' || d.type === 'EP' || d.type === 'Single') {
|
||||||
@ -218,7 +227,6 @@ export function MusicGraph(data) {
|
|||||||
this.api.getReleaseDetails(d.mbid, d.id)
|
this.api.getReleaseDetails(d.mbid, d.id)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.addNodes(data.newNodes, data.relations, d.id)
|
this.addNodes(data.newNodes, data.relations, d.id)
|
||||||
d.tagsExpanded = true
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,12 +238,28 @@ export function MusicGraph(data) {
|
|||||||
fn: fn
|
fn: fn
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (d.type === 'Tag') {
|
||||||
|
items.push({
|
||||||
|
idx: i++,
|
||||||
|
icon: icons.expand,
|
||||||
|
title: 'Related',
|
||||||
|
fn: (d) => {
|
||||||
|
this.api.getRelatedByTag(d.id)
|
||||||
|
.then(data => {
|
||||||
|
this.addNodes(data.newNodes, data.relations, d.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
items.push({
|
items.push({
|
||||||
idx: i,
|
idx: i,
|
||||||
icon: icons.delete,
|
icon: icons.delete,
|
||||||
title: 'Remove from graph',
|
title: 'Remove from graph',
|
||||||
fn: (d) => {
|
fn: (d) => {
|
||||||
this.removeNodes([d.id])
|
this.removeNodes([d.id])
|
||||||
|
if (this._data.hoverArtist && d.id === this._data.hoverArtist.id) {
|
||||||
|
this._data.hoverArtist = undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -295,17 +319,29 @@ export function MusicGraph(data) {
|
|||||||
this.nodeClick = (d) => {
|
this.nodeClick = (d) => {
|
||||||
if (d.type === 'Group' || d.type === 'Artist') {
|
if (d.type === 'Group' || d.type === 'Artist') {
|
||||||
// Toggle artistInfo
|
// Toggle artistInfo
|
||||||
|
this.nodes.forEach(x => {
|
||||||
|
x.hover = false
|
||||||
|
})
|
||||||
if (this._data.hoverArtist === d) {
|
if (this._data.hoverArtist === d) {
|
||||||
this._data.hoverArtist = undefined
|
this._data.hoverArtist = undefined
|
||||||
} else {
|
} else {
|
||||||
this._data.hoverArtist = d
|
this._data.hoverArtist = d
|
||||||
|
d.hover = true
|
||||||
}
|
}
|
||||||
|
this._update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addNode = function (newNode, relations) {
|
this.addNode = function (newNode, relations) {
|
||||||
// Convert {id, id} relation to {node, node}
|
// Convert {id, id} relation to {node, node}
|
||||||
if (this.nodeById.has(newNode.id)) {
|
if (this.nodeById.has(newNode.id)) {
|
||||||
|
// Node already exists, select it
|
||||||
|
this.nodes.forEach(x => {
|
||||||
|
x.hover = false
|
||||||
|
})
|
||||||
|
this._data.hoverArtist = this.nodeById.get(newNode.id)
|
||||||
|
this._data.hoverArtist.hover = true
|
||||||
|
this._update()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.nodeById.set(newNode.id, newNode)
|
this.nodeById.set(newNode.id, newNode)
|
||||||
@ -358,11 +394,13 @@ export function MusicGraph(data) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Convert {id, id} relation to {node, node}
|
// Convert {id, id} relation to {node, node}
|
||||||
let linksToAdd = relations.map(({weight, source, target}) => ({
|
let linksToAdd = relations
|
||||||
source: this.nodeById.get(source),
|
.filter(rel => this.nodeById.has(rel.source) && this.nodeById.has(rel.target))
|
||||||
target: this.nodeById.get(target),
|
.map(({weight, source, target}) => ({
|
||||||
weight: weight
|
source: this.nodeById.get(source),
|
||||||
}))
|
target: this.nodeById.get(target),
|
||||||
|
weight: weight
|
||||||
|
}))
|
||||||
|
|
||||||
// Update source/targetLinks, avoid bidirectional links
|
// Update source/targetLinks, avoid bidirectional links
|
||||||
for (const {source, target} of linksToAdd) {
|
for (const {source, target} of linksToAdd) {
|
||||||
@ -430,7 +468,7 @@ export function MusicGraph(data) {
|
|||||||
.force('link', d3.forceLink(this.links)
|
.force('link', d3.forceLink(this.links)
|
||||||
.id(d => d.id)
|
.id(d => d.id)
|
||||||
.strength(l => l.weight)
|
.strength(l => l.weight)
|
||||||
.distance(d => (1.15 / d.weight) * (82 * (this.expandedNodes.size + 1)))
|
.distance(d => (1.12 / d.weight) * 80 * (this.expandedNodes.size + 1))
|
||||||
)
|
)
|
||||||
|
|
||||||
this.simulation.alphaTarget(0.03).restart()
|
this.simulation.alphaTarget(0.03).restart()
|
||||||
@ -456,6 +494,7 @@ export function MusicGraph(data) {
|
|||||||
.enter()
|
.enter()
|
||||||
.append('circle')
|
.append('circle')
|
||||||
.merge(this.node)
|
.merge(this.node)
|
||||||
|
this.node
|
||||||
.classed('node', true)
|
.classed('node', true)
|
||||||
.attr('r', d => d.radius)
|
.attr('r', d => d.radius)
|
||||||
.attr('stroke', d => this._getNodeColor(d))
|
.attr('stroke', d => this._getNodeColor(d))
|
||||||
@ -485,11 +524,13 @@ export function MusicGraph(data) {
|
|||||||
|
|
||||||
this.setupKeyBindings = function () {
|
this.setupKeyBindings = function () {
|
||||||
document.body.onkeydown = (e) => {
|
document.body.onkeydown = (e) => {
|
||||||
let isPanMode = this.svg.classed('pan-mode')
|
if (e.ctrlKey) {
|
||||||
|
this.svg.classed('pan-mode', true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (e.key === 'q') {
|
document.body.onkeyup = (e) => {
|
||||||
this.svg.classed('pan-mode', !isPanMode)
|
if (e.key === 'Control') {
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
this.svg.classed('pan-mode', false)
|
this.svg.classed('pan-mode', false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -511,6 +552,9 @@ export function MusicGraph(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._getNodeColor = function (node) {
|
this._getNodeColor = function (node) {
|
||||||
|
if (node.hover) {
|
||||||
|
return '#FF0000'
|
||||||
|
}
|
||||||
if (this.expandedNodes.has(node.id)) {
|
if (this.expandedNodes.has(node.id)) {
|
||||||
return '#1cb3c8'
|
return '#1cb3c8'
|
||||||
}
|
}
|
||||||
@ -524,7 +568,7 @@ export function MusicGraph(data) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addTagById = function(tagid) {
|
this.addTagById = function (tagid) {
|
||||||
if (this.nodeById.has(tagid)) {
|
if (this.nodeById.has(tagid)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -546,7 +590,7 @@ export function MusicGraph(data) {
|
|||||||
.attr('cx', d => d.x)
|
.attr('cx', d => d.x)
|
||||||
.attr('cy', d => d.y)
|
.attr('cy', d => d.y)
|
||||||
this.label
|
this.label
|
||||||
.attr('x', d => Math.round(d.node.x))
|
.attr('x', d => d.node.x)
|
||||||
.attr('y', d => d.node.y + d.baseline)
|
.attr('y', d => d.node.y + d.baseline)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -63,6 +63,10 @@ export function MusicGraphApi() {
|
|||||||
return d3.json(this.url + '/artist/details/' + mbid)
|
return d3.json(this.url + '/artist/details/' + mbid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Works in both directions
|
||||||
|
* @returns {Promise<{newNodes: *, relations: *} | never>}
|
||||||
|
*/
|
||||||
this.getGroupMembers = function (mbid, originId) {
|
this.getGroupMembers = function (mbid, originId) {
|
||||||
return d3.json(this.url + '/artist/members/' + mbid)
|
return d3.json(this.url + '/artist/members/' + mbid)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
@ -70,8 +74,8 @@ export function MusicGraphApi() {
|
|||||||
newNodes: r.artists.map(nodeUtils.fromRawDict),
|
newNodes: r.artists.map(nodeUtils.fromRawDict),
|
||||||
relations: r.artists.map(a => {
|
relations: r.artists.map(a => {
|
||||||
return {
|
return {
|
||||||
source: a.id,
|
source: originId,
|
||||||
target: originId,
|
target: a.id,
|
||||||
weight: 0.8
|
weight: 0.8
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -99,6 +103,29 @@ export function MusicGraphApi() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.getArtistLabels = function (mbid, originId) {
|
||||||
|
return d3.json(this.url + '/artist/details/' + mbid)
|
||||||
|
.then((r) => {
|
||||||
|
const newNodes = r.labels
|
||||||
|
.map(l => {
|
||||||
|
l.labels = ['Label']
|
||||||
|
return l
|
||||||
|
})
|
||||||
|
.map(nodeUtils.fromRawDict)
|
||||||
|
|
||||||
|
return {
|
||||||
|
newNodes: newNodes,
|
||||||
|
relations: newNodes.map(t => {
|
||||||
|
return {
|
||||||
|
source: originId,
|
||||||
|
target: t.id,
|
||||||
|
weight: 0.8
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this._filterTags = function (tags) {
|
this._filterTags = function (tags) {
|
||||||
if (ONLY_GENRE_TAGS) {
|
if (ONLY_GENRE_TAGS) {
|
||||||
return tags.filter(tag => genres.has(tag.name))
|
return tags.filter(tag => genres.has(tag.name))
|
||||||
@ -133,6 +160,30 @@ export function MusicGraphApi() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.getRelatedTags = function (tagId) {
|
||||||
|
return d3.json(this.url + '/tag/tag/' + tagId)
|
||||||
|
.then((r) => {
|
||||||
|
const tags = this._filterTags(r.tags)
|
||||||
|
let directedRelations = r.relations.map(rel => {
|
||||||
|
// Make new nodes children of the expanded nodes, no matter the original direction
|
||||||
|
if (rel.source === tagId) {
|
||||||
|
return rel
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
source: rel.target,
|
||||||
|
target: rel.source,
|
||||||
|
weight: rel.weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
newNodes: this._addTagLabel(tags).map(nodeUtils.fromRawDict),
|
||||||
|
relations: directedRelations
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.getRelatedByMbid = function (mbid) {
|
this.getRelatedByMbid = function (mbid) {
|
||||||
return d3.json(this.url + '/artist/related/' + mbid)
|
return d3.json(this.url + '/artist/related/' + mbid)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
@ -168,7 +219,14 @@ export function MusicGraphApi() {
|
|||||||
name: r.tag.name
|
name: r.tag.name
|
||||||
}),
|
}),
|
||||||
newNodes: r.artists.map(nodeUtils.fromRawDict),
|
newNodes: r.artists.map(nodeUtils.fromRawDict),
|
||||||
relations: r.relations
|
relations: r.relations.map(rel => {
|
||||||
|
// Invert relation direction
|
||||||
|
return {
|
||||||
|
source: rel.target,
|
||||||
|
target: rel.source,
|
||||||
|
weight: rel.weight
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -195,6 +253,6 @@ export function MusicGraphApi() {
|
|||||||
prefix = prefix.replace(/[^\w.\-!?& ]/g, '_').toUpperCase()
|
prefix = prefix.replace(/[^\w.\-!?& ]/g, '_').toUpperCase()
|
||||||
prefix = prefix.replace(/ /g, '+')
|
prefix = prefix.replace(/ /g, '+')
|
||||||
|
|
||||||
return d3.json(this.url + '/artist/autocomplete/' + prefix)
|
return d3.json(this.url + '/autocomplete/' + prefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
25
music_graph/src/components/AboutPage.vue
Normal file
25
music_graph/src/components/AboutPage.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<el-main>
|
||||||
|
<span>This is the about page</span>
|
||||||
|
<object data="/static/diagram.svg" type="image/svg+xml"></object>
|
||||||
|
</el-main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AboutPage'
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#watermark {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(100% - 30px);
|
||||||
|
left: 1%;
|
||||||
|
pointer-events: none;
|
||||||
|
color: rgba(0,0,0,0.67);
|
||||||
|
font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
|
||||||
|
}
|
||||||
|
</style>
|
@ -7,14 +7,18 @@
|
|||||||
style="float: right"
|
style="float: right"
|
||||||
>
|
>
|
||||||
<figure>
|
<figure>
|
||||||
<img
|
<el-image
|
||||||
alt=""
|
alt=""
|
||||||
style="height: 128px"
|
style="height: 128px"
|
||||||
width="128"
|
width="128"
|
||||||
height="128"
|
height="128"
|
||||||
|
class="block"
|
||||||
v-bind:src="api.resolveCoverUrl(release.mbid)"
|
v-bind:src="api.resolveCoverUrl(release.mbid)"
|
||||||
onerror="this.src='/static/album.png'"
|
|
||||||
>
|
>
|
||||||
|
<div slot="error" class="image-slot">
|
||||||
|
<i class="el-icon-full-screen"></i>
|
||||||
|
</div>
|
||||||
|
</el-image>
|
||||||
<figcaption>{{release.name}} ({{release.year}})</figcaption>
|
<figcaption>{{release.name}} ({{release.year}})</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
@ -62,12 +66,25 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
|
|
||||||
figure {
|
figure {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin: 0 20px 3em 20px;
|
||||||
|
width: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-image {
|
||||||
width: 128px;
|
width: 128px;
|
||||||
height: 180px;
|
height: 180px;
|
||||||
margin: 0 20px 3em 20px;
|
}
|
||||||
|
.image-slot {
|
||||||
|
font-size: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
background: #f5f7fa;
|
||||||
|
color: #909399;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -58,15 +58,16 @@ export default {
|
|||||||
this.api.getArtistDetails(artist.mbid)
|
this.api.getArtistDetails(artist.mbid)
|
||||||
.then(info => {
|
.then(info => {
|
||||||
this.artistInfo = info
|
this.artistInfo = info
|
||||||
this.artistInfo.releases = this.artistInfo.releases.filter(r =>
|
this.artistInfo.releases = this.artistInfo.releases
|
||||||
r.labels.indexOf('Album') !== -1 || r.labels.indexOf('EP') !== -1)
|
.sort((a, b) => a.year - b.year)
|
||||||
|
.filter(r => r.labels.indexOf('Album') !== -1 || r.labels.indexOf('EP') !== -1)
|
||||||
this.artistInfo.tags = info.tags.sort((a, b) => b.weight - a.weight).splice(0, 6).map(t => {
|
this.artistInfo.tags = info.tags.sort((a, b) => b.weight - a.weight).splice(0, 6).map(t => {
|
||||||
t.type = genres.has(t.name) ? '' : 'info'
|
t.type = genres.has(t.name) ? '' : 'info'
|
||||||
return t
|
return t
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onTagClick: function(tag) {
|
onTagClick: function (tag) {
|
||||||
this.$emit('addTag', tag)
|
this.$emit('addTag', tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div id="mm"></div>
|
<div id="mm"></div>
|
||||||
<InputBar v-on:query="onQuery($event)"></InputBar>
|
<InputBar v-on:addArtist="onAddArtist($event)" v-on:addTag="onAddTag($event)"></InputBar>
|
||||||
<ArtistInfo
|
<ArtistInfo
|
||||||
v-bind:artist="hoverArtist"
|
v-bind:artist="hoverArtist"
|
||||||
v-on:addTag="onAddTag($event)"
|
v-on:addTag="onAddTag($event)"
|
||||||
/>
|
/>
|
||||||
<canvas id="textMeasurementCanvas"></canvas>
|
<canvas id="textMeasurementCanvas"></canvas>
|
||||||
|
<Watermark text="music-graph v1.0"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ArtistInfo from './ArtistInfo'
|
import ArtistInfo from './ArtistInfo'
|
||||||
|
import Watermark from './Watermark'
|
||||||
import {MusicGraph} from '../MusicGraph'
|
import {MusicGraph} from '../MusicGraph'
|
||||||
import InputBar from './InputBar'
|
import InputBar from './InputBar'
|
||||||
|
|
||||||
@ -21,12 +23,12 @@ let data = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {InputBar, ArtistInfo},
|
components: {InputBar, ArtistInfo, Watermark},
|
||||||
data() {
|
data() {
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onQuery: function (e) {
|
onAddArtist: function (e) {
|
||||||
this.mm.addArtistByMbid(e)
|
this.mm.addArtistByMbid(e)
|
||||||
},
|
},
|
||||||
onAddTag: function(e) {
|
onAddTag: function(e) {
|
||||||
@ -35,6 +37,13 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.mm = new MusicGraph(data)
|
this.mm = new MusicGraph(data)
|
||||||
|
|
||||||
|
this.$notify({
|
||||||
|
title: 'Welcome!',
|
||||||
|
message: 'Use the search bar to add nodes. Right click nodes for more options',
|
||||||
|
type: 'info',
|
||||||
|
duration: 15 * 1000
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -63,7 +72,7 @@ export default {
|
|||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.pan-mode .pan-rect {
|
.pan-rect {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,11 +80,6 @@ export default {
|
|||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.pan-mode {
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 5px red solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Link */
|
/* Link */
|
||||||
svg .link.selected {
|
svg .link.selected {
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
@ -123,7 +127,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
svg .label.tag {
|
svg .label.tag {
|
||||||
fill: darkgrey;
|
fill: #409EFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.hover .label:not(.selected) {
|
svg.hover .label:not(.selected) {
|
||||||
|
@ -9,8 +9,10 @@
|
|||||||
@select="onSubmit"
|
@select="onSubmit"
|
||||||
>
|
>
|
||||||
<template slot-scope="{ item }">
|
<template slot-scope="{ item }">
|
||||||
<div class="value" >{{ item.value }} <span class="year"
|
<div class="value" v-bind:class="{tag: item.type === 'tag'}">{{ item.value }} <span class="year"
|
||||||
v-if="item.year !== 0">[{{item.year}}]</span></div>
|
v-if="item.year">[{{item.year}}]</span>
|
||||||
|
<span v-if="item.type === 'tag'" class="year">[tag]</span>
|
||||||
|
</div>
|
||||||
<span class="comment" v-if="item.comment">{{ item.comment }}</span>
|
<span class="comment" v-if="item.comment">{{ item.comment }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-autocomplete>
|
</el-autocomplete>
|
||||||
@ -18,7 +20,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as _ from 'lodash'
|
|
||||||
import {MusicGraphApi} from '../MusicGraphApi'
|
import {MusicGraphApi} from '../MusicGraphApi'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -29,28 +30,34 @@ export default {
|
|||||||
api: new MusicGraphApi()
|
api: new MusicGraphApi()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
query: _.debounce(function () {
|
|
||||||
if (this.query.length >= 3) {
|
|
||||||
this.api.autoComplete(this.query)
|
|
||||||
}
|
|
||||||
}, 500)
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
onSubmit: function (artist) {
|
onSubmit: function (line) {
|
||||||
this.$emit('query', artist.mbid)
|
if (line.type === 'artist') {
|
||||||
|
this.$emit('addArtist', line.mbid)
|
||||||
|
} else if (line.type === 'tag') {
|
||||||
|
this.$emit('addTag', line.id)
|
||||||
|
}
|
||||||
this.query = ''
|
this.query = ''
|
||||||
},
|
},
|
||||||
fetchSuggestions: function (query, callback) {
|
fetchSuggestions: function (query, callback) {
|
||||||
if (this.query.length >= 3) {
|
if (this.query.length >= 1) {
|
||||||
this.api.autoComplete(query)
|
this.api.autoComplete(query)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
callback(data.artists.map(a => {
|
callback(data.lines.map(line => {
|
||||||
return {
|
if (line.type === 'artist') {
|
||||||
'value': a.name,
|
return {
|
||||||
'year': a.year,
|
'value': line.name,
|
||||||
'comment': a.comment,
|
'year': line.year,
|
||||||
'mbid': a.mbid
|
'comment': line.comment,
|
||||||
|
'type': line.type,
|
||||||
|
'mbid': line.id
|
||||||
|
}
|
||||||
|
} else if (line.type === 'tag') {
|
||||||
|
return {
|
||||||
|
'value': line.name,
|
||||||
|
'type': line.type,
|
||||||
|
'id': line.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
@ -83,4 +90,8 @@ export default {
|
|||||||
margin-left: 0.1em;
|
margin-left: 0.1em;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
23
music_graph/src/components/Watermark.vue
Normal file
23
music_graph/src/components/Watermark.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<span id="watermark">{{text}}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Watermark',
|
||||||
|
props: ['text']
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#watermark {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(100% - 30px);
|
||||||
|
left: 1%;
|
||||||
|
pointer-events: none;
|
||||||
|
color: rgba(0,0,0,0.67);
|
||||||
|
font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
|
||||||
|
}
|
||||||
|
</style>
|
@ -35,7 +35,15 @@ const icons = {
|
|||||||
'h58.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' +
|
'h58.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' +
|
||||||
'v-47.289h-58.647l6.901-52.791H273.661z M167.326,167.109h-60.084l6.9-52.791h60.082L167.326,167.109z"/></svg>',
|
'v-47.289h-58.647l6.901-52.791H273.661z M167.326,167.109h-60.084l6.9-52.791h60.082L167.326,167.109z"/></svg>',
|
||||||
delete: '<svg width="28px" height="28px" viewBox="0 0 510 510" ><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' +
|
delete: '<svg width="28px" height="28px" viewBox="0 0 510 510" ><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' +
|
||||||
'L255,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"/></svg>'
|
'L255,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"/></svg>',
|
||||||
|
label: '<svg height="29px" width="29px" viewBox="1 0 511.999 511"><path d="m224.828125.5-10.148437 1.675781c-59.347657 9.816407-113.6875' +
|
||||||
|
' 40.507813-153.007813 86.417969-39.769531 46.429688-61.671875 105.707031-61.671875 166.910156 0 68.59375 26.722656 133.085938 75.246094' +
|
||||||
|
' 181.585938 48.519531 48.5 113.03125 75.210937 181.652344 75.210937 61.222656 0 120.519531-21.890625 166.964843-61.640625 45.929688-39.304687' +
|
||||||
|
' 76.632813-93.625 86.453125-152.953125l1.683594-10.15625zm9 65.621094 211.324219 211.234375c-115.246094-2.863281-208.464844-96.039063-211.324219-211.234375zm23.070313' +
|
||||||
|
' 406.136718c-119.574219 0-216.851563-97.238281-216.851563-216.753906 0-97.101562 63.300781-180.390625 153.996094-207.527344-.210938 4.234376-.328125 8.472657-.328125 12.695313' +
|
||||||
|
' 0 40.6875 9.402344 79.925781 27.175781 115.242187h-62.707031v40.046876h87.792968c7.007813 9.191406 14.667969 17.984374 22.984376 26.296874 4.4375 4.433594 9.007812 8.683594' +
|
||||||
|
' 13.703124 12.75h-167.53125v40.042969h230.199219c32.671875 14.683594 68.378907 22.417969 105.277344 22.417969 4.75 0 9.515625-.144531 14.28125-.40625-26.75 91.328125-110.402344' +
|
||||||
|
' 155.195312-207.992187 155.195312zm-98.714844-137.160156h197.226562v40.046875h-197.226562zm0 0"/></svg>'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Router from 'vue-router'
|
import Router from 'vue-router'
|
||||||
import HelloWorld from '@/components/HelloWorld'
|
import HelloWorld from '../components/HelloWorld'
|
||||||
|
import AboutPage from '../components/AboutPage'
|
||||||
|
|
||||||
Vue.use(Router)
|
Vue.use(Router)
|
||||||
|
|
||||||
export default new Router({
|
export default new Router({
|
||||||
|
mode: 'history',
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'HelloWorld',
|
name: 'HelloWorld',
|
||||||
component: HelloWorld
|
component: HelloWorld
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/about',
|
||||||
|
name: 'AboutPage',
|
||||||
|
component: AboutPage
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.0 KiB |
BIN
music_graph/static/diagram.png
Normal file
BIN
music_graph/static/diagram.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
Loading…
x
Reference in New Issue
Block a user