├── .eslintignore ├── media └── mindmap.png ├── .gitignore ├── test ├── .eslintrc ├── specs │ └── MindMap.spec.js ├── index.js ├── helpers │ ├── wait-for-update.js │ ├── index.js │ ├── Test.vue │ ├── utils.js │ └── map.js ├── karma.conf.js └── visual.js ├── .stylelintrc ├── .editorconfig ├── .babelrc ├── src ├── index.js ├── utils │ ├── nodeToHTML.js │ ├── subnodesToHTML.js │ ├── dimensions.js │ └── d3.js ├── parser │ ├── regex.js │ └── emojis.js └── Mindmap.js ├── .eslintrc.js ├── LICENSE ├── CONTRIBUTING.md ├── sass └── mindmap.sass ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/*.js 2 | -------------------------------------------------------------------------------- /media/mindmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anteriovieira/vue-mindmap/HEAD/media/mindmap.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | test/coverage 5 | dist 6 | yarn-error.log 7 | reports 8 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": ["stylelint-processor-html"], 3 | "extends": "stylelint-config-standard", 4 | "rules": { 5 | "no-empty-source": null 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | "last 2 versions" 9 | ] 10 | } 11 | } 12 | ] 13 | ], 14 | "plugins": [ 15 | "transform-vue-jsx", 16 | "transform-object-rest-spread" 17 | ], 18 | "env": { 19 | "test": { 20 | "plugins": [ 21 | "istanbul" 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Mindmap from './Mindmap.js' 2 | import '../sass/mindmap.sass' 3 | 4 | function plugin (Vue, options = { tag: 'mindmap' }) { 5 | Vue.component(options.tag, Mindmap) 6 | } 7 | 8 | // Install by default if using the script tag 9 | if (typeof window !== 'undefined' && window.Vue) { 10 | window.Vue.use(plugin) 11 | } 12 | 13 | export default plugin 14 | const version = '__VERSION__' 15 | // Export all components too 16 | export { 17 | Mindmap, 18 | version 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | extends: 'vue', 8 | // add your custom rules here 9 | 'rules': { 10 | // allow async-await 11 | 'generator-star-spacing': 0, 12 | // allow debugger during development 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 14 | }, 15 | globals: { 16 | requestAnimationFrame: true, 17 | performance: true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/specs/MindMap.spec.js: -------------------------------------------------------------------------------- 1 | import Mindmap from 'src/Mindmap' 2 | import { createVM } from '../helpers/utils' 3 | 4 | import map from '../helpers/map' 5 | 6 | const newVM = (context) => createVM( 7 | context, 8 | ``, 9 | { data: map, components: { Mindmap }} 10 | ) 11 | 12 | describe('Mind Map', function () { 13 | it('should exist svg', function () { 14 | const vm = newVM(this) 15 | 16 | vm.$el.querySelector('svg').should.exist 17 | }) 18 | 19 | it('should exist by svg class', function () { 20 | const vm = newVM(this) 21 | 22 | vm.$el.querySelector('.mindmap-svg').should.exist 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/utils/nodeToHTML.js: -------------------------------------------------------------------------------- 1 | import { categoryToIMG } from '../parser/emojis' 2 | 3 | /* 4 | * Return the HTML representation of a node. 5 | * The node is an object that has text, url, and category attributes 6 | * all of them optional. 7 | */ 8 | export default (node) => { 9 | let href = `href="${node.url}"` 10 | let emoji = categoryToIMG(node.category) 11 | 12 | // If url is not specified remove the emoji and the href attribute, 13 | // so that the node isn't clickable, and the user can see that without 14 | // having to hover the node. 15 | if (!node.url) { 16 | href = '' 17 | emoji = '' 18 | } 19 | 20 | return `${node.text || ''} ${emoji}` 21 | } 22 | -------------------------------------------------------------------------------- /src/parser/regex.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Extract text from the inner HTML of a node. 3 | */ 4 | const getText = (html) => { 5 | const res = [] 6 | 7 | // Match all text inside A tags. If there's no A tags, 8 | // match text inside P tags instead. 9 | const matchText = /]*>([^<]*)<\/a>|]*>([^>]*)<\/p>/g 10 | let match = matchText.exec(html) 11 | 12 | while (match) { 13 | res.push(match[1] || match[2]) 14 | match = matchText.exec(html) 15 | } 16 | 17 | return res.join(' ') 18 | } 19 | 20 | /* 21 | * Extract HREF content from the first link on a node. 22 | */ 23 | const getURL = (html) => { 24 | // Match HREF content inside A tags. 25 | const matchURL = /]*href="([^"]*)"[^>]*>[^<]*<\/a>/ 26 | const match = matchURL.exec(html) 27 | 28 | if (match) { 29 | return match[1] 30 | } 31 | 32 | return '' 33 | } 34 | 35 | module.exports = { 36 | getText, 37 | getURL 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/subnodesToHTML.js: -------------------------------------------------------------------------------- 1 | import { categoryToIMG } from '../parser/emojis' 2 | 3 | /* 4 | * Return the HTML representation of a node. 5 | * The node is an object that has text, url, and category attributes 6 | * all of them optional. 7 | */ 8 | const subnodesToHTML = (subnodes = [], fcolor) => { 9 | let color = fcolor || '' 10 | 11 | if (!fcolor && subnodes.length > 0 && subnodes[0].color) { 12 | color = `style="border-left-color: ${subnodes[0].color}"` 13 | } 14 | 15 | return subnodes.map((subnode) => { 16 | let href = `href="${subnode.url}"` 17 | let emoji = categoryToIMG(subnode.category) 18 | 19 | if (!subnode.url) { 20 | href = '' 21 | emoji = '' 22 | } 23 | 24 | return `
25 | ${subnode.text || ''} ${emoji} 26 |
${subnodesToHTML(subnode.nodes, color)}
27 |
` 28 | }).join('') 29 | } 30 | 31 | export default subnodesToHTML 32 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // Polyfill fn.bind() for PhantomJS 2 | import bind from 'function-bind' 3 | /* eslint-disable no-extend-native */ 4 | Function.prototype.bind = bind 5 | 6 | // Polyfill Object.assign for PhantomJS 7 | import objectAssign from 'object-assign' 8 | Object.assign = objectAssign 9 | 10 | // require all src files for coverage. 11 | // you can also change this to match only the subset of files that 12 | // you want coverage for. 13 | const srcContext = require.context('../src', true, /^\.\/(?!index(\.js)?$)/) 14 | srcContext.keys().forEach(srcContext) 15 | 16 | // Use a div to insert elements 17 | before(function () { 18 | const el = document.createElement('DIV') 19 | el.id = 'tests' 20 | document.body.appendChild(el) 21 | }) 22 | 23 | // Remove every test html scenario 24 | afterEach(function () { 25 | const el = document.getElementById('tests') 26 | for (let i = 0; i < el.children.length; ++i) { 27 | el.removeChild(el.children[i]) 28 | } 29 | }) 30 | 31 | const specsContext = require.context('./specs', true) 32 | specsContext.keys().forEach(specsContext) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Antério vieira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/anteriovieira/vue-mindmap). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **Keep the same style** - eslint will automatically be ran before committing 11 | 12 | - **Tip** to pass lint tests easier use the `npm run lint:fix` command 13 | 14 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 15 | 16 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 17 | 18 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 19 | 20 | - **Create feature branches** - Don't ask us to pull from your master branch. 21 | 22 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 23 | 24 | - **Send coherent history** - Make sure your commits message means something 25 | 26 | 27 | ## Running Tests 28 | 29 | Launch visual tests and watch the components at the same time 30 | 31 | ``` bash 32 | $ npm run dev 33 | ``` 34 | 35 | 36 | **Happy coding**! 37 | -------------------------------------------------------------------------------- /test/helpers/wait-for-update.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | 3 | // Testing helper 4 | // nextTick().then(() => { 5 | // 6 | // Automatically waits for nextTick 7 | // }).then(() => { 8 | // return a promise or value to skip the wait 9 | // }) 10 | function nextTick () { 11 | const jobs = [] 12 | let done 13 | 14 | const chainer = { 15 | then (cb) { 16 | jobs.push(cb) 17 | return chainer 18 | } 19 | } 20 | 21 | function shift (...args) { 22 | const job = jobs.shift() 23 | let result 24 | try { 25 | result = job(...args) 26 | } catch (e) { 27 | jobs.length = 0 28 | done(e) 29 | } 30 | 31 | // wait for nextTick 32 | if (result !== undefined) { 33 | if (result.then) { 34 | result.then(shift) 35 | } else { 36 | shift(result) 37 | } 38 | } else if (jobs.length) { 39 | requestAnimationFrame(() => Vue.nextTick(shift)) 40 | } 41 | } 42 | 43 | // First time 44 | Vue.nextTick(() => { 45 | done = jobs[jobs.length - 1] 46 | if (done.toString().slice(0, 14) !== 'function (err)') { 47 | throw new Error('waitForUpdate chain is missing .then(done)') 48 | } 49 | shift() 50 | }) 51 | 52 | return chainer 53 | } 54 | 55 | exports.nextTick = nextTick 56 | exports.delay = time => new Promise(resolve => setTimeout(resolve, time)) 57 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const baseConfig = require('../build/webpack.config.dev.js') 3 | 4 | const webpackConfig = merge(baseConfig, { 5 | // use inline sourcemap for karma-sourcemap-loader 6 | devtool: '#inline-source-map' 7 | }) 8 | 9 | webpackConfig.plugins = [] 10 | 11 | const vueRule = webpackConfig.module.rules.find(rule => rule.loader === 'vue-loader') 12 | vueRule.options = vueRule.options || {} 13 | vueRule.options.loaders = vueRule.options.loaders || {} 14 | vueRule.options.loaders.js = 'babel-loader' 15 | 16 | // no need for app entry during tests 17 | delete webpackConfig.entry 18 | 19 | module.exports = function (config) { 20 | config.set({ 21 | // to run in additional browsers: 22 | // 1. install corresponding karma launcher 23 | // http://karma-runner.github.io/0.13/config/browsers.html 24 | // 2. add it to the `browsers` array below. 25 | browsers: ['Chrome'], 26 | frameworks: ['mocha', 'chai-dom', 'sinon-chai'], 27 | reporters: ['spec', 'coverage'], 28 | files: ['./index.js'], 29 | preprocessors: { 30 | './index.js': ['webpack', 'sourcemap'] 31 | }, 32 | webpack: webpackConfig, 33 | webpackMiddleware: { 34 | noInfo: true 35 | }, 36 | coverageReporter: { 37 | dir: './coverage', 38 | reporters: [ 39 | { type: 'lcov', subdir: '.' }, 40 | { type: 'text-summary' } 41 | ] 42 | } 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | import camelcase from 'camelcase' 2 | import { createVM, Vue } from './utils' 3 | import { nextTick } from './wait-for-update' 4 | 5 | export function dataPropagationTest (Component) { 6 | return function () { 7 | const spy = sinon.spy() 8 | const vm = createVM(this, function (h) { 9 | return ( 10 | Hello 11 | ) 12 | }) 13 | spy.should.have.not.been.called 14 | vm.$('.custom').should.exist 15 | vm.$('.custom').click() 16 | spy.should.have.been.calledOnce 17 | } 18 | } 19 | 20 | export function attrTest (it, base, Component, attr) { 21 | const attrs = Array.isArray(attr) ? attr : [attr] 22 | 23 | attrs.forEach(attr => { 24 | it(attr, function (done) { 25 | const vm = createVM(this, function (h) { 26 | const opts = { 27 | props: { 28 | [camelcase(attr)]: this.active 29 | } 30 | } 31 | return ( 32 | {attr} 33 | ) 34 | }, { 35 | data: { active: true } 36 | }) 37 | vm.$(`.${base}`).should.have.class(`${base}--${attr}`) 38 | vm.active = false 39 | nextTick().then(() => { 40 | vm.$(`.${base}`).should.not.have.class(`${base}--${attr}`) 41 | vm.active = true 42 | }).then(done) 43 | }) 44 | }) 45 | } 46 | 47 | export { 48 | createVM, 49 | Vue, 50 | nextTick 51 | } 52 | -------------------------------------------------------------------------------- /sass/mindmap.sass: -------------------------------------------------------------------------------- 1 | $grey100: #f5f5f5 2 | $grey500: #9e9e9e 3 | $grey800: #424242 4 | $grey900: #212121 5 | $orange700: #f57c00 6 | $white: #fff 7 | $shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12) 8 | 9 | 10 | .mindmap-svg 11 | height: 100vh 12 | width: 100% 13 | 14 | &:focus 15 | outline: none 16 | 17 | .mindmap-node > a 18 | background: $grey100 19 | border-radius: 10px 20 | box-shadow: $shadow 21 | color: $grey900 22 | display: inline-block 23 | font-family: 'Raleway' 24 | font-size: 22px 25 | margin: 0 auto 26 | padding: 15px 27 | text-align: center 28 | text-decoration: none 29 | transition: background-color .2s, color .2s ease-out 30 | 31 | &[href] 32 | &:hover 33 | background-color: $orange700 34 | color: $white 35 | cursor: pointer 36 | 37 | .mindmap-node--editable 38 | cursor: all-scroll 39 | 40 | & > a 41 | pointer-events: none 42 | 43 | .mindmap-subnode-group 44 | align-items: center 45 | border-left: 4px solid $grey500 46 | display: flex 47 | margin-left: 15px 48 | padding: 5px 49 | 50 | a 51 | color: $grey900 52 | font-family: 'Raleway' 53 | font-size: 16px 54 | padding: 2px 5px 55 | 56 | .mindmap-connection 57 | fill: transparent 58 | stroke: $grey500 59 | stroke-dasharray: 10px 4px 60 | stroke-width: 3px 61 | 62 | .mindmap-emoji 63 | height: 24px 64 | vertical-align: bottom 65 | width: 24px 66 | 67 | .reddit-emoji 68 | border-radius: 50% -------------------------------------------------------------------------------- /src/utils/dimensions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Return the dimensions (width & height) that some HTML 3 | * with a given style would take in the page. 4 | */ 5 | export const getDimensions = (html, style, classname) => { 6 | const el = document.createElement('span') 7 | const dimensions = {} 8 | 9 | // Set display: inline-block so that the size of el 10 | // will depend on the size of its children. 11 | el.style.display = 'inline-block' 12 | 13 | // Hide the element (it will be added to the page for a short time). 14 | el.style.visibility = 'hidden' 15 | 16 | el.className = classname 17 | el.innerHTML = html 18 | 19 | // Apply CSS rules. 20 | Object.keys(style).forEach((rule) => { 21 | el.style[rule] = style[rule] 22 | }) 23 | document.body.append(el) 24 | 25 | dimensions.width = el.offsetWidth 26 | dimensions.height = el.offsetHeight 27 | 28 | el.remove() 29 | return dimensions 30 | } 31 | 32 | /* 33 | * Return the dimensions of an SVG viewport calculated from 34 | * some given nodes. 35 | */ 36 | export const getViewBox = (nodes) => { 37 | const Xs = [] 38 | const Ys = [] 39 | 40 | nodes.forEach((node) => { 41 | const x = node.x || node.fx 42 | const y = node.y || node.fy 43 | 44 | if (x) { 45 | Xs.push(x) 46 | } 47 | 48 | if (y) { 49 | Ys.push(y) 50 | } 51 | }) 52 | 53 | if (Xs.length === 0 || Ys.length === 0) { 54 | return '0 0 0 0' 55 | } 56 | 57 | // Find the smallest coordinates... 58 | const min = [ 59 | Math.min(...Xs) - 150, 60 | Math.min(...Ys) - 150 61 | ] 62 | 63 | // ...and the biggest ones. 64 | const max = [ 65 | (Math.max(...Xs) - min[0]) + 150, 66 | (Math.max(...Ys) - min[1]) + 150 67 | ] 68 | 69 | return `${min.join(' ')} ${max.join(' ')}` 70 | } 71 | -------------------------------------------------------------------------------- /test/visual.js: -------------------------------------------------------------------------------- 1 | import 'style-loader!css-loader!mocha-css' 2 | 3 | // create a div where mocha can add its stuff 4 | const mochaDiv = document.createElement('DIV') 5 | mochaDiv.id = 'mocha' 6 | document.body.appendChild(mochaDiv) 7 | 8 | import 'mocha/mocha.js' 9 | import sinon from 'sinon' 10 | import chai from 'chai' 11 | window.mocha.setup({ 12 | ui: 'bdd', 13 | slow: 750, 14 | timeout: 5000, 15 | globals: [ 16 | '__VUE_DEVTOOLS_INSTANCE_MAP__', 17 | 'script', 18 | 'inject', 19 | 'originalOpenFunction' 20 | ] 21 | }) 22 | window.sinon = sinon 23 | chai.use(require('chai-dom')) 24 | chai.use(require('sinon-chai')) 25 | chai.should() 26 | 27 | let vms = [] 28 | let testId = 0 29 | 30 | beforeEach(function () { 31 | this.DOMElement = document.createElement('DIV') 32 | this.DOMElement.id = `test-${++testId}` 33 | document.body.appendChild(this.DOMElement) 34 | }) 35 | 36 | afterEach(function () { 37 | const testReportElements = document.getElementsByClassName('test') 38 | const lastReportElement = testReportElements[testReportElements.length - 1] 39 | 40 | if (!lastReportElement) return 41 | const el = document.getElementById(this.DOMElement.id) 42 | if (el) lastReportElement.appendChild(el) 43 | // Save the vm to hide it later 44 | if (this.DOMElement.vm) vms.push(this.DOMElement.vm) 45 | }) 46 | 47 | // Hide all tests at the end to prevent some weird bugs 48 | before(function () { 49 | vms = [] 50 | testId = 0 51 | }) 52 | after(function () { 53 | requestAnimationFrame(function () { 54 | setTimeout(function () { 55 | vms.forEach(vm => { 56 | // Hide if test passed 57 | if (!vm.$el.parentElement.classList.contains('fail')) { 58 | vm.$children[0].visible = false 59 | } 60 | }) 61 | }, 100) 62 | }) 63 | }) 64 | 65 | const specsContext = require.context('./specs', true) 66 | specsContext.keys().forEach(specsContext) 67 | 68 | window.mocha.checkLeaks() 69 | window.mocha.run() 70 | -------------------------------------------------------------------------------- /test/helpers/Test.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 39 | 40 | 108 | -------------------------------------------------------------------------------- /test/helpers/utils.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | import Test from './Test.vue' 3 | 4 | Vue.config.productionTip = false 5 | const isKarma = !!window.__karma__ 6 | 7 | export function createVM (context, template, opts = {}) { 8 | return isKarma 9 | ? createKarmaTest(context, template, opts) 10 | : createVisualTest(context, template, opts) 11 | } 12 | 13 | const emptyNodes = document.querySelectorAll('nonexistant') 14 | Vue.prototype.$$ = function $$ (selector) { 15 | const els = document.querySelectorAll(selector) 16 | const vmEls = this.$el.querySelectorAll(selector) 17 | const fn = vmEls.length 18 | ? el => vmEls.find(el) 19 | : el => this.$el === el 20 | const found = Array.from(els).filter(fn) 21 | return found.length 22 | ? found 23 | : emptyNodes 24 | } 25 | 26 | Vue.prototype.$ = function $ (selector) { 27 | const els = document.querySelectorAll(selector) 28 | const vmEl = this.$el.querySelector(selector) 29 | const fn = vmEl 30 | ? el => el === vmEl 31 | : el => el === this.$el 32 | // Allow should chaining for tests 33 | return Array.from(els).find(fn) || emptyNodes 34 | } 35 | 36 | export function createKarmaTest (context, template, opts) { 37 | const el = document.createElement('div') 38 | document.getElementById('tests').appendChild(el) 39 | const render = typeof template === 'string' 40 | ? { template: `
${template}
` } 41 | : { render: template } 42 | return new Vue({ 43 | el, 44 | name: 'Test', 45 | ...render, 46 | ...opts 47 | }) 48 | } 49 | 50 | export function createVisualTest (context, template, opts) { 51 | let vm 52 | if (typeof template === 'string') { 53 | opts.components = opts.components || {} 54 | // Let the user define a test component 55 | if (!opts.components.Test) { 56 | opts.components.Test = Test 57 | } 58 | vm = new Vue({ 59 | name: 'TestContainer', 60 | el: context.DOMElement, 61 | template: `${template}`, 62 | ...opts 63 | }) 64 | } else { 65 | // TODO allow redefinition of Test component 66 | vm = new Vue({ 67 | name: 'TestContainer', 68 | el: context.DOMElement, 69 | render (h) { 70 | return h(Test, { 71 | attrs: { 72 | id: context.DOMElement.id 73 | } 74 | // render the passed component with this scope 75 | }, [template.call(this, h)]) 76 | }, 77 | ...opts 78 | }) 79 | } 80 | 81 | context.DOMElement.vm = vm 82 | return vm 83 | } 84 | 85 | export function register (name, component) { 86 | Vue.component(name, component) 87 | } 88 | 89 | export { isKarma, Vue } 90 | -------------------------------------------------------------------------------- /src/parser/emojis.js: -------------------------------------------------------------------------------- 1 | // Regex that matches all emojis in a string. 2 | const matchEmojis = /([\uD800-\uDBFF][\uDC00-\uDFFF])/g 3 | 4 | // Emoji to category table. 5 | const conversionTable = { 6 | '🗺': 'mindmap', 7 | '🌐': 'wiki', 8 | '🗂': 'stack exchange', 9 | '📝': 'course', 10 | '📖': 'free book', 11 | '📕': 'non-free book', 12 | '📄': 'paper', 13 | '👀': 'video', 14 | '🖋': 'article', 15 | '🗃': 'blog', 16 | '🐙': 'github', 17 | '👾': 'interactive', 18 | '🖌': 'image', 19 | '🎙': 'podcast', 20 | '📮': 'newsletter', 21 | '💬': 'chat', 22 | '🎥': 'youtube', 23 | '🤖': 'reddit', 24 | '🔎': 'quora', 25 | '🔗': undefined 26 | } 27 | 28 | // Category to emoji table, based on the table above. 29 | const revConversionTable = {} 30 | 31 | Object.keys(conversionTable).forEach((key) => { 32 | revConversionTable[conversionTable[key]] = key 33 | }) 34 | 35 | /* 36 | * Return an emoji as a GitHub image. 37 | */ 38 | const emojiTemplate = (unicode, category) => ( 39 | `` 40 | ) 41 | 42 | const customEmojiTemplate = (emoji, category) => ( 43 | `` 44 | ) 45 | 46 | /* 47 | * Return the category represented by the given emoji. 48 | */ 49 | const emojiToCategory = emoji => conversionTable[emoji] || '' 50 | 51 | /* 52 | * Convert all emojis to an IMG tag. 53 | * The bitwise magic is explained at http://crocodillon.com/blog/parsing-emoji-unicode-in-javascript 54 | */ 55 | const emojiToIMG = html => ( 56 | /* eslint-disable no-bitwise */ 57 | html.replace(matchEmojis, (match) => { 58 | switch (match) { 59 | case '🤖': 60 | return '' 61 | 62 | case '🗂': 63 | return '' 64 | 65 | case '🐙': 66 | return customEmojiTemplate('octocat', 'github') 67 | 68 | case '🔎': 69 | return '' 70 | 71 | // Regular unicode Emojis. 72 | default: { 73 | // Keep the first 10 bits. 74 | const lead = match.charCodeAt(0) & 0x3FF 75 | const trail = match.charCodeAt(1) & 0x3FF 76 | 77 | // 0x[lead][trail] 78 | const unicode = ((lead << 10) + trail).toString(16) 79 | 80 | return emojiTemplate(`1${unicode}`, emojiToCategory(match)) 81 | } 82 | } 83 | }) 84 | /* eslint-enable no-bitwise */ 85 | ) 86 | 87 | /* 88 | * Inverse of emojiToCategory, but instead of returning an emoji 89 | * returns an IMG tag corresponding to that emoji. 90 | */ 91 | const categoryToIMG = category => emojiToIMG(revConversionTable[category] || '') 92 | 93 | module.exports = { 94 | matchEmojis, 95 | emojiToIMG, 96 | emojiTemplate, 97 | emojiToCategory, 98 | categoryToIMG 99 | } 100 | -------------------------------------------------------------------------------- /src/utils/d3.js: -------------------------------------------------------------------------------- 1 | import { drag, event, zoom } from 'd3' 2 | import { getViewBox } from './dimensions' 3 | 4 | /** 5 | * Bind data to a , inside a G element, inside the given root element. 6 | * Root is a D3 selection, data is an object or array, tag is a string. 7 | */ 8 | const bindData = (root, data, tag) => ( 9 | root.append('g') 10 | .selectAll(tag) 11 | .data(data) 12 | .enter() 13 | .append(tag) 14 | ) 15 | 16 | /** 17 | * Bind connections to PATH tags on the given SVG 18 | */ 19 | export const d3Connections = (svg, connections) => ( 20 | bindData(svg, connections, 'path') 21 | .attr('class', 'mindmap-connection') 22 | ) 23 | 24 | /* eslint-disable no-param-reassign */ 25 | /** 26 | * Bind rodes to FOREIGNOBJECT tags on the given SVG, 27 | * and set dimensions and html. 28 | */ 29 | export const d3Nodes = (svg, nodes) => { 30 | const selection = svg.append('g') 31 | .selectAll('foreignObject') 32 | .data(nodes) 33 | .enter() 34 | 35 | const d3nodes = selection 36 | .append('foreignObject') 37 | .attr('class', 'mindmap-node') 38 | .attr('width', node => node.width + 4) 39 | .attr('height', node => node.height) 40 | .html(node => node.html) 41 | 42 | const d3subnodes = selection 43 | .append('foreignObject') 44 | .attr('class', 'mindmap-subnodes') 45 | .attr('width', node => node.nodesWidth + 4) 46 | .attr('height', node => node.nodesHeight) 47 | .html(node => node.nodesHTML) 48 | 49 | return { 50 | nodes: d3nodes, 51 | subnodes: d3subnodes 52 | } 53 | } 54 | 55 | /** 56 | * Callback for forceSimulation tick event. 57 | */ 58 | export const onTick = (conns, nodes, subnodes) => { 59 | const d = conn => [ 60 | 'M', 61 | conn.source.x, 62 | conn.source.y, 63 | 'Q', 64 | conn.source.x + (conn.curve && conn.curve.x ? conn.curve.x : 0), 65 | conn.source.y + (conn.curve && conn.curve.y ? conn.curve.y : 0), 66 | ',', 67 | conn.target.x, 68 | conn.target.y 69 | ].join(' ') 70 | 71 | // Set the connections path. 72 | conns.attr('d', d) 73 | 74 | // Set nodes position. 75 | nodes 76 | .attr('x', node => node.x - (node.width / 2)) 77 | .attr('y', node => node.y - (node.height / 2)) 78 | 79 | // Set subnodes groups color and position. 80 | subnodes 81 | .attr('x', node => node.x + (node.width / 2)) 82 | .attr('y', node => node.y - (node.nodesHeight / 2)) 83 | } 84 | 85 | /* 86 | * Return drag behavior to use on d3.selection.call(). 87 | */ 88 | export const d3Drag = (simulation, svg, nodes) => { 89 | const dragStart = (node) => { 90 | if (!event.active) { 91 | simulation.alphaTarget(0.2).restart() 92 | } 93 | 94 | node.fx = node.x 95 | node.fy = node.y 96 | } 97 | 98 | const dragged = (node) => { 99 | node.fx = event.x 100 | node.fy = event.y 101 | } 102 | 103 | const dragEnd = () => { 104 | if (!event.active) { 105 | simulation.alphaTarget(0) 106 | } 107 | 108 | svg.attr('viewBox', getViewBox(nodes.data())) 109 | } 110 | 111 | return drag() 112 | .on('start', dragStart) 113 | .on('drag', dragged) 114 | .on('end', dragEnd) 115 | } 116 | 117 | /* eslint-enable no-param-reassign */ 118 | 119 | /* 120 | * Return pan and zoom behavior to use on d3.selection.call(). 121 | */ 122 | export const d3PanZoom = el => ( 123 | zoom().scaleExtent([0.3, 5]) 124 | .on('zoom', () => ( 125 | el.selectAll('svg > g').attr('transform', event.transform) 126 | )) 127 | ) 128 | -------------------------------------------------------------------------------- /src/Mindmap.js: -------------------------------------------------------------------------------- 1 | import { 2 | forceCollide, 3 | forceLink, 4 | forceManyBody, 5 | forceSimulation, 6 | select, 7 | zoom, 8 | zoomIdentity 9 | } from 'd3' 10 | 11 | import { 12 | d3Connections, 13 | d3Nodes, 14 | d3Drag, 15 | d3PanZoom, 16 | onTick 17 | } from './utils/d3' 18 | 19 | import { getDimensions, getViewBox } from './utils/dimensions' 20 | import subnodesToHTML from './utils/subnodesToHTML' 21 | import nodeToHTML from './utils/nodeToHTML' 22 | 23 | export default { 24 | props: { 25 | nodes: { 26 | type: Array, 27 | default: () => ([]) 28 | }, 29 | connections: { 30 | type: Array, 31 | default: () => ([]) 32 | }, 33 | editable: { 34 | type: Boolean, 35 | default: false 36 | } 37 | }, 38 | data () { 39 | return { 40 | simulation: null 41 | } 42 | }, 43 | methods: { 44 | prepareNodes () { 45 | const render = (node) => { 46 | node.html = nodeToHTML(node) 47 | node.nodesHTML = subnodesToHTML(node.nodes) 48 | 49 | const dimensions = getDimensions(node.html, {}, 'mindmap-node') 50 | node.width = dimensions.width 51 | node.height = dimensions.height 52 | 53 | const nodesDimensions = getDimensions(node.nodesHTML, {}, 'mindmap-subnodes-text') 54 | node.nodesWidth = nodesDimensions.width 55 | node.nodesHeight = nodesDimensions.height 56 | } 57 | 58 | this.nodes.forEach(node => render(node)) 59 | }, 60 | /** 61 | * Add new class to nodes, attach drag behevior, 62 | * and start simulation. 63 | */ 64 | prepareEditor (svg, conns, nodes, subnodes) { 65 | nodes 66 | .attr('class', 'mindmap-node mindmap-node--editable') 67 | .on('dbclick', (node) => { 68 | node.fx = null 69 | node.fy = null 70 | }) 71 | 72 | nodes.call(d3Drag(this.simulation, svg, nodes)) 73 | 74 | // Tick the simulation 100 times 75 | for (let i = 0; i < 100; i += 1) { 76 | this.simulation.tick() 77 | } 78 | 79 | setTimeout(() => { 80 | this.simulation 81 | .alphaTarget(0.5).on('tick', () => ( 82 | onTick(conns, nodes, subnodes) 83 | )) 84 | }, 200) 85 | }, 86 | /** 87 | * Render mind map unsing D3 88 | */ 89 | renderMap () { 90 | const svg = select(this.$refs.mountPoint) 91 | 92 | // Clear the SVG in case there's stuff already there. 93 | svg.selectAll('*').remove() 94 | 95 | // Add subnode group 96 | svg.append('g').attr('id', 'mindmap-subnodes') 97 | 98 | this.prepareNodes() 99 | 100 | // Bind data to SVG elements and set all the properties to render them 101 | const connections = d3Connections(svg, this.connections) 102 | const { nodes, subnodes } = d3Nodes(svg, this.nodes) 103 | 104 | nodes.append('title').text(node => node.note) 105 | 106 | // Bind nodes and connections to the simulation 107 | this.simulation 108 | .nodes(this.nodes) 109 | .force('link').links(this.connections) 110 | 111 | if (this.editable) { 112 | this.prepareEditor(svg, connections, nodes, subnodes) 113 | } 114 | 115 | // Tick the simulation 100 times 116 | for (let i = 0; i < 100; i += 1) { 117 | this.simulation.tick() 118 | } 119 | 120 | onTick(connections, nodes, subnodes) 121 | 122 | svg.attr('viewBox', getViewBox(nodes.data())) 123 | .call(d3PanZoom(svg)) 124 | .on('dbClick.zoom', null) 125 | } 126 | }, 127 | mounted () { 128 | this.renderMap() 129 | }, 130 | updated () { 131 | zoom().transform(select(this.$refs.mountPoint), zoomIdentity) 132 | 133 | this.renderMap() 134 | }, 135 | created () { 136 | // Create force simulation to position nodes that have 137 | // no coordinate, and add it to the component state 138 | this.simulation = forceSimulation() 139 | .force('link', forceLink().id(node => node.text)) 140 | .force('charge', forceManyBody()) 141 | .force('collide', forceCollide().radius(100)) 142 | }, 143 | render () { 144 | return ( 145 |
146 | 147 |
148 | ) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-mindmap", 3 | "version": "0.0.4", 4 | "description": "Vue component for mindnode maps", 5 | "author": "Antério vieira ", 6 | "main": "dist/vue-mindmap.common.js", 7 | "module": "dist/vue-mindmap.esm.js", 8 | "browser": "dist/vue-mindmap.js", 9 | "unpkg": "dist/vue-mindmap.js", 10 | "style": "dist/vue-mindmap.css", 11 | "files": [ 12 | "dist", 13 | "src" 14 | ], 15 | "scripts": { 16 | "clean": "rimraf dist", 17 | "build": "yon run build:common && yon run build:browser && yon run build:browser:min", 18 | "build:common": "cross-env NODE_ENV=common webpack --config build/webpack.config.common.js --progress --hide-modules", 19 | "build:browser:base": "webpack --config build/webpack.config.browser.js --progress --hide-modules", 20 | "build:browser": "cross-env NODE_ENV=browser yon run build:browser:base", 21 | "build:browser:min": "cross-env NODE_ENV=production yon run build:browser:base -- -p", 22 | "build:dll": "webpack --progress --config build/webpack.config.dll.js", 23 | "lint": "yon run lint:js", 24 | "lint:js": "eslint --ext js --ext jsx --ext vue src test/**/*.spec.js test/*.js build", 25 | "lint:js:fix": "yon run lint:js -- --fix", 26 | "lint:staged": "lint-staged", 27 | "pretest": "yon run lint", 28 | "test": "cross-env BABEL_ENV=test karma start test/karma.conf.js --single-run", 29 | "dev": "webpack-dashboard -- webpack-dev-server --config build/webpack.config.dev.js --open", 30 | "dev:coverage": "cross-env BABEL_ENV=test karma start test/karma.conf.js", 31 | "prepublish": "yon run build" 32 | }, 33 | "lint-staged": { 34 | "*.{vue,jsx,js}": [ 35 | "eslint --fix" 36 | ], 37 | "*.{vue,css}": [ 38 | "stylefmt", 39 | "stylelint" 40 | ] 41 | }, 42 | "pre-commit": "lint:staged", 43 | "devDependencies": { 44 | "add-asset-html-webpack-plugin": "^2.0.0", 45 | "babel-core": "^6.24.0", 46 | "babel-eslint": "^7.2.0", 47 | "babel-helper-vue-jsx-merge-props": "^2.0.0", 48 | "babel-loader": "^7.0.0", 49 | "babel-plugin-istanbul": "^4.1.0", 50 | "babel-plugin-syntax-jsx": "^6.18.0", 51 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 52 | "babel-plugin-transform-runtime": "^6.23.0", 53 | "babel-plugin-transform-vue-jsx": "^3.4.0", 54 | "babel-preset-env": "^1.4.0", 55 | "chai": "^3.5.0", 56 | "chai-dom": "^1.4.0", 57 | "cross-env": "^4.0.0", 58 | "css-loader": "^0.28.0", 59 | "eslint": "^3.19.0", 60 | "eslint-config-vue": "^2.0.0", 61 | "eslint-plugin-vue": "^2.0.0", 62 | "extract-text-webpack-plugin": "^2.1.0", 63 | "html-webpack-plugin": "^2.28.0", 64 | "karma": "^1.7.0", 65 | "karma-chai-dom": "^1.1.0", 66 | "karma-chrome-launcher": "^2.1.0", 67 | "karma-coverage": "^1.1.0", 68 | "karma-mocha": "^1.3.0", 69 | "karma-sinon-chai": "^1.3.0", 70 | "karma-sourcemap-loader": "^0.3.7", 71 | "karma-spec-reporter": "^0.0.31", 72 | "karma-webpack": "^2.0.0", 73 | "lint-staged": "^3.4.0", 74 | "mocha": "^3.3.0", 75 | "mocha-css": "^1.0.1", 76 | "null-loader": "^0.1.1", 77 | "postcss": "^6.0.0", 78 | "postcss-cssnext": "^2.10.0", 79 | "pre-commit": "^1.2.0", 80 | "rimraf": "^2.6.0", 81 | "sinon": "2.2.0", 82 | "sinon-chai": "^2.10.0", 83 | "style-loader": "^0.17.0", 84 | "stylefmt": "^5.3.0", 85 | "stylelint": "^7.10.0", 86 | "stylelint-config-standard": "^16.0.0", 87 | "stylelint-processor-html": "^1.0.0", 88 | "uppercamelcase": "^3.0.0", 89 | "vue": "^2.3.0", 90 | "vue-loader": "^12.0.0", 91 | "vue-template-compiler": "^2.3.0", 92 | "webpack": "^2.5.0", 93 | "webpack-bundle-analyzer": "^2.4.0", 94 | "webpack-dashboard": "^0.4.0", 95 | "webpack-dev-server": "^2.4.0", 96 | "webpack-merge": "^4.0.0", 97 | "yarn-or-npm": "^2.0.0" 98 | }, 99 | "peerDependencies": { 100 | "vue": "^2.3.0" 101 | }, 102 | "dllPlugin": { 103 | "name": "vuePluginTemplateDeps", 104 | "include": [ 105 | "mocha/mocha.js", 106 | "style-loader!css-loader!mocha-css", 107 | "html-entities", 108 | "vue/dist/vue.js", 109 | "chai", 110 | "core-js/library", 111 | "url", 112 | "sockjs-client", 113 | "vue-style-loader/lib/addStylesClient.js", 114 | "events", 115 | "ansi-html", 116 | "style-loader/addStyles.js" 117 | ] 118 | }, 119 | "repository": { 120 | "type": "git", 121 | "url": "git+https://github.com/anteriovieira/vue-mindmap.git" 122 | }, 123 | "bugs": { 124 | "url": "https://github.com/anteriovieira/vue-mindmap/issues" 125 | }, 126 | "homepage": "https://github.com/anteriovieira/vue-mindmap#readme", 127 | "license": "MIT", 128 | "dependencies": { 129 | "d3": "^4.12.2", 130 | "node-sass": "^4.7.2", 131 | "sass-loader": "^6.0.6" 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VueMindmap 2 | 3 | [![npm](https://img.shields.io/npm/v/vue-mindmap.svg)](https://www.npmjs.com/package/vue-mindmap) [![vue2](https://img.shields.io/badge/vue-2.x-brightgreen.svg)](https://vuejs.org/) 4 | 5 | > VueMindmap is a vue component for mindnode maps inspired by [react-mindmap](https://github.com/learn-anything/react-mindmap). 6 | 7 | [Live demo](https://codesandbox.io/s/jv7pl7wn15) built on top of the awesome [codesandbox](https://codesandbox.io). 8 | 9 |

10 | vue-mindmap 11 |

12 | 13 | 14 | ## Installation 15 | 16 | ```bash 17 | npm install --save vue-mindmap 18 | ``` 19 | 20 | ## Usage 21 | 22 | ### Bundler (Webpack, Rollup) 23 | 24 | ```js 25 | import Vue from 'vue' 26 | import VueMindmap from 'vue-mindmap' 27 | // You need a specific loader for CSS files like https://github.com/webpack/css-loader 28 | import 'vue-mindmap/dist/vue-mindmap.css' 29 | 30 | Vue.use(VueMindmap) 31 | ``` 32 | 33 | ```html 34 | 41 | 42 | 53 | ``` 54 | 55 | ### Browser 56 | 57 | ```html 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ``` 67 | 68 | 69 | ## Props 70 | 71 | | Prop | Type | Default | Description | 72 | |-----------------|:-------:|---------|--------------------------------------------------------| 73 | | **nodes** | Array | [ ] | Array of objects used to render nodes. | 74 | | **connections** | Array | [ ] | Array of objects used to render connections. | 75 | | **subnodes** | Array | [ ] | Array of objects used to render subnodes. | 76 | | **editable** | Boolean | false | Enable editor mode, which allows to move around nodes. | 77 | 78 | ### nodes 79 | 80 | Array of objects used to render nodes. Below an example of the node structure. 81 | 82 | ```json 83 | { 84 | "text": "python", 85 | "url": "http://www.wikiwand.com/en/Python_(programming_language)", 86 | "fx": -13.916222252976013, 87 | "fy": -659.1641376795345, 88 | "category": "wiki", 89 | "note": "" 90 | } 91 | ``` 92 | 93 | The possible attributes are: 94 | 95 | - **text**: title of the node 96 | - **url**: url contained in the node 97 | - **fx** and **fy**: coordinates (if not present they'll be generated) 98 | - **category**: category used to generate an emoji 99 | - **note**: note that will be visible on hover 100 | 101 | ### connections 102 | 103 | Array of objects used to render connections. Below an example of the connection 104 | structure. 105 | 106 | ```json 107 | { 108 | "source": "python", 109 | "target": "basics", 110 | "curve": { 111 | "x": -43.5535, 112 | "y": 299.545 113 | } 114 | } 115 | ``` 116 | 117 | The possible attributes are: 118 | 119 | - **source**: title of the node where the connection starts 120 | - **target**: title of the node where the connection ends 121 | - **curve.x** and **curve.y**: coordinates of the control point of a quadratic bezier curve 122 | (if not specified the connection will be straight) 123 | 124 | ### subnodes 125 | Array of objects used to render subnodes. The structure is the same as for nodes 126 | with two additional attributes: 127 | 128 | - **parent**: title of the parent node 129 | - **color**: used for the margin color, needs to be a valid CSS color 130 | 131 | 132 | ## Styling 133 | Here's a list of all CSS classes for styling: 134 | 135 | - **.mindmap-svg**: main `svg` element containing the map; 136 | - **.mindmap-node**: `foreignObject` element representing a node; 137 | - **.mindmap-node--editable**: `foreignObject` element representing a node in editor mode; 138 | - **.mindmap-subnode-group-text**: `foreignObject` element containing all subnodes of a given node; 139 | - **.mindmap-subnode-text**: `div` element containing a subnode; 140 | - **.mindmap-connection**: `path` element for each connection; 141 | - **.mindmap-emoji**: `img` tag for emoji 142 | 143 | ## Development 144 | 145 | ### Launch visual tests 146 | 147 | ```bash 148 | npm run dev 149 | ``` 150 | 151 | ### Launch Karma with coverage 152 | 153 | ```bash 154 | npm run dev:coverage 155 | ``` 156 | 157 | ### Build 158 | 159 | Bundle the js and css of to the `dist` folder: 160 | 161 | ```bash 162 | npm run build 163 | ``` 164 | 165 | ## License 166 | 167 | [MIT](http://opensource.org/licenses/MIT) 168 | -------------------------------------------------------------------------------- /test/helpers/map.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'title': 'learn anything - programming - programming languages - python', 3 | 'nodes': [{ 4 | 'text': 'python', 5 | 'url': 'http://www.wikiwand.com/en/Python_(programming_language)', 6 | 'fx': -13.916222252976013, 7 | 'fy': -659.1641376795345, 8 | 'nodes': [{ 9 | 'text': '', 10 | 'url': 'https://www.reddit.com/r/Python/', 11 | 'fx': 176.083777747024, 12 | 'fy': -665.1641376795345, 13 | 'nodes': [], 14 | 'category': 'reddit', 15 | 'color': 'rgba(255, 189, 10, 1.0)' 16 | }, 17 | { 18 | 'text': 'source', 19 | 'note': 'original python implementation in c, compiles python code into byte code and interprets the byte code in a evaluation loop', 20 | 'url': 'https://github.com/python/cpython', 21 | 'fx': 176.083777747024, 22 | 'fy': -625.1641376795345, 23 | 'nodes': [], 24 | 'category': 'github', 25 | 'color': 'rgba(36, 170, 255, 1.0)' 26 | } 27 | ], 28 | 'category': 'wiki' 29 | }, 30 | { 31 | 'text': 'help', 32 | 'url': '', 33 | 'fx': 154.3247731601375, 34 | 'fy': -429.73700786748157, 35 | 'nodes': [{ 36 | 'text': 'awesome python', 37 | 'url': 'https://github.com/vinta/awesome-python', 38 | 'fx': 291.3247731601375, 39 | 'fy': -546.2370078674815, 40 | 'nodes': [], 41 | 'category': 'github', 42 | 'color': 'rgba(175, 54, 242, 1.0)' 43 | }, 44 | { 45 | 'text': 'awesome asyncio', 46 | 'url': 'https://github.com/timofurrer/awesome-asyncio', 47 | 'fx': 291.3247731601375, 48 | 'fy': -506.23700786748157, 49 | 'nodes': [], 50 | 'category': 'github', 51 | 'color': 'rgba(36, 170, 255, 1.0)' 52 | }, 53 | { 54 | 'text': 'python data model', 55 | 'url': 'https://docs.python.org/3/reference/datamodel.html', 56 | 'fx': 291.3247731601375, 57 | 'fy': -466.23700786748157, 58 | 'nodes': [], 59 | 'color': 'rgba(255, 189, 10, 1.0)' 60 | }, 61 | { 62 | 'text': 'python testing', 63 | 'url': 'http://pythontesting.net/framework/pytest/pytest-introduction/', 64 | 'fx': 291.3247731601375, 65 | 'fy': -432.23700786748157, 66 | 'nodes': [], 67 | 'category': 'free book', 68 | 'color': 'rgba(34, 205, 224, 1.0)' 69 | }, 70 | { 71 | 'text': 'scientific python cheat sheet', 72 | 'url': 'https://ipgp.github.io/scientific_python_cheat_sheet/', 73 | 'fx': 291.3247731601375, 74 | 'fy': -392.23700786748157, 75 | 'nodes': [], 76 | 'color': 'rgba(209, 21, 88, 1.0)' 77 | }, 78 | { 79 | 'text': 'structuring your project', 80 | 'url': 'http://python-guide-pt-br.readthedocs.io/en/latest/writing/structure/', 81 | 'fx': 291.3247731601375, 82 | 'fy': -358.23700786748157, 83 | 'nodes': [], 84 | 'color': 'rgba(49, 187, 71, 1.0)' 85 | }, 86 | { 87 | 'text': 'style guide for python code', 88 | 'url': 'https://www.python.org/dev/peps/pep-0008/', 89 | 'fx': 291.3247731601375, 90 | 'fy': -324.23700786748157, 91 | 'nodes': [], 92 | 'color': 'rgba(175, 54, 242, 1.0)' 93 | }, 94 | { 95 | 'text': 'cpython internals ️', 96 | 'url': 'http://pgbovine.net/cpython-internals.htm', 97 | 'fx': 291.3247731601375, 98 | 'fy': -290.23700786748157, 99 | 'nodes': [], 100 | 'category': 'article', 101 | 'color': 'rgba(36, 170, 255, 1.0)' 102 | } 103 | ] 104 | }, 105 | { 106 | 'text': 'articles', 107 | 'url': '', 108 | 'fx': 455.7839253819375, 109 | 'fy': -183.5539283546699, 110 | 'nodes': [{ 111 | 'text': '16: the history behind the decision to move python to github ️', 112 | 'url': 'https://snarky.ca/the-history-behind-the-decision-to-move-python-to-github/', 113 | 'fx': 617.7839253819375, 114 | 'fy': -245.0539283546699, 115 | 'nodes': [], 116 | 'category': 'article', 117 | 'color': 'rgba(175, 54, 242, 1.0)' 118 | }, 119 | { 120 | 'text': '15: a modern python development toolchain ️', 121 | 'url': 'http://www.chriskrycho.com/2015/a-modern-python-development-toolchain.html', 122 | 'fx': 617.7839253819375, 123 | 'fy': -183.0539283546699, 124 | 'nodes': [], 125 | 'category': 'article', 126 | 'color': 'rgba(36, 170, 255, 1.0)' 127 | }, 128 | { 129 | 'text': '17: pythons instance, class, and static methods demystified', 130 | 'url': 'https://realpython.com/blog/python/instance-class-and-static-methods-demystified/', 131 | 'fx': 617.7839253819375, 132 | 'fy': -121.05392835466989, 133 | 'nodes': [], 134 | 'category': 'article', 135 | 'color': 'rgba(255, 189, 10, 1.0)' 136 | } 137 | ] 138 | }, 139 | { 140 | 'text': 'basics', 141 | 'note': '', 142 | 'url': '', 143 | 'fx': -98.5231997717085, 144 | 'fy': -60.07462866512333, 145 | 'nodes': [{ 146 | 'text': '1. the python tutorial', 147 | 'url': 'https://docs.python.org/3/tutorial/', 148 | 'fx': 83.4768002282915, 149 | 'fy': -96.57462866512333, 150 | 'nodes': [], 151 | 'color': 'rgba(255, 189, 10, 1.0)' 152 | }, 153 | { 154 | 'text': '1. dive into python 3', 155 | 'url': 'http://www.diveintopython3.net', 156 | 'fx': 83.4768002282915, 157 | 'fy': -62.57462866512333, 158 | 'nodes': [], 159 | 'category': 'free book', 160 | 'color': 'rgba(175, 54, 242, 1.0)' 161 | }, 162 | { 163 | 'text': '1. automate the boring stuff with python', 164 | 'url': 'https://automatetheboringstuff.com/', 165 | 'fx': 83.4768002282915, 166 | 'fy': -22.574628665123328, 167 | 'nodes': [], 168 | 'category': 'free book', 169 | 'color': 'rgba(36, 170, 255, 1.0)' 170 | } 171 | ] 172 | }, 173 | { 174 | 'text': 'package manager', 175 | 'url': 'http://www.wikiwand.com/en/Package_manager', 176 | 'fx': -346.2056231217888, 177 | 'fy': 39.035120728630204, 178 | 'nodes': [], 179 | 'category': 'wiki' 180 | }, 181 | { 182 | 'text': 'python libraries ️', 183 | 'fx': -78.69331502906573, 184 | 'fy': 100.14771605920942, 185 | 'nodes': [], 186 | 'category': 'mindmap' 187 | }, 188 | { 189 | 'text': 'pip', 190 | 'url': 'https://pypi.python.org/pypi/pip', 191 | 'fx': -317.77054724755226, 192 | 'fy': 153.56934975958518, 193 | 'nodes': [] 194 | } 195 | ], 196 | 'connections': [{ 197 | 'source': 'python', 198 | 'target': 'basics', 199 | 'curve': { 200 | 'x': -43.5535, 201 | 'y': 299.545 202 | } 203 | }, 204 | { 205 | 'source': 'help', 206 | 'target': 'python', 207 | 'curve': { 208 | 'x': -78.1206, 209 | 'y': -114.714 210 | } 211 | }, 212 | { 213 | 'source': 'basics', 214 | 'target': 'python libraries ️', 215 | 'curve': { 216 | 'x': 29.6649, 217 | 'y': 80.1111 218 | } 219 | }, 220 | { 221 | 'source': 'basics', 222 | 'target': 'package manager', 223 | 'curve': { 224 | 'x': -103.841, 225 | 'y': 49.5548 226 | } 227 | }, 228 | { 229 | 'source': 'package manager', 230 | 'target': 'pip', 231 | 'curve': { 232 | 'x': -19.7824, 233 | 'y': 57.2671 234 | } 235 | }, 236 | { 237 | 'source': 'articles', 238 | 'target': 'help', 239 | 'curve': { 240 | 'x': -238.287, 241 | 'y': -54.4818 242 | } 243 | } 244 | ] 245 | } 246 | --------------------------------------------------------------------------------