Bug fixes, labels, updated deps, tags in search bar

This commit is contained in:
simon 2019-06-22 17:08:09 -04:00
parent 55b47450df
commit f2760d8f3e
15 changed files with 271 additions and 74 deletions

View File

@ -3,10 +3,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>music_graph</title>
<title>music-graph v1.0</title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -3372,9 +3372,9 @@
"dev": true
},
"element-ui": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/element-ui/-/element-ui-2.7.2.tgz",
"integrity": "sha512-Exh9QTkm9gwMMPzg1TyaTlBKyr3k4K9XcC5vl0A/mneDvJX//RsURGuOWsCNDVQMdhh5h9e+W5icosh+pKfbCg==",
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/element-ui/-/element-ui-2.9.2.tgz",
"integrity": "sha512-HU5DDKivv2UFZghVWiP3I74IbTzSW8VXc070fM787i1X/u9f43olDuu3S6Pe9z87Z1oNjXt06ZmBlLYtySJMCw==",
"requires": {
"async-validator": "~1.8.1",
"babel-helper-vue-jsx-merge-props": "^2.0.0",

View File

@ -14,7 +14,7 @@
"d3": "^5.9.2",
"d3-force": "^2.0.1",
"d3-path": "^1.0.7",
"element-ui": "^2.7.2",
"element-ui": "^2.9.2",
"lodash": "^4.17.11",
"vue": "^2.6.10",
"vue-resource": "^1.5.1",

View File

@ -33,12 +33,13 @@ export function MusicGraph(data) {
this.simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody())
.force('collide', d3.forceCollide()
.radius(40)
.radius(35)
.strength(1))
.force('center', d3.forceCenter(width / 2, height / 2))
this.zoomed = () => {
this.container.attr('transform', d3.event.transform)
this.dismiss()
}
this.dismiss = () => {
@ -157,7 +158,7 @@ export function MusicGraph(data) {
this.makeMenu = function (d) {
let items = []
let i = 0
if (d.type === 'Group' && !d.membersExpanded) {
if ((d.type === 'Group' || d.type === 'Artist')) {
items.push({
idx: i++,
icon: icons.guitar,
@ -165,13 +166,12 @@ export function MusicGraph(data) {
fn: (d) => {
this.api.getGroupMembers(d.mbid, d.id)
.then(data => {
d.membersExpanded = true
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({
idx: i++,
icon: icons.expand,
@ -183,34 +183,43 @@ export function MusicGraph(data) {
this.expandedNodes.add(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({
idx: i++,
icon: icons.release,
title: 'Releases',
icon: icons.label,
title: 'Related',
fn: (d) => {
this.api.getArtistReleases(d.mbid, d.id)
this.api.getRelatedTags(d.id)
.then(data => {
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') &&
!d.tagsExpanded) {
if ((d.type === 'Artist' || d.type === 'Group')) {
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
if (d.type === 'Group' || d.type === 'Artist') {
fn = (d) => {
this.api.getArtistTags(d.mbid, d.id)
.then(data => {
this.addNodes(data.newNodes, data.relations, d.id)
d.tagsExpanded = true
})
}
} 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)
.then(data => {
this.addNodes(data.newNodes, data.relations, d.id)
d.tagsExpanded = true
})
}
}
@ -230,12 +238,28 @@ export function MusicGraph(data) {
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({
idx: i,
icon: icons.delete,
title: 'Remove from graph',
fn: (d) => {
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) => {
if (d.type === 'Group' || d.type === 'Artist') {
// Toggle artistInfo
this.nodes.forEach(x => {
x.hover = false
})
if (this._data.hoverArtist === d) {
this._data.hoverArtist = undefined
} else {
this._data.hoverArtist = d
d.hover = true
}
this._update()
}
}
this.addNode = function (newNode, relations) {
// Convert {id, id} relation to {node, node}
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
}
this.nodeById.set(newNode.id, newNode)
@ -358,11 +394,13 @@ export function MusicGraph(data) {
})
// Convert {id, id} relation to {node, node}
let linksToAdd = relations.map(({weight, source, target}) => ({
source: this.nodeById.get(source),
target: this.nodeById.get(target),
weight: weight
}))
let linksToAdd = relations
.filter(rel => this.nodeById.has(rel.source) && this.nodeById.has(rel.target))
.map(({weight, source, target}) => ({
source: this.nodeById.get(source),
target: this.nodeById.get(target),
weight: weight
}))
// Update source/targetLinks, avoid bidirectional links
for (const {source, target} of linksToAdd) {
@ -430,7 +468,7 @@ export function MusicGraph(data) {
.force('link', d3.forceLink(this.links)
.id(d => d.id)
.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()
@ -456,6 +494,7 @@ export function MusicGraph(data) {
.enter()
.append('circle')
.merge(this.node)
this.node
.classed('node', true)
.attr('r', d => d.radius)
.attr('stroke', d => this._getNodeColor(d))
@ -485,11 +524,13 @@ export function MusicGraph(data) {
this.setupKeyBindings = function () {
document.body.onkeydown = (e) => {
let isPanMode = this.svg.classed('pan-mode')
if (e.ctrlKey) {
this.svg.classed('pan-mode', true)
}
}
if (e.key === 'q') {
this.svg.classed('pan-mode', !isPanMode)
} else if (e.key === 'Escape') {
document.body.onkeyup = (e) => {
if (e.key === 'Control') {
this.svg.classed('pan-mode', false)
}
}
@ -511,6 +552,9 @@ export function MusicGraph(data) {
}
this._getNodeColor = function (node) {
if (node.hover) {
return '#FF0000'
}
if (this.expandedNodes.has(node.id)) {
return '#1cb3c8'
}
@ -524,7 +568,7 @@ export function MusicGraph(data) {
})
}
this.addTagById = function(tagid) {
this.addTagById = function (tagid) {
if (this.nodeById.has(tagid)) {
return
}
@ -546,7 +590,7 @@ export function MusicGraph(data) {
.attr('cx', d => d.x)
.attr('cy', d => d.y)
this.label
.attr('x', d => Math.round(d.node.x))
.attr('x', d => d.node.x)
.attr('y', d => d.node.y + d.baseline)
})

View File

@ -63,6 +63,10 @@ export function MusicGraphApi() {
return d3.json(this.url + '/artist/details/' + mbid)
}
/**
* Works in both directions
* @returns {Promise<{newNodes: *, relations: *} | never>}
*/
this.getGroupMembers = function (mbid, originId) {
return d3.json(this.url + '/artist/members/' + mbid)
.then((r) => {
@ -70,8 +74,8 @@ export function MusicGraphApi() {
newNodes: r.artists.map(nodeUtils.fromRawDict),
relations: r.artists.map(a => {
return {
source: a.id,
target: originId,
source: originId,
target: a.id,
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) {
if (ONLY_GENRE_TAGS) {
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) {
return d3.json(this.url + '/artist/related/' + mbid)
.then((r) => {
@ -168,7 +219,14 @@ export function MusicGraphApi() {
name: r.tag.name
}),
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(/ /g, '+')
return d3.json(this.url + '/artist/autocomplete/' + prefix)
return d3.json(this.url + '/autocomplete/' + prefix)
}
}

View 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>

View File

@ -7,14 +7,18 @@
style="float: right"
>
<figure>
<img
<el-image
alt=""
style="height: 128px"
width="128"
height="128"
class="block"
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>
</figure>
</div>
@ -62,12 +66,25 @@ export default {
}
</script>
<style scoped>
<style>
figure {
text-align: center;
margin: 0 20px 3em 20px;
width: 128px;
}
.el-image {
width: 128px;
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>

View File

@ -58,15 +58,16 @@ export default {
this.api.getArtistDetails(artist.mbid)
.then(info => {
this.artistInfo = info
this.artistInfo.releases = this.artistInfo.releases.filter(r =>
r.labels.indexOf('Album') !== -1 || r.labels.indexOf('EP') !== -1)
this.artistInfo.releases = this.artistInfo.releases
.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 => {
t.type = genres.has(t.name) ? '' : 'info'
return t
})
})
},
onTagClick: function(tag) {
onTagClick: function (tag) {
this.$emit('addTag', tag)
}
}

View File

@ -1,17 +1,19 @@
<template>
<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
v-bind:artist="hoverArtist"
v-on:addTag="onAddTag($event)"
/>
<canvas id="textMeasurementCanvas"></canvas>
<Watermark text="music-graph v1.0"/>
</div>
</template>
<script>
import ArtistInfo from './ArtistInfo'
import Watermark from './Watermark'
import {MusicGraph} from '../MusicGraph'
import InputBar from './InputBar'
@ -21,12 +23,12 @@ let data = {
}
export default {
components: {InputBar, ArtistInfo},
components: {InputBar, ArtistInfo, Watermark},
data() {
return data
},
methods: {
onQuery: function (e) {
onAddArtist: function (e) {
this.mm.addArtistByMbid(e)
},
onAddTag: function(e) {
@ -35,6 +37,13 @@ export default {
},
mounted() {
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>
@ -63,7 +72,7 @@ export default {
cursor: move;
}
svg.pan-mode .pan-rect {
.pan-rect {
pointer-events: all;
}
@ -71,11 +80,6 @@ export default {
pointer-events: all;
}
svg.pan-mode {
box-sizing: border-box;
border: 5px red solid;
}
/* Link */
svg .link.selected {
stroke-width: 2;
@ -123,7 +127,7 @@ export default {
}
svg .label.tag {
fill: darkgrey;
fill: #409EFF;
}
svg.hover .label:not(.selected) {

View File

@ -9,8 +9,10 @@
@select="onSubmit"
>
<template slot-scope="{ item }">
<div class="value" >{{ item.value }} <span class="year"
v-if="item.year !== 0">[{{item.year}}]</span></div>
<div class="value" v-bind:class="{tag: item.type === 'tag'}">{{ item.value }} <span class="year"
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>
</template>
</el-autocomplete>
@ -18,7 +20,6 @@
</template>
<script>
import * as _ from 'lodash'
import {MusicGraphApi} from '../MusicGraphApi'
export default {
@ -29,28 +30,34 @@ export default {
api: new MusicGraphApi()
}
},
watch: {
query: _.debounce(function () {
if (this.query.length >= 3) {
this.api.autoComplete(this.query)
}
}, 500)
},
methods: {
onSubmit: function (artist) {
this.$emit('query', artist.mbid)
onSubmit: function (line) {
if (line.type === 'artist') {
this.$emit('addArtist', line.mbid)
} else if (line.type === 'tag') {
this.$emit('addTag', line.id)
}
this.query = ''
},
fetchSuggestions: function (query, callback) {
if (this.query.length >= 3) {
if (this.query.length >= 1) {
this.api.autoComplete(query)
.then(data => {
callback(data.artists.map(a => {
return {
'value': a.name,
'year': a.year,
'comment': a.comment,
'mbid': a.mbid
callback(data.lines.map(line => {
if (line.type === 'artist') {
return {
'value': line.name,
'year': line.year,
'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;
vertical-align: top;
}
.tag {
color: #409EFF;
}
</style>

View 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>

View File

@ -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' +
'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' +
'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>'
}

View File

@ -1,15 +1,22 @@
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import HelloWorld from '../components/HelloWorld'
import AboutPage from '../components/AboutPage'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
},
{
path: '/about',
name: 'AboutPage',
component: AboutPage
}
]
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB