├── constants.js ├── .gitignore ├── package.json ├── README.md ├── index.js └── dist └── run.js /constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | FONT: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif', 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | coverage 11 | pids 12 | logs 13 | results 14 | reports 15 | .nyc_output 16 | .vscode 17 | 18 | npm-debug.log 19 | node_modules 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "cat node_modules/ngraph.forcelayout/dist/ngraph.forcelayout2d.js > dist/run.js && browserify -s run index.js >> dist/run.js" 4 | }, 5 | "dependencies": { 6 | "ngraph.forcelayout": "^3.1.0", 7 | "ngraph.graph": "^19.1.0", 8 | "panzoom": "^9.4.2", 9 | "simplesvg": "^0.1.1" 10 | }, 11 | "devDependencies": { 12 | "browserify": "^17.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitter related graph 2 | 3 | This repository contains [a demo](https://twitter.com/anvaka/status/1396546646733848580) for Twitter to show how to render a map of related users. 4 | 5 | ![map demo](https://i.imgur.com/EXrVuGh.png) 6 | 7 | ## How to use it 8 | 9 | This code is not supposed to be used, and was created for demo purposes only. 10 | 11 | To record the demo I opened developer's console on a twitter user profile mode and 12 | pasted the code from [dist/run.js](dist/run.js). Then I scrolled the page to trigger the map 13 | loading. 14 | 15 | Check the console for errors/warnings/logs -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const buildGraph = require('./buildGraph'); 2 | const sivg = require('simplesvg'); 3 | const createPanZoom = require('panzoom'); 4 | const { FONT } = require('./constants'); 5 | 6 | module.exports = run; 7 | 8 | function run() { 9 | let createLayout = window.ngraphCreate2dLayout; // require('ngraph.forcelayout/dist/ngraph.forcelayout2d.js') 10 | buildGraph().then(g => { 11 | let layout = createLayout(g, { 12 | adaptiveTimeStepWeight: 0.05, 13 | springLength: 15, 14 | gravity: -24 15 | }); 16 | for (let i = 0; i < 450; ++i) layout.step(); 17 | draw(layout, g); 18 | }) 19 | } 20 | 21 | function draw(layout, graph) { 22 | let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 23 | let container = sivg('g'); 24 | let nodes = sivg('g'); 25 | let links = sivg('g'); 26 | let nodeHalfSize = 2.5; 27 | 28 | svg.style.width = '100%'; 29 | svg.style.height = '300px'; 30 | // svg.style.background = '#efefef'; 31 | let clipCircle = sivg('clipPath', {id: 'clipCircle'}); 32 | clipCircle.appendChild(sivg('circle', { 33 | cx: nodeHalfSize, cy: nodeHalfSize, r: nodeHalfSize 34 | })) 35 | svg.appendChild(clipCircle) 36 | svg.appendChild(container); 37 | 38 | container.appendChild(links); 39 | container.appendChild(nodes); 40 | 41 | graph.forEachLink(addLinkUI); 42 | graph.forEachNode(addNodeUI); 43 | let box = layout.simulator.getBoundingBox(); 44 | let width = box.max_x - box.min_x; 45 | let height = box.max_y - box.min_y; 46 | let dx = width * 0.1; let dy = height * 0.1; 47 | let left = box.min_x - dx; 48 | let top = box.min_y - dy; 49 | width += dx * 2; height += 2 * dy; 50 | let viewBox = `${left} ${top} ${width} ${height}`; 51 | svg.setAttributeNS(null, 'viewBox', viewBox); 52 | 53 | let loader = document.querySelector('#map-loader') 54 | if (loader) loader.parentElement.removeChild(loader); 55 | 56 | let link = document.querySelector('a[href*="connect_people"]'); 57 | if (link) { 58 | link.insertAdjacentElement('beforebegin', svg); 59 | } else { 60 | document.body.appendChild(svg); 61 | } 62 | let pz = createPanZoom(container); 63 | //pz.showRectangle({left, top, right: right + width, bottom: top + height }) 64 | 65 | function addNodeUI(node) { 66 | let from = layout.getNodePosition(node.id); 67 | let nodeUI; 68 | if (node.data) { 69 | nodeUI = sivg('g', { 70 | 'transform': `translate(${from.x - nodeHalfSize}, ${from.y - nodeHalfSize})` 71 | }); 72 | let href = sivg('a', {target: '_blank'}); 73 | href.link('/' + node.data.screenName); 74 | href.appendChild(sivg('image', { 75 | href: node.data.image, 76 | width: nodeHalfSize * 2, 77 | height: nodeHalfSize * 2, 78 | 'clip-path': 'url(#clipCircle)' 79 | })) 80 | nodeUI.appendChild(href); 81 | let label = sivg('text', { 82 | x: nodeHalfSize, 83 | y: nodeHalfSize * 2 + nodeHalfSize * 0.5, 84 | 'text-anchor': 'middle', 85 | // 'paint-order': 'stroke', 86 | // 'stroke': '#D9D9D9', 87 | // 'stroke-width': 0.1, 88 | 'font-family': FONT, 89 | 'fill': '#d9d9d9', 90 | 'font-size': nodeHalfSize * 0.4 91 | }); 92 | label.text(node.data.screenName); 93 | nodeUI.appendChild(label); 94 | } else { 95 | nodeUI = sivg('circle', { 96 | cx: from.x, 97 | cy: from.y, 98 | fill: 'orange', 99 | r: nodeHalfSize * .9 100 | }); 101 | } 102 | nodes.appendChild(nodeUI); 103 | } 104 | function addLinkUI(link) { 105 | let from = layout.getNodePosition(link.fromId); 106 | let to = layout.getNodePosition(link.toId); 107 | let ui = sivg('line', { 108 | x1: from.x, 109 | y1: from.y, 110 | x2: to.x, 111 | y2: to.y, 112 | stroke: '#333333', 113 | 'stroke-width': '0.1' 114 | }); 115 | links.appendChild(ui); 116 | } 117 | } 118 | run(); -------------------------------------------------------------------------------- /dist/run.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ngraphCreate2dLayout = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o max_x) max_x = bodyPos.x; 452 | if (bodyPos.y > max_y) max_y = bodyPos.y; 453 | } 454 | 455 | boundingBox.min_x = min_x; 456 | boundingBox.min_y = min_y; 457 | boundingBox.max_x = max_x; 458 | boundingBox.max_y = max_y; 459 | } 460 | 461 | function resetBoundingBox() { 462 | boundingBox.min_x = boundingBox.max_x = 0; 463 | boundingBox.min_y = boundingBox.max_y = 0; 464 | } 465 | 466 | } } 467 | },{}],3:[function(require,module,exports){ 468 | function Vector(x, y) { 469 | 470 | if (typeof arguments[0] === 'object') { 471 | // could be another vector 472 | let v = arguments[0]; 473 | if (!Number.isFinite(v.x)) throw new Error("Expected value is not a finite number at Vector constructor (x)"); 474 | if (!Number.isFinite(v.y)) throw new Error("Expected value is not a finite number at Vector constructor (y)"); 475 | this.x = v.x; 476 | this.y = v.y; 477 | } else { 478 | this.x = typeof x === "number" ? x : 0; 479 | this.y = typeof y === "number" ? y : 0; 480 | } 481 | } 482 | 483 | Vector.prototype.reset = function () { 484 | this.x = this.y = 0; 485 | }; 486 | 487 | function Body(x, y) { 488 | this.isPinned = false; 489 | this.pos = new Vector(x, y); 490 | this.force = new Vector(); 491 | this.velocity = new Vector(); 492 | this.mass = 1; 493 | 494 | this.springCount = 0; 495 | this.springLength = 0; 496 | } 497 | 498 | Body.prototype.reset = function() { 499 | this.force.reset(); 500 | this.springCount = 0; 501 | this.springLength = 0; 502 | } 503 | 504 | Body.prototype.setPosition = function (x, y) { 505 | this.pos.x = x || 0; 506 | this.pos.y = y || 0; 507 | }; 508 | module.exports = function() { return Body; } 509 | },{}],4:[function(require,module,exports){ 510 | 511 | module.exports = function() { return function anonymous(options 512 | ) { 513 | 514 | if (!Number.isFinite(options.dragCoefficient)) throw new Error('dragCoefficient is not a finite number'); 515 | 516 | return { 517 | update: function(body) { 518 | body.force.x -= options.dragCoefficient * body.velocity.x; 519 | body.force.y -= options.dragCoefficient * body.velocity.y; 520 | } 521 | }; 522 | 523 | } } 524 | },{}],5:[function(require,module,exports){ 525 | 526 | module.exports = function() { return function anonymous(options,random 527 | ) { 528 | 529 | if (!Number.isFinite(options.springCoefficient)) throw new Error('Spring coefficient is not a number'); 530 | if (!Number.isFinite(options.springLength)) throw new Error('Spring length is not a number'); 531 | 532 | return { 533 | /** 534 | * Updates forces acting on a spring 535 | */ 536 | update: function (spring) { 537 | var body1 = spring.from; 538 | var body2 = spring.to; 539 | var length = spring.length < 0 ? options.springLength : spring.length; 540 | var dx = body2.pos.x - body1.pos.x; 541 | var dy = body2.pos.y - body1.pos.y; 542 | var r = Math.sqrt(dx * dx + dy * dy); 543 | 544 | if (r === 0) { 545 | dx = (random.nextDouble() - 0.5) / 50; 546 | dy = (random.nextDouble() - 0.5) / 50; 547 | r = Math.sqrt(dx * dx + dy * dy); 548 | } 549 | 550 | var d = r - length; 551 | var coefficient = ((spring.coefficient > 0) ? spring.coefficient : options.springCoefficient) * d / r; 552 | 553 | body1.force.x += coefficient * dx 554 | body1.force.y += coefficient * dy; 555 | body1.springCount += 1; 556 | body1.springLength += r; 557 | 558 | body2.force.x -= coefficient * dx 559 | body2.force.y -= coefficient * dy; 560 | body2.springCount += 1; 561 | body2.springLength += r; 562 | } 563 | }; 564 | 565 | } } 566 | },{}],6:[function(require,module,exports){ 567 | 568 | module.exports = function() { return function anonymous(bodies,timeStep,adaptiveTimeStepWeight 569 | ) { 570 | 571 | var length = bodies.length; 572 | if (length === 0) return 0; 573 | 574 | var dx = 0, tx = 0; 575 | var dy = 0, ty = 0; 576 | 577 | for (var i = 0; i < length; ++i) { 578 | var body = bodies[i]; 579 | if (body.isPinned) continue; 580 | 581 | if (adaptiveTimeStepWeight && body.springCount) { 582 | timeStep = (adaptiveTimeStepWeight * body.springLength/body.springCount); 583 | } 584 | 585 | var coeff = timeStep / body.mass; 586 | 587 | body.velocity.x += coeff * body.force.x; 588 | body.velocity.y += coeff * body.force.y; 589 | var vx = body.velocity.x; 590 | var vy = body.velocity.y; 591 | var v = Math.sqrt(vx * vx + vy * vy); 592 | 593 | if (v > 1) { 594 | // We normalize it so that we move within timeStep range. 595 | // for the case when v <= 1 - we let velocity to fade out. 596 | body.velocity.x = vx / v; 597 | body.velocity.y = vy / v; 598 | } 599 | 600 | dx = timeStep * body.velocity.x; 601 | dy = timeStep * body.velocity.y; 602 | 603 | body.pos.x += dx; 604 | body.pos.y += dy; 605 | 606 | tx += Math.abs(dx); 607 | ty += Math.abs(dy); 608 | } 609 | 610 | return (tx * tx + ty * ty)/length; 611 | 612 | } } 613 | },{}],7:[function(require,module,exports){ 614 | 615 | /** 616 | * Our implementation of QuadTree is non-recursive to avoid GC hit 617 | * This data structure represent stack of elements 618 | * which we are trying to insert into quad tree. 619 | */ 620 | function InsertStack () { 621 | this.stack = []; 622 | this.popIdx = 0; 623 | } 624 | 625 | InsertStack.prototype = { 626 | isEmpty: function() { 627 | return this.popIdx === 0; 628 | }, 629 | push: function (node, body) { 630 | var item = this.stack[this.popIdx]; 631 | if (!item) { 632 | // we are trying to avoid memory pressure: create new element 633 | // only when absolutely necessary 634 | this.stack[this.popIdx] = new InsertStackElement(node, body); 635 | } else { 636 | item.node = node; 637 | item.body = body; 638 | } 639 | ++this.popIdx; 640 | }, 641 | pop: function () { 642 | if (this.popIdx > 0) { 643 | return this.stack[--this.popIdx]; 644 | } 645 | }, 646 | reset: function () { 647 | this.popIdx = 0; 648 | } 649 | }; 650 | 651 | function InsertStackElement(node, body) { 652 | this.node = node; // QuadTree node 653 | this.body = body; // physical body which needs to be inserted to node 654 | } 655 | 656 | 657 | function QuadNode() { 658 | // body stored inside this node. In quad tree only leaf nodes (by construction) 659 | // contain bodies: 660 | this.body = null; 661 | 662 | // Child nodes are stored in quads. Each quad is presented by number: 663 | // 0 | 1 664 | // ----- 665 | // 2 | 3 666 | this.quad0 = null; 667 | this.quad1 = null; 668 | this.quad2 = null; 669 | this.quad3 = null; 670 | 671 | // Total mass of current node 672 | this.mass = 0; 673 | 674 | // Center of mass coordinates 675 | this.mass_x = 0; 676 | this.mass_y = 0; 677 | 678 | // bounding box coordinates 679 | this.min_x = 0; 680 | this.min_y = 0; 681 | this.max_x = 0; 682 | this.max_y = 0; 683 | } 684 | 685 | 686 | function isSamePosition(point1, point2) { 687 | var dx = Math.abs(point1.x - point2.x); 688 | var dy = Math.abs(point1.y - point2.y); 689 | 690 | return dx < 1e-8 && dy < 1e-8; 691 | } 692 | 693 | function getChild(node, idx) { 694 | if (idx === 0) return node.quad0; 695 | if (idx === 1) return node.quad1; 696 | if (idx === 2) return node.quad2; 697 | if (idx === 3) return node.quad3; 698 | return null; 699 | } 700 | 701 | function setChild(node, idx, child) { 702 | if (idx === 0) node.quad0 = child; 703 | else if (idx === 1) node.quad1 = child; 704 | else if (idx === 2) node.quad2 = child; 705 | else if (idx === 3) node.quad3 = child; 706 | } 707 | module.exports = function() { return function createQuadTree(options, random) { 708 | options = options || {}; 709 | options.gravity = typeof options.gravity === 'number' ? options.gravity : -1; 710 | options.theta = typeof options.theta === 'number' ? options.theta : 0.8; 711 | 712 | var gravity = options.gravity; 713 | var updateQueue = []; 714 | var insertStack = new InsertStack(); 715 | var theta = options.theta; 716 | 717 | var nodesCache = []; 718 | var currentInCache = 0; 719 | var root = newNode(); 720 | 721 | return { 722 | insertBodies: insertBodies, 723 | 724 | /** 725 | * Gets root node if it is present 726 | */ 727 | getRoot: function() { 728 | return root; 729 | }, 730 | 731 | updateBodyForce: update, 732 | 733 | options: function(newOptions) { 734 | if (newOptions) { 735 | if (typeof newOptions.gravity === 'number') { 736 | gravity = newOptions.gravity; 737 | } 738 | if (typeof newOptions.theta === 'number') { 739 | theta = newOptions.theta; 740 | } 741 | 742 | return this; 743 | } 744 | 745 | return { 746 | gravity: gravity, 747 | theta: theta 748 | }; 749 | } 750 | }; 751 | 752 | function newNode() { 753 | // To avoid pressure on GC we reuse nodes. 754 | var node = nodesCache[currentInCache]; 755 | if (node) { 756 | node.quad0 = null; 757 | node.quad1 = null; 758 | node.quad2 = null; 759 | node.quad3 = null; 760 | node.body = null; 761 | node.mass = node.mass_x = node.mass_y = 0; 762 | node.min_x = node.max_x = node.min_y = node.max_y = 0; 763 | } else { 764 | node = new QuadNode(); 765 | nodesCache[currentInCache] = node; 766 | } 767 | 768 | ++currentInCache; 769 | return node; 770 | } 771 | 772 | function update(sourceBody) { 773 | var queue = updateQueue; 774 | var v; 775 | var dx; 776 | var dy; 777 | var r; 778 | var fx = 0; 779 | var fy = 0; 780 | var queueLength = 1; 781 | var shiftIdx = 0; 782 | var pushIdx = 1; 783 | 784 | queue[0] = root; 785 | 786 | while (queueLength) { 787 | var node = queue[shiftIdx]; 788 | var body = node.body; 789 | 790 | queueLength -= 1; 791 | shiftIdx += 1; 792 | var differentBody = (body !== sourceBody); 793 | if (body && differentBody) { 794 | // If the current node is a leaf node (and it is not source body), 795 | // calculate the force exerted by the current node on body, and add this 796 | // amount to body's net force. 797 | dx = body.pos.x - sourceBody.pos.x; 798 | dy = body.pos.y - sourceBody.pos.y; 799 | r = Math.sqrt(dx * dx + dy * dy); 800 | 801 | if (r === 0) { 802 | // Poor man's protection against zero distance. 803 | dx = (random.nextDouble() - 0.5) / 50; 804 | dy = (random.nextDouble() - 0.5) / 50; 805 | r = Math.sqrt(dx * dx + dy * dy); 806 | } 807 | 808 | // This is standard gravitation force calculation but we divide 809 | // by r^3 to save two operations when normalizing force vector. 810 | v = gravity * body.mass * sourceBody.mass / (r * r * r); 811 | fx += v * dx; 812 | fy += v * dy; 813 | } else if (differentBody) { 814 | // Otherwise, calculate the ratio s / r, where s is the width of the region 815 | // represented by the internal node, and r is the distance between the body 816 | // and the node's center-of-mass 817 | dx = node.mass_x / node.mass - sourceBody.pos.x; 818 | dy = node.mass_y / node.mass - sourceBody.pos.y; 819 | r = Math.sqrt(dx * dx + dy * dy); 820 | 821 | if (r === 0) { 822 | // Sorry about code duplication. I don't want to create many functions 823 | // right away. Just want to see performance first. 824 | dx = (random.nextDouble() - 0.5) / 50; 825 | dy = (random.nextDouble() - 0.5) / 50; 826 | r = Math.sqrt(dx * dx + dy * dy); 827 | } 828 | // If s / r < θ, treat this internal node as a single body, and calculate the 829 | // force it exerts on sourceBody, and add this amount to sourceBody's net force. 830 | if ((node.max_x - node.min_x) / r < theta) { 831 | // in the if statement above we consider node's width only 832 | // because the region was made into square during tree creation. 833 | // Thus there is no difference between using width or height. 834 | v = gravity * node.mass * sourceBody.mass / (r * r * r); 835 | fx += v * dx; 836 | fy += v * dy; 837 | } else { 838 | // Otherwise, run the procedure recursively on each of the current node's children. 839 | 840 | // I intentionally unfolded this loop, to save several CPU cycles. 841 | if (node.quad0) { 842 | queue[pushIdx] = node.quad0; 843 | queueLength += 1; 844 | pushIdx += 1; 845 | } 846 | if (node.quad1) { 847 | queue[pushIdx] = node.quad1; 848 | queueLength += 1; 849 | pushIdx += 1; 850 | } 851 | if (node.quad2) { 852 | queue[pushIdx] = node.quad2; 853 | queueLength += 1; 854 | pushIdx += 1; 855 | } 856 | if (node.quad3) { 857 | queue[pushIdx] = node.quad3; 858 | queueLength += 1; 859 | pushIdx += 1; 860 | } 861 | } 862 | } 863 | } 864 | 865 | sourceBody.force.x += fx; 866 | sourceBody.force.y += fy; 867 | } 868 | 869 | function insertBodies(bodies) { 870 | var xmin = Number.MAX_VALUE; 871 | var ymin = Number.MAX_VALUE; 872 | var xmax = Number.MIN_VALUE; 873 | var ymax = Number.MIN_VALUE; 874 | var i = bodies.length; 875 | 876 | // To reduce quad tree depth we are looking for exact bounding box of all particles. 877 | while (i--) { 878 | var pos = bodies[i].pos; 879 | if (pos.x < xmin) xmin = pos.x; 880 | if (pos.y < ymin) ymin = pos.y; 881 | if (pos.x > xmax) xmax = pos.x; 882 | if (pos.y > ymax) ymax = pos.y; 883 | } 884 | 885 | // Makes the bounds square. 886 | var maxSideLength = -Infinity; 887 | if (xmax - xmin > maxSideLength) maxSideLength = xmax - xmin ; 888 | if (ymax - ymin > maxSideLength) maxSideLength = ymax - ymin ; 889 | 890 | currentInCache = 0; 891 | root = newNode(); 892 | root.min_x = xmin; 893 | root.min_y = ymin; 894 | root.max_x = xmin + maxSideLength; 895 | root.max_y = ymin + maxSideLength; 896 | 897 | i = bodies.length - 1; 898 | if (i >= 0) { 899 | root.body = bodies[i]; 900 | } 901 | while (i--) { 902 | insert(bodies[i], root); 903 | } 904 | } 905 | 906 | function insert(newBody) { 907 | insertStack.reset(); 908 | insertStack.push(root, newBody); 909 | 910 | while (!insertStack.isEmpty()) { 911 | var stackItem = insertStack.pop(); 912 | var node = stackItem.node; 913 | var body = stackItem.body; 914 | 915 | if (!node.body) { 916 | // This is internal node. Update the total mass of the node and center-of-mass. 917 | var x = body.pos.x; 918 | var y = body.pos.y; 919 | node.mass += body.mass; 920 | node.mass_x += body.mass * x; 921 | node.mass_y += body.mass * y; 922 | 923 | // Recursively insert the body in the appropriate quadrant. 924 | // But first find the appropriate quadrant. 925 | var quadIdx = 0; // Assume we are in the 0's quad. 926 | var min_x = node.min_x; 927 | var min_y = node.min_y; 928 | var max_x = (min_x + node.max_x) / 2; 929 | var max_y = (min_y + node.max_y) / 2; 930 | 931 | if (x > max_x) { 932 | quadIdx = quadIdx + 1; 933 | min_x = max_x; 934 | max_x = node.max_x; 935 | } 936 | if (y > max_y) { 937 | quadIdx = quadIdx + 2; 938 | min_y = max_y; 939 | max_y = node.max_y; 940 | } 941 | 942 | var child = getChild(node, quadIdx); 943 | 944 | if (!child) { 945 | // The node is internal but this quadrant is not taken. Add 946 | // subnode to it. 947 | child = newNode(); 948 | child.min_x = min_x; 949 | child.min_y = min_y; 950 | child.max_x = max_x; 951 | child.max_y = max_y; 952 | child.body = body; 953 | 954 | setChild(node, quadIdx, child); 955 | } else { 956 | // continue searching in this quadrant. 957 | insertStack.push(child, body); 958 | } 959 | } else { 960 | // We are trying to add to the leaf node. 961 | // We have to convert current leaf into internal node 962 | // and continue adding two nodes. 963 | var oldBody = node.body; 964 | node.body = null; // internal nodes do not cary bodies 965 | 966 | if (isSamePosition(oldBody.pos, body.pos)) { 967 | // Prevent infinite subdivision by bumping one node 968 | // anywhere in this quadrant 969 | var retriesCount = 3; 970 | do { 971 | var offset = random.nextDouble(); 972 | var dx = (node.max_x - node.min_x) * offset; 973 | var dy = (node.max_y - node.min_y) * offset; 974 | 975 | oldBody.pos.x = node.min_x + dx; 976 | oldBody.pos.y = node.min_y + dy; 977 | retriesCount -= 1; 978 | // Make sure we don't bump it out of the box. If we do, next iteration should fix it 979 | } while (retriesCount > 0 && isSamePosition(oldBody.pos, body.pos)); 980 | 981 | if (retriesCount === 0 && isSamePosition(oldBody.pos, body.pos)) { 982 | // This is very bad, we ran out of precision. 983 | // if we do not return from the method we'll get into 984 | // infinite loop here. So we sacrifice correctness of layout, and keep the app running 985 | // Next layout iteration should get larger bounding box in the first step and fix this 986 | return; 987 | } 988 | } 989 | // Next iteration should subdivide node further. 990 | insertStack.push(node, oldBody); 991 | insertStack.push(node, body); 992 | } 993 | } 994 | } 995 | } } 996 | },{}],8:[function(require,module,exports){ 997 | /** 998 | * Manages a simulation of physical forces acting on bodies and springs. 999 | */ 1000 | module.exports = createPhysicsSimulator; 1001 | 1002 | var generateCreateBodyFunction = require('./codeGenerators/generateCreateBody'); 1003 | var generateQuadTreeFunction = require('./codeGenerators/generateQuadTree'); 1004 | var generateBoundsFunction = require('./codeGenerators/generateBounds'); 1005 | var generateCreateDragForceFunction = require('./codeGenerators/generateCreateDragForce'); 1006 | var generateCreateSpringForceFunction = require('./codeGenerators/generateCreateSpringForce'); 1007 | var generateIntegratorFunction = require('./codeGenerators/generateIntegrator'); 1008 | 1009 | var dimensionalCache = {}; 1010 | 1011 | function createPhysicsSimulator(settings) { 1012 | var Spring = require('./spring'); 1013 | var merge = require('ngraph.merge'); 1014 | var eventify = require('ngraph.events'); 1015 | if (settings) { 1016 | // Check for names from older versions of the layout 1017 | if (settings.springCoeff !== undefined) throw new Error('springCoeff was renamed to springCoefficient'); 1018 | if (settings.dragCoeff !== undefined) throw new Error('dragCoeff was renamed to dragCoefficient'); 1019 | } 1020 | 1021 | settings = merge(settings, { 1022 | /** 1023 | * Ideal length for links (springs in physical model). 1024 | */ 1025 | springLength: 10, 1026 | 1027 | /** 1028 | * Hook's law coefficient. 1 - solid spring. 1029 | */ 1030 | springCoefficient: 0.8, 1031 | 1032 | /** 1033 | * Coulomb's law coefficient. It's used to repel nodes thus should be negative 1034 | * if you make it positive nodes start attract each other :). 1035 | */ 1036 | gravity: -12, 1037 | 1038 | /** 1039 | * Theta coefficient from Barnes Hut simulation. Ranged between (0, 1). 1040 | * The closer it's to 1 the more nodes algorithm will have to go through. 1041 | * Setting it to one makes Barnes Hut simulation no different from 1042 | * brute-force forces calculation (each node is considered). 1043 | */ 1044 | theta: 0.8, 1045 | 1046 | /** 1047 | * Drag force coefficient. Used to slow down system, thus should be less than 1. 1048 | * The closer it is to 0 the less tight system will be. 1049 | */ 1050 | dragCoefficient: 0.9, // TODO: Need to rename this to something better. E.g. `dragCoefficient` 1051 | 1052 | /** 1053 | * Default time step (dt) for forces integration 1054 | */ 1055 | timeStep : 0.5, 1056 | 1057 | /** 1058 | * Adaptive time step uses average spring length to compute actual time step: 1059 | * See: https://twitter.com/anvaka/status/1293067160755957760 1060 | */ 1061 | adaptiveTimeStepWeight: 0, 1062 | 1063 | /** 1064 | * This parameter defines number of dimensions of the space where simulation 1065 | * is performed. 1066 | */ 1067 | dimensions: 2, 1068 | 1069 | /** 1070 | * In debug mode more checks are performed, this will help you catch errors 1071 | * quickly, however for production build it is recommended to turn off this flag 1072 | * to speed up computation. 1073 | */ 1074 | debug: false 1075 | }); 1076 | 1077 | var factory = dimensionalCache[settings.dimensions]; 1078 | if (!factory) { 1079 | var dimensions = settings.dimensions; 1080 | factory = { 1081 | Body: generateCreateBodyFunction(dimensions, settings.debug), 1082 | createQuadTree: generateQuadTreeFunction(dimensions), 1083 | createBounds: generateBoundsFunction(dimensions), 1084 | createDragForce: generateCreateDragForceFunction(dimensions), 1085 | createSpringForce: generateCreateSpringForceFunction(dimensions), 1086 | integrate: generateIntegratorFunction(dimensions), 1087 | }; 1088 | dimensionalCache[dimensions] = factory; 1089 | } 1090 | 1091 | var Body = factory.Body; 1092 | var createQuadTree = factory.createQuadTree; 1093 | var createBounds = factory.createBounds; 1094 | var createDragForce = factory.createDragForce; 1095 | var createSpringForce = factory.createSpringForce; 1096 | var integrate = factory.integrate; 1097 | var createBody = pos => new Body(pos); 1098 | 1099 | var random = require('ngraph.random').random(42); 1100 | var bodies = []; // Bodies in this simulation. 1101 | var springs = []; // Springs in this simulation. 1102 | 1103 | var quadTree = createQuadTree(settings, random); 1104 | var bounds = createBounds(bodies, settings, random); 1105 | var springForce = createSpringForce(settings, random); 1106 | var dragForce = createDragForce(settings); 1107 | 1108 | var totalMovement = 0; // how much movement we made on last step 1109 | var forces = []; 1110 | var forceMap = new Map(); 1111 | var iterationNumber = 0; 1112 | 1113 | addForce('nbody', nbodyForce); 1114 | addForce('spring', updateSpringForce); 1115 | 1116 | var publicApi = { 1117 | /** 1118 | * Array of bodies, registered with current simulator 1119 | * 1120 | * Note: To add new body, use addBody() method. This property is only 1121 | * exposed for testing/performance purposes. 1122 | */ 1123 | bodies: bodies, 1124 | 1125 | quadTree: quadTree, 1126 | 1127 | /** 1128 | * Array of springs, registered with current simulator 1129 | * 1130 | * Note: To add new spring, use addSpring() method. This property is only 1131 | * exposed for testing/performance purposes. 1132 | */ 1133 | springs: springs, 1134 | 1135 | /** 1136 | * Returns settings with which current simulator was initialized 1137 | */ 1138 | settings: settings, 1139 | 1140 | /** 1141 | * Adds a new force to simulation 1142 | */ 1143 | addForce: addForce, 1144 | 1145 | /** 1146 | * Removes a force from the simulation. 1147 | */ 1148 | removeForce: removeForce, 1149 | 1150 | /** 1151 | * Returns a map of all registered forces. 1152 | */ 1153 | getForces: getForces, 1154 | 1155 | /** 1156 | * Performs one step of force simulation. 1157 | * 1158 | * @returns {boolean} true if system is considered stable; False otherwise. 1159 | */ 1160 | step: function () { 1161 | for (var i = 0; i < forces.length; ++i) { 1162 | forces[i](iterationNumber); 1163 | } 1164 | var movement = integrate(bodies, settings.timeStep, settings.adaptiveTimeStepWeight); 1165 | iterationNumber += 1; 1166 | return movement; 1167 | }, 1168 | 1169 | /** 1170 | * Adds body to the system 1171 | * 1172 | * @param {ngraph.physics.primitives.Body} body physical body 1173 | * 1174 | * @returns {ngraph.physics.primitives.Body} added body 1175 | */ 1176 | addBody: function (body) { 1177 | if (!body) { 1178 | throw new Error('Body is required'); 1179 | } 1180 | bodies.push(body); 1181 | 1182 | return body; 1183 | }, 1184 | 1185 | /** 1186 | * Adds body to the system at given position 1187 | * 1188 | * @param {Object} pos position of a body 1189 | * 1190 | * @returns {ngraph.physics.primitives.Body} added body 1191 | */ 1192 | addBodyAt: function (pos) { 1193 | if (!pos) { 1194 | throw new Error('Body position is required'); 1195 | } 1196 | var body = createBody(pos); 1197 | bodies.push(body); 1198 | 1199 | return body; 1200 | }, 1201 | 1202 | /** 1203 | * Removes body from the system 1204 | * 1205 | * @param {ngraph.physics.primitives.Body} body to remove 1206 | * 1207 | * @returns {Boolean} true if body found and removed. falsy otherwise; 1208 | */ 1209 | removeBody: function (body) { 1210 | if (!body) { return; } 1211 | 1212 | var idx = bodies.indexOf(body); 1213 | if (idx < 0) { return; } 1214 | 1215 | bodies.splice(idx, 1); 1216 | if (bodies.length === 0) { 1217 | bounds.reset(); 1218 | } 1219 | return true; 1220 | }, 1221 | 1222 | /** 1223 | * Adds a spring to this simulation. 1224 | * 1225 | * @returns {Object} - a handle for a spring. If you want to later remove 1226 | * spring pass it to removeSpring() method. 1227 | */ 1228 | addSpring: function (body1, body2, springLength, springCoefficient) { 1229 | if (!body1 || !body2) { 1230 | throw new Error('Cannot add null spring to force simulator'); 1231 | } 1232 | 1233 | if (typeof springLength !== 'number') { 1234 | springLength = -1; // assume global configuration 1235 | } 1236 | 1237 | var spring = new Spring(body1, body2, springLength, springCoefficient >= 0 ? springCoefficient : -1); 1238 | springs.push(spring); 1239 | 1240 | // TODO: could mark simulator as dirty. 1241 | return spring; 1242 | }, 1243 | 1244 | /** 1245 | * Returns amount of movement performed on last step() call 1246 | */ 1247 | getTotalMovement: function () { 1248 | return totalMovement; 1249 | }, 1250 | 1251 | /** 1252 | * Removes spring from the system 1253 | * 1254 | * @param {Object} spring to remove. Spring is an object returned by addSpring 1255 | * 1256 | * @returns {Boolean} true if spring found and removed. falsy otherwise; 1257 | */ 1258 | removeSpring: function (spring) { 1259 | if (!spring) { return; } 1260 | var idx = springs.indexOf(spring); 1261 | if (idx > -1) { 1262 | springs.splice(idx, 1); 1263 | return true; 1264 | } 1265 | }, 1266 | 1267 | getBestNewBodyPosition: function (neighbors) { 1268 | return bounds.getBestNewPosition(neighbors); 1269 | }, 1270 | 1271 | /** 1272 | * Returns bounding box which covers all bodies 1273 | */ 1274 | getBBox: getBoundingBox, 1275 | getBoundingBox: getBoundingBox, 1276 | 1277 | invalidateBBox: function () { 1278 | console.warn('invalidateBBox() is deprecated, bounds always recomputed on `getBBox()` call'); 1279 | }, 1280 | 1281 | // TODO: Move the force specific stuff to force 1282 | gravity: function (value) { 1283 | if (value !== undefined) { 1284 | settings.gravity = value; 1285 | quadTree.options({gravity: value}); 1286 | return this; 1287 | } else { 1288 | return settings.gravity; 1289 | } 1290 | }, 1291 | 1292 | theta: function (value) { 1293 | if (value !== undefined) { 1294 | settings.theta = value; 1295 | quadTree.options({theta: value}); 1296 | return this; 1297 | } else { 1298 | return settings.theta; 1299 | } 1300 | }, 1301 | 1302 | /** 1303 | * Returns pseudo-random number generator instance. 1304 | */ 1305 | random: random 1306 | }; 1307 | 1308 | // allow settings modification via public API: 1309 | expose(settings, publicApi); 1310 | 1311 | eventify(publicApi); 1312 | 1313 | return publicApi; 1314 | 1315 | function getBoundingBox() { 1316 | bounds.update(); 1317 | return bounds.box; 1318 | } 1319 | 1320 | function addForce(forceName, forceFunction) { 1321 | if (forceMap.has(forceName)) throw new Error('Force ' + forceName + ' is already added'); 1322 | 1323 | forceMap.set(forceName, forceFunction); 1324 | forces.push(forceFunction); 1325 | } 1326 | 1327 | function removeForce(forceName) { 1328 | var forceIndex = forces.indexOf(forceMap.get(forceName)); 1329 | if (forceIndex < 0) return; 1330 | forces.splice(forceIndex, 1); 1331 | forceMap.delete(forceName); 1332 | } 1333 | 1334 | function getForces() { 1335 | // TODO: Should I trust them or clone the forces? 1336 | return forceMap; 1337 | } 1338 | 1339 | function nbodyForce(/* iterationUmber */) { 1340 | if (bodies.length === 0) return; 1341 | 1342 | quadTree.insertBodies(bodies); 1343 | var i = bodies.length; 1344 | while (i--) { 1345 | var body = bodies[i]; 1346 | if (!body.isPinned) { 1347 | body.reset(); 1348 | quadTree.updateBodyForce(body); 1349 | dragForce.update(body); 1350 | } 1351 | } 1352 | } 1353 | 1354 | function updateSpringForce() { 1355 | var i = springs.length; 1356 | while (i--) { 1357 | springForce.update(springs[i]); 1358 | } 1359 | } 1360 | 1361 | } 1362 | 1363 | function expose(settings, target) { 1364 | for (var key in settings) { 1365 | augment(settings, target, key); 1366 | } 1367 | } 1368 | 1369 | function augment(source, target, key) { 1370 | if (!source.hasOwnProperty(key)) return; 1371 | if (typeof target[key] === 'function') { 1372 | // this accessor is already defined. Ignore it 1373 | return; 1374 | } 1375 | var sourceIsNumber = Number.isFinite(source[key]); 1376 | 1377 | if (sourceIsNumber) { 1378 | target[key] = function (value) { 1379 | if (value !== undefined) { 1380 | if (!Number.isFinite(value)) throw new Error('Value of ' + key + ' should be a valid number.'); 1381 | source[key] = value; 1382 | return target; 1383 | } 1384 | return source[key]; 1385 | }; 1386 | } else { 1387 | target[key] = function (value) { 1388 | if (value !== undefined) { 1389 | source[key] = value; 1390 | return target; 1391 | } 1392 | return source[key]; 1393 | }; 1394 | } 1395 | } 1396 | 1397 | },{"./codeGenerators/generateBounds":2,"./codeGenerators/generateCreateBody":3,"./codeGenerators/generateCreateDragForce":4,"./codeGenerators/generateCreateSpringForce":5,"./codeGenerators/generateIntegrator":6,"./codeGenerators/generateQuadTree":7,"./spring":9,"ngraph.events":10,"ngraph.merge":11,"ngraph.random":12}],9:[function(require,module,exports){ 1398 | module.exports = Spring; 1399 | 1400 | /** 1401 | * Represents a physical spring. Spring connects two bodies, has rest length 1402 | * stiffness coefficient and optional weight 1403 | */ 1404 | function Spring(fromBody, toBody, length, springCoefficient) { 1405 | this.from = fromBody; 1406 | this.to = toBody; 1407 | this.length = length; 1408 | this.coefficient = springCoefficient; 1409 | } 1410 | 1411 | },{}],10:[function(require,module,exports){ 1412 | module.exports = function eventify(subject) { 1413 | validateSubject(subject); 1414 | 1415 | var eventsStorage = createEventsStorage(subject); 1416 | subject.on = eventsStorage.on; 1417 | subject.off = eventsStorage.off; 1418 | subject.fire = eventsStorage.fire; 1419 | return subject; 1420 | }; 1421 | 1422 | function createEventsStorage(subject) { 1423 | // Store all event listeners to this hash. Key is event name, value is array 1424 | // of callback records. 1425 | // 1426 | // A callback record consists of callback function and its optional context: 1427 | // { 'eventName' => [{callback: function, ctx: object}] } 1428 | var registeredEvents = Object.create(null); 1429 | 1430 | return { 1431 | on: function (eventName, callback, ctx) { 1432 | if (typeof callback !== 'function') { 1433 | throw new Error('callback is expected to be a function'); 1434 | } 1435 | var handlers = registeredEvents[eventName]; 1436 | if (!handlers) { 1437 | handlers = registeredEvents[eventName] = []; 1438 | } 1439 | handlers.push({callback: callback, ctx: ctx}); 1440 | 1441 | return subject; 1442 | }, 1443 | 1444 | off: function (eventName, callback) { 1445 | var wantToRemoveAll = (typeof eventName === 'undefined'); 1446 | if (wantToRemoveAll) { 1447 | // Killing old events storage should be enough in this case: 1448 | registeredEvents = Object.create(null); 1449 | return subject; 1450 | } 1451 | 1452 | if (registeredEvents[eventName]) { 1453 | var deleteAllCallbacksForEvent = (typeof callback !== 'function'); 1454 | if (deleteAllCallbacksForEvent) { 1455 | delete registeredEvents[eventName]; 1456 | } else { 1457 | var callbacks = registeredEvents[eventName]; 1458 | for (var i = 0; i < callbacks.length; ++i) { 1459 | if (callbacks[i].callback === callback) { 1460 | callbacks.splice(i, 1); 1461 | } 1462 | } 1463 | } 1464 | } 1465 | 1466 | return subject; 1467 | }, 1468 | 1469 | fire: function (eventName) { 1470 | var callbacks = registeredEvents[eventName]; 1471 | if (!callbacks) { 1472 | return subject; 1473 | } 1474 | 1475 | var fireArguments; 1476 | if (arguments.length > 1) { 1477 | fireArguments = Array.prototype.splice.call(arguments, 1); 1478 | } 1479 | for(var i = 0; i < callbacks.length; ++i) { 1480 | var callbackInfo = callbacks[i]; 1481 | callbackInfo.callback.apply(callbackInfo.ctx, fireArguments); 1482 | } 1483 | 1484 | return subject; 1485 | } 1486 | }; 1487 | } 1488 | 1489 | function validateSubject(subject) { 1490 | if (!subject) { 1491 | throw new Error('Eventify cannot use falsy object as events subject'); 1492 | } 1493 | var reservedWords = ['on', 'fire', 'off']; 1494 | for (var i = 0; i < reservedWords.length; ++i) { 1495 | if (subject.hasOwnProperty(reservedWords[i])) { 1496 | throw new Error("Subject cannot be eventified, since it already has property '" + reservedWords[i] + "'"); 1497 | } 1498 | } 1499 | } 1500 | 1501 | },{}],11:[function(require,module,exports){ 1502 | module.exports = merge; 1503 | 1504 | /** 1505 | * Augments `target` with properties in `options`. Does not override 1506 | * target's properties if they are defined and matches expected type in 1507 | * options 1508 | * 1509 | * @returns {Object} merged object 1510 | */ 1511 | function merge(target, options) { 1512 | var key; 1513 | if (!target) { target = {}; } 1514 | if (options) { 1515 | for (key in options) { 1516 | if (options.hasOwnProperty(key)) { 1517 | var targetHasIt = target.hasOwnProperty(key), 1518 | optionsValueType = typeof options[key], 1519 | shouldReplace = !targetHasIt || (typeof target[key] !== optionsValueType); 1520 | 1521 | if (shouldReplace) { 1522 | target[key] = options[key]; 1523 | } else if (optionsValueType === 'object') { 1524 | // go deep, don't care about loops here, we are simple API!: 1525 | target[key] = merge(target[key], options[key]); 1526 | } 1527 | } 1528 | } 1529 | } 1530 | 1531 | return target; 1532 | } 1533 | 1534 | },{}],12:[function(require,module,exports){ 1535 | module.exports = random; 1536 | 1537 | // TODO: Deprecate? 1538 | module.exports.random = random, 1539 | module.exports.randomIterator = randomIterator 1540 | 1541 | /** 1542 | * Creates seeded PRNG with two methods: 1543 | * next() and nextDouble() 1544 | */ 1545 | function random(inputSeed) { 1546 | var seed = typeof inputSeed === 'number' ? inputSeed : (+new Date()); 1547 | return new Generator(seed) 1548 | } 1549 | 1550 | function Generator(seed) { 1551 | this.seed = seed; 1552 | } 1553 | 1554 | /** 1555 | * Generates random integer number in the range from 0 (inclusive) to maxValue (exclusive) 1556 | * 1557 | * @param maxValue Number REQUIRED. Omitting this number will result in NaN values from PRNG. 1558 | */ 1559 | Generator.prototype.next = next; 1560 | 1561 | /** 1562 | * Generates random double number in the range from 0 (inclusive) to 1 (exclusive) 1563 | * This function is the same as Math.random() (except that it could be seeded) 1564 | */ 1565 | Generator.prototype.nextDouble = nextDouble; 1566 | 1567 | /** 1568 | * Returns a random real number uniformly in [0, 1) 1569 | */ 1570 | Generator.prototype.uniform = nextDouble; 1571 | 1572 | Generator.prototype.gaussian = gaussian; 1573 | 1574 | function gaussian() { 1575 | // use the polar form of the Box-Muller transform 1576 | // based on https://introcs.cs.princeton.edu/java/23recursion/StdRandom.java 1577 | var r, x, y; 1578 | do { 1579 | x = this.nextDouble() * 2 - 1; 1580 | y = this.nextDouble() * 2 - 1; 1581 | r = x * x + y * y; 1582 | } while (r >= 1 || r === 0); 1583 | 1584 | return x * Math.sqrt(-2 * Math.log(r)/r); 1585 | } 1586 | 1587 | function nextDouble() { 1588 | var seed = this.seed; 1589 | // Robert Jenkins' 32 bit integer hash function. 1590 | seed = ((seed + 0x7ed55d16) + (seed << 12)) & 0xffffffff; 1591 | seed = ((seed ^ 0xc761c23c) ^ (seed >>> 19)) & 0xffffffff; 1592 | seed = ((seed + 0x165667b1) + (seed << 5)) & 0xffffffff; 1593 | seed = ((seed + 0xd3a2646c) ^ (seed << 9)) & 0xffffffff; 1594 | seed = ((seed + 0xfd7046c5) + (seed << 3)) & 0xffffffff; 1595 | seed = ((seed ^ 0xb55a4f09) ^ (seed >>> 16)) & 0xffffffff; 1596 | this.seed = seed; 1597 | return (seed & 0xfffffff) / 0x10000000; 1598 | } 1599 | 1600 | function next(maxValue) { 1601 | return Math.floor(this.nextDouble() * maxValue); 1602 | } 1603 | 1604 | /* 1605 | * Creates iterator over array, which returns items of array in random order 1606 | * Time complexity is guaranteed to be O(n); 1607 | */ 1608 | function randomIterator(array, customRandom) { 1609 | var localRandom = customRandom || random(); 1610 | if (typeof localRandom.next !== 'function') { 1611 | throw new Error('customRandom does not match expected API: next() function is missing'); 1612 | } 1613 | 1614 | return { 1615 | forEach: forEach, 1616 | 1617 | /** 1618 | * Shuffles array randomly, in place. 1619 | */ 1620 | shuffle: shuffle 1621 | }; 1622 | 1623 | function shuffle() { 1624 | var i, j, t; 1625 | for (i = array.length - 1; i > 0; --i) { 1626 | j = localRandom.next(i + 1); // i inclusive 1627 | t = array[j]; 1628 | array[j] = array[i]; 1629 | array[i] = t; 1630 | } 1631 | 1632 | return array; 1633 | } 1634 | 1635 | function forEach(callback) { 1636 | var i, j, t; 1637 | for (i = array.length - 1; i > 0; --i) { 1638 | j = localRandom.next(i + 1); // i inclusive 1639 | t = array[j]; 1640 | array[j] = array[i]; 1641 | array[i] = t; 1642 | 1643 | callback(t); 1644 | } 1645 | 1646 | if (array.length) { 1647 | callback(array[0]); 1648 | } 1649 | } 1650 | } 1651 | },{}]},{},[1])(1) 1652 | });(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.run = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i (html.scrollTop = prevScroll), 100); 1666 | let link = document.querySelector('a[href*="connect_people"]'); 1667 | if (link) { 1668 | let loading = document.createElement('div'); 1669 | loading.setAttribute('id', 'map-loader'); 1670 | loading.innerText = 'Loading map...' 1671 | loading.style.color = 'rgb(110, 118, 125)'; 1672 | loading.style.fontFamily = FONT; 1673 | loading.style.padding = '18px'; 1674 | loading.style.fontSize = '18px'; 1675 | link.insertAdjacentElement('beforebegin', loading); 1676 | } 1677 | 1678 | let currentScreenName = window.location.pathname.substring(1); 1679 | let img = document.querySelector(`a[href*="photo"] img[src*="profile_images"]`); 1680 | let currentUserData = { 1681 | id: userId, 1682 | name: currentScreenName, 1683 | screenName: currentScreenName, 1684 | } 1685 | if (img) { 1686 | currentUserData.image = img.src; 1687 | } 1688 | 1689 | return fetchHeaders().then(headers => { 1690 | return buildGraphInternal(userId, headers); 1691 | }); 1692 | 1693 | function buildGraphInternal(startFrom, headers) { 1694 | let graph = createGraph(); 1695 | 1696 | return findRelated(userId, usersPerRequest, headers).then(queue => { 1697 | console.log('Initial users: ', queue); 1698 | graph.addNode(userId, currentUserData); 1699 | return downloadQueue(userId, queue, 0); 1700 | }).then(() => { 1701 | console.log('all done', graph); 1702 | return graph; 1703 | }); 1704 | 1705 | function downloadQueue(cameFrom, queue, itemIndex) { 1706 | if(itemIndex >= queue.length) return; 1707 | 1708 | let user = queue[itemIndex]; 1709 | let currentUserId = user.id; 1710 | graph.addNode(currentUserId, user); 1711 | graph.addLink(cameFrom, currentUserId); 1712 | 1713 | return findRelated(currentUserId, usersPerRequest, headers).then(related => { 1714 | related.forEach(other => { 1715 | if (!graph.hasNode(other.id)) graph.addNode(other.id, other); 1716 | graph.addLink(currentUserId, other.id); 1717 | }); 1718 | }).then(x => { 1719 | return downloadQueue(cameFrom, queue, itemIndex + 1); 1720 | }); 1721 | } 1722 | } 1723 | } 1724 | 1725 | function getCurrentPageUserId() { 1726 | const USER_ID_RELATED = /connect_people\?user_id=(\d+)/; 1727 | let users = Array.from(document.querySelectorAll('a[href*="connect_people"]')).filter(a => a.href.match(USER_ID_RELATED)) 1728 | 1729 | if (users.length > 0) { 1730 | return users[0].href.match(USER_ID_RELATED)[1]; 1731 | } 1732 | } 1733 | 1734 | // This is for anonymous browser. 1735 | // we need to know `authorization` and `x-guest-token` headers 1736 | function fetchHeaders() { 1737 | // x-csrf-token 1738 | let requiredHeaders = new Set(['authorization', document.cookie.indexOf('twid') > -1 ? 'x-csrf-token' : 'x-guest-token']); 1739 | let headers = {}; 1740 | 1741 | return new Promise((resolve, reject) => { 1742 | (function(setRequestHeader) { 1743 | XMLHttpRequest.prototype.setRequestHeader = function(key, value) { 1744 | let shouldResolve = false; 1745 | if (requiredHeaders.has(key)) { 1746 | headers[key] = value; 1747 | shouldResolve = Object.keys(headers).length === requiredHeaders.size; 1748 | } 1749 | setRequestHeader.apply(this, arguments); 1750 | 1751 | if (shouldResolve) { 1752 | // restore to default handler 1753 | XMLHttpRequest.prototype.setRequestHeader = setRequestHeader; 1754 | } 1755 | resolve(headers); 1756 | }; 1757 | })(XMLHttpRequest.prototype.setRequestHeader); 1758 | }); 1759 | } 1760 | 1761 | function findRelated(userId, limit, headers) { 1762 | let nextReset; 1763 | return fetch('https://twitter.com/i/api/1.1/users/recommendations.json?' + 1764 | [['include_profile_interstitial_type', 1], 1765 | ['include_blocking', 1], 1766 | ['include_blocked_by', 1], 1767 | ['include_followed_by', 1], 1768 | ['include_want_retweets', 1], 1769 | ['include_mute_edge', 1], 1770 | ['include_can_dm', 1], 1771 | ['include_can_media_tag', 1], 1772 | ['skip_status', 1], 1773 | ['pc', 'true'], 1774 | ['display_location', 'profile_accounts_sidebar'], 1775 | ['limit', limit], 1776 | ['user_id', userId], 1777 | ['ext','mediaStats%2ChighlightedLabel']].map(pair => pair.join('=')).join('&'), { 1778 | headers: headers, 1779 | mode: 'cors' 1780 | }).then(x => { 1781 | let rateLimitRemaining = x.headers.get('x-rate-limit-remaining'); 1782 | let reset = Number.parseInt(x.headers.get('x-rate-limit-reset'), 10); 1783 | nextReset = new Date(reset * 1000); 1784 | console.log('Rate limit: ' + rateLimitRemaining + ', reset: ' + nextReset); 1785 | return x.json(); 1786 | }).then(x => { 1787 | if (x.errors && x.errors[0] && x.errors[0].code === 88) { 1788 | let loader = document.querySelector('#map-loader'); 1789 | loader.innerText = 'Cannot build a graph: API rate limit exceeded. Try again at ' + nextReset.toLocaleTimeString(); 1790 | return new Promise(() => { 1791 | // this never resolves 1792 | }) 1793 | } 1794 | return x.map(({user}) => { 1795 | return { 1796 | description: user.description, 1797 | id: user.id_str, 1798 | name: user.name, 1799 | screenName: user.screen_name, 1800 | followers: user.followers_count, 1801 | following: user.friends_count, 1802 | statuses: user.statuses_count, 1803 | location: user.location, 1804 | image: user.profile_image_url_https, 1805 | } 1806 | }); 1807 | }) 1808 | } 1809 | },{"./constants":2,"ngraph.graph":8}],2:[function(require,module,exports){ 1810 | module.exports = { 1811 | FONT: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif', 1812 | } 1813 | },{}],3:[function(require,module,exports){ 1814 | const buildGraph = require('./buildGraph'); 1815 | const sivg = require('simplesvg'); 1816 | const createPanZoom = require('panzoom'); 1817 | const { FONT } = require('./constants'); 1818 | 1819 | module.exports = run; 1820 | 1821 | function run() { 1822 | let createLayout = window.ngraphCreate2dLayout; // require('ngraph.forcelayout/dist/ngraph.forcelayout2d.js') 1823 | buildGraph().then(g => { 1824 | let layout = createLayout(g, { 1825 | adaptiveTimeStepWeight: 0.05, 1826 | springLength: 15, 1827 | gravity: -24 1828 | }); 1829 | for (let i = 0; i < 450; ++i) layout.step(); 1830 | draw(layout, g); 1831 | }) 1832 | } 1833 | 1834 | function draw(layout, graph) { 1835 | let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 1836 | let container = sivg('g'); 1837 | let nodes = sivg('g'); 1838 | let links = sivg('g'); 1839 | let nodeHalfSize = 2.5; 1840 | 1841 | svg.style.width = '100%'; 1842 | svg.style.height = '300px'; 1843 | // svg.style.background = '#efefef'; 1844 | let clipCircle = sivg('clipPath', {id: 'clipCircle'}); 1845 | clipCircle.appendChild(sivg('circle', { 1846 | cx: nodeHalfSize, cy: nodeHalfSize, r: nodeHalfSize 1847 | })) 1848 | svg.appendChild(clipCircle) 1849 | svg.appendChild(container); 1850 | 1851 | container.appendChild(links); 1852 | container.appendChild(nodes); 1853 | 1854 | graph.forEachLink(addLinkUI); 1855 | graph.forEachNode(addNodeUI); 1856 | let box = layout.simulator.getBoundingBox(); 1857 | let width = box.max_x - box.min_x; 1858 | let height = box.max_y - box.min_y; 1859 | let dx = width * 0.1; let dy = height * 0.1; 1860 | let left = box.min_x - dx; 1861 | let top = box.min_y - dy; 1862 | width += dx * 2; height += 2 * dy; 1863 | let viewBox = `${left} ${top} ${width} ${height}`; 1864 | svg.setAttributeNS(null, 'viewBox', viewBox); 1865 | 1866 | let loader = document.querySelector('#map-loader') 1867 | if (loader) loader.parentElement.removeChild(loader); 1868 | 1869 | let link = document.querySelector('a[href*="connect_people"]'); 1870 | if (link) { 1871 | link.insertAdjacentElement('beforebegin', svg); 1872 | } else { 1873 | document.body.appendChild(svg); 1874 | } 1875 | let pz = createPanZoom(container); 1876 | //pz.showRectangle({left, top, right: right + width, bottom: top + height }) 1877 | 1878 | function addNodeUI(node) { 1879 | let from = layout.getNodePosition(node.id); 1880 | let nodeUI; 1881 | if (node.data) { 1882 | nodeUI = sivg('g', { 1883 | 'transform': `translate(${from.x - nodeHalfSize}, ${from.y - nodeHalfSize})` 1884 | }); 1885 | let href = sivg('a', {target: '_blank'}); 1886 | href.link('/' + node.data.screenName); 1887 | href.appendChild(sivg('image', { 1888 | href: node.data.image, 1889 | width: nodeHalfSize * 2, 1890 | height: nodeHalfSize * 2, 1891 | 'clip-path': 'url(#clipCircle)' 1892 | })) 1893 | nodeUI.appendChild(href); 1894 | let label = sivg('text', { 1895 | x: nodeHalfSize, 1896 | y: nodeHalfSize * 2 + nodeHalfSize * 0.5, 1897 | 'text-anchor': 'middle', 1898 | // 'paint-order': 'stroke', 1899 | // 'stroke': '#D9D9D9', 1900 | // 'stroke-width': 0.1, 1901 | 'font-family': FONT, 1902 | 'fill': '#d9d9d9', 1903 | 'font-size': nodeHalfSize * 0.4 1904 | }); 1905 | label.text(node.data.screenName); 1906 | nodeUI.appendChild(label); 1907 | } else { 1908 | nodeUI = sivg('circle', { 1909 | cx: from.x, 1910 | cy: from.y, 1911 | fill: 'orange', 1912 | r: nodeHalfSize * .9 1913 | }); 1914 | } 1915 | nodes.appendChild(nodeUI); 1916 | } 1917 | function addLinkUI(link) { 1918 | let from = layout.getNodePosition(link.fromId); 1919 | let to = layout.getNodePosition(link.toId); 1920 | let ui = sivg('line', { 1921 | x1: from.x, 1922 | y1: from.y, 1923 | x2: to.x, 1924 | y2: to.y, 1925 | stroke: '#333333', 1926 | 'stroke-width': '0.1' 1927 | }); 1928 | links.appendChild(ui); 1929 | } 1930 | } 1931 | run(); 1932 | },{"./buildGraph":1,"./constants":2,"panzoom":9,"simplesvg":15}],4:[function(require,module,exports){ 1933 | addEventListener.removeEventListener = removeEventListener 1934 | addEventListener.addEventListener = addEventListener 1935 | 1936 | module.exports = addEventListener 1937 | 1938 | var Events = null 1939 | 1940 | function addEventListener(el, eventName, listener, useCapture) { 1941 | Events = Events || ( 1942 | document.addEventListener ? 1943 | {add: stdAttach, rm: stdDetach} : 1944 | {add: oldIEAttach, rm: oldIEDetach} 1945 | ) 1946 | 1947 | return Events.add(el, eventName, listener, useCapture) 1948 | } 1949 | 1950 | function removeEventListener(el, eventName, listener, useCapture) { 1951 | Events = Events || ( 1952 | document.addEventListener ? 1953 | {add: stdAttach, rm: stdDetach} : 1954 | {add: oldIEAttach, rm: oldIEDetach} 1955 | ) 1956 | 1957 | return Events.rm(el, eventName, listener, useCapture) 1958 | } 1959 | 1960 | function stdAttach(el, eventName, listener, useCapture) { 1961 | el.addEventListener(eventName, listener, useCapture) 1962 | } 1963 | 1964 | function stdDetach(el, eventName, listener, useCapture) { 1965 | el.removeEventListener(eventName, listener, useCapture) 1966 | } 1967 | 1968 | function oldIEAttach(el, eventName, listener, useCapture) { 1969 | if(useCapture) { 1970 | throw new Error('cannot useCapture in oldIE') 1971 | } 1972 | 1973 | el.attachEvent('on' + eventName, listener) 1974 | } 1975 | 1976 | function oldIEDetach(el, eventName, listener, useCapture) { 1977 | el.detachEvent('on' + eventName, listener) 1978 | } 1979 | 1980 | },{}],5:[function(require,module,exports){ 1981 | var BezierEasing = require('bezier-easing') 1982 | 1983 | // Predefined set of animations. Similar to CSS easing functions 1984 | var animations = { 1985 | ease: BezierEasing(0.25, 0.1, 0.25, 1), 1986 | easeIn: BezierEasing(0.42, 0, 1, 1), 1987 | easeOut: BezierEasing(0, 0, 0.58, 1), 1988 | easeInOut: BezierEasing(0.42, 0, 0.58, 1), 1989 | linear: BezierEasing(0, 0, 1, 1) 1990 | } 1991 | 1992 | 1993 | module.exports = animate; 1994 | module.exports.makeAggregateRaf = makeAggregateRaf; 1995 | module.exports.sharedScheduler = makeAggregateRaf(); 1996 | 1997 | 1998 | function animate(source, target, options) { 1999 | var start = Object.create(null) 2000 | var diff = Object.create(null) 2001 | options = options || {} 2002 | // We let clients specify their own easing function 2003 | var easing = (typeof options.easing === 'function') ? options.easing : animations[options.easing] 2004 | 2005 | // if nothing is specified, default to ease (similar to CSS animations) 2006 | if (!easing) { 2007 | if (options.easing) { 2008 | console.warn('Unknown easing function in amator: ' + options.easing); 2009 | } 2010 | easing = animations.ease 2011 | } 2012 | 2013 | var step = typeof options.step === 'function' ? options.step : noop 2014 | var done = typeof options.done === 'function' ? options.done : noop 2015 | 2016 | var scheduler = getScheduler(options.scheduler) 2017 | 2018 | var keys = Object.keys(target) 2019 | keys.forEach(function(key) { 2020 | start[key] = source[key] 2021 | diff[key] = target[key] - source[key] 2022 | }) 2023 | 2024 | var durationInMs = typeof options.duration === 'number' ? options.duration : 400 2025 | var durationInFrames = Math.max(1, durationInMs * 0.06) // 0.06 because 60 frames pers 1,000 ms 2026 | var previousAnimationId 2027 | var frame = 0 2028 | 2029 | previousAnimationId = scheduler.next(loop) 2030 | 2031 | return { 2032 | cancel: cancel 2033 | } 2034 | 2035 | function cancel() { 2036 | scheduler.cancel(previousAnimationId) 2037 | previousAnimationId = 0 2038 | } 2039 | 2040 | function loop() { 2041 | var t = easing(frame/durationInFrames) 2042 | frame += 1 2043 | setValues(t) 2044 | if (frame <= durationInFrames) { 2045 | previousAnimationId = scheduler.next(loop) 2046 | step(source) 2047 | } else { 2048 | previousAnimationId = 0 2049 | setTimeout(function() { done(source) }, 0) 2050 | } 2051 | } 2052 | 2053 | function setValues(t) { 2054 | keys.forEach(function(key) { 2055 | source[key] = diff[key] * t + start[key] 2056 | }) 2057 | } 2058 | } 2059 | 2060 | function noop() { } 2061 | 2062 | function getScheduler(scheduler) { 2063 | if (!scheduler) { 2064 | var canRaf = typeof window !== 'undefined' && window.requestAnimationFrame 2065 | return canRaf ? rafScheduler() : timeoutScheduler() 2066 | } 2067 | if (typeof scheduler.next !== 'function') throw new Error('Scheduler is supposed to have next(cb) function') 2068 | if (typeof scheduler.cancel !== 'function') throw new Error('Scheduler is supposed to have cancel(handle) function') 2069 | 2070 | return scheduler 2071 | } 2072 | 2073 | function rafScheduler() { 2074 | return { 2075 | next: window.requestAnimationFrame.bind(window), 2076 | cancel: window.cancelAnimationFrame.bind(window) 2077 | } 2078 | } 2079 | 2080 | function timeoutScheduler() { 2081 | return { 2082 | next: function(cb) { 2083 | return setTimeout(cb, 1000/60) 2084 | }, 2085 | cancel: function (id) { 2086 | return clearTimeout(id) 2087 | } 2088 | } 2089 | } 2090 | 2091 | function makeAggregateRaf() { 2092 | var frontBuffer = new Set(); 2093 | var backBuffer = new Set(); 2094 | var frameToken = 0; 2095 | 2096 | return { 2097 | next: next, 2098 | cancel: next, 2099 | clearAll: clearAll 2100 | } 2101 | 2102 | function clearAll() { 2103 | frontBuffer.clear(); 2104 | backBuffer.clear(); 2105 | cancelAnimationFrame(frameToken); 2106 | frameToken = 0; 2107 | } 2108 | 2109 | function next(callback) { 2110 | backBuffer.add(callback); 2111 | renderNextFrame(); 2112 | } 2113 | 2114 | function renderNextFrame() { 2115 | if (!frameToken) frameToken = requestAnimationFrame(renderFrame); 2116 | } 2117 | 2118 | function renderFrame() { 2119 | frameToken = 0; 2120 | 2121 | var t = backBuffer; 2122 | backBuffer = frontBuffer; 2123 | frontBuffer = t; 2124 | 2125 | frontBuffer.forEach(function(callback) { 2126 | callback(); 2127 | }); 2128 | frontBuffer.clear(); 2129 | } 2130 | 2131 | function cancel(callback) { 2132 | backBuffer.delete(callback); 2133 | } 2134 | } 2135 | 2136 | },{"bezier-easing":6}],6:[function(require,module,exports){ 2137 | /** 2138 | * https://github.com/gre/bezier-easing 2139 | * BezierEasing - use bezier curve for transition easing function 2140 | * by Gaëtan Renaudeau 2014 - 2015 – MIT License 2141 | */ 2142 | 2143 | // These values are established by empiricism with tests (tradeoff: performance VS precision) 2144 | var NEWTON_ITERATIONS = 4; 2145 | var NEWTON_MIN_SLOPE = 0.001; 2146 | var SUBDIVISION_PRECISION = 0.0000001; 2147 | var SUBDIVISION_MAX_ITERATIONS = 10; 2148 | 2149 | var kSplineTableSize = 11; 2150 | var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0); 2151 | 2152 | var float32ArraySupported = typeof Float32Array === 'function'; 2153 | 2154 | function A (aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } 2155 | function B (aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; } 2156 | function C (aA1) { return 3.0 * aA1; } 2157 | 2158 | // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. 2159 | function calcBezier (aT, aA1, aA2) { return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; } 2160 | 2161 | // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. 2162 | function getSlope (aT, aA1, aA2) { return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); } 2163 | 2164 | function binarySubdivide (aX, aA, aB, mX1, mX2) { 2165 | var currentX, currentT, i = 0; 2166 | do { 2167 | currentT = aA + (aB - aA) / 2.0; 2168 | currentX = calcBezier(currentT, mX1, mX2) - aX; 2169 | if (currentX > 0.0) { 2170 | aB = currentT; 2171 | } else { 2172 | aA = currentT; 2173 | } 2174 | } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); 2175 | return currentT; 2176 | } 2177 | 2178 | function newtonRaphsonIterate (aX, aGuessT, mX1, mX2) { 2179 | for (var i = 0; i < NEWTON_ITERATIONS; ++i) { 2180 | var currentSlope = getSlope(aGuessT, mX1, mX2); 2181 | if (currentSlope === 0.0) { 2182 | return aGuessT; 2183 | } 2184 | var currentX = calcBezier(aGuessT, mX1, mX2) - aX; 2185 | aGuessT -= currentX / currentSlope; 2186 | } 2187 | return aGuessT; 2188 | } 2189 | 2190 | function LinearEasing (x) { 2191 | return x; 2192 | } 2193 | 2194 | module.exports = function bezier (mX1, mY1, mX2, mY2) { 2195 | if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { 2196 | throw new Error('bezier x values must be in [0, 1] range'); 2197 | } 2198 | 2199 | if (mX1 === mY1 && mX2 === mY2) { 2200 | return LinearEasing; 2201 | } 2202 | 2203 | // Precompute samples table 2204 | var sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize); 2205 | for (var i = 0; i < kSplineTableSize; ++i) { 2206 | sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2); 2207 | } 2208 | 2209 | function getTForX (aX) { 2210 | var intervalStart = 0.0; 2211 | var currentSample = 1; 2212 | var lastSample = kSplineTableSize - 1; 2213 | 2214 | for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) { 2215 | intervalStart += kSampleStepSize; 2216 | } 2217 | --currentSample; 2218 | 2219 | // Interpolate to provide an initial guess for t 2220 | var dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]); 2221 | var guessForT = intervalStart + dist * kSampleStepSize; 2222 | 2223 | var initialSlope = getSlope(guessForT, mX1, mX2); 2224 | if (initialSlope >= NEWTON_MIN_SLOPE) { 2225 | return newtonRaphsonIterate(aX, guessForT, mX1, mX2); 2226 | } else if (initialSlope === 0.0) { 2227 | return guessForT; 2228 | } else { 2229 | return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2); 2230 | } 2231 | } 2232 | 2233 | return function BezierEasing (x) { 2234 | // Because JavaScript number are imprecise, we should guarantee the extremes are right. 2235 | if (x === 0) { 2236 | return 0; 2237 | } 2238 | if (x === 1) { 2239 | return 1; 2240 | } 2241 | return calcBezier(getTForX(x), mY1, mY2); 2242 | }; 2243 | }; 2244 | 2245 | },{}],7:[function(require,module,exports){ 2246 | module.exports = function eventify(subject) { 2247 | validateSubject(subject); 2248 | 2249 | var eventsStorage = createEventsStorage(subject); 2250 | subject.on = eventsStorage.on; 2251 | subject.off = eventsStorage.off; 2252 | subject.fire = eventsStorage.fire; 2253 | return subject; 2254 | }; 2255 | 2256 | function createEventsStorage(subject) { 2257 | // Store all event listeners to this hash. Key is event name, value is array 2258 | // of callback records. 2259 | // 2260 | // A callback record consists of callback function and its optional context: 2261 | // { 'eventName' => [{callback: function, ctx: object}] } 2262 | var registeredEvents = Object.create(null); 2263 | 2264 | return { 2265 | on: function (eventName, callback, ctx) { 2266 | if (typeof callback !== 'function') { 2267 | throw new Error('callback is expected to be a function'); 2268 | } 2269 | var handlers = registeredEvents[eventName]; 2270 | if (!handlers) { 2271 | handlers = registeredEvents[eventName] = []; 2272 | } 2273 | handlers.push({callback: callback, ctx: ctx}); 2274 | 2275 | return subject; 2276 | }, 2277 | 2278 | off: function (eventName, callback) { 2279 | var wantToRemoveAll = (typeof eventName === 'undefined'); 2280 | if (wantToRemoveAll) { 2281 | // Killing old events storage should be enough in this case: 2282 | registeredEvents = Object.create(null); 2283 | return subject; 2284 | } 2285 | 2286 | if (registeredEvents[eventName]) { 2287 | var deleteAllCallbacksForEvent = (typeof callback !== 'function'); 2288 | if (deleteAllCallbacksForEvent) { 2289 | delete registeredEvents[eventName]; 2290 | } else { 2291 | var callbacks = registeredEvents[eventName]; 2292 | for (var i = 0; i < callbacks.length; ++i) { 2293 | if (callbacks[i].callback === callback) { 2294 | callbacks.splice(i, 1); 2295 | } 2296 | } 2297 | } 2298 | } 2299 | 2300 | return subject; 2301 | }, 2302 | 2303 | fire: function (eventName) { 2304 | var callbacks = registeredEvents[eventName]; 2305 | if (!callbacks) { 2306 | return subject; 2307 | } 2308 | 2309 | var fireArguments; 2310 | if (arguments.length > 1) { 2311 | fireArguments = Array.prototype.splice.call(arguments, 1); 2312 | } 2313 | for(var i = 0; i < callbacks.length; ++i) { 2314 | var callbackInfo = callbacks[i]; 2315 | callbackInfo.callback.apply(callbackInfo.ctx, fireArguments); 2316 | } 2317 | 2318 | return subject; 2319 | } 2320 | }; 2321 | } 2322 | 2323 | function validateSubject(subject) { 2324 | if (!subject) { 2325 | throw new Error('Eventify cannot use falsy object as events subject'); 2326 | } 2327 | var reservedWords = ['on', 'fire', 'off']; 2328 | for (var i = 0; i < reservedWords.length; ++i) { 2329 | if (subject.hasOwnProperty(reservedWords[i])) { 2330 | throw new Error("Subject cannot be eventified, since it already has property '" + reservedWords[i] + "'"); 2331 | } 2332 | } 2333 | } 2334 | 2335 | },{}],8:[function(require,module,exports){ 2336 | /** 2337 | * @fileOverview Contains definition of the core graph object. 2338 | */ 2339 | 2340 | // TODO: need to change storage layer: 2341 | // 1. Be able to get all nodes O(1) 2342 | // 2. Be able to get number of links O(1) 2343 | 2344 | /** 2345 | * @example 2346 | * var graph = require('ngraph.graph')(); 2347 | * graph.addNode(1); // graph has one node. 2348 | * graph.addLink(2, 3); // now graph contains three nodes and one link. 2349 | * 2350 | */ 2351 | module.exports = createGraph; 2352 | 2353 | var eventify = require('ngraph.events'); 2354 | 2355 | /** 2356 | * Creates a new graph 2357 | */ 2358 | function createGraph(options) { 2359 | // Graph structure is maintained as dictionary of nodes 2360 | // and array of links. Each node has 'links' property which 2361 | // hold all links related to that node. And general links 2362 | // array is used to speed up all links enumeration. This is inefficient 2363 | // in terms of memory, but simplifies coding. 2364 | options = options || {}; 2365 | if ('uniqueLinkId' in options) { 2366 | console.warn( 2367 | 'ngraph.graph: Starting from version 0.14 `uniqueLinkId` is deprecated.\n' + 2368 | 'Use `multigraph` option instead\n', 2369 | '\n', 2370 | 'Note: there is also change in default behavior: From now on each graph\n'+ 2371 | 'is considered to be not a multigraph by default (each edge is unique).' 2372 | ); 2373 | 2374 | options.multigraph = options.uniqueLinkId; 2375 | } 2376 | 2377 | // Dear reader, the non-multigraphs do not guarantee that there is only 2378 | // one link for a given pair of node. When this option is set to false 2379 | // we can save some memory and CPU (18% faster for non-multigraph); 2380 | if (options.multigraph === undefined) options.multigraph = false; 2381 | 2382 | if (typeof Map !== 'function') { 2383 | // TODO: Should we polyfill it ourselves? We don't use much operations there.. 2384 | throw new Error('ngraph.graph requires `Map` to be defined. Please polyfill it before using ngraph'); 2385 | } 2386 | 2387 | var nodes = new Map(); 2388 | var links = [], 2389 | // Hash of multi-edges. Used to track ids of edges between same nodes 2390 | multiEdges = {}, 2391 | suspendEvents = 0, 2392 | 2393 | createLink = options.multigraph ? createUniqueLink : createSingleLink, 2394 | 2395 | // Our graph API provides means to listen to graph changes. Users can subscribe 2396 | // to be notified about changes in the graph by using `on` method. However 2397 | // in some cases they don't use it. To avoid unnecessary memory consumption 2398 | // we will not record graph changes until we have at least one subscriber. 2399 | // Code below supports this optimization. 2400 | // 2401 | // Accumulates all changes made during graph updates. 2402 | // Each change element contains: 2403 | // changeType - one of the strings: 'add', 'remove' or 'update'; 2404 | // node - if change is related to node this property is set to changed graph's node; 2405 | // link - if change is related to link this property is set to changed graph's link; 2406 | changes = [], 2407 | recordLinkChange = noop, 2408 | recordNodeChange = noop, 2409 | enterModification = noop, 2410 | exitModification = noop; 2411 | 2412 | // this is our public API: 2413 | var graphPart = { 2414 | /** 2415 | * Adds node to the graph. If node with given id already exists in the graph 2416 | * its data is extended with whatever comes in 'data' argument. 2417 | * 2418 | * @param nodeId the node's identifier. A string or number is preferred. 2419 | * @param [data] additional data for the node being added. If node already 2420 | * exists its data object is augmented with the new one. 2421 | * 2422 | * @return {node} The newly added node or node with given id if it already exists. 2423 | */ 2424 | addNode: addNode, 2425 | 2426 | /** 2427 | * Adds a link to the graph. The function always create a new 2428 | * link between two nodes. If one of the nodes does not exists 2429 | * a new node is created. 2430 | * 2431 | * @param fromId link start node id; 2432 | * @param toId link end node id; 2433 | * @param [data] additional data to be set on the new link; 2434 | * 2435 | * @return {link} The newly created link 2436 | */ 2437 | addLink: addLink, 2438 | 2439 | /** 2440 | * Removes link from the graph. If link does not exist does nothing. 2441 | * 2442 | * @param link - object returned by addLink() or getLinks() methods. 2443 | * 2444 | * @returns true if link was removed; false otherwise. 2445 | */ 2446 | removeLink: removeLink, 2447 | 2448 | /** 2449 | * Removes node with given id from the graph. If node does not exist in the graph 2450 | * does nothing. 2451 | * 2452 | * @param nodeId node's identifier passed to addNode() function. 2453 | * 2454 | * @returns true if node was removed; false otherwise. 2455 | */ 2456 | removeNode: removeNode, 2457 | 2458 | /** 2459 | * Gets node with given identifier. If node does not exist undefined value is returned. 2460 | * 2461 | * @param nodeId requested node identifier; 2462 | * 2463 | * @return {node} in with requested identifier or undefined if no such node exists. 2464 | */ 2465 | getNode: getNode, 2466 | 2467 | /** 2468 | * Gets number of nodes in this graph. 2469 | * 2470 | * @return number of nodes in the graph. 2471 | */ 2472 | getNodeCount: getNodeCount, 2473 | 2474 | /** 2475 | * Gets total number of links in the graph. 2476 | */ 2477 | getLinkCount: getLinkCount, 2478 | 2479 | /** 2480 | * Synonym for `getLinkCount()` 2481 | */ 2482 | getLinksCount: getLinkCount, 2483 | 2484 | /** 2485 | * Synonym for `getNodeCount()` 2486 | */ 2487 | getNodesCount: getNodeCount, 2488 | 2489 | /** 2490 | * Gets all links (inbound and outbound) from the node with given id. 2491 | * If node with given id is not found null is returned. 2492 | * 2493 | * @param nodeId requested node identifier. 2494 | * 2495 | * @return Array of links from and to requested node if such node exists; 2496 | * otherwise null is returned. 2497 | */ 2498 | getLinks: getLinks, 2499 | 2500 | /** 2501 | * Invokes callback on each node of the graph. 2502 | * 2503 | * @param {Function(node)} callback Function to be invoked. The function 2504 | * is passed one argument: visited node. 2505 | */ 2506 | forEachNode: forEachNode, 2507 | 2508 | /** 2509 | * Invokes callback on every linked (adjacent) node to the given one. 2510 | * 2511 | * @param nodeId Identifier of the requested node. 2512 | * @param {Function(node, link)} callback Function to be called on all linked nodes. 2513 | * The function is passed two parameters: adjacent node and link object itself. 2514 | * @param oriented if true graph treated as oriented. 2515 | */ 2516 | forEachLinkedNode: forEachLinkedNode, 2517 | 2518 | /** 2519 | * Enumerates all links in the graph 2520 | * 2521 | * @param {Function(link)} callback Function to be called on all links in the graph. 2522 | * The function is passed one parameter: graph's link object. 2523 | * 2524 | * Link object contains at least the following fields: 2525 | * fromId - node id where link starts; 2526 | * toId - node id where link ends, 2527 | * data - additional data passed to graph.addLink() method. 2528 | */ 2529 | forEachLink: forEachLink, 2530 | 2531 | /** 2532 | * Suspend all notifications about graph changes until 2533 | * endUpdate is called. 2534 | */ 2535 | beginUpdate: enterModification, 2536 | 2537 | /** 2538 | * Resumes all notifications about graph changes and fires 2539 | * graph 'changed' event in case there are any pending changes. 2540 | */ 2541 | endUpdate: exitModification, 2542 | 2543 | /** 2544 | * Removes all nodes and links from the graph. 2545 | */ 2546 | clear: clear, 2547 | 2548 | /** 2549 | * Detects whether there is a link between two nodes. 2550 | * Operation complexity is O(n) where n - number of links of a node. 2551 | * NOTE: this function is synonim for getLink() 2552 | * 2553 | * @returns link if there is one. null otherwise. 2554 | */ 2555 | hasLink: getLink, 2556 | 2557 | /** 2558 | * Detects whether there is a node with given id 2559 | * 2560 | * Operation complexity is O(1) 2561 | * NOTE: this function is synonim for getNode() 2562 | * 2563 | * @returns node if there is one; Falsy value otherwise. 2564 | */ 2565 | hasNode: getNode, 2566 | 2567 | /** 2568 | * Gets an edge between two nodes. 2569 | * Operation complexity is O(n) where n - number of links of a node. 2570 | * 2571 | * @param {string} fromId link start identifier 2572 | * @param {string} toId link end identifier 2573 | * 2574 | * @returns link if there is one. null otherwise. 2575 | */ 2576 | getLink: getLink 2577 | }; 2578 | 2579 | // this will add `on()` and `fire()` methods. 2580 | eventify(graphPart); 2581 | 2582 | monitorSubscribers(); 2583 | 2584 | return graphPart; 2585 | 2586 | function monitorSubscribers() { 2587 | var realOn = graphPart.on; 2588 | 2589 | // replace real `on` with our temporary on, which will trigger change 2590 | // modification monitoring: 2591 | graphPart.on = on; 2592 | 2593 | function on() { 2594 | // now it's time to start tracking stuff: 2595 | graphPart.beginUpdate = enterModification = enterModificationReal; 2596 | graphPart.endUpdate = exitModification = exitModificationReal; 2597 | recordLinkChange = recordLinkChangeReal; 2598 | recordNodeChange = recordNodeChangeReal; 2599 | 2600 | // this will replace current `on` method with real pub/sub from `eventify`. 2601 | graphPart.on = realOn; 2602 | // delegate to real `on` handler: 2603 | return realOn.apply(graphPart, arguments); 2604 | } 2605 | } 2606 | 2607 | function recordLinkChangeReal(link, changeType) { 2608 | changes.push({ 2609 | link: link, 2610 | changeType: changeType 2611 | }); 2612 | } 2613 | 2614 | function recordNodeChangeReal(node, changeType) { 2615 | changes.push({ 2616 | node: node, 2617 | changeType: changeType 2618 | }); 2619 | } 2620 | 2621 | function addNode(nodeId, data) { 2622 | if (nodeId === undefined) { 2623 | throw new Error('Invalid node identifier'); 2624 | } 2625 | 2626 | enterModification(); 2627 | 2628 | var node = getNode(nodeId); 2629 | if (!node) { 2630 | node = new Node(nodeId, data); 2631 | recordNodeChange(node, 'add'); 2632 | } else { 2633 | node.data = data; 2634 | recordNodeChange(node, 'update'); 2635 | } 2636 | 2637 | nodes.set(nodeId, node); 2638 | 2639 | exitModification(); 2640 | return node; 2641 | } 2642 | 2643 | function getNode(nodeId) { 2644 | return nodes.get(nodeId); 2645 | } 2646 | 2647 | function removeNode(nodeId) { 2648 | var node = getNode(nodeId); 2649 | if (!node) { 2650 | return false; 2651 | } 2652 | 2653 | enterModification(); 2654 | 2655 | var prevLinks = node.links; 2656 | if (prevLinks) { 2657 | node.links = null; 2658 | for(var i = 0; i < prevLinks.length; ++i) { 2659 | removeLink(prevLinks[i]); 2660 | } 2661 | } 2662 | 2663 | nodes.delete(nodeId) 2664 | 2665 | recordNodeChange(node, 'remove'); 2666 | 2667 | exitModification(); 2668 | 2669 | return true; 2670 | } 2671 | 2672 | 2673 | function addLink(fromId, toId, data) { 2674 | enterModification(); 2675 | 2676 | var fromNode = getNode(fromId) || addNode(fromId); 2677 | var toNode = getNode(toId) || addNode(toId); 2678 | 2679 | var link = createLink(fromId, toId, data); 2680 | 2681 | links.push(link); 2682 | 2683 | // TODO: this is not cool. On large graphs potentially would consume more memory. 2684 | addLinkToNode(fromNode, link); 2685 | if (fromId !== toId) { 2686 | // make sure we are not duplicating links for self-loops 2687 | addLinkToNode(toNode, link); 2688 | } 2689 | 2690 | recordLinkChange(link, 'add'); 2691 | 2692 | exitModification(); 2693 | 2694 | return link; 2695 | } 2696 | 2697 | function createSingleLink(fromId, toId, data) { 2698 | var linkId = makeLinkId(fromId, toId); 2699 | return new Link(fromId, toId, data, linkId); 2700 | } 2701 | 2702 | function createUniqueLink(fromId, toId, data) { 2703 | // TODO: Get rid of this method. 2704 | var linkId = makeLinkId(fromId, toId); 2705 | var isMultiEdge = multiEdges.hasOwnProperty(linkId); 2706 | if (isMultiEdge || getLink(fromId, toId)) { 2707 | if (!isMultiEdge) { 2708 | multiEdges[linkId] = 0; 2709 | } 2710 | var suffix = '@' + (++multiEdges[linkId]); 2711 | linkId = makeLinkId(fromId + suffix, toId + suffix); 2712 | } 2713 | 2714 | return new Link(fromId, toId, data, linkId); 2715 | } 2716 | 2717 | function getNodeCount() { 2718 | return nodes.size; 2719 | } 2720 | 2721 | function getLinkCount() { 2722 | return links.length; 2723 | } 2724 | 2725 | function getLinks(nodeId) { 2726 | var node = getNode(nodeId); 2727 | return node ? node.links : null; 2728 | } 2729 | 2730 | function removeLink(link) { 2731 | if (!link) { 2732 | return false; 2733 | } 2734 | var idx = indexOfElementInArray(link, links); 2735 | if (idx < 0) { 2736 | return false; 2737 | } 2738 | 2739 | enterModification(); 2740 | 2741 | links.splice(idx, 1); 2742 | 2743 | var fromNode = getNode(link.fromId); 2744 | var toNode = getNode(link.toId); 2745 | 2746 | if (fromNode) { 2747 | idx = indexOfElementInArray(link, fromNode.links); 2748 | if (idx >= 0) { 2749 | fromNode.links.splice(idx, 1); 2750 | } 2751 | } 2752 | 2753 | if (toNode) { 2754 | idx = indexOfElementInArray(link, toNode.links); 2755 | if (idx >= 0) { 2756 | toNode.links.splice(idx, 1); 2757 | } 2758 | } 2759 | 2760 | recordLinkChange(link, 'remove'); 2761 | 2762 | exitModification(); 2763 | 2764 | return true; 2765 | } 2766 | 2767 | function getLink(fromNodeId, toNodeId) { 2768 | // TODO: Use sorted links to speed this up 2769 | var node = getNode(fromNodeId), 2770 | i; 2771 | if (!node || !node.links) { 2772 | return null; 2773 | } 2774 | 2775 | for (i = 0; i < node.links.length; ++i) { 2776 | var link = node.links[i]; 2777 | if (link.fromId === fromNodeId && link.toId === toNodeId) { 2778 | return link; 2779 | } 2780 | } 2781 | 2782 | return null; // no link. 2783 | } 2784 | 2785 | function clear() { 2786 | enterModification(); 2787 | forEachNode(function(node) { 2788 | removeNode(node.id); 2789 | }); 2790 | exitModification(); 2791 | } 2792 | 2793 | function forEachLink(callback) { 2794 | var i, length; 2795 | if (typeof callback === 'function') { 2796 | for (i = 0, length = links.length; i < length; ++i) { 2797 | callback(links[i]); 2798 | } 2799 | } 2800 | } 2801 | 2802 | function forEachLinkedNode(nodeId, callback, oriented) { 2803 | var node = getNode(nodeId); 2804 | 2805 | if (node && node.links && typeof callback === 'function') { 2806 | if (oriented) { 2807 | return forEachOrientedLink(node.links, nodeId, callback); 2808 | } else { 2809 | return forEachNonOrientedLink(node.links, nodeId, callback); 2810 | } 2811 | } 2812 | } 2813 | 2814 | function forEachNonOrientedLink(links, nodeId, callback) { 2815 | var quitFast; 2816 | for (var i = 0; i < links.length; ++i) { 2817 | var link = links[i]; 2818 | var linkedNodeId = link.fromId === nodeId ? link.toId : link.fromId; 2819 | 2820 | quitFast = callback(nodes.get(linkedNodeId), link); 2821 | if (quitFast) { 2822 | return true; // Client does not need more iterations. Break now. 2823 | } 2824 | } 2825 | } 2826 | 2827 | function forEachOrientedLink(links, nodeId, callback) { 2828 | var quitFast; 2829 | for (var i = 0; i < links.length; ++i) { 2830 | var link = links[i]; 2831 | if (link.fromId === nodeId) { 2832 | quitFast = callback(nodes.get(link.toId), link) 2833 | if (quitFast) { 2834 | return true; // Client does not need more iterations. Break now. 2835 | } 2836 | } 2837 | } 2838 | } 2839 | 2840 | // we will not fire anything until users of this library explicitly call `on()` 2841 | // method. 2842 | function noop() {} 2843 | 2844 | // Enter, Exit modification allows bulk graph updates without firing events. 2845 | function enterModificationReal() { 2846 | suspendEvents += 1; 2847 | } 2848 | 2849 | function exitModificationReal() { 2850 | suspendEvents -= 1; 2851 | if (suspendEvents === 0 && changes.length > 0) { 2852 | graphPart.fire('changed', changes); 2853 | changes.length = 0; 2854 | } 2855 | } 2856 | 2857 | function forEachNode(callback) { 2858 | if (typeof callback !== 'function') { 2859 | throw new Error('Function is expected to iterate over graph nodes. You passed ' + callback); 2860 | } 2861 | 2862 | var valuesIterator = nodes.values(); 2863 | var nextValue = valuesIterator.next(); 2864 | while (!nextValue.done) { 2865 | if (callback(nextValue.value)) { 2866 | return true; // client doesn't want to proceed. Return. 2867 | } 2868 | nextValue = valuesIterator.next(); 2869 | } 2870 | } 2871 | } 2872 | 2873 | // need this for old browsers. Should this be a separate module? 2874 | function indexOfElementInArray(element, array) { 2875 | if (!array) return -1; 2876 | 2877 | if (array.indexOf) { 2878 | return array.indexOf(element); 2879 | } 2880 | 2881 | var len = array.length, 2882 | i; 2883 | 2884 | for (i = 0; i < len; i += 1) { 2885 | if (array[i] === element) { 2886 | return i; 2887 | } 2888 | } 2889 | 2890 | return -1; 2891 | } 2892 | 2893 | /** 2894 | * Internal structure to represent node; 2895 | */ 2896 | function Node(id, data) { 2897 | this.id = id; 2898 | this.links = null; 2899 | this.data = data; 2900 | } 2901 | 2902 | function addLinkToNode(node, link) { 2903 | if (node.links) { 2904 | node.links.push(link); 2905 | } else { 2906 | node.links = [link]; 2907 | } 2908 | } 2909 | 2910 | /** 2911 | * Internal structure to represent links; 2912 | */ 2913 | function Link(fromId, toId, data, id) { 2914 | this.fromId = fromId; 2915 | this.toId = toId; 2916 | this.data = data; 2917 | this.id = id; 2918 | } 2919 | 2920 | function makeLinkId(fromId, toId) { 2921 | return fromId.toString() + '👉 ' + toId.toString(); 2922 | } 2923 | 2924 | },{"ngraph.events":7}],9:[function(require,module,exports){ 2925 | 'use strict'; 2926 | /** 2927 | * Allows to drag and zoom svg elements 2928 | */ 2929 | var wheel = require('wheel'); 2930 | var animate = require('amator'); 2931 | var eventify = require('ngraph.events'); 2932 | var kinetic = require('./lib/kinetic.js'); 2933 | var createTextSelectionInterceptor = require('./lib/createTextSelectionInterceptor.js'); 2934 | var domTextSelectionInterceptor = createTextSelectionInterceptor(); 2935 | var fakeTextSelectorInterceptor = createTextSelectionInterceptor(true); 2936 | var Transform = require('./lib/transform.js'); 2937 | var makeSvgController = require('./lib/svgController.js'); 2938 | var makeDomController = require('./lib/domController.js'); 2939 | 2940 | var defaultZoomSpeed = 1; 2941 | var defaultDoubleTapZoomSpeed = 1.75; 2942 | var doubleTapSpeedInMS = 300; 2943 | 2944 | module.exports = createPanZoom; 2945 | 2946 | /** 2947 | * Creates a new instance of panzoom, so that an object can be panned and zoomed 2948 | * 2949 | * @param {DOMElement} domElement where panzoom should be attached. 2950 | * @param {Object} options that configure behavior. 2951 | */ 2952 | function createPanZoom(domElement, options) { 2953 | options = options || {}; 2954 | 2955 | var panController = options.controller; 2956 | 2957 | if (!panController) { 2958 | if (makeSvgController.canAttach(domElement)) { 2959 | panController = makeSvgController(domElement, options); 2960 | } else if (makeDomController.canAttach(domElement)) { 2961 | panController = makeDomController(domElement, options); 2962 | } 2963 | } 2964 | 2965 | if (!panController) { 2966 | throw new Error( 2967 | 'Cannot create panzoom for the current type of dom element' 2968 | ); 2969 | } 2970 | var owner = panController.getOwner(); 2971 | // just to avoid GC pressure, every time we do intermediate transform 2972 | // we return this object. For internal use only. Never give it back to the consumer of this library 2973 | var storedCTMResult = { x: 0, y: 0 }; 2974 | 2975 | var isDirty = false; 2976 | var transform = new Transform(); 2977 | 2978 | if (panController.initTransform) { 2979 | panController.initTransform(transform); 2980 | } 2981 | 2982 | var filterKey = typeof options.filterKey === 'function' ? options.filterKey : noop; 2983 | // TODO: likely need to unite pinchSpeed with zoomSpeed 2984 | var pinchSpeed = typeof options.pinchSpeed === 'number' ? options.pinchSpeed : 1; 2985 | var bounds = options.bounds; 2986 | var maxZoom = typeof options.maxZoom === 'number' ? options.maxZoom : Number.POSITIVE_INFINITY; 2987 | var minZoom = typeof options.minZoom === 'number' ? options.minZoom : 0; 2988 | 2989 | var boundsPadding = typeof options.boundsPadding === 'number' ? options.boundsPadding : 0.05; 2990 | var zoomDoubleClickSpeed = typeof options.zoomDoubleClickSpeed === 'number' ? options.zoomDoubleClickSpeed : defaultDoubleTapZoomSpeed; 2991 | var beforeWheel = options.beforeWheel || noop; 2992 | var beforeMouseDown = options.beforeMouseDown || noop; 2993 | var speed = typeof options.zoomSpeed === 'number' ? options.zoomSpeed : defaultZoomSpeed; 2994 | var transformOrigin = parseTransformOrigin(options.transformOrigin); 2995 | var textSelection = options.enableTextSelection ? fakeTextSelectorInterceptor : domTextSelectionInterceptor; 2996 | 2997 | validateBounds(bounds); 2998 | 2999 | if (options.autocenter) { 3000 | autocenter(); 3001 | } 3002 | 3003 | var frameAnimation; 3004 | var lastTouchEndTime = 0; 3005 | var lastSingleFingerOffset; 3006 | var touchInProgress = false; 3007 | 3008 | // We only need to fire panstart when actual move happens 3009 | var panstartFired = false; 3010 | 3011 | // cache mouse coordinates here 3012 | var mouseX; 3013 | var mouseY; 3014 | 3015 | var pinchZoomLength; 3016 | 3017 | var smoothScroll; 3018 | if ('smoothScroll' in options && !options.smoothScroll) { 3019 | // If user explicitly asked us not to use smooth scrolling, we obey 3020 | smoothScroll = rigidScroll(); 3021 | } else { 3022 | // otherwise we use forward smoothScroll settings to kinetic API 3023 | // which makes scroll smoothing. 3024 | smoothScroll = kinetic(getPoint, scroll, options.smoothScroll); 3025 | } 3026 | 3027 | var moveByAnimation; 3028 | var zoomToAnimation; 3029 | 3030 | var multiTouch; 3031 | var paused = false; 3032 | 3033 | listenForEvents(); 3034 | 3035 | var api = { 3036 | dispose: dispose, 3037 | moveBy: internalMoveBy, 3038 | moveTo: moveTo, 3039 | smoothMoveTo: smoothMoveTo, 3040 | centerOn: centerOn, 3041 | zoomTo: publicZoomTo, 3042 | zoomAbs: zoomAbs, 3043 | smoothZoom: smoothZoom, 3044 | smoothZoomAbs: smoothZoomAbs, 3045 | showRectangle: showRectangle, 3046 | 3047 | pause: pause, 3048 | resume: resume, 3049 | isPaused: isPaused, 3050 | 3051 | getTransform: getTransformModel, 3052 | 3053 | getMinZoom: getMinZoom, 3054 | setMinZoom: setMinZoom, 3055 | 3056 | getMaxZoom: getMaxZoom, 3057 | setMaxZoom: setMaxZoom, 3058 | 3059 | getTransformOrigin: getTransformOrigin, 3060 | setTransformOrigin: setTransformOrigin, 3061 | 3062 | getZoomSpeed: getZoomSpeed, 3063 | setZoomSpeed: setZoomSpeed 3064 | }; 3065 | 3066 | eventify(api); 3067 | 3068 | var initialX = typeof options.initialX === 'number' ? options.initialX : transform.x; 3069 | var initialY = typeof options.initialY === 'number' ? options.initialY : transform.y; 3070 | var initialZoom = typeof options.initialZoom === 'number' ? options.initialZoom : transform.scale; 3071 | 3072 | if(initialX != transform.x || initialY != transform.y || initialZoom != transform.scale){ 3073 | zoomAbs(initialX, initialY, initialZoom); 3074 | } 3075 | 3076 | return api; 3077 | 3078 | function pause() { 3079 | releaseEvents(); 3080 | paused = true; 3081 | } 3082 | 3083 | function resume() { 3084 | if (paused) { 3085 | listenForEvents(); 3086 | paused = false; 3087 | } 3088 | } 3089 | 3090 | function isPaused() { 3091 | return paused; 3092 | } 3093 | 3094 | function showRectangle(rect) { 3095 | // TODO: this duplicates autocenter. I think autocenter should go. 3096 | var clientRect = owner.getBoundingClientRect(); 3097 | var size = transformToScreen(clientRect.width, clientRect.height); 3098 | 3099 | var rectWidth = rect.right - rect.left; 3100 | var rectHeight = rect.bottom - rect.top; 3101 | if (!Number.isFinite(rectWidth) || !Number.isFinite(rectHeight)) { 3102 | throw new Error('Invalid rectangle'); 3103 | } 3104 | 3105 | var dw = size.x / rectWidth; 3106 | var dh = size.y / rectHeight; 3107 | var scale = Math.min(dw, dh); 3108 | transform.x = -(rect.left + rectWidth / 2) * scale + size.x / 2; 3109 | transform.y = -(rect.top + rectHeight / 2) * scale + size.y / 2; 3110 | transform.scale = scale; 3111 | } 3112 | 3113 | function transformToScreen(x, y) { 3114 | if (panController.getScreenCTM) { 3115 | var parentCTM = panController.getScreenCTM(); 3116 | var parentScaleX = parentCTM.a; 3117 | var parentScaleY = parentCTM.d; 3118 | var parentOffsetX = parentCTM.e; 3119 | var parentOffsetY = parentCTM.f; 3120 | storedCTMResult.x = x * parentScaleX - parentOffsetX; 3121 | storedCTMResult.y = y * parentScaleY - parentOffsetY; 3122 | } else { 3123 | storedCTMResult.x = x; 3124 | storedCTMResult.y = y; 3125 | } 3126 | 3127 | return storedCTMResult; 3128 | } 3129 | 3130 | function autocenter() { 3131 | var w; // width of the parent 3132 | var h; // height of the parent 3133 | var left = 0; 3134 | var top = 0; 3135 | var sceneBoundingBox = getBoundingBox(); 3136 | if (sceneBoundingBox) { 3137 | // If we have bounding box - use it. 3138 | left = sceneBoundingBox.left; 3139 | top = sceneBoundingBox.top; 3140 | w = sceneBoundingBox.right - sceneBoundingBox.left; 3141 | h = sceneBoundingBox.bottom - sceneBoundingBox.top; 3142 | } else { 3143 | // otherwise just use whatever space we have 3144 | var ownerRect = owner.getBoundingClientRect(); 3145 | w = ownerRect.width; 3146 | h = ownerRect.height; 3147 | } 3148 | var bbox = panController.getBBox(); 3149 | if (bbox.width === 0 || bbox.height === 0) { 3150 | // we probably do not have any elements in the SVG 3151 | // just bail out; 3152 | return; 3153 | } 3154 | var dh = h / bbox.height; 3155 | var dw = w / bbox.width; 3156 | var scale = Math.min(dw, dh); 3157 | transform.x = -(bbox.left + bbox.width / 2) * scale + w / 2 + left; 3158 | transform.y = -(bbox.top + bbox.height / 2) * scale + h / 2 + top; 3159 | transform.scale = scale; 3160 | } 3161 | 3162 | function getTransformModel() { 3163 | // TODO: should this be read only? 3164 | return transform; 3165 | } 3166 | 3167 | function getMinZoom() { 3168 | return minZoom; 3169 | } 3170 | 3171 | function setMinZoom(newMinZoom) { 3172 | minZoom = newMinZoom; 3173 | } 3174 | 3175 | function getMaxZoom() { 3176 | return maxZoom; 3177 | } 3178 | 3179 | function setMaxZoom(newMaxZoom) { 3180 | maxZoom = newMaxZoom; 3181 | } 3182 | 3183 | function getTransformOrigin() { 3184 | return transformOrigin; 3185 | } 3186 | 3187 | function setTransformOrigin(newTransformOrigin) { 3188 | transformOrigin = parseTransformOrigin(newTransformOrigin); 3189 | } 3190 | 3191 | function getZoomSpeed() { 3192 | return speed; 3193 | } 3194 | 3195 | function setZoomSpeed(newSpeed) { 3196 | if (!Number.isFinite(newSpeed)) { 3197 | throw new Error('Zoom speed should be a number'); 3198 | } 3199 | speed = newSpeed; 3200 | } 3201 | 3202 | function getPoint() { 3203 | return { 3204 | x: transform.x, 3205 | y: transform.y 3206 | }; 3207 | } 3208 | 3209 | function moveTo(x, y) { 3210 | transform.x = x; 3211 | transform.y = y; 3212 | 3213 | keepTransformInsideBounds(); 3214 | 3215 | triggerEvent('pan'); 3216 | makeDirty(); 3217 | } 3218 | 3219 | function moveBy(dx, dy) { 3220 | moveTo(transform.x + dx, transform.y + dy); 3221 | } 3222 | 3223 | function keepTransformInsideBounds() { 3224 | var boundingBox = getBoundingBox(); 3225 | if (!boundingBox) return; 3226 | 3227 | var adjusted = false; 3228 | var clientRect = getClientRect(); 3229 | 3230 | var diff = boundingBox.left - clientRect.right; 3231 | if (diff > 0) { 3232 | transform.x += diff; 3233 | adjusted = true; 3234 | } 3235 | // check the other side: 3236 | diff = boundingBox.right - clientRect.left; 3237 | if (diff < 0) { 3238 | transform.x += diff; 3239 | adjusted = true; 3240 | } 3241 | 3242 | // y axis: 3243 | diff = boundingBox.top - clientRect.bottom; 3244 | if (diff > 0) { 3245 | // we adjust transform, so that it matches exactly our bounding box: 3246 | // transform.y = boundingBox.top - (boundingBox.height + boundingBox.y) * transform.scale => 3247 | // transform.y = boundingBox.top - (clientRect.bottom - transform.y) => 3248 | // transform.y = diff + transform.y => 3249 | transform.y += diff; 3250 | adjusted = true; 3251 | } 3252 | 3253 | diff = boundingBox.bottom - clientRect.top; 3254 | if (diff < 0) { 3255 | transform.y += diff; 3256 | adjusted = true; 3257 | } 3258 | return adjusted; 3259 | } 3260 | 3261 | /** 3262 | * Returns bounding box that should be used to restrict scene movement. 3263 | */ 3264 | function getBoundingBox() { 3265 | if (!bounds) return; // client does not want to restrict movement 3266 | 3267 | if (typeof bounds === 'boolean') { 3268 | // for boolean type we use parent container bounds 3269 | var ownerRect = owner.getBoundingClientRect(); 3270 | var sceneWidth = ownerRect.width; 3271 | var sceneHeight = ownerRect.height; 3272 | 3273 | return { 3274 | left: sceneWidth * boundsPadding, 3275 | top: sceneHeight * boundsPadding, 3276 | right: sceneWidth * (1 - boundsPadding), 3277 | bottom: sceneHeight * (1 - boundsPadding) 3278 | }; 3279 | } 3280 | 3281 | return bounds; 3282 | } 3283 | 3284 | function getClientRect() { 3285 | var bbox = panController.getBBox(); 3286 | var leftTop = client(bbox.left, bbox.top); 3287 | 3288 | return { 3289 | left: leftTop.x, 3290 | top: leftTop.y, 3291 | right: bbox.width * transform.scale + leftTop.x, 3292 | bottom: bbox.height * transform.scale + leftTop.y 3293 | }; 3294 | } 3295 | 3296 | function client(x, y) { 3297 | return { 3298 | x: x * transform.scale + transform.x, 3299 | y: y * transform.scale + transform.y 3300 | }; 3301 | } 3302 | 3303 | function makeDirty() { 3304 | isDirty = true; 3305 | frameAnimation = window.requestAnimationFrame(frame); 3306 | } 3307 | 3308 | function zoomByRatio(clientX, clientY, ratio) { 3309 | if (isNaN(clientX) || isNaN(clientY) || isNaN(ratio)) { 3310 | throw new Error('zoom requires valid numbers'); 3311 | } 3312 | 3313 | var newScale = transform.scale * ratio; 3314 | 3315 | if (newScale < minZoom) { 3316 | if (transform.scale === minZoom) return; 3317 | 3318 | ratio = minZoom / transform.scale; 3319 | } 3320 | if (newScale > maxZoom) { 3321 | if (transform.scale === maxZoom) return; 3322 | 3323 | ratio = maxZoom / transform.scale; 3324 | } 3325 | 3326 | var size = transformToScreen(clientX, clientY); 3327 | 3328 | transform.x = size.x - ratio * (size.x - transform.x); 3329 | transform.y = size.y - ratio * (size.y - transform.y); 3330 | 3331 | // TODO: https://github.com/anvaka/panzoom/issues/112 3332 | if (bounds && boundsPadding === 1 && minZoom === 1) { 3333 | transform.scale *= ratio; 3334 | keepTransformInsideBounds(); 3335 | } else { 3336 | var transformAdjusted = keepTransformInsideBounds(); 3337 | if (!transformAdjusted) transform.scale *= ratio; 3338 | } 3339 | 3340 | triggerEvent('zoom'); 3341 | 3342 | makeDirty(); 3343 | } 3344 | 3345 | function zoomAbs(clientX, clientY, zoomLevel) { 3346 | var ratio = zoomLevel / transform.scale; 3347 | zoomByRatio(clientX, clientY, ratio); 3348 | } 3349 | 3350 | function centerOn(ui) { 3351 | var parent = ui.ownerSVGElement; 3352 | if (!parent) 3353 | throw new Error('ui element is required to be within the scene'); 3354 | 3355 | // TODO: should i use controller's screen CTM? 3356 | var clientRect = ui.getBoundingClientRect(); 3357 | var cx = clientRect.left + clientRect.width / 2; 3358 | var cy = clientRect.top + clientRect.height / 2; 3359 | 3360 | var container = parent.getBoundingClientRect(); 3361 | var dx = container.width / 2 - cx; 3362 | var dy = container.height / 2 - cy; 3363 | 3364 | internalMoveBy(dx, dy, true); 3365 | } 3366 | 3367 | function smoothMoveTo(x, y){ 3368 | internalMoveBy(x - transform.x, y - transform.y, true); 3369 | } 3370 | 3371 | function internalMoveBy(dx, dy, smooth) { 3372 | if (!smooth) { 3373 | return moveBy(dx, dy); 3374 | } 3375 | 3376 | if (moveByAnimation) moveByAnimation.cancel(); 3377 | 3378 | var from = { x: 0, y: 0 }; 3379 | var to = { x: dx, y: dy }; 3380 | var lastX = 0; 3381 | var lastY = 0; 3382 | 3383 | moveByAnimation = animate(from, to, { 3384 | step: function (v) { 3385 | moveBy(v.x - lastX, v.y - lastY); 3386 | 3387 | lastX = v.x; 3388 | lastY = v.y; 3389 | } 3390 | }); 3391 | } 3392 | 3393 | function scroll(x, y) { 3394 | cancelZoomAnimation(); 3395 | moveTo(x, y); 3396 | } 3397 | 3398 | function dispose() { 3399 | releaseEvents(); 3400 | } 3401 | 3402 | function listenForEvents() { 3403 | owner.addEventListener('mousedown', onMouseDown, { passive: false }); 3404 | owner.addEventListener('dblclick', onDoubleClick, { passive: false }); 3405 | owner.addEventListener('touchstart', onTouch, { passive: false }); 3406 | owner.addEventListener('keydown', onKeyDown, { passive: false }); 3407 | 3408 | // Need to listen on the owner container, so that we are not limited 3409 | // by the size of the scrollable domElement 3410 | wheel.addWheelListener(owner, onMouseWheel, { passive: false }); 3411 | 3412 | makeDirty(); 3413 | } 3414 | 3415 | function releaseEvents() { 3416 | wheel.removeWheelListener(owner, onMouseWheel); 3417 | owner.removeEventListener('mousedown', onMouseDown); 3418 | owner.removeEventListener('keydown', onKeyDown); 3419 | owner.removeEventListener('dblclick', onDoubleClick); 3420 | owner.removeEventListener('touchstart', onTouch); 3421 | 3422 | if (frameAnimation) { 3423 | window.cancelAnimationFrame(frameAnimation); 3424 | frameAnimation = 0; 3425 | } 3426 | 3427 | smoothScroll.cancel(); 3428 | 3429 | releaseDocumentMouse(); 3430 | releaseTouches(); 3431 | textSelection.release(); 3432 | 3433 | triggerPanEnd(); 3434 | } 3435 | 3436 | function frame() { 3437 | if (isDirty) applyTransform(); 3438 | } 3439 | 3440 | function applyTransform() { 3441 | isDirty = false; 3442 | 3443 | // TODO: Should I allow to cancel this? 3444 | panController.applyTransform(transform); 3445 | 3446 | triggerEvent('transform'); 3447 | frameAnimation = 0; 3448 | } 3449 | 3450 | function onKeyDown(e) { 3451 | var x = 0, 3452 | y = 0, 3453 | z = 0; 3454 | if (e.keyCode === 38) { 3455 | y = 1; // up 3456 | } else if (e.keyCode === 40) { 3457 | y = -1; // down 3458 | } else if (e.keyCode === 37) { 3459 | x = 1; // left 3460 | } else if (e.keyCode === 39) { 3461 | x = -1; // right 3462 | } else if (e.keyCode === 189 || e.keyCode === 109) { 3463 | // DASH or SUBTRACT 3464 | z = 1; // `-` - zoom out 3465 | } else if (e.keyCode === 187 || e.keyCode === 107) { 3466 | // EQUAL SIGN or ADD 3467 | z = -1; // `=` - zoom in (equal sign on US layout is under `+`) 3468 | } 3469 | 3470 | if (filterKey(e, x, y, z)) { 3471 | // They don't want us to handle the key: https://github.com/anvaka/panzoom/issues/45 3472 | return; 3473 | } 3474 | 3475 | if (x || y) { 3476 | e.preventDefault(); 3477 | e.stopPropagation(); 3478 | 3479 | var clientRect = owner.getBoundingClientRect(); 3480 | // movement speed should be the same in both X and Y direction: 3481 | var offset = Math.min(clientRect.width, clientRect.height); 3482 | var moveSpeedRatio = 0.05; 3483 | var dx = offset * moveSpeedRatio * x; 3484 | var dy = offset * moveSpeedRatio * y; 3485 | 3486 | // TODO: currently we do not animate this. It could be better to have animation 3487 | internalMoveBy(dx, dy); 3488 | } 3489 | 3490 | if (z) { 3491 | var scaleMultiplier = getScaleMultiplier(z * 100); 3492 | var offset = transformOrigin ? getTransformOriginOffset() : midPoint(); 3493 | publicZoomTo(offset.x, offset.y, scaleMultiplier); 3494 | } 3495 | } 3496 | 3497 | function midPoint() { 3498 | var ownerRect = owner.getBoundingClientRect(); 3499 | return { 3500 | x: ownerRect.width / 2, 3501 | y: ownerRect.height / 2 3502 | }; 3503 | } 3504 | 3505 | function onTouch(e) { 3506 | // let the override the touch behavior 3507 | beforeTouch(e); 3508 | 3509 | if (e.touches.length === 1) { 3510 | return handleSingleFingerTouch(e, e.touches[0]); 3511 | } else if (e.touches.length === 2) { 3512 | // handleTouchMove() will care about pinch zoom. 3513 | pinchZoomLength = getPinchZoomLength(e.touches[0], e.touches[1]); 3514 | multiTouch = true; 3515 | startTouchListenerIfNeeded(); 3516 | } 3517 | } 3518 | 3519 | function beforeTouch(e) { 3520 | // TODO: Need to unify this filtering names. E.g. use `beforeTouch` 3521 | if (options.onTouch && !options.onTouch(e)) { 3522 | // if they return `false` from onTouch, we don't want to stop 3523 | // events propagation. Fixes https://github.com/anvaka/panzoom/issues/12 3524 | return; 3525 | } 3526 | 3527 | e.stopPropagation(); 3528 | e.preventDefault(); 3529 | } 3530 | 3531 | function beforeDoubleClick(e) { 3532 | // TODO: Need to unify this filtering names. E.g. use `beforeDoubleClick`` 3533 | if (options.onDoubleClick && !options.onDoubleClick(e)) { 3534 | // if they return `false` from onTouch, we don't want to stop 3535 | // events propagation. Fixes https://github.com/anvaka/panzoom/issues/46 3536 | return; 3537 | } 3538 | 3539 | e.preventDefault(); 3540 | e.stopPropagation(); 3541 | } 3542 | 3543 | function handleSingleFingerTouch(e) { 3544 | var touch = e.touches[0]; 3545 | var offset = getOffsetXY(touch); 3546 | lastSingleFingerOffset = offset; 3547 | var point = transformToScreen(offset.x, offset.y); 3548 | mouseX = point.x; 3549 | mouseY = point.y; 3550 | 3551 | smoothScroll.cancel(); 3552 | startTouchListenerIfNeeded(); 3553 | } 3554 | 3555 | function startTouchListenerIfNeeded() { 3556 | if (touchInProgress) { 3557 | // no need to do anything, as we already listen to events; 3558 | return; 3559 | } 3560 | 3561 | touchInProgress = true; 3562 | document.addEventListener('touchmove', handleTouchMove); 3563 | document.addEventListener('touchend', handleTouchEnd); 3564 | document.addEventListener('touchcancel', handleTouchEnd); 3565 | } 3566 | 3567 | function handleTouchMove(e) { 3568 | if (e.touches.length === 1) { 3569 | e.stopPropagation(); 3570 | var touch = e.touches[0]; 3571 | 3572 | var offset = getOffsetXY(touch); 3573 | var point = transformToScreen(offset.x, offset.y); 3574 | 3575 | var dx = point.x - mouseX; 3576 | var dy = point.y - mouseY; 3577 | 3578 | if (dx !== 0 && dy !== 0) { 3579 | triggerPanStart(); 3580 | } 3581 | mouseX = point.x; 3582 | mouseY = point.y; 3583 | internalMoveBy(dx, dy); 3584 | } else if (e.touches.length === 2) { 3585 | // it's a zoom, let's find direction 3586 | multiTouch = true; 3587 | var t1 = e.touches[0]; 3588 | var t2 = e.touches[1]; 3589 | var currentPinchLength = getPinchZoomLength(t1, t2); 3590 | 3591 | // since the zoom speed is always based on distance from 1, we need to apply 3592 | // pinch speed only on that distance from 1: 3593 | var scaleMultiplier = 3594 | 1 + (currentPinchLength / pinchZoomLength - 1) * pinchSpeed; 3595 | 3596 | var firstTouchPoint = getOffsetXY(t1); 3597 | var secondTouchPoint = getOffsetXY(t2); 3598 | mouseX = (firstTouchPoint.x + secondTouchPoint.x) / 2; 3599 | mouseY = (firstTouchPoint.y + secondTouchPoint.y) / 2; 3600 | if (transformOrigin) { 3601 | var offset = getTransformOriginOffset(); 3602 | mouseX = offset.x; 3603 | mouseY = offset.y; 3604 | } 3605 | 3606 | publicZoomTo(mouseX, mouseY, scaleMultiplier); 3607 | 3608 | pinchZoomLength = currentPinchLength; 3609 | e.stopPropagation(); 3610 | e.preventDefault(); 3611 | } 3612 | } 3613 | 3614 | function handleTouchEnd(e) { 3615 | if (e.touches.length > 0) { 3616 | var offset = getOffsetXY(e.touches[0]); 3617 | var point = transformToScreen(offset.x, offset.y); 3618 | mouseX = point.x; 3619 | mouseY = point.y; 3620 | } else { 3621 | var now = new Date(); 3622 | if (now - lastTouchEndTime < doubleTapSpeedInMS) { 3623 | if (transformOrigin) { 3624 | var offset = getTransformOriginOffset(); 3625 | smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed); 3626 | } else { 3627 | // We want untransformed x/y here. 3628 | smoothZoom(lastSingleFingerOffset.x, lastSingleFingerOffset.y, zoomDoubleClickSpeed); 3629 | } 3630 | } 3631 | 3632 | lastTouchEndTime = now; 3633 | 3634 | triggerPanEnd(); 3635 | releaseTouches(); 3636 | } 3637 | } 3638 | 3639 | function getPinchZoomLength(finger1, finger2) { 3640 | var dx = finger1.clientX - finger2.clientX; 3641 | var dy = finger1.clientY - finger2.clientY; 3642 | return Math.sqrt(dx * dx + dy * dy); 3643 | } 3644 | 3645 | function onDoubleClick(e) { 3646 | beforeDoubleClick(e); 3647 | var offset = getOffsetXY(e); 3648 | if (transformOrigin) { 3649 | // TODO: looks like this is duplicated in the file. 3650 | // Need to refactor 3651 | offset = getTransformOriginOffset(); 3652 | } 3653 | smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed); 3654 | } 3655 | 3656 | function onMouseDown(e) { 3657 | // if client does not want to handle this event - just ignore the call 3658 | if (beforeMouseDown(e)) return; 3659 | 3660 | if (touchInProgress) { 3661 | // modern browsers will fire mousedown for touch events too 3662 | // we do not want this: touch is handled separately. 3663 | e.stopPropagation(); 3664 | return false; 3665 | } 3666 | // for IE, left click == 1 3667 | // for Firefox, left click == 0 3668 | var isLeftButton = 3669 | (e.button === 1 && window.event !== null) || e.button === 0; 3670 | if (!isLeftButton) return; 3671 | 3672 | smoothScroll.cancel(); 3673 | 3674 | var offset = getOffsetXY(e); 3675 | var point = transformToScreen(offset.x, offset.y); 3676 | mouseX = point.x; 3677 | mouseY = point.y; 3678 | 3679 | // We need to listen on document itself, since mouse can go outside of the 3680 | // window, and we will loose it 3681 | document.addEventListener('mousemove', onMouseMove); 3682 | document.addEventListener('mouseup', onMouseUp); 3683 | textSelection.capture(e.target || e.srcElement); 3684 | 3685 | return false; 3686 | } 3687 | 3688 | function onMouseMove(e) { 3689 | // no need to worry about mouse events when touch is happening 3690 | if (touchInProgress) return; 3691 | 3692 | triggerPanStart(); 3693 | 3694 | var offset = getOffsetXY(e); 3695 | var point = transformToScreen(offset.x, offset.y); 3696 | var dx = point.x - mouseX; 3697 | var dy = point.y - mouseY; 3698 | 3699 | mouseX = point.x; 3700 | mouseY = point.y; 3701 | 3702 | internalMoveBy(dx, dy); 3703 | } 3704 | 3705 | function onMouseUp() { 3706 | textSelection.release(); 3707 | triggerPanEnd(); 3708 | releaseDocumentMouse(); 3709 | } 3710 | 3711 | function releaseDocumentMouse() { 3712 | document.removeEventListener('mousemove', onMouseMove); 3713 | document.removeEventListener('mouseup', onMouseUp); 3714 | panstartFired = false; 3715 | } 3716 | 3717 | function releaseTouches() { 3718 | document.removeEventListener('touchmove', handleTouchMove); 3719 | document.removeEventListener('touchend', handleTouchEnd); 3720 | document.removeEventListener('touchcancel', handleTouchEnd); 3721 | panstartFired = false; 3722 | multiTouch = false; 3723 | touchInProgress = false; 3724 | } 3725 | 3726 | function onMouseWheel(e) { 3727 | // if client does not want to handle this event - just ignore the call 3728 | if (beforeWheel(e)) return; 3729 | 3730 | smoothScroll.cancel(); 3731 | 3732 | var delta = e.deltaY; 3733 | if (e.deltaMode > 0) delta *= 100; 3734 | 3735 | var scaleMultiplier = getScaleMultiplier(delta); 3736 | 3737 | if (scaleMultiplier !== 1) { 3738 | var offset = transformOrigin 3739 | ? getTransformOriginOffset() 3740 | : getOffsetXY(e); 3741 | publicZoomTo(offset.x, offset.y, scaleMultiplier); 3742 | e.preventDefault(); 3743 | } 3744 | } 3745 | 3746 | function getOffsetXY(e) { 3747 | var offsetX, offsetY; 3748 | // I tried using e.offsetX, but that gives wrong results for svg, when user clicks on a path. 3749 | var ownerRect = owner.getBoundingClientRect(); 3750 | offsetX = e.clientX - ownerRect.left; 3751 | offsetY = e.clientY - ownerRect.top; 3752 | 3753 | return { x: offsetX, y: offsetY }; 3754 | } 3755 | 3756 | function smoothZoom(clientX, clientY, scaleMultiplier) { 3757 | var fromValue = transform.scale; 3758 | var from = { scale: fromValue }; 3759 | var to = { scale: scaleMultiplier * fromValue }; 3760 | 3761 | smoothScroll.cancel(); 3762 | cancelZoomAnimation(); 3763 | 3764 | zoomToAnimation = animate(from, to, { 3765 | step: function (v) { 3766 | zoomAbs(clientX, clientY, v.scale); 3767 | }, 3768 | done: triggerZoomEnd 3769 | }); 3770 | } 3771 | 3772 | function smoothZoomAbs(clientX, clientY, toScaleValue) { 3773 | var fromValue = transform.scale; 3774 | var from = { scale: fromValue }; 3775 | var to = { scale: toScaleValue }; 3776 | 3777 | smoothScroll.cancel(); 3778 | cancelZoomAnimation(); 3779 | 3780 | zoomToAnimation = animate(from, to, { 3781 | step: function (v) { 3782 | zoomAbs(clientX, clientY, v.scale); 3783 | } 3784 | }); 3785 | } 3786 | 3787 | function getTransformOriginOffset() { 3788 | var ownerRect = owner.getBoundingClientRect(); 3789 | return { 3790 | x: ownerRect.width * transformOrigin.x, 3791 | y: ownerRect.height * transformOrigin.y 3792 | }; 3793 | } 3794 | 3795 | function publicZoomTo(clientX, clientY, scaleMultiplier) { 3796 | smoothScroll.cancel(); 3797 | cancelZoomAnimation(); 3798 | return zoomByRatio(clientX, clientY, scaleMultiplier); 3799 | } 3800 | 3801 | function cancelZoomAnimation() { 3802 | if (zoomToAnimation) { 3803 | zoomToAnimation.cancel(); 3804 | zoomToAnimation = null; 3805 | } 3806 | } 3807 | 3808 | function getScaleMultiplier(delta) { 3809 | var sign = Math.sign(delta); 3810 | var deltaAdjustedSpeed = Math.min(0.25, Math.abs(speed * delta / 128)); 3811 | return 1 - sign * deltaAdjustedSpeed; 3812 | } 3813 | 3814 | function triggerPanStart() { 3815 | if (!panstartFired) { 3816 | triggerEvent('panstart'); 3817 | panstartFired = true; 3818 | smoothScroll.start(); 3819 | } 3820 | } 3821 | 3822 | function triggerPanEnd() { 3823 | if (panstartFired) { 3824 | // we should never run smooth scrolling if it was multiTouch (pinch zoom animation): 3825 | if (!multiTouch) smoothScroll.stop(); 3826 | triggerEvent('panend'); 3827 | } 3828 | } 3829 | 3830 | function triggerZoomEnd() { 3831 | triggerEvent('zoomend'); 3832 | } 3833 | 3834 | function triggerEvent(name) { 3835 | api.fire(name, api); 3836 | } 3837 | } 3838 | 3839 | function parseTransformOrigin(options) { 3840 | if (!options) return; 3841 | if (typeof options === 'object') { 3842 | if (!isNumber(options.x) || !isNumber(options.y)) 3843 | failTransformOrigin(options); 3844 | return options; 3845 | } 3846 | 3847 | failTransformOrigin(); 3848 | } 3849 | 3850 | function failTransformOrigin(options) { 3851 | console.error(options); 3852 | throw new Error( 3853 | [ 3854 | 'Cannot parse transform origin.', 3855 | 'Some good examples:', 3856 | ' "center center" can be achieved with {x: 0.5, y: 0.5}', 3857 | ' "top center" can be achieved with {x: 0.5, y: 0}', 3858 | ' "bottom right" can be achieved with {x: 1, y: 1}' 3859 | ].join('\n') 3860 | ); 3861 | } 3862 | 3863 | function noop() { } 3864 | 3865 | function validateBounds(bounds) { 3866 | var boundsType = typeof bounds; 3867 | if (boundsType === 'undefined' || boundsType === 'boolean') return; // this is okay 3868 | // otherwise need to be more thorough: 3869 | var validBounds = 3870 | isNumber(bounds.left) && 3871 | isNumber(bounds.top) && 3872 | isNumber(bounds.bottom) && 3873 | isNumber(bounds.right); 3874 | 3875 | if (!validBounds) 3876 | throw new Error( 3877 | 'Bounds object is not valid. It can be: ' + 3878 | 'undefined, boolean (true|false) or an object {left, top, right, bottom}' 3879 | ); 3880 | } 3881 | 3882 | function isNumber(x) { 3883 | return Number.isFinite(x); 3884 | } 3885 | 3886 | // IE 11 does not support isNaN: 3887 | function isNaN(value) { 3888 | if (Number.isNaN) { 3889 | return Number.isNaN(value); 3890 | } 3891 | 3892 | return value !== value; 3893 | } 3894 | 3895 | function rigidScroll() { 3896 | return { 3897 | start: noop, 3898 | stop: noop, 3899 | cancel: noop 3900 | }; 3901 | } 3902 | 3903 | function autoRun() { 3904 | if (typeof document === 'undefined') return; 3905 | 3906 | var scripts = document.getElementsByTagName('script'); 3907 | if (!scripts) return; 3908 | var panzoomScript; 3909 | 3910 | for (var i = 0; i < scripts.length; ++i) { 3911 | var x = scripts[i]; 3912 | if (x.src && x.src.match(/\bpanzoom(\.min)?\.js/)) { 3913 | panzoomScript = x; 3914 | break; 3915 | } 3916 | } 3917 | 3918 | if (!panzoomScript) return; 3919 | 3920 | var query = panzoomScript.getAttribute('query'); 3921 | if (!query) return; 3922 | 3923 | var globalName = panzoomScript.getAttribute('name') || 'pz'; 3924 | var started = Date.now(); 3925 | 3926 | tryAttach(); 3927 | 3928 | function tryAttach() { 3929 | var el = document.querySelector(query); 3930 | if (!el) { 3931 | var now = Date.now(); 3932 | var elapsed = now - started; 3933 | if (elapsed < 2000) { 3934 | // Let's wait a bit 3935 | setTimeout(tryAttach, 100); 3936 | return; 3937 | } 3938 | // If we don't attach within 2 seconds to the target element, consider it a failure 3939 | console.error('Cannot find the panzoom element', globalName); 3940 | return; 3941 | } 3942 | var options = collectOptions(panzoomScript); 3943 | console.log(options); 3944 | window[globalName] = createPanZoom(el, options); 3945 | } 3946 | 3947 | function collectOptions(script) { 3948 | var attrs = script.attributes; 3949 | var options = {}; 3950 | for (var j = 0; j < attrs.length; ++j) { 3951 | var attr = attrs[j]; 3952 | var nameValue = getPanzoomAttributeNameValue(attr); 3953 | if (nameValue) { 3954 | options[nameValue.name] = nameValue.value; 3955 | } 3956 | } 3957 | 3958 | return options; 3959 | } 3960 | 3961 | function getPanzoomAttributeNameValue(attr) { 3962 | if (!attr.name) return; 3963 | var isPanZoomAttribute = 3964 | attr.name[0] === 'p' && attr.name[1] === 'z' && attr.name[2] === '-'; 3965 | 3966 | if (!isPanZoomAttribute) return; 3967 | 3968 | var name = attr.name.substr(3); 3969 | var value = JSON.parse(attr.value); 3970 | return { name: name, value: value }; 3971 | } 3972 | } 3973 | 3974 | autoRun(); 3975 | 3976 | },{"./lib/createTextSelectionInterceptor.js":10,"./lib/domController.js":11,"./lib/kinetic.js":12,"./lib/svgController.js":13,"./lib/transform.js":14,"amator":5,"ngraph.events":7,"wheel":19}],10:[function(require,module,exports){ 3977 | /** 3978 | * Disallows selecting text. 3979 | */ 3980 | module.exports = createTextSelectionInterceptor; 3981 | 3982 | function createTextSelectionInterceptor(useFake) { 3983 | if (useFake) { 3984 | return { 3985 | capture: noop, 3986 | release: noop 3987 | }; 3988 | } 3989 | 3990 | var dragObject; 3991 | var prevSelectStart; 3992 | var prevDragStart; 3993 | var wasCaptured = false; 3994 | 3995 | return { 3996 | capture: capture, 3997 | release: release 3998 | }; 3999 | 4000 | function capture(domObject) { 4001 | wasCaptured = true; 4002 | prevSelectStart = window.document.onselectstart; 4003 | prevDragStart = window.document.ondragstart; 4004 | 4005 | window.document.onselectstart = disabled; 4006 | 4007 | dragObject = domObject; 4008 | dragObject.ondragstart = disabled; 4009 | } 4010 | 4011 | function release() { 4012 | if (!wasCaptured) return; 4013 | 4014 | wasCaptured = false; 4015 | window.document.onselectstart = prevSelectStart; 4016 | if (dragObject) dragObject.ondragstart = prevDragStart; 4017 | } 4018 | } 4019 | 4020 | function disabled(e) { 4021 | e.stopPropagation(); 4022 | return false; 4023 | } 4024 | 4025 | function noop() {} 4026 | 4027 | },{}],11:[function(require,module,exports){ 4028 | module.exports = makeDomController; 4029 | 4030 | module.exports.canAttach = isDomElement; 4031 | 4032 | function makeDomController(domElement, options) { 4033 | var elementValid = isDomElement(domElement); 4034 | if (!elementValid) { 4035 | throw new Error('panzoom requires DOM element to be attached to the DOM tree'); 4036 | } 4037 | 4038 | var owner = domElement.parentElement; 4039 | domElement.scrollTop = 0; 4040 | 4041 | if (!options.disableKeyboardInteraction) { 4042 | owner.setAttribute('tabindex', 0); 4043 | } 4044 | 4045 | var api = { 4046 | getBBox: getBBox, 4047 | getOwner: getOwner, 4048 | applyTransform: applyTransform, 4049 | }; 4050 | 4051 | return api; 4052 | 4053 | function getOwner() { 4054 | return owner; 4055 | } 4056 | 4057 | function getBBox() { 4058 | // TODO: We should probably cache this? 4059 | return { 4060 | left: 0, 4061 | top: 0, 4062 | width: domElement.clientWidth, 4063 | height: domElement.clientHeight 4064 | }; 4065 | } 4066 | 4067 | function applyTransform(transform) { 4068 | // TODO: Should we cache this? 4069 | domElement.style.transformOrigin = '0 0 0'; 4070 | domElement.style.transform = 'matrix(' + 4071 | transform.scale + ', 0, 0, ' + 4072 | transform.scale + ', ' + 4073 | transform.x + ', ' + transform.y + ')'; 4074 | } 4075 | } 4076 | 4077 | function isDomElement(element) { 4078 | return element && element.parentElement && element.style; 4079 | } 4080 | 4081 | },{}],12:[function(require,module,exports){ 4082 | /** 4083 | * Allows smooth kinetic scrolling of the surface 4084 | */ 4085 | module.exports = kinetic; 4086 | 4087 | function kinetic(getPoint, scroll, settings) { 4088 | if (typeof settings !== 'object') { 4089 | // setting could come as boolean, we should ignore it, and use an object. 4090 | settings = {}; 4091 | } 4092 | 4093 | var minVelocity = typeof settings.minVelocity === 'number' ? settings.minVelocity : 5; 4094 | var amplitude = typeof settings.amplitude === 'number' ? settings.amplitude : 0.25; 4095 | var cancelAnimationFrame = typeof settings.cancelAnimationFrame === 'function' ? settings.cancelAnimationFrame : getCancelAnimationFrame(); 4096 | var requestAnimationFrame = typeof settings.requestAnimationFrame === 'function' ? settings.requestAnimationFrame : getRequestAnimationFrame(); 4097 | 4098 | var lastPoint; 4099 | var timestamp; 4100 | var timeConstant = 342; 4101 | 4102 | var ticker; 4103 | var vx, targetX, ax; 4104 | var vy, targetY, ay; 4105 | 4106 | var raf; 4107 | 4108 | return { 4109 | start: start, 4110 | stop: stop, 4111 | cancel: dispose 4112 | }; 4113 | 4114 | function dispose() { 4115 | cancelAnimationFrame(ticker); 4116 | cancelAnimationFrame(raf); 4117 | } 4118 | 4119 | function start() { 4120 | lastPoint = getPoint(); 4121 | 4122 | ax = ay = vx = vy = 0; 4123 | timestamp = new Date(); 4124 | 4125 | cancelAnimationFrame(ticker); 4126 | cancelAnimationFrame(raf); 4127 | 4128 | // we start polling the point position to accumulate velocity 4129 | // Once we stop(), we will use accumulated velocity to keep scrolling 4130 | // an object. 4131 | ticker = requestAnimationFrame(track); 4132 | } 4133 | 4134 | function track() { 4135 | var now = Date.now(); 4136 | var elapsed = now - timestamp; 4137 | timestamp = now; 4138 | 4139 | var currentPoint = getPoint(); 4140 | 4141 | var dx = currentPoint.x - lastPoint.x; 4142 | var dy = currentPoint.y - lastPoint.y; 4143 | 4144 | lastPoint = currentPoint; 4145 | 4146 | var dt = 1000 / (1 + elapsed); 4147 | 4148 | // moving average 4149 | vx = 0.8 * dx * dt + 0.2 * vx; 4150 | vy = 0.8 * dy * dt + 0.2 * vy; 4151 | 4152 | ticker = requestAnimationFrame(track); 4153 | } 4154 | 4155 | function stop() { 4156 | cancelAnimationFrame(ticker); 4157 | cancelAnimationFrame(raf); 4158 | 4159 | var currentPoint = getPoint(); 4160 | 4161 | targetX = currentPoint.x; 4162 | targetY = currentPoint.y; 4163 | timestamp = Date.now(); 4164 | 4165 | if (vx < -minVelocity || vx > minVelocity) { 4166 | ax = amplitude * vx; 4167 | targetX += ax; 4168 | } 4169 | 4170 | if (vy < -minVelocity || vy > minVelocity) { 4171 | ay = amplitude * vy; 4172 | targetY += ay; 4173 | } 4174 | 4175 | raf = requestAnimationFrame(autoScroll); 4176 | } 4177 | 4178 | function autoScroll() { 4179 | var elapsed = Date.now() - timestamp; 4180 | 4181 | var moving = false; 4182 | var dx = 0; 4183 | var dy = 0; 4184 | 4185 | if (ax) { 4186 | dx = -ax * Math.exp(-elapsed / timeConstant); 4187 | 4188 | if (dx > 0.5 || dx < -0.5) moving = true; 4189 | else dx = ax = 0; 4190 | } 4191 | 4192 | if (ay) { 4193 | dy = -ay * Math.exp(-elapsed / timeConstant); 4194 | 4195 | if (dy > 0.5 || dy < -0.5) moving = true; 4196 | else dy = ay = 0; 4197 | } 4198 | 4199 | if (moving) { 4200 | scroll(targetX + dx, targetY + dy); 4201 | raf = requestAnimationFrame(autoScroll); 4202 | } 4203 | } 4204 | } 4205 | 4206 | function getCancelAnimationFrame() { 4207 | if (typeof cancelAnimationFrame === 'function') return cancelAnimationFrame; 4208 | return clearTimeout; 4209 | } 4210 | 4211 | function getRequestAnimationFrame() { 4212 | if (typeof requestAnimationFrame === 'function') return requestAnimationFrame; 4213 | 4214 | return function (handler) { 4215 | return setTimeout(handler, 16); 4216 | }; 4217 | } 4218 | },{}],13:[function(require,module,exports){ 4219 | module.exports = makeSvgController; 4220 | module.exports.canAttach = isSVGElement; 4221 | 4222 | function makeSvgController(svgElement, options) { 4223 | if (!isSVGElement(svgElement)) { 4224 | throw new Error('svg element is required for svg.panzoom to work'); 4225 | } 4226 | 4227 | var owner = svgElement.ownerSVGElement; 4228 | if (!owner) { 4229 | throw new Error( 4230 | 'Do not apply panzoom to the root element. ' + 4231 | 'Use its child instead (e.g. ). ' + 4232 | 'As of March 2016 only FireFox supported transform on the root element'); 4233 | } 4234 | 4235 | if (!options.disableKeyboardInteraction) { 4236 | owner.setAttribute('tabindex', 0); 4237 | } 4238 | 4239 | var api = { 4240 | getBBox: getBBox, 4241 | getScreenCTM: getScreenCTM, 4242 | getOwner: getOwner, 4243 | applyTransform: applyTransform, 4244 | initTransform: initTransform 4245 | }; 4246 | 4247 | return api; 4248 | 4249 | function getOwner() { 4250 | return owner; 4251 | } 4252 | 4253 | function getBBox() { 4254 | var bbox = svgElement.getBBox(); 4255 | return { 4256 | left: bbox.x, 4257 | top: bbox.y, 4258 | width: bbox.width, 4259 | height: bbox.height, 4260 | }; 4261 | } 4262 | 4263 | function getScreenCTM() { 4264 | var ctm = owner.getCTM(); 4265 | if (!ctm) { 4266 | // This is likely firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=873106 4267 | // The code below is not entirely correct, but still better than nothing 4268 | return owner.getScreenCTM(); 4269 | } 4270 | return ctm; 4271 | } 4272 | 4273 | function initTransform(transform) { 4274 | var screenCTM = svgElement.getCTM(); 4275 | 4276 | // The above line returns null on Firefox 4277 | if (screenCTM === null) { 4278 | screenCTM = document.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGMatrix(); 4279 | } 4280 | 4281 | transform.x = screenCTM.e; 4282 | transform.y = screenCTM.f; 4283 | transform.scale = screenCTM.a; 4284 | owner.removeAttributeNS(null, 'viewBox'); 4285 | } 4286 | 4287 | function applyTransform(transform) { 4288 | svgElement.setAttribute('transform', 'matrix(' + 4289 | transform.scale + ' 0 0 ' + 4290 | transform.scale + ' ' + 4291 | transform.x + ' ' + transform.y + ')'); 4292 | } 4293 | } 4294 | 4295 | function isSVGElement(element) { 4296 | return element && element.ownerSVGElement && element.getCTM; 4297 | } 4298 | },{}],14:[function(require,module,exports){ 4299 | module.exports = Transform; 4300 | 4301 | function Transform() { 4302 | this.x = 0; 4303 | this.y = 0; 4304 | this.scale = 1; 4305 | } 4306 | 4307 | },{}],15:[function(require,module,exports){ 4308 | module.exports = svg; 4309 | 4310 | svg.compile = require('./lib/compile'); 4311 | 4312 | var compileTemplate = svg.compileTemplate = require('./lib/compile_template'); 4313 | 4314 | var domEvents = require('add-event-listener'); 4315 | 4316 | var svgns = "http://www.w3.org/2000/svg"; 4317 | var xlinkns = "http://www.w3.org/1999/xlink"; 4318 | 4319 | function svg(element, attrBag) { 4320 | var svgElement = augment(element); 4321 | if (attrBag === undefined) { 4322 | return svgElement; 4323 | } 4324 | 4325 | svgElement.attr(attrBag); 4326 | 4327 | return svgElement; 4328 | } 4329 | 4330 | function augment(element) { 4331 | var svgElement = element; 4332 | 4333 | if (typeof element === "string") { 4334 | svgElement = window.document.createElementNS(svgns, element); 4335 | } else if (element.simplesvg) { 4336 | return element; 4337 | } 4338 | 4339 | var compiledTempalte; 4340 | 4341 | svgElement.simplesvg = true; // this is not good, since we are monkey patching svg 4342 | svgElement.attr = attr; 4343 | svgElement.append = append; 4344 | svgElement.link = link; 4345 | svgElement.text = text; 4346 | 4347 | // add easy eventing 4348 | svgElement.on = on; 4349 | svgElement.off = off; 4350 | 4351 | // data binding: 4352 | svgElement.dataSource = dataSource; 4353 | 4354 | return svgElement; 4355 | 4356 | function dataSource(model) { 4357 | if (!compiledTempalte) compiledTempalte = compileTemplate(svgElement); 4358 | compiledTempalte.link(model); 4359 | return svgElement; 4360 | } 4361 | 4362 | function on(name, cb, useCapture) { 4363 | domEvents.addEventListener(svgElement, name, cb, useCapture); 4364 | return svgElement; 4365 | } 4366 | 4367 | function off(name, cb, useCapture) { 4368 | domEvents.removeEventListener(svgElement, name, cb, useCapture); 4369 | return svgElement; 4370 | } 4371 | 4372 | function append(content) { 4373 | var child = svg(content); 4374 | svgElement.appendChild(child); 4375 | 4376 | return child; 4377 | } 4378 | 4379 | function attr(name, value) { 4380 | if (arguments.length === 2) { 4381 | if (value !== null) { 4382 | svgElement.setAttributeNS(null, name, value); 4383 | } else { 4384 | svgElement.removeAttributeNS(null, name); 4385 | } 4386 | 4387 | return svgElement; 4388 | } 4389 | if (typeof name === 'string') { 4390 | // someone wants to get value of an attribute: 4391 | return svgElement.getAttributeNS(null, name); 4392 | } 4393 | 4394 | if (typeof name !== 'object') throw new Error('attr() expects to have either string or object as first argument'); 4395 | 4396 | var attrBag = name; 4397 | var attributes = Object.keys(attrBag); 4398 | for (var i = 0; i < attributes.length; ++i) { 4399 | var attributeName = attributes[i]; 4400 | var value = attrBag[attributeName]; 4401 | if (attributeName === 'link') { 4402 | svgElement.link(value); 4403 | } else { 4404 | svgElement.attr(attributeName, value); 4405 | } 4406 | } 4407 | 4408 | return svgElement; 4409 | } 4410 | 4411 | function link(target) { 4412 | if (arguments.length) { 4413 | svgElement.setAttributeNS(xlinkns, "xlink:href", target); 4414 | return svgElement; 4415 | } 4416 | 4417 | return svgElement.getAttributeNS(xlinkns, "xlink:href"); 4418 | } 4419 | 4420 | function text(textContent) { 4421 | if (textContent !== undefined) { 4422 | svgElement.textContent = textContent; 4423 | return svgElement; 4424 | } 4425 | return svgElement.textContent; 4426 | } 4427 | } 4428 | 4429 | },{"./lib/compile":16,"./lib/compile_template":17,"add-event-listener":4}],16:[function(require,module,exports){ 4430 | var parser = require('./domparser.js'); 4431 | var svg = require('../'); 4432 | 4433 | module.exports = compile; 4434 | 4435 | function compile(svgText) { 4436 | try { 4437 | svgText = addNamespaces(svgText); 4438 | return svg(parser.parseFromString(svgText, "text/xml").documentElement); 4439 | } catch (e) { 4440 | throw e; 4441 | } 4442 | } 4443 | 4444 | function addNamespaces(text) { 4445 | if (!text) return; 4446 | 4447 | var namespaces = 'xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"'; 4448 | var match = text.match(/^<\w+/); 4449 | if (match) { 4450 | var tagLength = match[0].length; 4451 | return text.substr(0, tagLength) + ' ' + namespaces + ' ' + text.substr(tagLength); 4452 | } else { 4453 | throw new Error('Cannot parse input text: invalid xml?'); 4454 | } 4455 | } 4456 | 4457 | },{"../":15,"./domparser.js":18}],17:[function(require,module,exports){ 4458 | module.exports = template; 4459 | 4460 | var BINDING_EXPR = /{{(.+?)}}/; 4461 | 4462 | function template(domNode) { 4463 | var allBindings = Object.create(null); 4464 | extractAllBindings(domNode, allBindings); 4465 | 4466 | return { 4467 | link: function(model) { 4468 | Object.keys(allBindings).forEach(function(key) { 4469 | var setter = allBindings[key]; 4470 | setter.forEach(changeModel); 4471 | }); 4472 | 4473 | function changeModel(setter) { 4474 | setter(model); 4475 | } 4476 | } 4477 | }; 4478 | } 4479 | 4480 | function extractAllBindings(domNode, allBindings) { 4481 | var nodeType = domNode.nodeType; 4482 | var typeSupported = (nodeType === 1) || (nodeType === 3); 4483 | if (!typeSupported) return; 4484 | var i; 4485 | if (domNode.hasChildNodes()) { 4486 | var domChildren = domNode.childNodes; 4487 | for (i = 0; i < domChildren.length; ++i) { 4488 | extractAllBindings(domChildren[i], allBindings); 4489 | } 4490 | } 4491 | 4492 | if (nodeType === 3) { // text: 4493 | bindTextContent(domNode, allBindings); 4494 | } 4495 | 4496 | if (!domNode.attributes) return; // this might be a text. Need to figure out what to do in that case 4497 | 4498 | var attrs = domNode.attributes; 4499 | for (i = 0; i < attrs.length; ++i) { 4500 | bindDomAttribute(attrs[i], domNode, allBindings); 4501 | } 4502 | } 4503 | 4504 | function bindDomAttribute(domAttribute, element, allBindings) { 4505 | var value = domAttribute.value; 4506 | if (!value) return; // unary attribute? 4507 | 4508 | var modelNameMatch = value.match(BINDING_EXPR); 4509 | if (!modelNameMatch) return; // does not look like a binding 4510 | 4511 | var attrName = domAttribute.localName; 4512 | var modelPropertyName = modelNameMatch[1]; 4513 | var isSimpleValue = modelPropertyName.indexOf('.') < 0; 4514 | 4515 | if (!isSimpleValue) throw new Error('simplesvg currently does not support nested bindings'); 4516 | 4517 | var propertyBindings = allBindings[modelPropertyName]; 4518 | if (!propertyBindings) { 4519 | propertyBindings = allBindings[modelPropertyName] = [attributeSetter]; 4520 | } else { 4521 | propertyBindings.push(attributeSetter); 4522 | } 4523 | 4524 | function attributeSetter(model) { 4525 | element.setAttributeNS(null, attrName, model[modelPropertyName]); 4526 | } 4527 | } 4528 | function bindTextContent(element, allBindings) { 4529 | // todo reduce duplication 4530 | var value = element.nodeValue; 4531 | if (!value) return; // unary attribute? 4532 | 4533 | var modelNameMatch = value.match(BINDING_EXPR); 4534 | if (!modelNameMatch) return; // does not look like a binding 4535 | 4536 | var modelPropertyName = modelNameMatch[1]; 4537 | var isSimpleValue = modelPropertyName.indexOf('.') < 0; 4538 | 4539 | var propertyBindings = allBindings[modelPropertyName]; 4540 | if (!propertyBindings) { 4541 | propertyBindings = allBindings[modelPropertyName] = [textSetter]; 4542 | } else { 4543 | propertyBindings.push(textSetter); 4544 | } 4545 | 4546 | function textSetter(model) { 4547 | element.nodeValue = model[modelPropertyName]; 4548 | } 4549 | } 4550 | 4551 | },{}],18:[function(require,module,exports){ 4552 | module.exports = createDomparser(); 4553 | 4554 | function createDomparser() { 4555 | if (typeof DOMParser === 'undefined') { 4556 | return { 4557 | parseFromString: fail 4558 | }; 4559 | } 4560 | return new DOMParser(); 4561 | } 4562 | 4563 | function fail() { 4564 | throw new Error('DOMParser is not supported by this platform. Please open issue here https://github.com/anvaka/simplesvg'); 4565 | } 4566 | 4567 | },{}],19:[function(require,module,exports){ 4568 | /** 4569 | * This module used to unify mouse wheel behavior between different browsers in 2014 4570 | * Now it's just a wrapper around addEventListener('wheel'); 4571 | * 4572 | * Usage: 4573 | * var addWheelListener = require('wheel').addWheelListener; 4574 | * var removeWheelListener = require('wheel').removeWheelListener; 4575 | * addWheelListener(domElement, function (e) { 4576 | * // mouse wheel event 4577 | * }); 4578 | * removeWheelListener(domElement, function); 4579 | */ 4580 | 4581 | module.exports = addWheelListener; 4582 | 4583 | // But also expose "advanced" api with unsubscribe: 4584 | module.exports.addWheelListener = addWheelListener; 4585 | module.exports.removeWheelListener = removeWheelListener; 4586 | 4587 | 4588 | function addWheelListener(element, listener, useCapture) { 4589 | element.addEventListener('wheel', listener, useCapture); 4590 | } 4591 | 4592 | function removeWheelListener( element, listener, useCapture ) { 4593 | element.removeEventListener('wheel', listener, useCapture); 4594 | } 4595 | },{}]},{},[3])(3) 4596 | }); 4597 | --------------------------------------------------------------------------------