├── tests ├── __init__.py └── test_dagre_py.py ├── dagre_py ├── __init__.py ├── js │ ├── index.html │ ├── style.css │ ├── script.js │ └── tippy.all.min.js └── core.py ├── mypy.ini ├── screens ├── simple.png ├── multi-edges.png └── duplicate-labels.png ├── tox.ini ├── .travis.yml ├── pyproject.toml ├── LICENSE ├── README.org └── .gitignore /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dagre_py/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.7" 2 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports=True 3 | -------------------------------------------------------------------------------- /screens/simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skit-ai/dagre-py/HEAD/screens/simple.png -------------------------------------------------------------------------------- /screens/multi-edges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skit-ai/dagre-py/HEAD/screens/multi-edges.png -------------------------------------------------------------------------------- /screens/duplicate-labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skit-ai/dagre-py/HEAD/screens/duplicate-labels.png -------------------------------------------------------------------------------- /tests/test_dagre_py.py: -------------------------------------------------------------------------------- 1 | from dagre_py import __version__ 2 | 3 | 4 | def test_version(): 5 | assert __version__ == "0.1.7" 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = True 3 | envlist = py36 4 | 5 | [testenv] 6 | whitelist_externals = poetry 7 | skip_install = true 8 | commands = 9 | poetry install -v 10 | poetry run pytest tests/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | install: 5 | - pip install tox 6 | - curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python 7 | - source ~/.poetry/env 8 | 9 | script: 10 | - tox 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "dagre-py" 3 | version = "0.1.7" 4 | description = "Thin python wrapper around dagre-d3" 5 | authors = ["Abhinav Tushar "] 6 | license = "MIT" 7 | homepage = "https://github.com/Vernacular-ai/dagre-py" 8 | repository = "https://github.com/Vernacular-ai/dagre-py" 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.6" 12 | 13 | [tool.poetry.dev-dependencies] 14 | pytest = "^4.0" 15 | mypy = "^0.701" 16 | -------------------------------------------------------------------------------- /dagre_py/js/index.html: -------------------------------------------------------------------------------- 1 | 2 | dagre-py | visualization 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | dagre-py 13 |
14 |
15 |
16 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vernacular.ai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dagre_py/js/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | font: 300 14px 'Helvetica Neue', Helvetica; 7 | } 8 | 9 | svg { 10 | height: 100%; 11 | width: 100%; 12 | } 13 | 14 | .node rect, 15 | .node circle, 16 | .node ellipse { 17 | stroke: #333; 18 | fill: #fff; 19 | stroke-width: 1px; 20 | cursor: pointer; 21 | } 22 | 23 | .node text { 24 | cursor: pointer; 25 | } 26 | 27 | .edgePath path { 28 | stroke: #777; 29 | fill: #777; 30 | stroke-width: 1.5px; 31 | } 32 | 33 | #side-panel { 34 | position: fixed; 35 | top: -100px; 36 | bottom: -100px; 37 | left: -100px; 38 | width: 20%; 39 | padding: 130px; 40 | padding-right: 30px; 41 | color: #333; 42 | background-color: #eee; 43 | box-shadow: 0 0 10px 0 #888 inset; 44 | } 45 | 46 | #side-panel .title { 47 | font-size: 20px; 48 | margin: 20px 0; 49 | } 50 | 51 | #side-panel .desc { 52 | font-family: monospace; 53 | flex-wrap: wrap; 54 | font-size: 12px; 55 | width: 100%; 56 | height: 90%; 57 | background-color: transparent; 58 | border: none; 59 | resize: none; 60 | } 61 | 62 | #side-panel .header { 63 | text-transform: uppercase; 64 | color: #888; 65 | letter-spacing: 10px; 66 | } 67 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: dagre-py 2 | 3 | [[https://travis-ci.com/Vernacular-ai/dagre-py][https://img.shields.io/travis/com/Vernacular-ai/dagre-py/master.svg?style=flat-square]] 4 | [[https://pypi.org/project/dagre-py/][https://img.shields.io/pypi/v/dagre-py.svg?style=flat-square]] 5 | 6 | Thin python wrapper around [[https://github.com/dagrejs/dagre-d3][dagre-d3]] for building quick dags with tooltips and a 7 | few other niceties. It's not really generically usable right now, but you can 8 | try something like: 9 | 10 | #+begin_src python 11 | from dagre_py.core import plot 12 | 13 | plot({ 14 | "nodes": [ 15 | {"label": "A"}, 16 | {"label": "B", "description": "This is an output node"} 17 | ], 18 | "edges": [{"source": "A", "target": "B", "label": "edge1"}, 19 | {"source": "A", "target": "B", "label": "edge2"}] 20 | }) 21 | #+end_src 22 | 23 | With the following effect: 24 | 25 | [[file:./screens/multi-edges.png]] 26 | 27 | It's also possible to use an id field coupled with a label to disambiguate nodes that you want to have the same label. 28 | Where source and target fields in edges use node id instead of node label. 29 | For example: 30 | 31 | #+begin_src python 32 | from dagre_py.core import plot 33 | 34 | plot({ 35 | "nodes": [ 36 | {"label": "A", "id": "A"}, 37 | {"label": "A", "id": "B", "description": "This is an output node"} 38 | ], 39 | "edges": [{"source": "A", "target": "B"}] 40 | }) 41 | #+end_src 42 | 43 | With the following effect: 44 | 45 | [[file:./screens/duplicate-labels.png]] 46 | 47 | For more description of the data that goes in, check the docstring of ~dagre_py.core.plot~ function. 48 | -------------------------------------------------------------------------------- /dagre_py/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Low level API for dagre-py 3 | """ 4 | 5 | import json 6 | import os 7 | import webbrowser 8 | from typing import Dict, List 9 | 10 | import pkg_resources 11 | 12 | 13 | def plot(dagre_data: Dict[str, List[Dict]], json_encoder=None, open_browser=True): 14 | """ 15 | Plot and, optionally, display the dag. `dagre_data` is a dictionary with 16 | specifications for `nodes` (a list of node dictionaries) and `edges` (a 17 | list of edge dictionaries). 18 | 19 | A `node` dictionary looks like the following: 20 | 21 | ``` 22 | { 23 | # `label` is the text to show on the node (also is the unique identifier, 24 | # we might create a separate `id` key for the uniqueness piece) 25 | "label": "node-name", 26 | 27 | # Rest are optional 28 | # `description` is the description to be displayed on the side panel. 29 | "description": "something like a json dump here", 30 | 31 | # `tooltip` is the text to show in tooltip (fallback is description) 32 | "tooltip": "hello world", 33 | 34 | "attributes": { 35 | "disabled": True, 36 | "style": { 37 | "fill": "white", 38 | "stroke": "#333" 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | An `edge` dictionary looks like the following: 45 | ``` 46 | { 47 | "source": "source-node-label", 48 | "target": "target-node-label", 49 | "label": "edge-label", 50 | 51 | # Rest are optional 52 | "attributes": { 53 | "disabled": False 54 | } 55 | } 56 | ``` 57 | 58 | An optional `attributes` dictionary for attaching to the main graph can 59 | also be passed: 60 | ``` 61 | { 62 | "rankdir": "LR" 63 | } 64 | ``` 65 | """ 66 | 67 | serve_dir = pkg_resources.resource_filename("dagre_py", "js") 68 | 69 | with open(os.path.join(serve_dir, "data.js"), "w", encoding="utf-8") as fp: 70 | fp.write("let data = {}".format(json.dumps(dagre_data, cls=json_encoder, indent=2))) 71 | 72 | if open_browser: 73 | webbrowser.open("file://" + os.path.join(serve_dir, "index.html")) 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python 2 | # Edit at https://www.gitignore.io/?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # celery beat schedule file 97 | celerybeat-schedule 98 | 99 | # SageMath parsed files 100 | *.sage.py 101 | 102 | # Environments 103 | .env 104 | .venv 105 | env/ 106 | venv/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | .spyproject 114 | 115 | # Rope project settings 116 | .ropeproject 117 | 118 | # mkdocs documentation 119 | /site 120 | 121 | # mypy 122 | .mypy_cache/ 123 | .dmypy.json 124 | dmypy.json 125 | 126 | # Pyre type checker 127 | .pyre/ 128 | 129 | # End of https://www.gitignore.io/api/python 130 | /poetry.lock 131 | 132 | .tern-port 133 | /dagre_py/js/data.js 134 | -------------------------------------------------------------------------------- /dagre_py/js/script.js: -------------------------------------------------------------------------------- 1 | /* global d3 tippy dagreD3 data */ 2 | 3 | document.addEventListener('DOMContentLoaded', function () { 4 | let g = new dagreD3.graphlib.Graph({multigraph: true}).setGraph({}) 5 | 6 | // A few global attributes 7 | if (data.attributes && data.attributes.rankdir) { 8 | g.graph().rankDir = data.attributes.rankdir 9 | } 10 | 11 | const disabledShade = '#ccc' 12 | 13 | for (let node of data.nodes) { 14 | let defaultBg = 'white' 15 | let defaultFg = '#333' 16 | let fg, bg 17 | 18 | if (node.attributes) { 19 | // NOTE: `disabled` takes priority for now 20 | if (node.attributes.disabled) { 21 | fg = disabledShade 22 | } else if (node.attributes.style) { 23 | fg = node.attributes.style.stroke 24 | bg = node.attributes.style.fill 25 | } 26 | } 27 | 28 | let value = { 29 | rx: 3, 30 | ry: 3, 31 | shape: 'rect', 32 | label: node.label, 33 | labelStyle: `fill: ${fg || defaultFg}`, 34 | style: `fill: ${bg || defaultBg}; stroke: ${fg || defaultFg}`, 35 | description: node.description || node.label, 36 | ttText: node.tooltip || node.description || node.label 37 | } 38 | if ("id" in node) { 39 | g.setNode(node.id, value) 40 | } else { 41 | g.setNode(node.label, value) 42 | } 43 | } 44 | 45 | for (let edge of data.edges) { 46 | let arrowProps = { arrowhead: 'vee', label: edge.label} 47 | 48 | if (edge.attributes && edge.attributes.disabled) { 49 | g.setEdge(edge.source, edge.target, { 50 | arrowheadStyle: `stroke: ${disabledShade}; fill: ${disabledShade};`, 51 | style: `stroke: ${disabledShade}; fill: transparent;`, 52 | ...arrowProps 53 | }) 54 | } else { 55 | g.setEdge(edge.source, edge.target, arrowProps) 56 | } 57 | } 58 | 59 | let render = new dagreD3.render() 60 | 61 | let svg = d3.select('svg') 62 | let inner = svg.append('g') 63 | 64 | let zoom = d3.zoom().on('zoom', function () { 65 | inner.attr('transform', d3.event.transform) 66 | }) 67 | 68 | render(inner, g) 69 | svg.call(zoom) 70 | 71 | let { height, width } = svg.node().getBoundingClientRect() 72 | svg.call(zoom.transform, d3.zoomIdentity.translate((width - g.graph().width) / 2, (height - g.graph().height) / 2)) 73 | 74 | inner.selectAll('g.node') 75 | .attr('title', v => g.node(v).ttText) 76 | 77 | inner.selectAll('g.node') 78 | .on('click', function (v) { 79 | let node = g.node(v) 80 | d3.select('.desc').html(node.description) 81 | d3.select('.title').text(node.label) 82 | }) 83 | 84 | tippy('g.node', { size: 'small', interactive: true }) 85 | }) 86 | -------------------------------------------------------------------------------- /dagre_py/js/tippy.all.min.js: -------------------------------------------------------------------------------- 1 | (function(t,e){'object'==typeof exports&&'undefined'!=typeof module?module.exports=e():'function'==typeof define&&define.amd?define(e):t.tippy=e()})(this,function(){'use strict';function t(t){return'[object Object]'==={}.toString.call(t)}function a(t){return[].slice.call(t)}function o(e){if(e instanceof Element||t(e))return[e];if(e instanceof NodeList)return a(e);if(Array.isArray(e))return e;try{return a(document.querySelectorAll(e))}catch(t){return[]}}function r(t){t.refObj=!0,t.attributes=t.attributes||{},t.setAttribute=function(e,a){t.attributes[e]=a},t.getAttribute=function(e){return t.attributes[e]},t.removeAttribute=function(e){delete t.attributes[e]},t.hasAttribute=function(e){return e in t.attributes},t.addEventListener=function(){},t.removeEventListener=function(){},t.classList={classNames:{},add:function(e){return t.classList.classNames[e]=!0},remove:function(e){return delete t.classList.classNames[e],!0},contains:function(e){return e in t.classList.classNames}}}function p(t){for(var e=['','webkit'],a=t.charAt(0).toUpperCase()+t.slice(1),o=0;o'):s.classList.add('tippy-arrow'),o.appendChild(s)}if(a.animateFill){o.setAttribute('data-animatefill','');var l=n();l.classList.add('tippy-backdrop'),l.setAttribute('data-state','hidden'),o.appendChild(l)}a.inertia&&o.setAttribute('data-inertia',''),a.interactive&&o.setAttribute('data-interactive','');var d=a.html;if(d){var c;d instanceof Element?(r.appendChild(d),c='#'+(d.id||'tippy-html-template')):(r.innerHTML=document.querySelector(d).innerHTML,c=d),i.setAttribute('data-html',''),o.setAttribute('data-template-id',c),a.interactive&&i.setAttribute('tabindex','-1')}else r[a.allowTitleHTML?'innerHTML':'textContent']=e;return o.appendChild(r),i.appendChild(o),i}function l(t,e,a,i){var o=a.onTrigger,r=a.onMouseLeave,p=a.onBlur,n=a.onDelegateShow,s=a.onDelegateHide,l=[];if('manual'===t)return l;var d=function(t,a){e.addEventListener(t,a),l.push({event:t,handler:a})};return i.target?(qt.supportsTouch&&i.touchHold&&(d('touchstart',n),d('touchend',s)),'mouseenter'===t&&(d('mouseover',n),d('mouseout',s)),'focus'===t&&(d('focusin',n),d('focusout',s)),'click'===t&&d('click',n)):(d(t,o),qt.supportsTouch&&i.touchHold&&(d('touchstart',o),d('touchend',r)),'mouseenter'===t&&d('mouseleave',r),'focus'===t&&d(Ft?'focusout':'blur',p)),l}function d(t,e){var a=Gt.reduce(function(a,i){var o=t.getAttribute('data-tippy-'+i.toLowerCase())||e[i];return'false'===o&&(o=!1),'true'===o&&(o=!0),isFinite(o)&&!isNaN(parseFloat(o))&&(o=parseFloat(o)),'target'!==i&&'string'==typeof o&&'['===o.trim().charAt(0)&&(o=JSON.parse(o)),a[i]=o,a},{});return Jt({},e,a)}function c(t,e){return e.arrow&&(e.animateFill=!1),e.appendTo&&'function'==typeof e.appendTo&&(e.appendTo=e.appendTo()),'function'==typeof e.html&&(e.html=e.html(t)),e}function m(t){var e=function(e){return t.querySelector(e)};return{tooltip:e(jt.TOOLTIP),backdrop:e(jt.BACKDROP),content:e(jt.CONTENT),arrow:e(jt.ARROW)||e(jt.ROUND_ARROW)}}function f(t){var e=t.getAttribute('title');e&&t.setAttribute('data-original-title',e),t.removeAttribute('title')}function h(t){return t&&'[object Function]'==={}.toString.call(t)}function b(t,e){if(1!==t.nodeType)return[];var a=getComputedStyle(t,null);return e?a[e]:a}function u(t){return'HTML'===t.nodeName?t:t.parentNode||t.host}function y(t){if(!t)return document.body;switch(t.nodeName){case'HTML':case'BODY':return t.ownerDocument.body;case'#document':return t.body;}var e=b(t),a=e.overflow,i=e.overflowX,o=e.overflowY;return /(auto|scroll|overlay)/.test(a+o+i)?t:y(u(t))}function g(t){return 11===t?ie:10===t?oe:ie||oe}function w(t){if(!t)return document.documentElement;for(var e=g(10)?document.body:null,a=t.offsetParent;a===e&&t.nextElementSibling;)a=(t=t.nextElementSibling).offsetParent;var i=a&&a.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TD','TABLE'].indexOf(a.nodeName)&&'static'===b(a,'position')?w(a):a:t?t.ownerDocument.documentElement:document.documentElement}function x(t){var e=t.nodeName;return'BODY'!==e&&('HTML'===e||w(t.firstElementChild)===t)}function v(t){return null===t.parentNode?t:v(t.parentNode)}function k(t,e){if(!t||!t.nodeType||!e||!e.nodeType)return document.documentElement;var a=t.compareDocumentPosition(e)&Node.DOCUMENT_POSITION_FOLLOWING,i=a?t:e,o=a?e:t,r=document.createRange();r.setStart(i,0),r.setEnd(o,0);var p=r.commonAncestorContainer;if(t!==p&&e!==p||i.contains(o))return x(p)?p:w(p);var n=v(t);return n.host?k(n.host,e):k(t,v(e).host)}function E(t){var e=1=a.clientWidth&&i>=a.clientHeight}),d=0r,bottom:o-n.bottom>r,left:n.left-i>r,right:i-n.right>r};return'top'===s?d.top=n.top-o>l:'bottom'===s?d.bottom=o-n.bottom>l:'left'===s?d.left=n.left-i>l:'right'===s?d.right=i-n.right>l:void 0,d.top||d.bottom||d.left||d.right}function ft(t,e,a,i){if(!e.length)return'';var o={scale:function(){return 1===e.length?''+e[0]:a?e[0]+', '+e[1]:e[1]+', '+e[0]}(),translate:function(){return 1===e.length?i?-e[0]+'px':e[0]+'px':a?i?e[0]+'px, '+-e[1]+'px':e[0]+'px, '+e[1]+'px':i?-e[1]+'px, '+e[0]+'px':e[1]+'px, '+e[0]+'px'}()};return o[t]}function ht(t,e){if(!t)return'';return e?t:{X:'Y',Y:'X'}[t]}function bt(t,e,a){var i=ct(t),o='top'===i||'bottom'===i,r='right'===i||'bottom'===i,n=function(t){var e=a.match(t);return e?e[1]:''},s=function(t){var e=a.match(t);return e?e[1].split(',').map(parseFloat):[]},l={translate:/translateX?Y?\(([^)]+)\)/,scale:/scaleX?Y?\(([^)]+)\)/},d={translate:{axis:n(/translate([XY])/),numbers:s(l.translate)},scale:{axis:n(/scale([XY])/),numbers:s(l.scale)}},c=a.replace(l.translate,'translate'+ht(d.translate.axis,o)+'('+ft('translate',d.translate.numbers,o,r)+')').replace(l.scale,'scale'+ht(d.scale.axis,o)+'('+ft('scale',d.scale.numbers,o,r)+')');e.style[p('transform')]=c}function ut(t){return-(t-Kt.distance)+'px'}function yt(t){requestAnimationFrame(function(){setTimeout(t,1)})}function gt(t,a){var i=Element.prototype.closest||function(t){for(var a=this;a;){if(e.call(a,t))return a;a=a.parentElement}};return i.call(t,a)}function wt(t,e){return Array.isArray(t)?t[e]:t}function xt(t,e){t.forEach(function(t){t&&t.setAttribute('data-state',e)})}function vt(t,e){t.filter(Boolean).forEach(function(t){t.style[p('transitionDuration')]=e+'ms'})}function kt(t){var e=window.scrollX||window.pageXOffset,a=window.scrollY||window.pageYOffset;t.focus(),scroll(e,a)}function Et(){var t=this._(be).lastTriggerEvent;return this.options.followCursor&&!qt.usingTouch&&t&&'focus'!==t.type}function Tt(t){var e=gt(t.target,this.options.target);if(e&&!e._tippy){var a=e.getAttribute('title')||this.title;a&&(e.setAttribute('title',a),Ht(e,Jt({},this.options,{target:null})),Lt.call(e._tippy,t))}}function Lt(t){var e=this,a=this.options;if(Yt.call(this),!this.state.visible){if(a.target)return void Tt.call(this,t);if(this._(be).isPreparingToShow=!0,a.wait)return void a.wait.call(this.popper,this.show.bind(this),t);if(Et.call(this)){this._(be).followCursorListener||Pt.call(this);var i=m(this.popper),o=i.arrow;o&&(o.style.margin='0'),document.addEventListener('mousemove',this._(be).followCursorListener)}var r=wt(a.delay,0);r?this._(be).showTimeout=setTimeout(function(){e.show()},r):this.show()}}function Ot(){var t=this;if(Yt.call(this),!!this.state.visible){this._(be).isPreparingToShow=!1;var e=wt(this.options.delay,1);e?this._(be).hideTimeout=setTimeout(function(){t.state.visible&&t.hide()},e):this.hide()}}function At(){var t=this;return{onTrigger:function(e){if(t.state.enabled){var a=qt.supportsTouch&&qt.usingTouch&&-1<['mouseenter','mouseover','focus'].indexOf(e.type);a&&t.options.touchHold||(t._(be).lastTriggerEvent=e,'click'===e.type&&'persistent'!==t.options.hideOnClick&&t.state.visible?Ot.call(t):Lt.call(t,e))}},onMouseLeave:function(e){if(!(-1<['mouseleave','mouseout'].indexOf(e.type)&&qt.supportsTouch&&qt.usingTouch&&t.options.touchHold)){if(t.options.interactive){var a=Ot.bind(t),i=function e(i){var o=gt(i.target,jt.REFERENCE),r=gt(i.target,jt.POPPER)===t.popper,p=o===t.reference;r||p||mt(i,t.popper,t.options)&&(document.body.removeEventListener('mouseleave',a),document.removeEventListener('mousemove',e),Ot.call(t,e))};return document.body.addEventListener('mouseleave',a),void document.addEventListener('mousemove',i)}Ot.call(t)}},onBlur:function(e){if(!(e.target!==t.reference||qt.usingTouch)){if(t.options.interactive){if(!e.relatedTarget)return;if(gt(e.relatedTarget,jt.POPPER))return}Ot.call(t)}},onDelegateShow:function(e){gt(e.target,t.options.target)&&Lt.call(t,e)},onDelegateHide:function(e){gt(e.target,t.options.target)&&Ot.call(t)}}}function Ct(){var t=this,e=this.popper,a=this.reference,i=this.options,o=m(e),r=o.tooltip,p=i.popperOptions,n='round'===i.arrowType?jt.ROUND_ARROW:jt.ARROW,s=r.querySelector(n),l=Jt({placement:i.placement},p||{},{modifiers:Jt({},p?p.modifiers:{},{arrow:Jt({element:n},p&&p.modifiers?p.modifiers.arrow:{}),flip:Jt({enabled:i.flip,padding:i.distance+5,behavior:i.flipBehavior},p&&p.modifiers?p.modifiers.flip:{}),offset:Jt({offset:i.offset},p&&p.modifiers?p.modifiers.offset:{})}),onCreate:function(){r.style[ct(e)]=ut(i.distance),s&&i.arrowTransform&&bt(e,s,i.arrowTransform)},onUpdate:function(){var t=r.style;t.top='',t.bottom='',t.left='',t.right='',t[ct(e)]=ut(i.distance),s&&i.arrowTransform&&bt(e,s,i.arrowTransform)}});return It.call(this,{target:e,callback:function(){t.popperInstance.update()},options:{childList:!0,subtree:!0,characterData:!0}}),new me(a,e,l)}function St(t){var e=this.options;if(this.popperInstance?(this.popperInstance.scheduleUpdate(),e.livePlacement&&!Et.call(this)&&this.popperInstance.enableEventListeners()):(this.popperInstance=Ct.call(this),!e.livePlacement&&this.popperInstance.disableEventListeners()),!Et.call(this)){var a=m(this.popper),i=a.arrow;i&&(i.style.margin=''),this.popperInstance.reference=this.reference}dt(this.popperInstance,t,!0),e.appendTo.contains(this.popper)||e.appendTo.appendChild(this.popper)}function Yt(){var t=this._(be),e=t.showTimeout,a=t.hideTimeout;clearTimeout(e),clearTimeout(a)}function Pt(){var t=this;this._(be).followCursorListener=function(e){var a=t._(be).lastMouseMoveEvent=e,i=a.clientX,o=a.clientY;t.popperInstance&&(t.popperInstance.reference={getBoundingClientRect:function(){return{width:0,height:0,top:o,left:i,right:i,bottom:o}},clientWidth:0,clientHeight:0},t.popperInstance.scheduleUpdate())}}function Xt(){var t=this,e=function(){t.popper.style[p('transitionDuration')]=t.options.updateDuration+'ms'},a=function(){t.popper.style[p('transitionDuration')]=''};(function i(){t.popperInstance&&t.popperInstance.update(),e(),t.state.visible?requestAnimationFrame(i):a()})()}function It(t){var e=t.target,a=t.callback,i=t.options;if(window.MutationObserver){var o=new MutationObserver(a);o.observe(e,i),this._(be).mutationObservers.push(o)}}function Dt(t,a){if(!t)return a();var e=m(this.popper),i=e.tooltip,o=function(t,e){e&&i[t+'EventListener']('transition'in document.body.style?'transitionend':'webkitTransitionEnd',e)},r=function t(r){r.target===i&&(o('remove',t),a())};o('remove',this._(be).transitionendListener),o('add',r),this._(be).transitionendListener=r}function _t(t,e){return t.reduce(function(t,a){var i=ge,o=c(a,e.performance?e:d(a,e)),r=a.getAttribute('title');if(!r&&!o.target&&!o.html&&!o.dynamicTitle)return t;a.setAttribute(o.target?'data-tippy-delegate':'data-tippy',''),f(a);var p=s(i,r,o),n=new ye({id:i,reference:a,popper:p,options:o,title:r,popperInstance:null});o.createPopperInstanceOnInit&&(n.popperInstance=Ct.call(n),n.popperInstance.disableEventListeners());var h=At.call(n);return n.listeners=o.trigger.trim().split(' ').reduce(function(t,e){return t.concat(l(e,a,h,o))},[]),o.dynamicTitle&&It.call(n,{target:a,callback:function(){var t=m(p),e=t.content,i=a.getAttribute('title');i&&(e[o.allowTitleHTML?'innerHTML':'textContent']=n.title=i,f(a))},options:{attributes:!0}}),a._tippy=n,p._tippy=n,p._reference=a,t.push(n),ge++,t},[])}function Rt(t){var e=a(document.querySelectorAll(jt.POPPER));e.forEach(function(e){var a=e._tippy;if(a){var i=a.options;(!0===i.hideOnClick||-1e-t&&(qt.usingTouch=!1,document.removeEventListener('mousemove',i),!qt.iOS&&document.body.classList.remove('tippy-touch'),qt.onUserInputChange('mouse')),t=e}}();document.addEventListener('click',function(t){if(!(t.target instanceof Element))return Rt();var e=gt(t.target,jt.REFERENCE),a=gt(t.target,jt.POPPER);if(!(a&&a._tippy&&a._tippy.options.interactive)){if(e&&e._tippy){var i=e._tippy.options,o=-1s[t]&&!e.escapeWithReference&&(i=Mt(d[a],s[t]-('right'===t?d.width:d.height))),ne({},a,i)}};return l.forEach(function(t){var e=-1===['left','top'].indexOf(t)?'secondary':'primary';d=se({},d,c[e](t))}),t.offsets.popper=d,t},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(t){var e=t.offsets,a=e.popper,i=e.reference,o=t.placement.split('-')[0],r=Wt,p=-1!==['top','bottom'].indexOf(o),n=p?'right':'bottom',s=p?'left':'top',l=p?'width':'height';return a[n]r(i[n])&&(t.offsets.popper[s]=r(i[n])),t}},arrow:{order:500,enabled:!0,fn:function(t,e){var a;if(!it(t.instance.modifiers,'arrow','keepTogether'))return t;var i=e.element;if('string'==typeof i){if(i=t.instance.popper.querySelector(i),!i)return t;}else if(!t.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),t;var o=t.placement.split('-')[0],r=t.offsets,p=r.popper,n=r.reference,s=-1!==['left','right'].indexOf(o),l=s?'height':'width',d=s?'Top':'Left',c=d.toLowerCase(),m=s?'left':'top',f=s?'bottom':'right',h=H(i)[l];n[f]-hp[f]&&(t.offsets.popper[c]+=n[c]+h-p[f]),t.offsets.popper=C(t.offsets.popper);var u=n[c]+n[l]/2-h/2,y=b(t.instance.popper),g=parseFloat(y['margin'+d],10),w=parseFloat(y['border'+d+'Width'],10),x=u-t.offsets.popper[c]-g-w;return x=Ut(Mt(p[l]-h,x),0),t.arrowElement=i,t.offsets.arrow=(a={},ne(a,c,Bt(x)),ne(a,m,''),a),t},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(t,e){if(q(t.instance.modifiers,'inner'))return t;if(t.flipped&&t.placement===t.originalPlacement)return t;var a=D(t.instance.popper,t.instance.reference,e.padding,e.boundariesElement,t.positionFixed),i=t.placement.split('-')[0],o=M(i),r=t.placement.split('-')[1]||'',p=[];switch(e.behavior){case ce.FLIP:p=[i,o];break;case ce.CLOCKWISE:p=rt(i);break;case ce.COUNTERCLOCKWISE:p=rt(i,!0);break;default:p=e.behavior;}return p.forEach(function(n,s){if(i!==n||p.length===s+1)return t;i=t.placement.split('-')[0],o=M(i);var l=t.offsets.popper,d=t.offsets.reference,c=Wt,m='left'===i&&c(l.right)>c(d.left)||'right'===i&&c(l.left)c(d.top)||'bottom'===i&&c(l.top)c(a.right),b=c(l.top)c(a.bottom),y='left'===i&&f||'right'===i&&h||'top'===i&&b||'bottom'===i&&u,g=-1!==['top','bottom'].indexOf(i),w=!!e.flipVariations&&(g&&'start'===r&&f||g&&'end'===r&&h||!g&&'start'===r&&b||!g&&'end'===r&&u);(m||y||w)&&(t.flipped=!0,(m||y)&&(i=p[s+1]),w&&(r=ot(r)),t.placement=i+(r?'-'+r:''),t.offsets.popper=se({},t.offsets.popper,B(t.instance.popper,t.offsets.reference,t.placement)),t=z(t.instance.modifiers,t,'flip'))}),t},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(t){var e=t.placement,a=e.split('-')[0],i=t.offsets,o=i.popper,r=i.reference,p=-1!==['left','right'].indexOf(a),n=-1===['top','left'].indexOf(a);return o[p?'left':'top']=r[a]-(n?o[p?'width':'height']:0),t.placement=M(e),t.offsets.popper=C(o),t}},hide:{order:800,enabled:!0,fn:function(t){if(!it(t.instance.modifiers,'hide','preventOverflow'))return t;var e=t.offsets.reference,a=W(t.instance.modifiers,function(t){return'preventOverflow'===t.name}).boundaries;if(e.bottoma.right||e.top>a.bottom||e.right