├── .babelrc ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── d3-annotation.independent.js ├── d3-annotation.independent.js.map ├── d3-annotation.independent.min.js ├── d3-annotation.js ├── d3-annotation.js.map ├── d3-annotation.min.js ├── docs ├── About.md ├── browserify-docs.js ├── bundle.js ├── bundle.map ├── content │ ├── api.md │ ├── contents.md │ ├── extend-explanation.md │ ├── extend.md │ ├── inpractice.md │ ├── introduction.md │ ├── notes.md │ └── start.md ├── custom-highlightjs-build │ ├── README.md │ ├── highlight.js │ └── index.js ├── docs-compiled.js ├── docs.js ├── img │ ├── a-badge.png │ ├── a-callout.png │ ├── a-circle.png │ ├── a-curve.png │ ├── a-elbow.png │ ├── a-label.png │ ├── a-rect.png │ ├── a-threshold.png │ ├── anatomy.png │ ├── arrow.png │ ├── basis.png │ ├── cardinal.png │ ├── circle-pack.png │ ├── classes.png │ ├── curve.png │ ├── dot.png │ ├── dots.png │ ├── down.png │ ├── dynamic.png │ ├── elbow.png │ ├── encircle.png │ ├── example1.png │ ├── example2.png │ ├── example3.png │ ├── heart.png │ ├── horizontal.png │ ├── json.png │ ├── left.png │ ├── leftRight.png │ ├── line.png │ ├── linear.png │ ├── makeover.png │ ├── map-edit-mode.jpg │ ├── menu.png │ ├── middle.png │ ├── none.png │ ├── overlapping.png │ ├── post.png │ ├── resize.png │ ├── right.png │ ├── step.png │ ├── tooltip.png │ ├── top.png │ ├── topBottom.png │ └── vertical.png ├── index.html └── vendor │ ├── d3-dispatch.js │ ├── d3-drag.js │ ├── d3-path.js │ ├── d3-selection.js │ ├── d3-shape.js │ ├── d3-transition.js │ ├── highlight.css │ ├── hightlight.js │ ├── jquery2.1.1.min.js │ ├── materialize.min.css │ └── materialize.min.js ├── index.js ├── indexRollup.js ├── indexRollup.js.map ├── indexRollupNext.js ├── indexRollupNext.js.map ├── package.json ├── rollup.config.js ├── rollupNext.config.js ├── rollupWeb.config.js ├── rollupWebIndependent.config.js ├── src ├── Adapter-d3.js ├── Annotation.js ├── AnnotationCollection.js ├── Builder.js ├── Connector │ ├── end-arrow.js │ ├── end-dot.js │ ├── type-curve.js │ ├── type-elbow.js │ └── type-line.js ├── Handles.js ├── Note │ ├── alignment.js │ ├── lineType-horizontal.js │ ├── lineType-vertical.js │ └── type-text.js ├── Subject │ ├── badge.js │ ├── circle.js │ ├── rect.js │ └── threshold.js └── Types-d3.js ├── test ├── connector.test.js ├── note.test.js ├── subject.badge.test.js └── utils.js ├── tsconfig.json ├── types └── d3-svg-annotation.d.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015" ], 3 | "plugins": ["transform-object-rest-spread", "transform-object-assign"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "array-bracket-spacing": 0, 5 | "curly": 0, 6 | "indent": [2, 2], 7 | "no-var": 2, 8 | "object-curly-spacing": 0, 9 | "quotes": 0, 10 | "eqeqeq": 2, 11 | "eol-last": 0, 12 | "prefer-const": 0, 13 | "no-cond-assign": 0, 14 | "space-before-blocks": 2, 15 | "no-unused-vars": 2, 16 | "no-loop-func": 0, 17 | "no-extra-parens": 2, 18 | "dot-notation": 2, 19 | "comma-dangle": 2, 20 | "no-unused-expressions": [ 21 | 2, 22 | { "allowShortCircuit": true, "allowTernary": true } 23 | ], 24 | "no-multiple-empty-lines": 0, 25 | "no-sequences": 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | docs/ga.js 4 | docs/favicon.ico 5 | bower_components 6 | npm 7 | .vscode 8 | gifs 9 | *.psd 10 | *.zip 11 | blocks -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright (c) 2017, Susie Lu 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # d3-annotation 2 | 3 | Full documentation: [http://d3-annotation.susielu.com](http://d3-annotation.susielu.com) 4 | 5 | ## Setup 6 | ### Include the file directly 7 | 8 | You must include the [d3 library](http://d3js.org/) before including the annotation file. Then you can add the compiled js file to your website 9 | 10 | - [Unminified](https://github.com/susielu/d3-annotation/blob/master/d3-annotation.js) 11 | - [Minified](https://github.com/susielu/d3-annotation/blob/master/d3-annotation.min.js) 12 | 13 | ### Using CDN 14 | 15 | You can add the latest version of [d3-annotation hosted on cdnjs](https://cdnjs.com/libraries/d3-annotation). 16 | 17 | ### Using NPM 18 | 19 | You can add d3-annotation as a node module by running 20 | 21 | ```bash 22 | npm i d3-svg-annotation -S 23 | ``` 24 | 25 | ## Feedback 26 | I would love to hear from you about any additional features that would be useful, please say hi on twitter [@DataToViz](https://www.twitter.com/DataToViz). 27 | 28 | ## Prior art 29 | 30 | - [Andrew Mollica](https://bl.ocks.org/armollica/67f3cf7bf08a02d95d48dc9f0c91f26c), [d3-ring-note](https://github.com/armollica/d3-ring-note) D3 plugin for placing circle and text annotation, and [HTML Annotation](http://bl.ocks.org/armollica/78894d0b3cbd46d8d8d19d135c6ca34d) 31 | 32 | - [Scatterplot with d3-annotate](https://bl.ocks.org/cmpolis/f9805a98b8a455aaccb56e5ee59964f8), by Chris Polis, example using [d3-annotate](https://github.com/cmpolis/d3-annotate) 33 | 34 | - [Rickshaw](http://code.shutterstock.com/rickshaw/) has an annotation tool 35 | 36 | - [Benn Stancil](https://modeanalytics.com/benn/reports/21ebfb6b6138) has an annotation example for a line chart 37 | 38 | - [Adam Pearce](http://blockbuilder.org/1wheel/68073eeba4d19c454a8c25fcd6e9e68a) has arc-arrows and [swoopy drag](http://1wheel.github.io/swoopy-drag/) 39 | 40 | - [Micah Stubbs](http://bl.ocks.org/micahstubbs/fa129089b7989975e96b166077f74de4#annotations.json) has a nice VR chart based on swoopy drag 41 | 42 | - [Scott Logic](http://blog.scottlogic.com/2014/08/26/two-line-components-for-d3-charts.html) evokes “line annotation” in a graph (different concept). 43 | 44 | - [Seven Features You’ll Want In Your Next Charting Tool](http://vis4.net/blog/posts/seven-features-youll-wantin-your-next-charting-tool/) shows how the NYT does annotations 45 | 46 | - [John Burn-Murdoch](https://bl.ocks.org/johnburnmurdoch/bcdb4e85c7523a2b0e64961f0d227154) example with adding/removing and repositioning annotations 47 | -------------------------------------------------------------------------------- /docs/About.md: -------------------------------------------------------------------------------- 1 | # svg-annotation 2 | 3 | 4 | ## Annotation Properties 5 | - location 6 | - associated data 7 | - priority 8 | - draggable 9 | 10 | ## Annotation Types 11 | - callout 12 | - label 13 | - threshold 14 | 15 | ## Annotation Components 16 | - connector 17 | - line 18 | - elbow 19 | - text 20 | - title 21 | - point 22 | - circle 23 | - path 24 | - rect 25 | - badge 26 | 27 | ## Annotation Intents 28 | - anomaly 29 | - insight 30 | - details/context 31 | - 32 | 33 | ## Annotation States 34 | - expanded 35 | - minimized 36 | 37 | ## Interactivity 38 | - hover expand 39 | 40 | ## Adaptors 41 | - d3 42 | 43 | ## 44 | -------------------------------------------------------------------------------- /docs/browserify-docs.js: -------------------------------------------------------------------------------- 1 | var browserify = require('browserify'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var markedify = require('markedify'); 5 | var marked = require('marked'); 6 | var babelify = require('babelify'); 7 | var highlight = require('highlight.js') 8 | 9 | marked.setOptions({ 10 | highlight: function(code, lang) { 11 | return highlight.highlight(lang, code).value; 12 | } 13 | }); 14 | 15 | var renderer = new marked.Renderer(); 16 | var markedOptions = { 17 | renderer: renderer, 18 | }; 19 | 20 | browserify(path.join(__dirname, 'docs.js')) 21 | .transform(markedify, {marked: markedOptions}) 22 | .transform(babelify, {presets: ['es2015']}) 23 | .bundle() 24 | .on('error', function(err) { console.log('ERROR: ' + err); }) 25 | .pipe(fs.createWriteStream(path.join(__dirname, 'docs-compiled.js'))); 26 | -------------------------------------------------------------------------------- /docs/content/api.md: -------------------------------------------------------------------------------- 1 |

#API

2 | 3 | **d3.annotation()** 4 | 5 | annotation.**annotations([ objects ])** 6 | 7 | Pass an array of objects with annotation properties: 8 | 9 | - **id**: This can be anything that will help you filter and parse your annotations 10 | 11 | ![Annotation JSON](img/json.png) 12 | 13 | - **x,y (number:pixels)**: Position of the subject and one end of the connector 14 | - **data (object)**: If you also set accessor functions, you can give data instead of x,y coordinates for placing your annotations 15 | - **dx, dy (number:pixels)**: Position of the note and one end of the connector, as an offset from x,y 16 | - **nx, ny (number:pixels)**: Position of the note and one end of the connector, as the raw x,y position **not** an offset 17 | - **type ([d3annotationType](#types))**: Type for this annotation. Recommended to set the base type at the d3.annotation().type() property and use this to override the base 18 | - **disable ([string])**: takes the values 'connector', 'subject', and 'note' pass them in this array if you want to disable those parts from rendering 19 | - **color([string])**: only in version 2.0, you can pass a color string that will be applied to the annotation. This color can be overridden via css or inline-styles 20 | - **note (object)**: You can specify a title and label property here. All of the annotation types that come with d3-annotation have built in functionality to take the title and the label and add them to the note, however the underlying system is composable in a way that you could customize the note to contain any type of content. You can also use this to overwrite the default note properties (align, orientation, lineType, wrap, padding) in the type. For example if on one of the notes you wanted to align it differently. In v2.1.0 and higher you can pass a regex or string to customize the wrapping { wrapSplitter: /\n/ }. In v2.3.0 and higher, you can pass a bgPadding that accepts a number or an object with one or more top, bottom, left, and right properties, to increase the size of the rectangle behind the text. 21 | - **connector (object)**: Some connectors such as the curve connector require additional parameters to set up the annotation. You can also use this to overwrite the default connector properties (type, end) in the type. For example if you wanted to add an arrow to the end of some of the annotations in the array you could add { end: "arrow" } to this connector property on the relevant annotations. In v2.1.0 and higher, there is also a { endScale: 2 } that allows you to scale the size of the dot or arrow end types 22 | - **subject (object)**: Some subjects such as the circle require additional parameters to set up the annotation. 23 | 24 | If you don't pass anything to this function, it returns the current array of annotations. 25 | 26 | annotation.**accessors({ x: function, y: function })** 27 | 28 | Functions that would map the .data attribute of your annotation to x and y positions: 29 | 30 | ```js 31 | //Sample .data for an annotation 32 | //{date: "2-Jan-08", close: 194.84} 33 | const parseTime = d3.timeParse("%d-%b-%y") 34 | 35 | d3.annotation().accessors({ 36 | x: d => x(parseTime(d.date)), 37 | y: d => y(d.close) 38 | }) 39 | ``` 40 | 41 | annotation.**accessorsInverse({ <x property mapping>: function, <y property mapping>: function })** 42 | 43 | The inverse of the accessor function. If you are given x, y coordinates, how to get back to the original data properties. Only for the x and y accessors: 44 | 45 | ```js 46 | //Sample .data for an annotation 47 | //{date: "2-Jan-08", close: 194.84} 48 | const timeFormat = d3.timeFormat("%d-%b-%y") 49 | 50 | d3.annotation().accessorsInverse({ 51 | date: d => timeFormat(x.invert(d.x)), 52 | close: d => y.invert(d.y) 53 | }) 54 | ``` 55 | 56 | annotation.**editMode(boolean)** 57 | 58 | If this is true, then the annotation will create handles for parts of the annotation that are draggable. You can style these handles with the circle.handle selector. If you are hooking this up to a button, you will need to run the update function below, after changing the editMode. Example in [Map with Tooltips and Edit Mode](#map) 59 | 60 | annotation.**update()** 61 | 62 | Redraws all of the annotations. Typically used to reflect updated settings. If you are only updating the position (x, y) or the offset (dx, dy) you do not need to run `call` on makeAnnotations afterwards. Example in [Layout - Encircling Annotation](#encircle). 63 | 64 | annotation.**updateText()** 65 | 66 | If you only want to update the text then use this function. It will re-evaluate with the new text and text wrapping settings. This is separated from the `update()` function for flexibility with performance. If you call the entire set again it will run both functions. 67 | 68 | annotation.**updatedAccessors()** 69 | 70 | Redraws all the annotations with updated accessor scales. Example in [Responsive with Types and Hover](#responsive) 71 | 72 | annotation.**type( d3annotationType )** 73 | You can pass different types into the annotation objects themselves, but you can also set a default type here. If you want to change the type, you will need to re-call the d3.annotation function on your element to recreate the annotations with the new type. Example in [Responsive with Types and Hover](#responsive) 74 | 75 | annotation.**json()** 76 | 77 | You can run this in the developer console and it will print out the current annotation settings and copy them to your clipboard. Please note that in the console each annotation will also include the type that you've associated with it. 78 | 79 | annotation.**collection()** 80 | 81 | Access to the collection of annotations with the instantiated types. 82 | 83 | annotation.**textWrap()** 84 | Change the overall textWrap, otherwise in the annotation object array you can change each individual one with the {note: {wrap: 30}} property. This function calls `updateText()` internally so you do not need to call both functions when updating `textWrap`. 85 | 86 | annotation.**notePadding()** 87 | Change the overall notePadding, otherwise in the annotation object array you can change each individual one with the {note: {padding: 30}} property 88 | 89 | annotation.**disable()** 90 | Takes the values 'connector', 'subject', and 'note' pass them in this array if you want to disable those parts from rendering 91 | 92 | annotation.**on()** 93 | Takes the values 'subjectover', 'subjectout', 'subjectclick', 'connectorover', 'connectorout', 'connectorclick', 'noteover', 'noteout', 'noteclick', 'dragend', 'dragstart' as custom dispatch events you can hook into. 94 | -------------------------------------------------------------------------------- /docs/content/contents.md: -------------------------------------------------------------------------------- 1 | - [Introduction](#introduction) 2 | - [Setup](#setup) 3 | - [Types](#types) 4 | - [In Practice](#in-practice) 5 | - [Examples](#examples) 6 | - [Essays](#essays) 7 | - [API](#api) 8 | - [Extending Types](#extend) 9 | - [Notes](#notes) 10 | -------------------------------------------------------------------------------- /docs/content/extend-explanation.md: -------------------------------------------------------------------------------- 1 | ## Extending Annotation Types 2 | 3 | ### Javascript Classes 4 | 5 | The underlying structure for the annotations and types are built with es6 JavaScript classes. To make your own custom type you can take any of the base types and extend them. 6 | 7 | ### Static Functions 8 | 9 | **STATIC className** 10 | 11 | A class that return a string for the class name you want to give your custom type. 12 | 13 | **STATIC init** 14 | 15 | An init function that is looped through 16 | 17 | Example, default init function 18 | 19 | Exampe, xyThreshold init function 20 | 21 | ### Drawing Functions 22 | These functions have a context parameter. Context is an object with gives you access to the annotation with all of its properties, and the relevant bounding box. 23 | 24 | **drawNote(context)** 25 | 26 | **drawNoteContent(context)** 27 | 28 | **drawConnector(context)** 29 | 30 | **drawSubject(context)** 31 | 32 | ### Overall Code structure 33 | 34 | **Annotation Class** 35 | 36 | Each annotation is an instantiation of this class. 37 | 38 | Reference the [souce code](https://github.com/susielu/d3-annotation/blob/master/src/Annotation.js) for the full set of properties and functions. Most relevant properties: 39 | 40 | - dx 41 | - dy 42 | - x 43 | - y 44 | - data 45 | - offset: returns the dx, and dy, values as an object {x, y} 46 | - position: returns the x, and y, values as an object {x, y} 47 | 48 | 49 | **Annotation Collection Class** 50 | 51 | When you run d3.annotation() it creates all of the annotations you pass it as Annotation Class instances and places them into an array as part of an Annotation Collection. 52 | 53 | **Types Class** 54 | 55 | Each of the annotation types is created 56 | -------------------------------------------------------------------------------- /docs/content/extend.md: -------------------------------------------------------------------------------- 1 |

#Extending Annotation Types

2 | 3 | 4 | The underlying code for d3-annotation has a base annotation type that all of the annotation types extend. The settings and components that make up the different types are customizable. 5 | 6 | The goal was to make a system that was easy to add new types and implement layout algorithms with. A longer post with details about how you can make your own type will be coming out soon. 7 | 8 | If you're interested in looking at the architecture before the post you can find the [source code here](https://github.com/susielu/d3-annotation/tree/master/src). -------------------------------------------------------------------------------- /docs/content/inpractice.md: -------------------------------------------------------------------------------- 1 |

#In Practice

2 | 3 | All annotations are made of just three parts, a **note**, a **connector**, and a **subject**. 4 | 5 | Anatomy of an annotation 6 | 7 | They are the foundational blocks of this library. 8 | 9 | ### Customize the Subject by picking a base annotation 10 | 11 | Settings for subject types are in the annotation object's .subject: 12 | 13 | ```js 14 | const annotations = [ 15 | { 16 | note: { label: "Hi" }, 17 | x: 100, 18 | y: 100, 19 | dy: 137, 20 | dx: 162, 21 | subject: { radius: 50, radiusPadding: 10 }, 22 | }, 23 | ]; 24 | 25 | d3.annotation().annotations(annotations); 26 | ``` 27 | 28 | **d3.annotationCalloutCircle** 29 | 30 | - radius or outerRadius and innerRadius: Number, pixels 31 | - radiusPadding: Number, pixels 32 | 33 | **d3.annotationCalloutRect** 34 | 35 | - width: Number, pixels 36 | - height: Number, pixels 37 | 38 | **d3.annotationXYThreshold** 39 | 40 | - x1, x2 or y1, y2: Number, pixels 41 | 42 | **d3.annotationBadge**: this is the only base annotation that doesn't have a connector or note 43 | 44 | - text: String 45 | - radius: Number, pixels 46 | - x: "left" or "right" 47 | - y: "top" or "bottom" 48 | 49 | **No subject** 50 | 51 | - d3.annotationLabel 52 | - d3.annotationCallout 53 | - d3.annotationCalloutElbow 54 | - d3.annotationCalloutCurve 55 | 56 | ### Customize the Connector and Note 57 | 58 | The Options panel in the [Annotation Types UI](#types) exposes all of the options for connectors and notes. So the "Line Type" in the UI maps to { connector: { lineType : "horizontal" } } 59 | 60 | There are two ways to customize the connectors and notes. You can either change these properties per annotation: 61 | 62 | ```js 63 | const annotations = [ 64 | { 65 | note: { label: "Hi" }, 66 | x: 100, 67 | y: 100, 68 | dy: 137, 69 | dx: 162, 70 | type: d3.annotationCalloutElbow, 71 | connector: { end: "arrow" }, 72 | }, 73 | ]; 74 | 75 | d3.annotation().annotations(annotations); 76 | ``` 77 | 78 | Or if you want all of the annotations to have these settings create a custom type with 79 | **d3.annotationCustomType(annotationType, typeSettings)**: 80 | 81 | ```js 82 | const calloutWithArrow = d3.annotationCustomType(d3.annotationCalloutElbow, { 83 | connector: { end: "arrow" }, 84 | }); 85 | 86 | d3.annotation() 87 | .type(calloutWithArrow) 88 | .annotations([ 89 | { 90 | text: "Plant paradise", 91 | data: { date: "18-Sep-09", close: 185.02 }, 92 | dy: 37, 93 | dx: 42, 94 | }, 95 | ]) 96 | .editMode(true); 97 | ``` 98 | 99 | Both examples above produce the same results. 100 | 101 |

#Selecting Elements

102 | 103 | - All of the visible shapes (aside from the edit handles) in the default annotations are **paths** 104 | - There is an invisible rect (rect.annotation-note-bg) behind the text in the notes as a helper for more click area etc. 105 | - Hierarchy of classes: 106 | ![Annotation classes](img/classes.png) 107 | - Within the g.annotation-note-content there could be three additional elements: text.annotation-note-label, text.annotation-note-title, rect.annotation-note-bg 108 | 109 |

# Basic Styles

110 | 111 | Now the library comes with default styles, read more about it in the [2.0 release](http://www.susielu.com/data-viz/d3-annotation-2) post. 112 | 113 | Before v2, there were style sheets you needed to use: 114 | 115 | Available on [github](https://github.com/susielu/d3-annotation/blob/e7ba1e83f279a63e056964b080019d647f57e34c/d3-annotation.css). 116 | 117 |

#Tips

118 | 119 | - In addition to the alignment settings for the note, you can also use the css `text-anchor` attribute to align the text within the note 120 | - When you update the d3.annotation().type() you will need to use the call functionality again to set up the annotations with the new type. See the [Responsive with Types and Hover](#responsive) example 121 | - You do not need to call d3.annotation().update() if you are only changing the position(x,y) or the offset(dx, dy). See the [Overlapping](#overlapping) example 122 | - If you are importing custom fonts, you may notice the annotations don't load perfectly with text wrapping and alignment. To fix that you can use, `document.fonts.ready` to make sure the fonts are loaded first to reflect the custom font's spacing for all of the calculations. Here's an example: 123 | 124 | ```js 125 | document.fonts.ready.then(function () { 126 | d3.select("svg") 127 | .append("g") 128 | .attr("class", "annotation-group") 129 | .style("font-size", fontSize(ratio)) 130 | .call(makeAnnotations); 131 | }); 132 | ``` 133 | -------------------------------------------------------------------------------- /docs/content/introduction.md: -------------------------------------------------------------------------------- 1 |

#Introduction

2 | 3 | Annotations **establish context, and direct our users to insights and anomalies**. So why are annotations so few and far between in visualizations on the web? Because **implementing them is difficult.** 4 | 5 | **But it shouldn't be.** 6 | 7 | Use d3-annotation with built-in annotation types, or extend it to make custom annotations. It is made for [d3-v4](https://github.com/d3/d3/blob/master/CHANGES.md) in SVG. 8 | 9 | 10 | Contact me through the [github repo](https://www.github.com/susielu/d3-annotation) or [twitter](https://www.twitter.com/DataToViz). -------------------------------------------------------------------------------- /docs/content/notes.md: -------------------------------------------------------------------------------- 1 |

#Notes

2 | 3 | 4 | Extremely grateful to my team at [Netflix](https://twitter.com/netflixdata) for mentoring me, giving me feedback, and helping out on this project. Cheers [Elijah](https://twitter.com/Elijah_Meeks), [James](https://twitter.com/james_womack), [Jason](https://twitter.com/onemerovingian), and [Nathan](https://twitter.com/nathantowery). 5 | 6 | Invaluable help from [Fil](https://twitter.com/recifs), thanks for jumping in early to help with testing and discussion. 7 | 8 | [Sam](https://twitter.com/samccone) keeps me sane and gets all the perf wins \o/ 9 | 10 | This is a data visualization project that wouldn't be possible without [Mike Bostock](https://twitter.com/mbostock)'s work on [d3](https://d3js.org/), and all of the inspiring [prior art](https://github.com/susielu/d3-annotation#prior-art) in annotations, particularly [Adam Pearce](https://twitter.com/adamrpearce)'s [Swoopy Drag](https://1wheel.github.io/swoopy-drag/), and [Andrew Mollica](https://twitter.com/armollica)'s [Ring Note](https://github.com/armollica/d3-ring-note). 11 | 12 | Thumbs up to [Nunito](https://fonts.google.com/specimen/Nunito) and [Bungee](https://fonts.google.com/specimen/Bungee) via Google Fonts and [Materialize](http://materializecss.com/) for making the docs site building a breeze. 13 | 14 | -------------------------------------------------------------------------------- /docs/content/start.md: -------------------------------------------------------------------------------- 1 |

#Setup

2 | 3 | ### Include the file directly 4 | 5 | You must include the [d3 library](http://d3js.org/) before including the annotation file. Then you can add the compiled js file to your website 6 | 7 | - [Unminified](https://github.com/susielu/d3-annotation/blob/master/d3-annotation.js) 8 | - [Minified](https://github.com/susielu/d3-annotation/blob/master/d3-annotation.min.js) 9 | 10 | ### Using CDN 11 | 12 | You can add the latest version of [d3-annotation hosted on cdnjs](https://cdnjs.com/libraries/d3-annotation). 13 | 14 | ### Using NPM 15 | 16 | You can add d3-annotation as a node module by running 17 | 18 | ```bash 19 | npm i d3-svg-annotation -S 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/custom-highlightjs-build/README.md: -------------------------------------------------------------------------------- 1 | build by cloning highlight.js 2 | 3 | From within highlight.js repo folder: 4 | 5 | ``` 6 | node tools/build.js -t node -n js 7 | ``` 8 | 9 | Then cping the files from build/lib into this folder `\o/` 10 | -------------------------------------------------------------------------------- /docs/custom-highlightjs-build/index.js: -------------------------------------------------------------------------------- 1 | var hljs = require('./highlight'); 2 | 3 | 4 | module.exports = hljs; -------------------------------------------------------------------------------- /docs/docs.js: -------------------------------------------------------------------------------- 1 | const contents = require("./content/contents.md"); 2 | const introduction = require("./content/introduction.md"); 3 | const start = require("./content/start.md"); 4 | const inpractice = require("./content/inpractice.md"); 5 | const api = require("./content/api.md"); 6 | const extend = require("./content/extend.md"); 7 | const notes = require("./content/notes.md"); 8 | const highlight = require("./custom-highlightjs-build"); 9 | const highlightjs = require("highlight.js/lib/languages/javascript"); 10 | highlight.registerLanguage("js", highlightjs); 11 | 12 | document.getElementById("toc1").innerHTML = contents; 13 | document.getElementById("slide-out").innerHTML = 14 | '
  • d3-annotation
  • ' + 15 | contents; 16 | document.getElementById("introduction").innerHTML = introduction; 17 | document.getElementById("setup").innerHTML = start; 18 | document.getElementById("in-practice").innerHTML = inpractice; 19 | document.getElementById("api").innerHTML = api; 20 | document.getElementById("extend").innerHTML = extend; 21 | document.getElementById("notes").innerHTML = notes; 22 | 23 | $(document).ready(function () { 24 | $(".scrollspy").scrollSpy({ scrollOffset: 0 }); 25 | $(".button-collapse").sideNav(); 26 | $(".toc").pushpin({ 27 | top: 140, 28 | offset: 0, 29 | }); 30 | 31 | $(".collapsible").collapsible(); 32 | 33 | const defaultSettings = { 34 | className: "custom", 35 | subject: {}, 36 | connector: {}, 37 | note: {}, 38 | }; 39 | 40 | let typeSettings = JSON.parse(JSON.stringify(defaultSettings)); 41 | 42 | let typeKey = "annotationLabel"; 43 | let curve = "curveCatmullRom"; 44 | let points = 2; 45 | 46 | const types = { 47 | annotationLabel: { 48 | typeSettings: { 49 | note: { 50 | align: "middle", 51 | orientation: "topBottom", 52 | bgPadding: 20, 53 | padding: 15, 54 | }, 55 | connector: { type: "line" }, 56 | className: "show-bg", 57 | }, 58 | summary: "A centered label annotation", 59 | }, 60 | annotationCallout: { 61 | typeSettings: { 62 | note: { 63 | align: "dynamic", 64 | lineType: "horizontal", 65 | bgPadding: { top: 15, left: 10, right: 10, bottom: 10 }, 66 | padding: 15, 67 | }, 68 | connector: { type: "line" }, 69 | className: "show-bg", 70 | }, 71 | summary: "Adds a line along the note", 72 | }, 73 | annotationCalloutElbow: { 74 | typeSettings: { 75 | note: { align: "dynamic", lineType: "horizontal" }, 76 | connector: { type: "elbow" }, 77 | }, 78 | summary: "Keeps connector at 45 and 90 degree angles", 79 | }, 80 | annotationCalloutCircle: { 81 | typeSettings: { 82 | note: { align: "dynamic", lineType: "horizontal" }, 83 | connector: { type: "elbow" }, 84 | }, 85 | summary: "Subject options: radius, innerRadius, outerRadius, ", 86 | summaryCont: "radiusPadding", 87 | subject: { 88 | radius: 50, 89 | radiusPadding: 5, 90 | }, 91 | }, 92 | annotationCalloutRect: { 93 | typeSettings: { 94 | note: { align: "dynamic", lineType: "horizontal" }, 95 | connector: { type: "elbow" }, 96 | }, 97 | summary: "Subject options: width, height", 98 | subject: { 99 | width: -50, 100 | height: 100, 101 | }, 102 | }, 103 | annotationCalloutCurve: { 104 | typeSettings: { 105 | note: { align: "dynamic", lineType: "horizontal" }, 106 | connector: { type: "curve" }, 107 | }, 108 | summary: "Connector options: curve, ", 109 | summaryCont: "points(array of [x,y]s or number)", 110 | }, 111 | annotationXYThreshold: { 112 | typeSettings: { 113 | note: { align: "dynamic", lineType: "horizontal" }, 114 | connector: { type: "elbow" }, 115 | }, 116 | summary: "Subject options: x1, x2 or y1, y2", 117 | subject: { 118 | x1: 0, 119 | x2: 1000, 120 | }, 121 | }, 122 | annotationBadge: { 123 | typeSettings: { 124 | note: { align: "dynamic", lineType: "horizontal" }, 125 | connector: { type: "elbow" }, 126 | }, 127 | summary: 128 | "Subject options: radius, text, x:left or right, y:top or bottom", 129 | subject: { 130 | radius: 14, 131 | text: "A", 132 | }, 133 | }, 134 | }; 135 | let currentType = d3.annotationCustomType( 136 | d3.annotationLabel, 137 | types.annotationLabel.typeSettings 138 | ); 139 | let editMode = true; 140 | let textWrap = 120; 141 | let padding = types[typeKey].typeSettings.note.padding || 5; 142 | 143 | const annotation = { 144 | note: { 145 | label: "Longer text to show text wrapping", 146 | title: "Annotations :)", 147 | }, 148 | className: types[typeKey].className, 149 | x: 150, 150 | y: 170, 151 | dy: 117, 152 | dx: 162, 153 | }; 154 | 155 | const changeOption = function () { 156 | let type = d3.event.target.attributes["data-section"].value; 157 | const value = d3.event.target.attributes["data-setting"].value; 158 | d3.selectAll(`[data-section="${type}"]`).classed("active", false); 159 | 160 | d3.selectAll(`[data-section="${type}"][data-setting="${value}"]`).classed( 161 | "active", 162 | true 163 | ); 164 | 165 | if (type === "note:lineType") { 166 | if (value === "none") { 167 | d3.selectAll(".icons .orientation").classed("hidden", false); 168 | } else { 169 | d3.selectAll(".icons .orientation").classed("hidden", true); 170 | } 171 | } 172 | 173 | if ( 174 | (type === "note:lineType" && value === "vertical") || 175 | (type === "note:orientation" && value === "leftRight") 176 | ) { 177 | d3.selectAll("[data-section='note:align'].horizontal").classed( 178 | "hidden", 179 | true 180 | ); 181 | d3.selectAll("[data-section='note:align'].vertical").classed( 182 | "hidden", 183 | false 184 | ); 185 | } else if ( 186 | (type === "note:lineType" && value === "horizontal") || 187 | (type === "note:orientation" && value === "topBottom") 188 | ) { 189 | d3.selectAll("[data-section='note:align'].vertical").classed( 190 | "hidden", 191 | true 192 | ); 193 | d3.selectAll("[data-section='note:align'].horizontal").classed( 194 | "hidden", 195 | false 196 | ); 197 | } 198 | 199 | type = type.split(":"); 200 | if (value === "none") { 201 | delete typeSettings[type[0]][type[1]]; 202 | if (type[0] == "connector" && type[1] == "type") { 203 | makeAnnotations.disable(["connector"]); 204 | makeAnnotations.update(); 205 | } 206 | } else { 207 | if (type[0] == "connector" && type[1] == "type") { 208 | const connectorTypes = [ 209 | "annotationCallout", 210 | "annotationCalloutElbow", 211 | "annotationCalloutCurve", 212 | ]; 213 | if (connectorTypes.indexOf(typeKey) !== -1) { 214 | if (value == "line") { 215 | typeKey = "annotationCallout"; 216 | } else if (value == "elbow") { 217 | typeKey = "annotationCalloutElbow"; 218 | } else if (value == "curve") { 219 | typeKey = "annotationCalloutCurve"; 220 | } 221 | 222 | d3.selectAll(`.icons .presets img`).classed("active", false); 223 | 224 | d3.selectAll(`[data-type="${typeKey}"]`).classed("active", true); 225 | } else { 226 | typeSettings[type[0]][type[1]] = value; 227 | } 228 | 229 | if (value == "curve") { 230 | d3.select("#curveButtons").classed("hidden", false); 231 | } else if (typeKey !== "annotationCalloutCurve") { 232 | d3.select("#curveButtons").classed("hidden", true); 233 | } 234 | 235 | makeAnnotations.disable([]); 236 | makeAnnotations.update(); 237 | } else { 238 | typeSettings[type[0]][type[1]] = value; 239 | } 240 | } 241 | 242 | currentType = d3.annotationCustomType(d3[typeKey], typeSettings); 243 | 244 | updateAnnotations(); 245 | sandboxCode(); 246 | }; 247 | 248 | d3.selectAll(".icons .options img").on("click", changeOption); 249 | 250 | d3.selectAll(".icons .presets .types img").on("click", function () { 251 | typeKey = d3.event.target.attributes["data-type"].value; 252 | currentType = d3.annotationCustomType(d3[typeKey], { 253 | className: types[typeKey].typeSettings.className, 254 | note: { bgPadding: types[typeKey].typeSettings.note.bgPadding }, 255 | }); 256 | 257 | d3.selectAll(`.icons .presets img`).classed("active", false); 258 | 259 | d3.selectAll(`[data-type="${typeKey}"]`).classed("active", true); 260 | 261 | typeSettings = JSON.parse(JSON.stringify(defaultSettings)); 262 | 263 | if (typeKey == "annotationBadge") { 264 | d3.select("li.options").classed("hidden", true); 265 | } else { 266 | d3.select("li.options").classed("hidden", false); 267 | } 268 | 269 | //set options 270 | const options = types[typeKey].typeSettings; 271 | 272 | d3.selectAll(".icons .options img").classed("active", false); 273 | 274 | d3.select( 275 | `.icons img[data-section="note:align"][data-setting="${options.note.align}"]` 276 | ).classed("active", true); 277 | 278 | if (options.note.lineType) { 279 | d3.select( 280 | `.icons img[data-section="note:lineType"][data-setting=${options.note.lineType}]` 281 | ).classed("active", true); 282 | d3.selectAll(".icons .orientation").classed("hidden", true); 283 | } else { 284 | d3.select( 285 | `.icons img[data-section="note:lineType"][data-setting="none"]` 286 | ).classed("active", true); 287 | d3.selectAll(".icons .orientation").classed("hidden", false); 288 | d3.select(".icons .orientation img").classed("active", true); 289 | } 290 | 291 | d3.select('.icons img[data-section="connector:end"]').classed( 292 | "active", 293 | true 294 | ); 295 | 296 | d3.select( 297 | `.icons img[data-section="connector:type"][data-setting=${options.connector.type}]` 298 | ).classed("active", true); 299 | 300 | if (typeKey == "annotationCalloutCurve") { 301 | d3.select("#curveButtons").classed("hidden", false); 302 | } else { 303 | d3.select("#curveButtons").classed("hidden", true); 304 | } 305 | 306 | d3.select("#sandbox-title").text(`Use d3.${typeKey}:`); 307 | 308 | updateAnnotations(); 309 | sandboxCode(); 310 | }); 311 | 312 | d3.select("#editmode").on("change", function () { 313 | editMode = d3.event.target.checked; 314 | 315 | makeAnnotations.editMode(editMode); 316 | makeAnnotations.update(); 317 | 318 | sandboxCode(); 319 | }); 320 | 321 | d3.select("#textWrap").on("change", function () { 322 | textWrap = parseInt(d3.event.target.value); 323 | makeAnnotations.textWrap(textWrap).update(); 324 | sandboxCode(); 325 | }); 326 | 327 | d3.select("#padding").on("change", function () { 328 | padding = parseInt(d3.event.target.value); 329 | makeAnnotations.notePadding(padding).update(); 330 | sandboxCode(); 331 | }); 332 | 333 | const changeCurve = function () { 334 | curve = d3.event.target.attributes["data-curve"].value; 335 | updateAnnotations({ connector: { curve: d3[curve], points } }); 336 | sandboxCode(); 337 | }; 338 | 339 | d3.selectAll("#curveButtons ul.curves li img") 340 | .on("click", changeCurve) 341 | .on("pointerdown", changeCurve); 342 | 343 | const changePoints = function () { 344 | points = parseInt(d3.event.target.attributes["data-points"].value); 345 | updateAnnotations({ connector: { curve: d3[curve], points } }); 346 | sandboxCode(); 347 | }; 348 | 349 | d3.selectAll("#curveButtons ul.points li a") 350 | .on("click", changePoints) 351 | .on("pointerdown", changePoints); 352 | 353 | window.makeAnnotations = d3 354 | .annotation() 355 | .editMode(editMode) 356 | .type(currentType) 357 | .annotations([annotation]); 358 | 359 | d3.select(".sandbox") 360 | .append("g") 361 | .attr("class", "sandbox-annotations") 362 | .call(makeAnnotations); 363 | 364 | const updateAnnotations = (newSettings) => { 365 | d3.select(".sandbox g.sandbox-annotations").remove(); 366 | 367 | const subject = types[typeKey].subject || {}; 368 | makeAnnotations.type(currentType, { 369 | subject, 370 | connector: newSettings && newSettings.connector, 371 | }); 372 | 373 | d3.select(".sandbox") 374 | .append("g") 375 | .attr("class", "sandbox-annotations") 376 | .call(makeAnnotations); 377 | 378 | d3.select(".sandbox .type").text(`d3.${typeKey}`); 379 | 380 | d3.select(".sandbox .summary").text(types[typeKey].summary); 381 | 382 | d3.select(".sandbox .summaryCont").text(types[typeKey].summaryCont || ""); 383 | }; 384 | 385 | //change the text to have the right position for the annotation 386 | 387 | const sandboxCode = () => { 388 | const editModeText = editMode ? ` .editMode(true)\n` : ""; 389 | 390 | let typeText = "\nconst type = "; 391 | 392 | if (JSON.stringify(typeSettings) == JSON.stringify(defaultSettings)) { 393 | typeText += `d3.${typeKey}\n`; 394 | } else { 395 | let json = JSON.parse(JSON.stringify(typeSettings)); 396 | 397 | //if (Object.keys(json.subject).length === 0){ 398 | delete json.subject; 399 | //} 400 | 401 | if (Object.keys(json.connector).length === 0) { 402 | delete json.connector; 403 | } 404 | if (Object.keys(json.note).length === 0) { 405 | delete json.note; 406 | } 407 | typeText += 408 | `d3.annotationCustomType(\n` + 409 | ` d3.${typeKey}, \n` + 410 | ` ${JSON.stringify(json).replace(/,/g, ",\n ")}` + 411 | `)\n`; 412 | } 413 | 414 | let disableText = ""; 415 | 416 | if (makeAnnotations.disable().length !== 0) { 417 | disableText = 418 | " //could also be set in the a disable property\n //of the annotation object\n" + 419 | ` .disable(${JSON.stringify(makeAnnotations.disable())})\n`; 420 | } 421 | 422 | let textWrapText = ""; 423 | 424 | if (textWrap !== 120) { 425 | textWrapText = 426 | " //also can set and override in the note.wrap property\n //of the annotation object\n" + 427 | ` .textWrap(${textWrap})\n`; 428 | } 429 | 430 | let paddingText = ""; 431 | if (padding !== 5) { 432 | paddingText = 433 | " //also can set and override in the note.padding property\n //of the annotation object\n" + 434 | ` .notePadding(${padding})\n`; 435 | } 436 | 437 | let curveText = ""; 438 | if ( 439 | (typeKey == "annotationCalloutCurve" || 440 | typeSettings.connector.type == "curve") && 441 | (curve !== "curveCatmullRom" || points !== 2) 442 | ) { 443 | curveText = 444 | " connector: {\n" + 445 | (curve !== "curveCatmullRom" ? ` curve: d3.${curve}` : "") + 446 | (points !== 2 && curve !== "curveCatmullRom" ? ",\n" : "") + 447 | (points !== 2 ? ` points: ${points}` : "") + 448 | "\n" + 449 | " }"; 450 | } 451 | 452 | let subjectText = ""; 453 | if (typeKey === "annotationCalloutCircle") { 454 | subjectText = 455 | ` subject: {\n` + 456 | " radius: 50,\n" + 457 | " radiusPadding: 5\n" + 458 | " }\n"; 459 | } else if (typeKey == "annotationCalloutRect") { 460 | subjectText = 461 | ` subject: {\n` + " width: -50,\n" + " height: 100\n" + " }\n"; 462 | } else if (typeKey == "annotationXYThreshold") { 463 | subjectText = 464 | ` subject: {\n` + " x1: 0,\n" + " x2: 500\n" + " }\n"; 465 | } else if (typeKey == "annotationBadge") { 466 | subjectText = 467 | ` subject: {\n` + ' text: "A",\n' + " radius: 14\n" + " }\n"; 468 | } 469 | 470 | d3.select("#sandbox-code code").text( 471 | typeText + 472 | "\n" + 473 | "const annotations = [{\n" + 474 | " note: {\n" + 475 | ' label: "Longer text to show text wrapping",\n' + 476 | ((types[typeKey] && 477 | types[typeKey].typeSettings.note && 478 | types[typeKey].typeSettings.note.bgPadding && 479 | ` bgPadding: ${JSON.stringify( 480 | types[typeKey].typeSettings.note.bgPadding 481 | )},\n`) || 482 | "") + 483 | ' title: "Annotations :)"\n' + 484 | " },\n" + 485 | " //can use x, y directly instead of data\n" + 486 | ' data: { date: "18-Sep-09", close: 185.02 },\n' + 487 | ((types[typeKey] && 488 | types[typeKey].typeSettings.className && 489 | ` className: ${JSON.stringify( 490 | types[typeKey].typeSettings.className 491 | )},\n`) || 492 | "") + 493 | " dy: 137,\n" + 494 | ` dx: 162${curveText !== "" || subjectText !== "" ? "," : ""}\n` + 495 | curveText + 496 | (subjectText !== "" && curveText !== "" ? ",\n" : "") + 497 | subjectText + 498 | "}]\n" + 499 | "\n" + 500 | 'const parseTime = d3.timeParse("%d-%b-%y")\n' + 501 | 'const timeFormat = d3.timeFormat("%d-%b-%y")\n' + 502 | "\n" + 503 | "//Skipping setting domains for sake of example\n" + 504 | "const x = d3.scaleTime().range([0, 800])\n" + 505 | "const y = d3.scaleLinear().range([300, 0])\n" + 506 | "\n" + 507 | "const makeAnnotations = d3.annotation()\n" + 508 | editModeText + 509 | disableText + 510 | textWrapText + 511 | paddingText + 512 | ` .type(type)\n` + 513 | " //accessors & accessorsInverse not needed\n" + 514 | " //if using x, y in annotations JSON\n" + 515 | " .accessors({\n" + 516 | " x: d => x(parseTime(d.date)),\n" + 517 | " y: d => y(d.close)\n" + 518 | " })\n" + 519 | " .accessorsInverse({\n" + 520 | " date: d => timeFormat(x.invert(d.x)),\n" + 521 | " close: d => y.invert(d.y)\n" + 522 | " })\n" + 523 | ` .annotations(annotations)\n` + 524 | "\n" + 525 | 'd3.select("svg")\n' + 526 | ' .append("g")\n' + 527 | ' .attr("class", "annotation-group")\n' + 528 | " .call(makeAnnotations)\n" 529 | ); 530 | 531 | $("code").each(function (i, block) { 532 | highlight.highlightBlock(block); 533 | }); 534 | }; 535 | 536 | sandboxCode(); 537 | }); 538 | -------------------------------------------------------------------------------- /docs/img/a-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/a-badge.png -------------------------------------------------------------------------------- /docs/img/a-callout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/a-callout.png -------------------------------------------------------------------------------- /docs/img/a-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/a-circle.png -------------------------------------------------------------------------------- /docs/img/a-curve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/a-curve.png -------------------------------------------------------------------------------- /docs/img/a-elbow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/a-elbow.png -------------------------------------------------------------------------------- /docs/img/a-label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/a-label.png -------------------------------------------------------------------------------- /docs/img/a-rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/a-rect.png -------------------------------------------------------------------------------- /docs/img/a-threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/a-threshold.png -------------------------------------------------------------------------------- /docs/img/anatomy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/anatomy.png -------------------------------------------------------------------------------- /docs/img/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/arrow.png -------------------------------------------------------------------------------- /docs/img/basis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/basis.png -------------------------------------------------------------------------------- /docs/img/cardinal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/cardinal.png -------------------------------------------------------------------------------- /docs/img/circle-pack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/circle-pack.png -------------------------------------------------------------------------------- /docs/img/classes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/classes.png -------------------------------------------------------------------------------- /docs/img/curve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/curve.png -------------------------------------------------------------------------------- /docs/img/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/dot.png -------------------------------------------------------------------------------- /docs/img/dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/dots.png -------------------------------------------------------------------------------- /docs/img/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/down.png -------------------------------------------------------------------------------- /docs/img/dynamic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/dynamic.png -------------------------------------------------------------------------------- /docs/img/elbow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/elbow.png -------------------------------------------------------------------------------- /docs/img/encircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/encircle.png -------------------------------------------------------------------------------- /docs/img/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/example1.png -------------------------------------------------------------------------------- /docs/img/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/example2.png -------------------------------------------------------------------------------- /docs/img/example3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/example3.png -------------------------------------------------------------------------------- /docs/img/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/heart.png -------------------------------------------------------------------------------- /docs/img/horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/horizontal.png -------------------------------------------------------------------------------- /docs/img/json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/json.png -------------------------------------------------------------------------------- /docs/img/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/left.png -------------------------------------------------------------------------------- /docs/img/leftRight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/leftRight.png -------------------------------------------------------------------------------- /docs/img/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/line.png -------------------------------------------------------------------------------- /docs/img/linear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/linear.png -------------------------------------------------------------------------------- /docs/img/makeover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/makeover.png -------------------------------------------------------------------------------- /docs/img/map-edit-mode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/map-edit-mode.jpg -------------------------------------------------------------------------------- /docs/img/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/menu.png -------------------------------------------------------------------------------- /docs/img/middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/middle.png -------------------------------------------------------------------------------- /docs/img/none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/none.png -------------------------------------------------------------------------------- /docs/img/overlapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/overlapping.png -------------------------------------------------------------------------------- /docs/img/post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/post.png -------------------------------------------------------------------------------- /docs/img/resize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/resize.png -------------------------------------------------------------------------------- /docs/img/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/right.png -------------------------------------------------------------------------------- /docs/img/step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/step.png -------------------------------------------------------------------------------- /docs/img/tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/tooltip.png -------------------------------------------------------------------------------- /docs/img/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/top.png -------------------------------------------------------------------------------- /docs/img/topBottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/topBottom.png -------------------------------------------------------------------------------- /docs/img/vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susielu/d3-annotation/18c85b076387b99dc9a196d5bd0b9073198fab39/docs/img/vertical.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 283 | 284 | 285 | 286 | 287 |
    288 |
    289 |
    290 |

    d3-annotation

    291 |

    Made with 292 | by 293 | Susie Lu 294 |

    295 | 296 |
    297 |
    298 |
    299 |
    300 |
      301 |
    302 | 303 |
    304 | 305 |
    306 |
      307 | 308 |
    309 |
    310 | 311 | 326 | 327 |
    328 |
    329 |
    330 |
    331 |

    332 | #Annotation Types

    333 |
    334 |
    335 |
      336 |
    • 337 |
      338 | Presets 339 |
      340 |
      341 | 347 |
      348 | 349 |
      350 | 351 |
      352 |
      353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 |
      362 |
    • 363 |
    • 364 |
      365 | Options 366 |
      367 |
      368 |
      369 |
      370 |

      371 | Note 372 |

      373 |

      Line Type 374 | 375 |

      376 | 377 | 378 | 379 | 380 |
      381 |

      Orientation

      382 | 383 | 384 |
      385 | 386 |
      387 |
      388 |
      389 |

      Align

      390 | 391 | 392 | 393 | 394 | 395 | 396 |
      397 |
      398 |
      399 | 400 | 401 | 402 | 403 | 404 | 405 |
      406 |
      407 |

      408 | Connector 409 |

      410 |
      411 |

      Type 412 | 413 |

      414 | 415 | 416 | 417 | 418 |
      419 |
      420 |

      End 421 | 422 |

      423 | 424 | 425 | 426 | 427 |
      428 |
      429 |
      430 |
      431 |
    • 432 |
    433 |
    434 |
    435 |
    436 | 437 | 438 | d3.annotationLabel 439 | A centered label annotation 440 | 441 | 442 | Code below is ready to use with these setttings 443 | 444 |
    445 | 494 |
    495 |
    496 |

    Use d3.annotationLabel:

    497 |
    498 |               
    499 |               
    500 |             
    501 | 502 |
    503 |
    504 |
    505 |
    506 |

    507 | #Examples

    508 | 509 |
    510 |
    511 |

    512 | #Basic Without Scales

    513 | 514 | 515 | 516 | 517 |
    518 | 519 |
    520 |

    521 | #Tooltips

    522 | 523 | 524 | 525 | 526 |
    527 |
    528 |
    529 |
    530 | 531 |

    532 | #Responsive with Types and Hover

    533 | 534 |

    Example showing how to dynamically change anntation types

    535 | 536 | 537 | 538 |
    539 | 540 |
    541 |

    542 | #Reimagining the Circle Pack

    543 | 544 |

    Remake of the circle pack with annotations as the exterior circles

    545 | 546 | 547 | 548 |
    549 | 550 |
    551 |
    552 |
    553 |

    554 | #Thresholds and Annotation Color Styles

    555 |

    Using annotation design for emphasis

    556 | 557 | 558 | 559 |
    560 |
    561 |

    562 | #Points of Interest with Badges

    563 |

    Expanding on points of interest outside of the chart itself

    564 | 565 | 566 | 567 |
    568 | 569 | 570 |
    571 |
    572 |
    573 |

    574 | #Overlapping

    575 | 576 |

    Moving annotations algorithmically to prevent overlap using rect collision

    577 | 578 | 579 | 580 |
    581 |
    582 | 583 |

    584 | #Encircling

    585 | 586 |

    Annotations following a set of points using d3.packEnclose

    587 | 588 | 589 | 590 |
    591 |
    592 | 593 |
    594 | 595 |
    596 |

    597 | #Map with Tooltips and Edit Mode

    598 | 599 |

    A map with tooltips, double-click to enable/disable editMode

    600 | 601 | 602 | 603 | 604 |
    605 |
    606 |

    607 | #d3 v2.0 Features

    608 |

    Easy annotation coloring, new badge options, and new note positioning options

    609 | 610 | 611 | 612 |
    613 |
    614 |
    615 |
    616 |

    617 | #Essays

    618 | 619 | 620 | 621 |
    622 | 626 |
    627 |
    628 |
    629 | 634 |
    635 |
    636 |
    637 | 638 | 639 | -------------------------------------------------------------------------------- /docs/vendor/d3-dispatch.js: -------------------------------------------------------------------------------- 1 | // https://d3js.org/d3-dispatch/ Version 1.0.3. Copyright 2017 Mike Bostock. 2 | !function(n,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(n.d3=n.d3||{})}(this,function(n){"use strict";function e(){for(var n,e=0,r=arguments.length,o={};e=0&&(t=n.slice(r+1),n=n.slice(0,r)),n&&!e.hasOwnProperty(n))throw new Error("unknown type: "+n);return{type:n,name:t}})}function o(n,e){for(var t,r=0,o=n.length;r0)for(var t,r,o=new Array(t),i=0;i1e-6)if(Math.abs(c*r-a*u)>1e-6&&e){var x=s-n,y=_-o,M=r*r+a*a,l=x*x+y*y,d=Math.sqrt(M),p=Math.sqrt(f),v=e*Math.tan((h-Math.acos((M+f-l)/(2*d*p)))/2),b=v/p,w=v/d;Math.abs(b-1)>1e-6&&(this._+="L"+(t+b*u)+","+(i+b*c)),this._+="A"+e+","+e+",0,0,"+ +(c*x>u*y)+","+(this._x1=t+w*r)+","+(this._y1=i+w*a)}else this._+="L"+(this._x1=t)+","+(this._y1=i);else;},arc:function(t,i,s,n,o,r){t=+t,i=+i,s=+s;var a=s*Math.cos(n),u=s*Math.sin(n),c=t+a,f=i+u,x=1^r,y=r?n-o:o-n;if(s<0)throw new Error("negative radius: "+s);null===this._x1?this._+="M"+c+","+f:(Math.abs(this._x1-c)>1e-6||Math.abs(this._y1-f)>1e-6)&&(this._+="L"+c+","+f),s&&(y<0&&(y=y%_+_),y>e?this._+="A"+s+","+s+",0,1,"+x+","+(t-a)+","+(i-u)+"A"+s+","+s+",0,1,"+x+","+(this._x1=c)+","+(this._y1=f):y>1e-6&&(this._+="A"+s+","+s+",0,"+ +(y>=h)+","+x+","+(this._x1=t+s*Math.cos(o))+","+(this._y1=i+s*Math.sin(o))))},rect:function(t,i,s,h){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+i)+"h"+ +s+"v"+ +h+"h"+-s+"Z"},toString:function(){return this._}},t.path=s,Object.defineProperty(t,"__esModule",{value:!0})}); -------------------------------------------------------------------------------- /docs/vendor/d3-selection.js: -------------------------------------------------------------------------------- 1 | // https://d3js.org/d3-selection/ Version 1.0.6. Copyright 2017 Mike Bostock. 2 | !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})}(this,function(t){"use strict";function n(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===W&&n.documentElement.namespaceURI===W?n.createElement(t):n.createElementNS(e,t)}}function e(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function r(){return new i}function i(){this._="@"+(++et).toString(36)}function o(t,n,e){return t=u(t,n,e),function(n){var e=n.relatedTarget;e&&(e===this||8&e.compareDocumentPosition(this))||t.call(this,n)}}function u(n,e,r){return function(i){var o=t.event;t.event=i;try{n.call(this,this.__data__,e,r)}finally{t.event=o}}}function c(t){return t.trim().split(/^|\s+/).map(function(t){var n="",e=t.indexOf(".");return e>=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}function s(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;rn?1:t>=n?0:NaN}function m(t){return function(){this.removeAttribute(t)}}function y(t){return function(){this.removeAttributeNS(t.space,t.local)}}function g(t,n){return function(){this.setAttribute(t,n)}}function w(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function A(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function x(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function S(t){return function(){this.style.removeProperty(t)}}function b(t,n,e){return function(){this.style.setProperty(t,n,e)}}function E(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function N(t){return function(){delete this[t]}}function C(t,n){return function(){this[t]=n}}function M(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function L(t){return t.trim().split(/^|\s+/)}function P(t){return t.classList||new T(t)}function T(t){this._node=t,this._names=L(t.getAttribute("class")||"")}function q(t,n){for(var e=P(t),r=-1,i=n.length;++r=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Z.hasOwnProperty(n)?{space:Z[n],local:t}:t},nt=function(t){var r=tt(t);return(r.local?e:n)(r)},et=0;i.prototype=r.prototype={constructor:i,get:function(t){for(var n=this._;!(n in t);)if(!(t=t.parentNode))return;return t[n]},set:function(t,n){return t[this._]=n},remove:function(t){return this._ in t&&delete t[this._]},toString:function(){return this._}};var rt=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var it=document.documentElement;if(!it.matches){var ot=it.webkitMatchesSelector||it.msMatchesSelector||it.mozMatchesSelector||it.oMatchesSelector;rt=function(t){return function(){return ot.call(this,t)}}}}var ut=rt,ct={};if(t.event=null,"undefined"!=typeof document){"onmouseenter"in document.documentElement||(ct={mouseenter:"mouseover",mouseleave:"mouseout"})}var st=function(t,n,e){var r,i,o=c(t+""),u=o.length;{if(!(arguments.length<2)){for(l=n?a:s,null==e&&(e=!1),r=0;r=x&&(x=A+1);!(w=y[x])&&++x=0;)(r=i[o])&&(u&&u!==r.nextSibling&&u.parentNode.insertBefore(r,u),u=r);return this},Et=function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=d);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?S:"function"==typeof n?E:b)(t,n,null==e?"":e)):Ot(r=this.node()).getComputedStyle(r,null).getPropertyValue(t)},Dt=function(t,n){return arguments.length>1?this.each((null==n?N:"function"==typeof n?M:C)(t,n)):this.node()[t]};T.prototype={add:function(t){this._names.indexOf(t)<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var n=this._names.indexOf(t);n>=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var Vt=function(t,n){var e=L(t+"");if(arguments.length<2){for(var r=P(this.node()),i=-1,o=e.length;++iH)throw new Error("too late");return e}function s(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>K)throw new Error("too late");return e}function l(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("too late");return e}function f(t,n,e){function i(t){e.state=J,e.timer.restart(o,e.delay,e.time),e.delay<=t&&o(t-e.delay)}function o(i){var f,c,h,d;if(e.state!==J)return a();for(f in l)if(d=l[f],d.name===e.name){if(d.state===L)return r.timeout(o);d.state===Q?(d.state=W,d.timer.stop(),d.on.call("interrupt",t,t.__data__,d.index,d.group),delete l[f]):+f=0&&(t=t.slice(0,n)),!t||"start"===t})}function N(t,n,e){var r,i,o=E(n)?a:s;return function(){var u=o(this,t),a=u.on;a!==r&&(i=(r=a).copy()).on(n,e),u.on=i}}function T(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function q(t,e){var r,i,o;return function(){var u=n.window(this).getComputedStyle(this,null),a=u.getPropertyValue(t),s=(this.style.removeProperty(t),u.getPropertyValue(t));return a===s?null:a===r&&s===i?o:o=e(r=a,i=s)}}function V(t){return function(){this.style.removeProperty(t)}}function O(t,e,r){var i,o;return function(){var u=n.window(this).getComputedStyle(this,null).getPropertyValue(t);return u===r?null:u===i?o:o=e(i=u,r)}}function j(t,e,r){var i,o,u;return function(){var a=n.window(this).getComputedStyle(this,null),s=a.getPropertyValue(t),l=r(this);return null==l&&(this.style.removeProperty(t),l=a.getPropertyValue(t)),s===l?null:s===i&&l===o?u:u=e(i=s,o=l)}}function k(t,n,e){function r(){var r=this,i=n.apply(r,arguments);return i&&function(n){r.style.setProperty(t,i(n),e)}}return r._value=n,r}function z(t){return function(){this.textContent=t}}function M(t){return function(){var n=t(this);this.textContent=null==n?"":n}}function R(t,n,e,r){this._groups=t,this._parents=n,this._name=e,this._id=r}function I(t){return n.selection().transition(t)}function B(){return++mt}function D(t,n){for(var e;!(e=t.__transition)||!(e=e[n]);)if(!(t=t.parentNode))return wt.time=r.now(),wt;return e}var F=e.dispatch("start","end","interrupt"),G=[],H=0,J=1,K=2,L=3,Q=4,U=5,W=6,X=function(t,n,e,r,i,o){var u=t.__transition;if(u){if(e in u)return}else t.__transition={};f(t,e,{name:n,index:r,group:i,on:F,tween:G,time:o.time,delay:o.delay,duration:o.duration,ease:o.ease,timer:null,state:H})},Y=function(t,n){var e,r,i,o=t.__transition,u=!0;if(o){n=null==n?null:n+"";for(i in o)(e=o[i]).name===n?(r=e.state>K&&e.stateJ&&e.name===n)return new R([[t]],At,n,+r)}return null};t.transition=I,t.active=xt,t.interrupt=Y,Object.defineProperty(t,"__esModule",{value:!0})}); -------------------------------------------------------------------------------- /docs/vendor/highlight.css: -------------------------------------------------------------------------------- 1 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 2 | 3 | /* Tomorrow Comment */ 4 | .hljs-comment, 5 | .hljs-quote { 6 | color: #adafac; 7 | } 8 | 9 | /* Tomorrow Red */ 10 | .hljs-variable, 11 | .hljs-template-variable, 12 | .hljs-tag, 13 | .hljs-name, 14 | .hljs-selector-id, 15 | .hljs-selector-class, 16 | .hljs-regexp, 17 | .hljs-deletion { 18 | color: #c82829; 19 | } 20 | 21 | /* Tomorrow Orange */ 22 | .hljs-number, 23 | .hljs-built_in, 24 | .hljs-builtin-name, 25 | .hljs-literal, 26 | .hljs-type, 27 | .hljs-params, 28 | .hljs-meta, 29 | .hljs-link { 30 | color: #E8336D; 31 | } 32 | 33 | /* Tomorrow Yellow */ 34 | .hljs-attribute { 35 | color: #eab700; 36 | } 37 | 38 | /* Tomorrow Green */ 39 | .hljs-string, 40 | .hljs-symbol, 41 | .hljs-bullet, 42 | .hljs-addition { 43 | color: #00897b; 44 | } 45 | 46 | /* Tomorrow Blue */ 47 | .hljs-title, 48 | .hljs-section { 49 | color: #00897b; 50 | } 51 | 52 | /* Tomorrow Purple */ 53 | .hljs-keyword, 54 | .hljs-selector-tag { 55 | color: #00897b; 56 | } 57 | 58 | pre{ 59 | display: block; 60 | overflow-x: auto; 61 | background: #f3f3f3; 62 | border-top: 1px dotted #aaa; 63 | border-bottom: 1px dotted #aaa; 64 | color: #695B5B; 65 | padding: 0.5em; 66 | font-size: .9em; 67 | line-height: 1.4em; 68 | } 69 | 70 | .hljs-emphasis { 71 | font-style: italic; 72 | } 73 | 74 | .hljs-strong { 75 | font-weight: bold; 76 | } 77 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import annotation from './src/Adapter-d3' 2 | import { Type, d3Label, d3Callout, d3CalloutCurve, d3CalloutElbow, d3CalloutCircle, 3 | d3CalloutRect, d3XYThreshold, d3Badge, customType } from './src/Types-d3' 4 | 5 | export { 6 | annotation, 7 | Type as annotationTypeBase , 8 | d3Label as annotationLabel , 9 | d3Callout as annotationCallout , 10 | d3CalloutCurve as annotationCalloutCurve , 11 | d3CalloutElbow as annotationCalloutElbow , 12 | d3CalloutCircle as annotationCalloutCircle , 13 | d3CalloutRect as annotationCalloutRect , 14 | d3XYThreshold as annotationXYThreshold , 15 | d3Badge as annotationBadge , 16 | customType as annotationCustomType 17 | } 18 | 19 | export default { 20 | annotation, 21 | annotationTypeBase : Type, 22 | annotationLabel : d3Label, 23 | annotationCallout : d3Callout, 24 | annotationCalloutCurve : d3CalloutCurve, 25 | annotationCalloutElbow : d3CalloutElbow, 26 | annotationCalloutCircle : d3CalloutCircle, 27 | annotationCalloutRect : d3CalloutRect, 28 | annotationXYThreshold : d3XYThreshold, 29 | annotationBadge : d3Badge, 30 | annotationCustomType : customType 31 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-svg-annotation", 3 | "version": "2.5.0", 4 | "description": "", 5 | "main": "indexRollup.js", 6 | "jsnext:main": "indexRollupNext.js", 7 | "web": "d3-annotation.js", 8 | "web:independent": "d3-annotation.independent.js", 9 | "module": "indexRollupNext.js", 10 | "files": [ 11 | "index.js", 12 | "indexRollup.js", 13 | "indexRollup.js.map", 14 | "indexRollupNext.js", 15 | "indexRollupNext.js.map", 16 | "d3-annotation.*", 17 | "types/*" 18 | ], 19 | "keywords": [ 20 | "d3", 21 | "d3-module", 22 | "annotation" 23 | ], 24 | "directories": {}, 25 | "devDependencies": { 26 | "babel-eslint": "7.1.1", 27 | "babel-plugin-external-helpers": "6.18.0", 28 | "babel-plugin-transform-object-assign": "6.22.0", 29 | "babel-plugin-transform-object-rest-spread": "6.20.2", 30 | "babel-preset-es2015": "6.18.0", 31 | "babelify": "7.3.0", 32 | "babelrc-rollup": "3.0.0", 33 | "browserify": "13.1.1", 34 | "eslint": "3.17.1", 35 | "http-server": "0.9.0", 36 | "jest": "^20.0.4", 37 | "marked": "0.3.9", 38 | "markedify": "0.0.2", 39 | "npm-watch": "0.1.6", 40 | "reify": "0.4.16", 41 | "rollup": "0.41.4", 42 | "rollup-plugin-babel": "2.6.1", 43 | "rollup-plugin-node-resolve": "2.0.0", 44 | "uglify-js": "2.8.1", 45 | "@types/d3-dispatch": "1.0.4", 46 | "@types/d3-selection": "1.0.10" 47 | }, 48 | "scripts": { 49 | "start": "npm run server & npm run watch", 50 | "server": "http-server", 51 | "rollup:web": "rollup -c rollupWeb.config.js && rollup -c rollupWebIndependent.config.js", 52 | "docs": "node docs/browserify-docs.js && npm run docs-bundle", 53 | "docs-bundle": "uglifyjs --source-map=docs/bundle.map --source-map-url=bundle.map -- docs/vendor/jquery2.1.1.min.js docs/vendor/materialize.min.js docs/vendor/d3-selection.js docs/vendor/d3-path.js docs/vendor/d3-shape.js docs/vendor/d3-dispatch.js docs/vendor/d3-drag.js docs/vendor/d3-transition.js d3-annotation.js docs/docs-compiled.js > docs/bundle.js", 54 | "watch": "npm-watch", 55 | "uglify": "uglifyjs d3-annotation.js -o d3-annotation.min.js -c -m", 56 | "uglify:independent": "uglifyjs d3-annotation.independent.js -o d3-annotation.independent.min.js -c -m", 57 | "prepublishOnly": "eslint src/** && npm run test:only && npm run rollup:web && npm run uglify && npm run uglify:independent && rollup -c rollupWeb.config.js && rollup -c && rollup -c rollupNext.config.js", 58 | "test": "jest --watch", 59 | "test:only": "jest" 60 | }, 61 | "watch": { 62 | "rollup:web": [ 63 | "src/*.js", 64 | "src/**/*.js" 65 | ], 66 | "docs": { 67 | "patterns": [ 68 | "docs/docs.js", 69 | "d3-annotation.js", 70 | "docs/content/*.md" 71 | ], 72 | "extensions": "js,md" 73 | } 74 | }, 75 | "repository": { 76 | "type": "git", 77 | "url": "https://github.com/susielu/d3-annotation.git" 78 | }, 79 | "license": "Apache-2.0", 80 | "bugs": { 81 | "url": "https://github.com/susielu/d3-annotation/issues" 82 | }, 83 | "types": "./types/d3-svg-annotation.d.ts", 84 | "dependencies": { 85 | "d3-dispatch": "1.0.2", 86 | "d3-drag": "1.2.3", 87 | "d3-selection": "1.4.0", 88 | "d3-shape": "1.0.4", 89 | "d3-transition": "1.0.3" 90 | }, 91 | "homepage": "http://d3-annotation.susielu.com" 92 | } 93 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import babelrc from 'babelrc-rollup'; 3 | import nodeResolve from 'rollup-plugin-node-resolve'; 4 | 5 | let pkg = require('./package.json'); 6 | 7 | export default { 8 | entry: 'index.js', 9 | plugins: [ 10 | babel(babelrc()), 11 | nodeResolve() 12 | ], 13 | targets: [ 14 | { 15 | dest: pkg.main, 16 | format: 'umd', 17 | moduleName: 'd3', 18 | sourceMap: true, 19 | globals: { 20 | 'd3-selection': 'd3', 21 | 'd3-dispatch': 'd3', 22 | 'd3-shape': 'd3', 23 | 'd3-drag': 'd3' 24 | } 25 | } 26 | ] 27 | }; 28 | -------------------------------------------------------------------------------- /rollupNext.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import babelrc from 'babelrc-rollup'; 3 | 4 | let pkg = require('./package.json'); 5 | 6 | export default { 7 | entry: 'index.js', 8 | plugins: [ 9 | babel(babelrc()) 10 | ], 11 | targets: [ 12 | { 13 | dest: pkg['jsnext:main'], 14 | format: 'es', 15 | moduleName: 'd3', 16 | sourceMap: true, 17 | globals: { 18 | 'd3-selection': 'd3', 19 | 'd3-dispatch': 'd3', 20 | 'd3-shape': 'd3', 21 | 'd3-drag': 'd3' 22 | } 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /rollupWeb.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import babelrc from 'babelrc-rollup'; 3 | 4 | let pkg = require('./package.json'); 5 | 6 | export default { 7 | entry: 'index.js', 8 | plugins: [ 9 | babel(babelrc()) 10 | ], 11 | external:['d3-selection', 'd3-dispatch', 'd3-shape', 'd3-drag'], 12 | targets: [ 13 | { 14 | dest: pkg.web, 15 | format: 'umd', 16 | moduleName: 'd3', 17 | sourceMap: true, 18 | globals: { 19 | 'd3-selection': 'd3', 20 | 'd3-dispatch': 'd3', 21 | 'd3-shape': 'd3', 22 | 'd3-drag': 'd3' 23 | } 24 | } 25 | ] 26 | }; 27 | -------------------------------------------------------------------------------- /rollupWebIndependent.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import babelrc from 'babelrc-rollup'; 3 | 4 | let pkg = require('./package.json'); 5 | 6 | export default { 7 | entry: 'index.js', 8 | plugins: [ 9 | babel(babelrc()) 10 | ], 11 | external:['d3-selection', 'd3-dispatch', 'd3-shape', 'd3-drag'], 12 | targets: [ 13 | { 14 | dest: pkg["web:independent"], 15 | format: 'umd', 16 | moduleName: 'd3annotation', 17 | sourceMap: true, 18 | globals: { 19 | 'd3-selection': 'd3', 20 | 'd3-dispatch': 'd3', 21 | 'd3-shape': 'd3', 22 | 'd3-drag': 'd3' 23 | } 24 | } 25 | ] 26 | }; 27 | -------------------------------------------------------------------------------- /src/Adapter-d3.js: -------------------------------------------------------------------------------- 1 | import Annotation from "./Annotation" 2 | import AnnotationCollection from "./AnnotationCollection" 3 | import { newWithClass, d3Callout } from "./Types-d3" 4 | import { select } from "d3-selection" 5 | import { dispatch } from "d3-dispatch" 6 | 7 | export default function annotation() { 8 | let annotations = [], 9 | collection, 10 | context, //TODO: add canvas functionality 11 | disable = [], 12 | accessors = {}, 13 | accessorsInverse = {}, 14 | editMode = false, 15 | ids, 16 | type = d3Callout, 17 | textWrap, 18 | notePadding, 19 | annotationDispatcher = dispatch( 20 | "subjectover", 21 | "subjectout", 22 | "subjectclick", 23 | "connectorover", 24 | "connectorout", 25 | "connectorclick", 26 | "noteover", 27 | "noteout", 28 | "noteclick", 29 | "dragend", 30 | "dragstart" 31 | ), 32 | sel 33 | 34 | const annotation = function(selection) { 35 | sel = selection 36 | //TODO: check to see if this is still needed 37 | if (!editMode) { 38 | selection.selectAll("circle.handle").remove() 39 | } 40 | 41 | const translatedAnnotations = annotations.map(a => { 42 | if (!a.type) { 43 | a.type = type 44 | } 45 | if (!a.disable) { 46 | a.disable = disable 47 | } 48 | return new Annotation(a) 49 | }) 50 | 51 | collection = 52 | collection || 53 | new AnnotationCollection({ 54 | annotations: translatedAnnotations, 55 | accessors, 56 | accessorsInverse, 57 | ids 58 | }) 59 | 60 | const annotationG = selection.selectAll("g").data([collection]) 61 | annotationG.enter().append("g").attr("class", `annotations`) 62 | 63 | const group = selection.select("g.annotations") 64 | newWithClass(group, collection.annotations, "g", "annotation") 65 | 66 | const annotation = group.selectAll("g.annotation") 67 | 68 | annotation.each(function(d) { 69 | const a = select(this) 70 | 71 | a.attr("class", "annotation") 72 | 73 | newWithClass(a, [d], "g", "annotation-connector") 74 | newWithClass(a, [d], "g", "annotation-subject") 75 | newWithClass(a, [d], "g", "annotation-note") 76 | newWithClass( 77 | a.select("g.annotation-note"), 78 | [d], 79 | "g", 80 | "annotation-note-content" 81 | ) 82 | d.type = 83 | d.type.toString() === "[object Object]" 84 | ? d.type 85 | : new d.type({ 86 | a, 87 | annotation: d, 88 | textWrap, 89 | notePadding, 90 | editMode, 91 | dispatcher: annotationDispatcher, 92 | accessors 93 | }) 94 | d.type.draw() 95 | d.type.drawText && d.type.drawText() 96 | }) 97 | } 98 | 99 | annotation.json = function() { 100 | /* eslint-disable no-console */ 101 | console.log( 102 | "Annotations JSON was copied to your clipboard. Please note the annotation type is not JSON compatible. It appears in the objects array in the console, but not in the copied JSON.", 103 | collection.json 104 | ) 105 | /* eslint-enable no-console */ 106 | window.copy( 107 | JSON.stringify( 108 | collection.json.map(a => { 109 | delete a.type 110 | return a 111 | }) 112 | ) 113 | ) 114 | return annotation 115 | } 116 | 117 | annotation.update = function() { 118 | if (annotations && collection) { 119 | annotations = collection.annotations.map(a => { 120 | a.type.draw() 121 | return a 122 | }) 123 | } 124 | return annotation 125 | } 126 | 127 | annotation.updateText = function() { 128 | if (collection) { 129 | collection.updateText(textWrap) 130 | annotations = collection.annotations 131 | } 132 | return annotation 133 | } 134 | 135 | annotation.updatedAccessors = function() { 136 | collection.setPositionWithAccessors() 137 | annotations = collection.annotations 138 | return annotation 139 | } 140 | 141 | annotation.disable = function(_) { 142 | if (!arguments.length) return disable 143 | disable = _ 144 | if (collection) { 145 | collection.updateDisable(disable) 146 | annotations = collection.annotations 147 | } 148 | return annotation 149 | } 150 | 151 | annotation.textWrap = function(_) { 152 | if (!arguments.length) return textWrap 153 | textWrap = _ 154 | if (collection) { 155 | collection.updateTextWrap(textWrap) 156 | annotations = collection.annotations 157 | } 158 | return annotation 159 | } 160 | 161 | annotation.notePadding = function(_) { 162 | if (!arguments.length) return notePadding 163 | notePadding = _ 164 | if (collection) { 165 | collection.updateNotePadding(notePadding) 166 | annotations = collection.annotations 167 | } 168 | return annotation 169 | } 170 | //todo think of how to handle when undefined is sent 171 | annotation.type = function(_, settings) { 172 | if (!arguments.length) return type 173 | type = _ 174 | if (collection) { 175 | collection.annotations.map(a => { 176 | a.type.note && 177 | a.type.note.selectAll("*:not(.annotation-note-content)").remove() 178 | a.type.noteContent && a.type.noteContent.selectAll("*").remove() 179 | a.type.subject && a.type.subject.selectAll("*").remove() 180 | a.type.connector && a.type.connector.selectAll("*").remove() 181 | a.type.typeSettings = {} 182 | a.type = type 183 | 184 | a.subject = settings && settings.subject || a.subject 185 | a.connector = settings && settings.connector || a.connector 186 | a.note = settings && settings.note || a.note 187 | }) 188 | 189 | annotations = collection.annotations 190 | } 191 | return annotation 192 | } 193 | 194 | annotation.annotations = function(_) { 195 | if (!arguments.length) 196 | return collection && collection.annotations || annotations 197 | annotations = _ 198 | 199 | if (collection && collection.annotations) { 200 | const rerun = annotations.some( 201 | d => !d.type || d.type.toString() !== "[object Object]" 202 | ) 203 | 204 | if (rerun) { 205 | collection = null 206 | annotation(sel) 207 | } else { 208 | collection.annotations = annotations 209 | } 210 | } 211 | return annotation 212 | } 213 | 214 | annotation.context = function(_) { 215 | if (!arguments.length) return context 216 | context = _ 217 | return annotation 218 | } 219 | 220 | annotation.accessors = function(_) { 221 | if (!arguments.length) return accessors 222 | accessors = _ 223 | return annotation 224 | } 225 | 226 | annotation.accessorsInverse = function(_) { 227 | if (!arguments.length) return accessorsInverse 228 | accessorsInverse = _ 229 | return annotation 230 | } 231 | 232 | annotation.ids = function(_) { 233 | if (!arguments.length) return ids 234 | ids = _ 235 | return annotation 236 | } 237 | 238 | annotation.editMode = function(_) { 239 | if (!arguments.length) return editMode 240 | editMode = _ 241 | 242 | if (sel) { 243 | sel.selectAll("g.annotation").classed("editable", editMode) 244 | } 245 | 246 | if (collection) { 247 | collection.editMode(editMode) 248 | annotations = collection.annotations 249 | } 250 | return annotation 251 | } 252 | 253 | annotation.collection = function(_) { 254 | if (!arguments.length) return collection 255 | collection = _ 256 | return annotation 257 | } 258 | 259 | annotation.on = function() { 260 | const value = annotationDispatcher.on.apply(annotationDispatcher, arguments) 261 | return value === annotationDispatcher ? annotation : value 262 | } 263 | 264 | return annotation 265 | } 266 | -------------------------------------------------------------------------------- /src/Annotation.js: -------------------------------------------------------------------------------- 1 | export default class Annotation { 2 | constructor({ 3 | x = 0, 4 | y = 0, 5 | nx, 6 | ny, 7 | dy = 0, 8 | dx = 0, 9 | color = "grey", 10 | data, 11 | type, 12 | subject, 13 | connector, 14 | note, 15 | disable, 16 | id, 17 | className 18 | }) { 19 | this._dx = nx !== undefined ? nx - x : dx 20 | this._dy = ny !== undefined ? ny - y : dy 21 | this._x = x 22 | this._y = y 23 | this._color = color 24 | this.id = id 25 | this._className = className || "" 26 | 27 | this._type = type || "" 28 | this.data = data 29 | 30 | this.note = note || {} 31 | this.connector = connector || {} 32 | this.subject = subject || {} 33 | 34 | this.disable = disable || [] 35 | } 36 | 37 | updatePosition() { 38 | if (this.type.setPosition) { 39 | this.type.setPosition() 40 | if ( 41 | this.type.subject && 42 | this.type.subject.selectAll(":not(.handle)").nodes().length !== 0 43 | ) { 44 | this.type.redrawSubject() 45 | } 46 | } 47 | } 48 | 49 | clearComponents() { 50 | this.type.clearComponents && this.type.clearComponents() 51 | } 52 | 53 | get className() { 54 | return this._className 55 | } 56 | 57 | set className(className) { 58 | this._className = className 59 | if (this.type.setClassName) this.type.setClassName() 60 | } 61 | 62 | updateOffset() { 63 | if (this.type.setOffset) { 64 | this.type.setOffset() 65 | 66 | if (this.type.connector.selectAll(":not(.handle)").nodes().length !== 0) { 67 | this.type.redrawConnector() 68 | } 69 | 70 | this.type.redrawNote() 71 | } 72 | } 73 | 74 | get type() { 75 | return this._type 76 | } 77 | 78 | set type(type) { 79 | this._type = type 80 | this.clearComponents() 81 | } 82 | 83 | get x() { 84 | return this._x 85 | } 86 | set x(x) { 87 | this._x = x 88 | this.updatePosition() 89 | } 90 | 91 | get y() { 92 | return this._y 93 | } 94 | set y(y) { 95 | this._y = y 96 | this.updatePosition() 97 | } 98 | 99 | get color() { 100 | return this._color 101 | } 102 | set color(color) { 103 | this._color = color 104 | this.updatePosition() 105 | } 106 | 107 | get dx() { 108 | return this._dx 109 | } 110 | set dx(dx) { 111 | this._dx = dx 112 | this.updateOffset() 113 | } 114 | 115 | get dy() { 116 | return this._dy 117 | } 118 | set dy(dy) { 119 | this._dy = dy 120 | this.updateOffset() 121 | } 122 | 123 | set nx(nx) { 124 | this._dx = nx - this._x 125 | this.updateOffset() 126 | } 127 | 128 | set ny(ny) { 129 | this._dy = ny - this._y 130 | this.updateOffset() 131 | } 132 | 133 | get offset() { 134 | return { x: this._dx, y: this._dy } 135 | } 136 | 137 | set offset({ x, y }) { 138 | this._dx = x 139 | this._dy = y 140 | this.updateOffset() 141 | } 142 | 143 | get position() { 144 | return { x: this._x, y: this._y } 145 | } 146 | 147 | set position({ x, y }) { 148 | this._x = x 149 | this._y = y 150 | this.updatePosition() 151 | } 152 | 153 | get translation() { 154 | return { 155 | x: this._x + this._dx, 156 | y: this._y + this._dy 157 | } 158 | } 159 | 160 | get json() { 161 | const json = { 162 | x: this._x, 163 | y: this._y, 164 | dx: this._dx, 165 | dy: this._dy 166 | } 167 | 168 | if (this.data && Object.keys(this.data).length > 0) json.data = this.data 169 | if (this.type) json.type = this.type 170 | if (this._className) json.className = this._className 171 | 172 | if (Object.keys(this.connector).length > 0) json.connector = this.connector 173 | if (Object.keys(this.subject).length > 0) json.subject = this.subject 174 | if (Object.keys(this.note).length > 0) json.note = this.note 175 | 176 | return json 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/AnnotationCollection.js: -------------------------------------------------------------------------------- 1 | export default class AnnotationCollection { 2 | 3 | constructor ({ annotations, accessors, accessorsInverse }) { 4 | this.accessors = accessors 5 | this.accessorsInverse = accessorsInverse 6 | this.annotations = annotations 7 | } 8 | 9 | clearTypes (newSettings) { 10 | this.annotations.forEach(d => { 11 | d.type = undefined 12 | d.subject = newSettings && newSettings.subject || d.subject 13 | d.connector = newSettings && newSettings.connector || d.connector 14 | d.note = newSettings && newSettings.note || d.note 15 | }) 16 | } 17 | 18 | setPositionWithAccessors () { 19 | this.annotations.forEach(d => { 20 | d.type.setPositionWithAccessors(this.accessors) 21 | }) 22 | } 23 | 24 | editMode (editMode) { this.annotations.forEach(a => { 25 | if (a.type) { 26 | a.type.editMode = editMode 27 | a.type.updateEditMode() 28 | } 29 | }) 30 | } 31 | 32 | updateDisable (disable) { 33 | this.annotations.forEach(a => { 34 | a.disable = disable 35 | if (a.type) { 36 | disable.forEach(d => { 37 | if (a.type[d]) { 38 | a.type[d].remove && a.type[d].remove() 39 | a.type[d] = undefined 40 | } 41 | }) 42 | } 43 | }) 44 | } 45 | 46 | updateTextWrap (textWrap) { 47 | this.annotations.forEach(a => { 48 | if (a.type && a.type.updateTextWrap) { 49 | a.type.updateTextWrap(textWrap) 50 | } 51 | }) 52 | } 53 | 54 | updateText () { 55 | this.annotations.forEach(a => { 56 | if (a.type && a.type.drawText) { 57 | a.type.drawText() 58 | } 59 | }) 60 | } 61 | 62 | updateNotePadding (notePadding) { 63 | this.annotations.forEach(a => { 64 | if (a.type) { 65 | a.type.notePadding = notePadding 66 | } 67 | }) 68 | } 69 | 70 | get json () { 71 | return this.annotations.map(a => { 72 | const json = a.json 73 | if (this.accessorsInverse && a.data) { 74 | json.data = {} 75 | Object.keys(this.accessorsInverse).forEach(k => { 76 | json.data[k] = this.accessorsInverse[k]({ x: a.x, y: a.y}) 77 | 78 | //TODO make this feasible to map back to data for other types of subjects 79 | }) 80 | } 81 | return json 82 | }) 83 | } 84 | 85 | get noteNodes () { 86 | return this.annotations.map(a => ({ ...a.type.getNoteBBoxOffset(), positionX: a.x, positionY: a.y })) 87 | } 88 | 89 | //TODO: come back and rethink if a.x and a.y are applicable in all situations 90 | // get connectorNodes() { 91 | // return this.annotations.map(a => ({ ...a.type.getConnectorBBox(), startX: a.x, startY: a.y})) 92 | // } 93 | 94 | // get subjectNodes() { 95 | // return this.annotations.map(a => ({ ...a.type.getSubjectBBox(), startX: a.x, startY: a.y})) 96 | // } 97 | 98 | // get annotationNodes() { 99 | // return this.annotations.map(a => ({ ...a.type.getAnnotationBBox(), startX: a.x, startY: a.y})) 100 | // } 101 | } 102 | -------------------------------------------------------------------------------- /src/Builder.js: -------------------------------------------------------------------------------- 1 | import { line, arc, curveLinear } from "d3-shape" 2 | 3 | export const lineBuilder = ({ data, curve=curveLinear, canvasContext, className, classID }) => { 4 | const lineGen = line() 5 | .curve(curve) 6 | 7 | const builder = { 8 | type: 'path', 9 | className, 10 | classID, 11 | data 12 | } 13 | 14 | if (canvasContext) { 15 | lineGen.context(canvasContext) 16 | builder.pathMethods = lineGen 17 | 18 | } else { 19 | builder.attrs = { 20 | d: lineGen(data) 21 | } 22 | } 23 | 24 | return builder 25 | } 26 | 27 | export const arcBuilder = ({ data, canvasContext, className, classID }) => { 28 | 29 | const builder = { 30 | type: 'path', 31 | className, 32 | classID, 33 | data 34 | } 35 | 36 | const arcShape = arc() 37 | .innerRadius(data.innerRadius || 0) 38 | .outerRadius(data.outerRadius || data.radius || 2) 39 | .startAngle(data.startAngle || 0) 40 | .endAngle(data.endAngle || 2*Math.PI) 41 | 42 | if (canvasContext) { 43 | arcShape.context(canvasContext) 44 | builder.pathMethods = lineGen 45 | 46 | } else { 47 | 48 | builder.attrs = { 49 | d: arcShape() 50 | } 51 | } 52 | 53 | return builder 54 | } -------------------------------------------------------------------------------- /src/Connector/end-arrow.js: -------------------------------------------------------------------------------- 1 | import { lineBuilder } from "../Builder" 2 | 3 | export default ({ annotation, start, end, scale = 1 }) => { 4 | const offset = annotation.position 5 | if (!start) { 6 | start = [annotation.dx, annotation.dy] 7 | } else { 8 | start = [-end[0] + start[0], -end[1] + start[1]] 9 | } 10 | if (!end) { 11 | end = [annotation.x - offset.x, annotation.y - offset.y] 12 | } 13 | 14 | let x1 = end[0], 15 | y1 = end[1] 16 | 17 | let dx = start[0] 18 | let dy = start[1] 19 | 20 | let size = 10 * scale 21 | let angleOffset = 16 / 180 * Math.PI 22 | let angle = Math.atan(dy / dx) 23 | 24 | if (dx < 0) { 25 | angle += Math.PI 26 | } 27 | 28 | const data = [ 29 | [x1, y1], 30 | [ 31 | Math.cos(angle + angleOffset) * size + x1, 32 | Math.sin(angle + angleOffset) * size + y1 33 | ], 34 | [ 35 | Math.cos(angle - angleOffset) * size + x1, 36 | Math.sin(angle - angleOffset) * size + y1 37 | ], 38 | [x1, y1] 39 | ] 40 | 41 | //TODO add in reverse 42 | // if (canvasContext.arrowReverse){ 43 | // data = [[x1, y1], 44 | // [Math.cos(angle + angleOffset)*size, Math.sin(angle + angleOffset)*size], 45 | // [Math.cos(angle - angleOffset)*size, Math.sin(angle - angleOffset)*size], 46 | // [x1, y1] 47 | // ] 48 | // } else { 49 | // data = [[x1, y1], 50 | // [Math.cos(angle + angleOffset)*size, Math.sin(angle + angleOffset)*size], 51 | // [Math.cos(angle - angleOffset)*size, Math.sin(angle - angleOffset)*size], 52 | // [x1, y1] 53 | // ] 54 | // } 55 | 56 | return { 57 | components: [ 58 | lineBuilder({ 59 | data, 60 | className: "connector-end connector-arrow", 61 | classID: "connector-end" 62 | }) 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Connector/end-dot.js: -------------------------------------------------------------------------------- 1 | import { arcBuilder } from "../Builder" 2 | 3 | export default ({ line, scale = 1 }) => { 4 | let dot = arcBuilder({ 5 | className: "connector-end connector-dot", 6 | classID: "connector-end", 7 | data: { radius: 3 * Math.sqrt(scale) } 8 | }) 9 | dot.attrs.transform = `translate(${line.data[0][0]}, ${line.data[0][1]})` 10 | 11 | return { components: [dot] } 12 | } 13 | -------------------------------------------------------------------------------- /src/Connector/type-curve.js: -------------------------------------------------------------------------------- 1 | import { lineBuilder } from '../Builder' 2 | import { event } from 'd3-selection' 3 | import { lineSetup } from './type-line' 4 | import { curveCatmullRom } from 'd3-shape' 5 | import { pointHandle } from '../Handles' 6 | 7 | export default ({ type, connectorData, subjectType }) => { 8 | 9 | if (!connectorData) { connectorData = {} } 10 | if (!connectorData.points || typeof connectorData.points === "number") { 11 | connectorData.points = createPoints(type.annotation.offset, connectorData.points) 12 | } 13 | if (!connectorData.curve) { connectorData.curve = curveCatmullRom } 14 | 15 | let handles = [] 16 | 17 | if (type.editMode) { 18 | const cHandles = connectorData.points 19 | .map((c,i) => ({...pointHandle({cx: c[0], cy: c[1]}), index: i})) 20 | 21 | const updatePoint = (index) => { 22 | connectorData.points[index][0] += event.dx 23 | connectorData.points[index][1] += event.dy 24 | type.redrawConnector() 25 | } 26 | 27 | handles = type.mapHandles(cHandles 28 | .map(h => ({ ...h.move, drag: updatePoint.bind(type, h.index)}))) 29 | 30 | } 31 | 32 | let data = lineSetup({ type, subjectType }) 33 | data = [data[0], ...connectorData.points, data[1]] 34 | const components = [lineBuilder({ data, curve: connectorData.curve, className: "connector" })] 35 | 36 | return { components , handles } 37 | } 38 | 39 | const createPoints = function ( offset, anchors=2) { 40 | const diff = { x: offset.x/(anchors + 1), y: offset.y/(anchors + 1) } 41 | const p = [] 42 | 43 | let i = 1 44 | for (; i <= anchors; i++) { 45 | p.push([diff.x*i + i%2*20, diff.y*i - i%2*20]) 46 | } 47 | return p 48 | } -------------------------------------------------------------------------------- /src/Connector/type-elbow.js: -------------------------------------------------------------------------------- 1 | import { lineBuilder } from '../Builder' 2 | 3 | export default ({ type, subjectType }) => { 4 | 5 | const annotation = type.annotation 6 | const offset = annotation.position 7 | 8 | let x1 = annotation.x - offset.x, 9 | x2 = x1 + annotation.dx, 10 | y1 = annotation.y - offset.y, 11 | y2 = y1 + annotation.dy 12 | 13 | const subjectData = annotation.subject 14 | 15 | if (subjectType === "rect") { 16 | const { width, height } = subjectData 17 | 18 | if (width > 0 && annotation.dx > 0 || width < 0 && annotation.dx < 0) { 19 | if (Math.abs(width) > Math.abs(annotation.dx)) x1 = width/2 20 | else x1 = width 21 | } 22 | if (height > 0 && annotation.dy > 0 || height < 0 && annotation.dy < 0) { 23 | if (Math.abs(height) > Math.abs(annotation.dy)) y1 = height/2 24 | else y1 = height 25 | } 26 | if (x1 === width/2 && y1 === height/2) { x1 = x2; y1 = y2;} 27 | } 28 | 29 | let data = [[x1, y1], [x2, y2]] 30 | 31 | let diffY = y2 - y1 32 | let diffX = x2 - x1 33 | let xe = x2 34 | let ye = y2 35 | let opposite = y2 < y1 && x2 > x1 || x2 < x1 && y2 > y1 ? -1 : 1 36 | 37 | if (Math.abs(diffX) < Math.abs(diffY)) { 38 | xe = x2 39 | ye = y1 + diffX*opposite 40 | } else { 41 | ye = y2 42 | xe = x1 + diffY*opposite 43 | } 44 | 45 | if (subjectType === "circle" && (subjectData.outerRadius || subjectData.radius)) { 46 | const r = (subjectData.outerRadius || subjectData.radius) + (subjectData.radiusPadding || 0) 47 | const length = r/Math.sqrt(2) 48 | 49 | if (Math.abs(diffX) > length && Math.abs(diffY) > length) { 50 | x1 = length*(x2 < 0 ? -1 : 1) 51 | y1 = length*(y2 < 0 ? -1 : 1) 52 | data = [[x1, y1], [xe , ye ], [x2, y2]] 53 | 54 | } else if (Math.abs(diffX) > Math.abs(diffY)) { 55 | const angle = Math.asin(-y2/r) 56 | x1 = Math.abs(Math.cos(angle)*r)*(x2 < 0 ? -1 : 1) 57 | data = [[ x1, y2], [x2, y2]] 58 | } else { 59 | const angle = Math.acos(x2/r) 60 | y1 = Math.abs(Math.sin(angle)*r)*(y2 < 0 ? -1 : 1) 61 | data = [[ x2, y1], [x2, y2]] 62 | } 63 | } else { 64 | data = [[x1, y1], [xe , ye], [x2, y2]] 65 | } 66 | 67 | return { components: [lineBuilder({ data, className: "connector"})]} 68 | } 69 | -------------------------------------------------------------------------------- /src/Connector/type-line.js: -------------------------------------------------------------------------------- 1 | import { lineBuilder } from '../Builder' 2 | 3 | export const lineSetup = ({ type, subjectType }) => { 4 | let annotation = type.annotation 5 | let offset = annotation.position 6 | 7 | let x1 = annotation.x - offset.x, 8 | x2 = x1 + annotation.dx, 9 | y1 = annotation.y - offset.y, 10 | y2 = y1 + annotation.dy 11 | 12 | 13 | const subjectData = annotation.subject 14 | 15 | if (subjectType === "circle" && (subjectData.outerRadius || subjectData.radius)) { 16 | const h = Math.sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2)) 17 | const angle = Math.asin(-y2/h) 18 | const r = subjectData.outerRadius || subjectData.radius + (subjectData.radiusPadding || 0) 19 | 20 | x1 = Math.abs(Math.cos(angle)*r)*(x2 < 0 ? -1 : 1) 21 | y1 = Math.abs(Math.sin(angle)*r)*(y2 < 0 ? -1 : 1) 22 | 23 | } 24 | 25 | if (subjectType === "rect") { 26 | const { width, height } = subjectData 27 | 28 | if (width > 0 && annotation.dx > 0 || width < 0 && annotation.dx < 0) { 29 | if (Math.abs(width) > Math.abs(annotation.dx)) x1 = width/2 30 | else x1 = width 31 | } 32 | if (height > 0 && annotation.dy > 0 || height < 0 && annotation.dy < 0) { 33 | if (Math.abs(height) > Math.abs(annotation.dy)) y1 = height/2 34 | else y1 = height 35 | } 36 | if (x1 === width/2 && y1 === height/2) { x1 = x2; y1 = y2;} 37 | } 38 | 39 | 40 | return [[x1, y1], [x2, y2]] 41 | } 42 | 43 | export default (connectorData) => { 44 | const data = lineSetup(connectorData) 45 | return { components: [lineBuilder({ data, className : "connector" })]} 46 | } -------------------------------------------------------------------------------- /src/Handles.js: -------------------------------------------------------------------------------- 1 | import { select } from "d3-selection" 2 | import { drag } from "d3-drag" 3 | 4 | export const pointHandle = ({ cx = 0, cy = 0 }) => { 5 | return { move: { x: cx, y: cy } } 6 | } 7 | 8 | export const circleHandles = ({ cx = 0, cy = 0, r1, r2, padding }) => { 9 | const h = { move: { x: cx, y: cy } } 10 | 11 | if (r1 !== undefined) { 12 | h.r1 = { x: cx + r1 / Math.sqrt(2), y: cy + r1 / Math.sqrt(2) } 13 | } 14 | 15 | if (r2 !== undefined) { 16 | h.r2 = { x: cx + r2 / Math.sqrt(2), y: cy + r2 / Math.sqrt(2) } 17 | } 18 | 19 | if (padding !== undefined) { 20 | h.padding = { x: cx + r1 + padding, y: cy } 21 | } 22 | 23 | return h 24 | } 25 | 26 | export const rectHandles = ({ 27 | x1 = 0, 28 | y1 = 0, 29 | x2 = x1, 30 | y2 = y1, 31 | width, 32 | height 33 | }) => { 34 | const w = width || Math.abs(x2 - x1) 35 | const h = height || Math.abs(y2 - y1) 36 | 37 | return { 38 | move: { 39 | x: Math.min(x1, x2) + w / 2, 40 | y: Math.min(y1, y2) - 10 41 | }, 42 | width: { 43 | x: Math.max(x1, x2), 44 | y: Math.min(y1, y2) + h / 2 45 | }, 46 | height: { 47 | x: Math.min(x1, x2) + w / 2, 48 | y: Math.max(y1, y2) 49 | } 50 | } 51 | } 52 | 53 | export const lineHandles = ({ x1, y1, x2, y2, x, y }) => { 54 | const minY = Math.min(y1, y2) 55 | const minX = Math.min(x1, x2) 56 | 57 | const height = Math.abs(y2 - y1) 58 | const width = Math.abs(x2 - x1) 59 | 60 | return { 61 | move: { 62 | x: x || minX + width / 2, 63 | y: y || minY + height / 2 64 | } 65 | } 66 | } 67 | 68 | //arc handles 69 | export const addHandles = ({ group, handles, r = 10 }) => { 70 | //give it a group and x,y to draw handles 71 | //then give it instructions on what the handles change 72 | const h = group.selectAll("circle.handle").data(handles) 73 | 74 | h 75 | .enter() 76 | .append("circle") 77 | .attr("class", "handle") 78 | .attr("fill", "grey") 79 | .attr("fill-opacity", 0.1) 80 | .attr("cursor", "move") 81 | .attr("stroke-dasharray", 5) 82 | .attr("stroke", "grey") 83 | .call( 84 | drag() 85 | .container(select("g.annotations").node()) 86 | .on("start", d => d.start && d.start(d)) 87 | .on("drag", d => d.drag && d.drag(d)) 88 | .on("end", d => d.end && d.end(d)) 89 | ) 90 | 91 | group 92 | .selectAll("circle.handle") 93 | .attr("cx", d => d.x) 94 | .attr("cy", d => d.y) 95 | .attr("r", d => d.r || r) 96 | .attr("class", d => `handle ${d.className || ""}`) 97 | 98 | h.exit().remove() 99 | } 100 | -------------------------------------------------------------------------------- /src/Note/alignment.js: -------------------------------------------------------------------------------- 1 | 2 | export const leftRightDynamic = (align, y) => { 3 | if (align === "dynamic" || align === "left" || align === "right") { 4 | if (y < 0) { align = "top" } 5 | else { align = "bottom" } 6 | } 7 | return align 8 | } 9 | 10 | export const topBottomDynamic = (align, x) => { 11 | if (align === "dynamic" || align === "top" || align === "bottom") { 12 | if (x < 0) { align = "right" } 13 | else { align = "left" } 14 | } 15 | return align 16 | } 17 | 18 | const orientationTopBottom = ["topBottom", "top", "bottom"] 19 | const orientationLeftRight = ["leftRight", "left", "right"] 20 | 21 | export default ({ padding=0, bbox={x:0, y:0, width:0, height:0}, align, orientation, offset={x:0, y:0} }) => { 22 | let x = -bbox.x 23 | let y = 0//-bbox.y 24 | if ( orientationTopBottom.indexOf(orientation) !== -1 ) { 25 | align = topBottomDynamic(align, offset.x) 26 | if (offset.y < 0 && orientation === "topBottom" || orientation === "top") { 27 | y -= bbox.height + padding 28 | } else { 29 | y += padding 30 | } 31 | 32 | if ( align === "middle" ) { 33 | x -= bbox.width/2 34 | } else if (align === "right" ) { 35 | x -= bbox.width 36 | } 37 | 38 | } else if ( orientationLeftRight.indexOf(orientation) !== -1 ) { 39 | align = leftRightDynamic(align, offset.y) 40 | if (offset.x < 0 && orientation === "leftRight" || orientation === "left") { 41 | x -= bbox.width + padding 42 | } else { 43 | x += padding 44 | } 45 | 46 | if ( align === "middle" ) { 47 | y -= bbox.height/2 48 | } else if (align === "top" ) { 49 | y -= bbox.height 50 | } 51 | } 52 | 53 | return { x, y } 54 | } -------------------------------------------------------------------------------- /src/Note/lineType-horizontal.js: -------------------------------------------------------------------------------- 1 | import { lineBuilder } from '../Builder' 2 | import { topBottomDynamic } from './alignment' 3 | 4 | export default ({ align, x=0, y=0, offset, bbox }) => { 5 | align = topBottomDynamic(align, offset.x) 6 | 7 | if (align === "right") { x -= bbox.width } 8 | else if (align === "middle") { x -= bbox.width/2 } 9 | 10 | const data = [[x, y], [x + bbox.width, y]] 11 | return { components: [lineBuilder({ data, className : "note-line" })] } 12 | } -------------------------------------------------------------------------------- /src/Note/lineType-vertical.js: -------------------------------------------------------------------------------- 1 | import { lineBuilder } from '../Builder' 2 | import { leftRightDynamic } from './alignment' 3 | 4 | export default ({ align, x=0, y=0, bbox, offset }) => { 5 | align = leftRightDynamic(align, offset.y) 6 | 7 | if (align === "top") { y -= bbox.height } 8 | else if (align === "middle") { y -= bbox.height/2 } 9 | 10 | const data = [[x, y], [x, y + bbox.height]] 11 | return { components: [lineBuilder({ data, className : "note-line" })] } 12 | } -------------------------------------------------------------------------------- /src/Note/type-text.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default ({ noteData }) => { 4 | 5 | let components = [] 6 | 7 | if (noteData.text) { 8 | text = { 9 | type: "text", 10 | className: "noteText", 11 | attrs: { 12 | text: noteData.text, 13 | dy: "1.1em" 14 | } 15 | } 16 | } 17 | 18 | if (noteData.title) { 19 | title = { 20 | type: "text", 21 | className: "noteText", 22 | attrs: { 23 | text: noteData.text, 24 | dy: "1em" 25 | } 26 | } 27 | 28 | } 29 | 30 | return { components } 31 | } -------------------------------------------------------------------------------- /src/Subject/badge.js: -------------------------------------------------------------------------------- 1 | import { lineBuilder, arcBuilder } from "../Builder" 2 | import { event } from "d3-selection" 3 | 4 | export default ({ subjectData = {}, type = {} }, annotation = {}) => { 5 | const typeSettings = type.typeSettings && type.typeSettings.subject 6 | 7 | if (!subjectData.radius) { 8 | if (typeSettings && typeSettings.radius) { 9 | subjectData.radius = typeSettings.radius 10 | } else { 11 | subjectData.radius = 14 12 | } 13 | } 14 | if (!subjectData.x) { 15 | if (typeSettings && typeSettings.x) { 16 | subjectData.x = typeSettings.x 17 | } 18 | } 19 | if (!subjectData.y) { 20 | if (typeSettings && typeSettings.y) { 21 | subjectData.y = typeSettings.y 22 | } 23 | } 24 | 25 | let handles = [] 26 | const components = [] 27 | const radius = subjectData.radius 28 | const innerRadius = radius * 0.7 29 | let x = 0 30 | let y = 0 31 | 32 | const notCornerOffset = Math.sqrt(2) * radius 33 | const placement = { 34 | xleftcorner: -radius, 35 | xrightcorner: radius, 36 | ytopcorner: -radius, 37 | ybottomcorner: radius, 38 | xleft: -notCornerOffset, 39 | xright: notCornerOffset, 40 | ytop: -notCornerOffset, 41 | ybottom: notCornerOffset 42 | } 43 | 44 | if (subjectData.x && !subjectData.y) { 45 | x = placement[`x${subjectData.x}`] 46 | } else if (subjectData.y && !subjectData.x) { 47 | y = placement[`y${subjectData.y}`] 48 | } else if (subjectData.x && subjectData.y) { 49 | x = placement[`x${subjectData.x}corner`] 50 | y = placement[`y${subjectData.y}corner`] 51 | } 52 | 53 | const transform = `translate(${x}, ${y})` 54 | const circlebg = arcBuilder({ className: "subject", data: { radius } }) 55 | circlebg.attrs.transform = transform 56 | circlebg.attrs.fill = annotation.color 57 | circlebg.attrs["stroke-linecap"] = "round" 58 | circlebg.attrs["stroke-width"] = "3px" 59 | 60 | const circle = arcBuilder({ 61 | className: "subject-ring", 62 | data: { outerRadius: radius, innerRadius } 63 | }) 64 | 65 | circle.attrs.transform = transform 66 | // circle.attrs.fill = annotation.color 67 | circle.attrs["stroke-width"] = "3px" 68 | circle.attrs.fill = "white" 69 | 70 | let pointer 71 | if (x && y || !x && !y) { 72 | pointer = lineBuilder({ 73 | className: "subject-pointer", 74 | data: [[0, 0], [x || 0, 0], [0, y || 0], [0, 0]] 75 | }) 76 | } else if (x || y) { 77 | const notCornerPointerXY = (v, sign = 1) => 78 | v && v / Math.sqrt(2) / Math.sqrt(2) || sign * radius / Math.sqrt(2) 79 | 80 | pointer = lineBuilder({ 81 | className: "subject-pointer", 82 | data: [ 83 | [0, 0], 84 | [notCornerPointerXY(x), notCornerPointerXY(y)], 85 | [notCornerPointerXY(x, -1), notCornerPointerXY(y, -1)], 86 | [0, 0] 87 | ] 88 | }) 89 | } 90 | 91 | if (pointer) { 92 | pointer.attrs.fill = annotation.color 93 | pointer.attrs["stroke-linecap"] = "round" 94 | pointer.attrs["stroke-width"] = "3px" 95 | components.push(pointer) 96 | } 97 | 98 | if (type.editMode) { 99 | const dragBadge = () => { 100 | subjectData.x = 101 | event.x < -radius * 2 102 | ? "left" 103 | : event.x > radius * 2 ? "right" : undefined 104 | subjectData.y = 105 | event.y < -radius * 2 106 | ? "top" 107 | : event.y > radius * 2 ? "bottom" : undefined 108 | 109 | type.redrawSubject() 110 | } 111 | 112 | const bHandles = { x: x * 2, y: y * 2, drag: dragBadge.bind(type) } 113 | if (!bHandles.x && !bHandles.y) { 114 | bHandles.y = -radius 115 | } 116 | 117 | handles = type.mapHandles([bHandles]) 118 | } 119 | 120 | let text 121 | if (subjectData.text) { 122 | text = { 123 | type: "text", 124 | className: "badge-text", 125 | attrs: { 126 | fill: "white", 127 | stroke: "none", 128 | "font-size": ".7em", 129 | text: subjectData.text, 130 | "text-anchor": "middle", 131 | dy: ".25em", 132 | x, 133 | y 134 | } 135 | } 136 | } 137 | 138 | components.push(circlebg) 139 | components.push(circle) 140 | components.push(text) 141 | 142 | return { components, handles } 143 | } 144 | -------------------------------------------------------------------------------- /src/Subject/circle.js: -------------------------------------------------------------------------------- 1 | import { circleHandles } from "../Handles" 2 | import { arcBuilder } from "../Builder" 3 | import { event } from "d3-selection" 4 | 5 | export default ({ subjectData, type }) => { 6 | if (!subjectData.radius && !subjectData.outerRadius) { 7 | subjectData.radius = 20 8 | } 9 | 10 | let handles = [] 11 | const c = arcBuilder({ data: subjectData, className: "subject" }) 12 | if (type.editMode) { 13 | const h = circleHandles({ 14 | r1: c.data.outerRadius || c.data.radius, 15 | r2: c.data.innerRadius, 16 | padding: subjectData.radiusPadding 17 | }) 18 | 19 | const updateRadius = attr => { 20 | const r = subjectData[attr] + event.dx * Math.sqrt(2) 21 | subjectData[attr] = r 22 | type.redrawSubject() 23 | type.redrawConnector() 24 | } 25 | 26 | const cHandles = [ 27 | { 28 | ...h.r1, 29 | drag: updateRadius.bind( 30 | type, 31 | subjectData.outerRadius !== undefined ? "outerRadius" : "radius" 32 | ) 33 | } 34 | ] 35 | 36 | if (subjectData.innerRadius) { 37 | cHandles.push({ ...h.r2, drag: updateRadius.bind(type, "innerRadius") }) 38 | } 39 | handles = type.mapHandles(cHandles) 40 | } 41 | 42 | c.attrs["fill-opacity"] = 0 43 | 44 | return { components: [c], handles } 45 | } 46 | -------------------------------------------------------------------------------- /src/Subject/rect.js: -------------------------------------------------------------------------------- 1 | import { lineBuilder } from "../Builder" 2 | import { event } from "d3-selection" 3 | 4 | export default ({ subjectData, type }) => { 5 | if (!subjectData.width) { 6 | subjectData.width = 100 7 | } 8 | if (!subjectData.height) { 9 | subjectData.height = 100 10 | } 11 | 12 | let handles = [] 13 | let { width, height } = subjectData 14 | 15 | const data = [[0, 0], [width, 0], [width, height], [0, height], [0, 0]] 16 | let rect = lineBuilder({ data, className: "subject" }) 17 | 18 | if (type.editMode) { 19 | const updateWidth = () => { 20 | subjectData.width = event.x 21 | type.redrawSubject() 22 | type.redrawConnector() 23 | } 24 | 25 | const updateHeight = () => { 26 | subjectData.height = event.y 27 | type.redrawSubject() 28 | type.redrawConnector() 29 | } 30 | 31 | const rHandles = [ 32 | { x: width, y: height / 2, drag: updateWidth.bind(type) }, 33 | { x: width / 2, y: height, drag: updateHeight.bind(type) } 34 | ] 35 | 36 | handles = type.mapHandles(rHandles) 37 | } 38 | rect.attrs["fill-opacity"] = 0.1 39 | return { components: [rect], handles } 40 | } 41 | -------------------------------------------------------------------------------- /src/Subject/threshold.js: -------------------------------------------------------------------------------- 1 | import { lineBuilder } from '../Builder' 2 | 3 | 4 | export default ({ subjectData, type }) => { 5 | const offset = type.annotation.position 6 | 7 | let x1 = (subjectData.x1 !== undefined ? subjectData.x1 : offset.x) - offset.x, 8 | x2 = (subjectData.x2 !== undefined ? subjectData.x2 : offset.x) - offset.x, 9 | y1 = (subjectData.y1 !== undefined ? subjectData.y1 : offset.y) - offset.y, 10 | y2 = (subjectData.y2 !== undefined ? subjectData.y2 : offset.y) - offset.y 11 | 12 | const data = [[x1, y1], [x2, y2]] 13 | return { components: [lineBuilder({ data, className : 'subject'})]} 14 | } -------------------------------------------------------------------------------- /src/Types-d3.js: -------------------------------------------------------------------------------- 1 | import { select, event } from "d3-selection" 2 | import { drag } from "d3-drag" 3 | import { addHandles } from "./Handles" 4 | 5 | //Note options 6 | import noteAlignment from "./Note/alignment" 7 | import noteVertical from "./Note/lineType-vertical" 8 | import noteHorizontal from "./Note/lineType-horizontal" 9 | 10 | //Connector options 11 | import connectorLine from "./Connector/type-line" 12 | import connectorElbow from "./Connector/type-elbow" 13 | import connectorCurve from "./Connector/type-curve" 14 | import connectorArrow from "./Connector/end-arrow" 15 | import connectorDot from "./Connector/end-dot" 16 | 17 | //Subject options 18 | import subjectCircle from "./Subject/circle" 19 | import subjectRect from "./Subject/rect" 20 | import subjectThreshold from "./Subject/threshold" 21 | import subjectBadge from "./Subject/badge" 22 | 23 | export class Type { 24 | constructor({ a, annotation, editMode, dispatcher, notePadding, accessors }) { 25 | this.a = a 26 | 27 | this.note = 28 | annotation.disable.indexOf("note") === -1 && a.select("g.annotation-note") 29 | this.noteContent = this.note && a.select("g.annotation-note-content") 30 | this.connector = 31 | annotation.disable.indexOf("connector") === -1 && 32 | a.select("g.annotation-connector") 33 | this.subject = 34 | annotation.disable.indexOf("subject") === -1 && 35 | a.select("g.annotation-subject") 36 | this.dispatcher = dispatcher 37 | 38 | if (dispatcher) { 39 | const handler = addHandlers.bind(null, dispatcher, annotation) 40 | handler({ component: this.note, name: "note" }) 41 | handler({ component: this.connector, name: "connector" }) 42 | handler({ component: this.subject, name: "subject" }) 43 | } 44 | 45 | this.annotation = annotation 46 | this.editMode = annotation.editMode || editMode 47 | this.notePadding = notePadding !== undefined ? notePadding : 3 48 | this.offsetCornerX = 0 49 | this.offsetCornerY = 0 50 | 51 | if (accessors && annotation.data) { 52 | this.init(accessors) 53 | } 54 | } 55 | 56 | init(accessors) { 57 | if (!this.annotation.x) { 58 | this.mapX(accessors) 59 | } 60 | if (!this.annotation.y) { 61 | this.mapY(accessors) 62 | } 63 | } 64 | 65 | mapY(accessors) { 66 | if (accessors.y) { 67 | this.annotation.y = accessors.y(this.annotation.data) 68 | } 69 | } 70 | 71 | mapX(accessors) { 72 | if (accessors.x) { 73 | this.annotation.x = accessors.x(this.annotation.data) 74 | } 75 | } 76 | 77 | updateEditMode() { 78 | this.a.selectAll("circle.handle").remove() 79 | } 80 | 81 | drawOnSVG(component, builders) { 82 | if (!Array.isArray(builders)) { 83 | builders = [builders] 84 | } 85 | 86 | builders 87 | .filter(b => b) 88 | .forEach(({ type, className, attrs, handles, classID }) => { 89 | if (type === "handle") { 90 | addHandles({ group: component, r: attrs && attrs.r, handles }) 91 | } else { 92 | newWithClass(component, [this.annotation], type, className, classID) 93 | const el = component.select(`${type}.${classID || className}`) 94 | const addAttrs = Object.keys(attrs) 95 | const removeAttrs = [] 96 | 97 | const currentAttrs = el.node().attributes 98 | for (let i = currentAttrs.length - 1; i >= 0; i--) { 99 | const name = currentAttrs[i].name 100 | if (addAttrs.indexOf(name) === -1 && name !== "class") 101 | removeAttrs.push(name) 102 | } 103 | 104 | addAttrs.forEach(attr => { 105 | if (attr === "text") { 106 | el.text(attrs[attr]) 107 | } else { 108 | el.attr(attr, attrs[attr]) 109 | } 110 | }) 111 | 112 | removeAttrs.forEach(attr => el.attr(attr, null)) 113 | } 114 | }) 115 | } 116 | 117 | //TODO: how to extend this to a drawOnCanvas mode? 118 | 119 | getNoteBBox() { 120 | return bboxWithoutHandles(this.note, ".annotation-note-content text") 121 | } 122 | getNoteBBoxOffset() { 123 | const bbox = bboxWithoutHandles(this.note, ".annotation-note-content") 124 | const transform = this.noteContent.attr("transform").split(/\(|\,|\)/g) 125 | bbox.offsetCornerX = parseFloat(transform[1]) + this.annotation.dx 126 | bbox.offsetCornerY = parseFloat(transform[2]) + this.annotation.dy 127 | bbox.offsetX = this.annotation.dx 128 | bbox.offsetY = this.annotation.dy 129 | return bbox 130 | } 131 | 132 | drawSubject(context = {}) { 133 | const subjectData = this.annotation.subject 134 | const type = context.type 135 | const subjectParams = { type: this, subjectData } 136 | 137 | let subject = {} 138 | if (type === "circle") subject = subjectCircle(subjectParams) 139 | else if (type === "rect") subject = subjectRect(subjectParams) 140 | else if (type === "threshold") subject = subjectThreshold(subjectParams) 141 | else if (type === "badge") 142 | subject = subjectBadge(subjectParams, this.annotation) 143 | 144 | let { components = [], handles = [] } = subject 145 | components.forEach(c => { 146 | if (c && c.attrs && !c.attrs.stroke) { 147 | c.attrs.stroke = this.annotation.color 148 | } 149 | }) 150 | 151 | if (this.editMode) { 152 | handles = handles.concat( 153 | this.mapHandles([{ drag: this.dragSubject.bind(this) }]) 154 | ) 155 | components.push({ type: "handle", handles }) 156 | } 157 | 158 | return components 159 | } 160 | 161 | drawConnector(context = {}) { 162 | const connectorData = this.annotation.connector 163 | const type = connectorData.type || context.type 164 | const connectorParams = { type: this, connectorData } 165 | connectorParams.subjectType = 166 | this.typeSettings && 167 | this.typeSettings.subject && 168 | this.typeSettings.subject.type 169 | 170 | let connector = {} 171 | if (type === "curve") connector = connectorCurve(connectorParams) 172 | else if (type === "elbow") connector = connectorElbow(connectorParams) 173 | else connector = connectorLine(connectorParams) 174 | let { components = [], handles = [] } = connector 175 | const line = components[0] 176 | //TODO: genericize this into fill t/f stroke t/f 177 | if (line) { 178 | line.attrs.stroke = this.annotation.color 179 | line.attrs.fill = "none" 180 | } 181 | const endType = connectorData.end || context.end 182 | let end = {} 183 | if (endType === "arrow") { 184 | let s = line.data[1] 185 | const e = line.data[0] 186 | const distance = Math.sqrt( 187 | Math.pow(s[0] - e[0], 2) + Math.pow(s[1] - e[1], 2) 188 | ) 189 | if (distance < 5 && line.data[2]) { 190 | s = line.data[2] 191 | } 192 | end = connectorArrow({ 193 | annotation: this.annotation, 194 | start: s, 195 | end: e, 196 | scale: connectorData.endScale 197 | }) 198 | } else if (endType === "dot") { 199 | end = connectorDot({ line, scale: connectorData.endScale }) 200 | } else if (!endType || endType === "none") { 201 | this.connector && this.connector.select(".connector-end").remove() 202 | } 203 | 204 | if (end.components) { 205 | end.components.forEach(c => { 206 | c.attrs.fill = this.annotation.color 207 | c.attrs.stroke = this.annotation.color 208 | }) 209 | components = components.concat(end.components) 210 | } 211 | 212 | if (this.editMode) { 213 | if (handles.length !== 0) components.push({ type: "handle", handles }) 214 | } 215 | return components 216 | } 217 | 218 | drawNote(context = {}) { 219 | const noteData = this.annotation.note 220 | const align = noteData.align || context.align || "dynamic" 221 | const noteParams = { 222 | bbox: context.bbox, 223 | align, 224 | offset: this.annotation.offset 225 | } 226 | const lineType = noteData.lineType || context.lineType 227 | let note = {} 228 | if (lineType === "vertical") note = noteVertical(noteParams) 229 | else if (lineType === "horizontal") note = noteHorizontal(noteParams) 230 | 231 | let { components = [], handles = [] } = note 232 | components.forEach(c => { 233 | c.attrs.stroke = this.annotation.color 234 | }) 235 | 236 | if (this.editMode) { 237 | handles = this.mapHandles([ 238 | { x: 0, y: 0, drag: this.dragNote.bind(this) } 239 | ]) 240 | components.push({ type: "handle", handles }) 241 | 242 | const dragging = this.dragNote.bind(this), 243 | start = this.dragstarted.bind(this), 244 | end = this.dragended.bind(this) 245 | this.note.call( 246 | drag() 247 | .container(select("g.annotations").node()) 248 | .on("start", d => start(d)) 249 | .on("drag", d => dragging(d)) 250 | .on("end", d => end(d)) 251 | ) 252 | } else { 253 | this.note.on("mousedown.drag", null) 254 | } 255 | return components 256 | } 257 | 258 | drawNoteContent(context) { 259 | const noteData = this.annotation.note 260 | const padding = 261 | noteData.padding !== undefined ? noteData.padding : this.notePadding 262 | let orientation = noteData.orientation || context.orientation || "topBottom" 263 | const lineType = noteData.lineType || context.lineType 264 | const align = noteData.align || context.align || "dynamic" 265 | 266 | if (lineType === "vertical") orientation = "leftRight" 267 | else if (lineType === "horizontal") orientation = "topBottom" 268 | 269 | const noteParams = { 270 | padding, 271 | bbox: context.bbox, 272 | offset: this.annotation.offset, 273 | orientation, 274 | align 275 | } 276 | const { x, y } = noteAlignment(noteParams) 277 | this.offsetCornerX = x + this.annotation.dx 278 | this.offsetCornerY = y + this.annotation.dy 279 | this.note && this.noteContent.attr("transform", `translate(${x}, ${y})`) 280 | 281 | return [] 282 | } 283 | 284 | drawOnScreen(component, drawFunction) { 285 | return this.drawOnSVG(component, drawFunction) 286 | } 287 | 288 | redrawSubject() { 289 | this.subject && this.drawOnScreen(this.subject, this.drawSubject()) 290 | } 291 | 292 | redrawConnector() { 293 | this.connector && this.drawOnScreen(this.connector, this.drawConnector()) 294 | } 295 | 296 | redrawNote(bbox = this.getNoteBBox()) { 297 | this.noteContent && 298 | this.drawOnScreen(this.noteContent, this.drawNoteContent({ bbox })) 299 | this.note && this.drawOnScreen(this.note, this.drawNote({ bbox })) 300 | } 301 | 302 | setPosition() { 303 | const position = this.annotation.position 304 | this.a.attr("transform", `translate(${position.x}, ${position.y})`) 305 | } 306 | 307 | clearComponents() { 308 | this.subject && this.subject.select("*").remove() 309 | this.connector && this.connector.select("*").remove() 310 | // this.note && this.note.select("*").remove() 311 | } 312 | 313 | setOffset() { 314 | if (this.note) { 315 | const offset = this.annotation.offset 316 | this.note.attr("transform", `translate(${offset.x}, ${offset.y})`) 317 | } 318 | } 319 | 320 | setPositionWithAccessors(accessors) { 321 | if (accessors && this.annotation.data) { 322 | this.mapX(accessors) 323 | this.mapY(accessors) 324 | } 325 | this.setPosition() 326 | } 327 | 328 | setClassName() { 329 | this.a.attr( 330 | "class", 331 | `annotation ${this.className && this.className()} ${ 332 | this.editMode ? "editable" : "" 333 | } ${this.annotation.className || ""}` 334 | ) 335 | } 336 | 337 | draw() { 338 | this.setClassName() 339 | this.setPosition() 340 | this.setOffset() 341 | this.redrawSubject() 342 | this.redrawConnector() 343 | this.redrawNote() 344 | } 345 | 346 | dragstarted() { 347 | event.sourceEvent.stopPropagation() 348 | this.dispatcher && 349 | this.dispatcher.call("dragstart", this.a, this.annotation) 350 | this.a.classed("dragging", true) 351 | this.a.selectAll("circle.handle").style("pointer-events", "none") 352 | } 353 | dragended() { 354 | this.dispatcher && this.dispatcher.call("dragend", this.a, this.annotation) 355 | this.a.classed("dragging", false) 356 | this.a.selectAll("circle.handle").style("pointer-events", "all") 357 | } 358 | 359 | dragSubject() { 360 | const position = this.annotation.position 361 | position.x += event.dx 362 | position.y += event.dy 363 | this.annotation.position = position 364 | } 365 | 366 | dragNote() { 367 | const offset = this.annotation.offset 368 | offset.x += event.dx 369 | offset.y += event.dy 370 | this.annotation.offset = offset 371 | } 372 | 373 | mapHandles(handles) { 374 | return handles.map(h => ({ 375 | ...h, 376 | start: this.dragstarted.bind(this), 377 | end: this.dragended.bind(this) 378 | })) 379 | } 380 | } 381 | 382 | export const customType = (initialType, typeSettings, init) => { 383 | return class customType extends initialType { 384 | constructor(settings) { 385 | super(settings) 386 | this.typeSettings = typeSettings 387 | 388 | if (typeSettings.disable) { 389 | typeSettings.disable.forEach(d => { 390 | this[d] && this[d].remove() 391 | 392 | this[d] = undefined 393 | if (d === "note") { 394 | this.noteContent = undefined 395 | } 396 | }) 397 | } 398 | } 399 | 400 | static init(annotation, accessors) { 401 | super.init(annotation, accessors) 402 | if (init) { 403 | annotation = init(annotation, accessors) 404 | } 405 | return annotation 406 | } 407 | 408 | className() { 409 | return `${typeSettings.className || 410 | super.className && super.className() || 411 | ""}` 412 | } 413 | 414 | drawSubject(context) { 415 | this.typeSettings.subject = Object.assign( 416 | {}, 417 | typeSettings.subject, 418 | this.typeSettings.subject 419 | ) 420 | return super.drawSubject({ ...context, ...this.typeSettings.subject }) 421 | } 422 | 423 | drawConnector(context) { 424 | this.typeSettings.connector = Object.assign( 425 | {}, 426 | typeSettings.connector, 427 | this.typeSettings.connector 428 | ) 429 | return super.drawConnector({ 430 | ...context, 431 | ...typeSettings.connector, 432 | ...this.typeSettings.connector 433 | }) 434 | } 435 | 436 | drawNote(context) { 437 | this.typeSettings.note = Object.assign( 438 | {}, 439 | typeSettings.note, 440 | this.typeSettings.note 441 | ) 442 | return super.drawNote({ 443 | ...context, 444 | ...typeSettings.note, 445 | ...this.typeSettings.note 446 | }) 447 | } 448 | 449 | drawNoteContent(context) { 450 | return super.drawNoteContent({ 451 | ...context, 452 | ...typeSettings.note, 453 | ...this.typeSettings.note 454 | }) 455 | } 456 | } 457 | } 458 | 459 | export class d3NoteText extends Type { 460 | constructor(params) { 461 | super(params) 462 | this.textWrap = params.textWrap || 120 463 | this.drawText() 464 | } 465 | 466 | updateTextWrap(textWrap) { 467 | this.textWrap = textWrap 468 | this.drawText() 469 | } 470 | 471 | //TODO: add update text functionality 472 | 473 | drawText() { 474 | if (this.note) { 475 | newWithClass(this.note, [this.annotation], "g", "annotation-note-content") 476 | 477 | const noteContent = this.note.select("g.annotation-note-content") 478 | newWithClass(noteContent, [this.annotation], "rect", "annotation-note-bg") 479 | newWithClass( 480 | noteContent, 481 | [this.annotation], 482 | "text", 483 | "annotation-note-label" 484 | ) 485 | newWithClass( 486 | noteContent, 487 | [this.annotation], 488 | "text", 489 | "annotation-note-title" 490 | ) 491 | 492 | let titleBBox = { height: 0 } 493 | const label = this.a.select("text.annotation-note-label") 494 | const wrapLength = 495 | this.annotation.note && this.annotation.note.wrap || 496 | this.typeSettings && 497 | this.typeSettings.note && 498 | this.typeSettings.note.wrap || 499 | this.textWrap 500 | 501 | const wrapSplitter = 502 | this.annotation.note && this.annotation.note.wrapSplitter || 503 | this.typeSettings && 504 | this.typeSettings.note && 505 | this.typeSettings.note.wrapSplitter 506 | 507 | let bgPadding = 508 | this.annotation.note && this.annotation.note.bgPadding || 509 | this.typeSettings && 510 | this.typeSettings.note && 511 | this.typeSettings.note.bgPadding 512 | 513 | let bgPaddingFinal = { top: 0, bottom: 0, left: 0, right: 0 } 514 | if (typeof bgPadding === "number") { 515 | bgPaddingFinal = { 516 | top: bgPadding, 517 | bottom: bgPadding, 518 | left: bgPadding, 519 | right: bgPadding 520 | } 521 | } else if (bgPadding && typeof bgPadding === "object") { 522 | bgPaddingFinal = Object.assign(bgPaddingFinal, bgPadding) 523 | } 524 | 525 | if (this.annotation.note.title) { 526 | const title = this.a.select("text.annotation-note-title") 527 | title.text(this.annotation.note.title) 528 | title.attr("fill", this.annotation.color) 529 | title.attr("font-weight", "bold") 530 | title.call(wrap, wrapLength, wrapSplitter) 531 | titleBBox = title.node().getBBox() 532 | } 533 | 534 | label.text(this.annotation.note.label).attr("dx", "0") 535 | label.call(wrap, wrapLength, wrapSplitter) 536 | 537 | label.attr("y", titleBBox.height * 1.1 || 0) 538 | label.attr("fill", this.annotation.color) 539 | 540 | const bbox = this.getNoteBBox() 541 | 542 | this.a 543 | .select("rect.annotation-note-bg") 544 | .attr("width", bbox.width + bgPaddingFinal.left + bgPaddingFinal.right) 545 | .attr( 546 | "height", 547 | bbox.height + bgPaddingFinal.top + bgPaddingFinal.bottom 548 | ) 549 | .attr("x", bbox.x - bgPaddingFinal.left) 550 | .attr("y", -bgPaddingFinal.top) 551 | .attr("fill", "white") 552 | .attr("fill-opacity", 0) 553 | } 554 | } 555 | } 556 | 557 | export const d3Label = customType(d3NoteText, { 558 | className: "label", 559 | note: { align: "middle" } 560 | }) 561 | 562 | export const d3Callout = customType(d3NoteText, { 563 | className: "callout", 564 | note: { lineType: "horizontal" } 565 | }) 566 | 567 | export const d3CalloutElbow = customType(d3Callout, { 568 | className: "callout elbow", 569 | connector: { type: "elbow" } 570 | }) 571 | 572 | export const d3CalloutCurve = customType(d3Callout, { 573 | className: "callout curve", 574 | connector: { type: "curve" } 575 | }) 576 | 577 | export const d3Badge = customType(Type, { 578 | className: "badge", 579 | subject: { type: "badge" }, 580 | disable: ["connector", "note"] 581 | }) 582 | 583 | export const d3CalloutCircle = customType(d3NoteText, { 584 | className: "callout circle", 585 | subject: { type: "circle" }, 586 | note: { lineType: "horizontal" }, 587 | connector: { type: "elbow" } 588 | }) 589 | 590 | export const d3CalloutRect = customType(d3NoteText, { 591 | className: "callout rect", 592 | subject: { type: "rect" }, 593 | note: { lineType: "horizontal" }, 594 | connector: { type: "elbow" } 595 | }) 596 | 597 | class ThresholdMap extends d3Callout { 598 | mapY(accessors) { 599 | super.mapY(accessors) 600 | const a = this.annotation 601 | if ((a.subject.x1 || a.subject.x2) && a.data && accessors.y) { 602 | a.y = accessors.y(a.data) 603 | } 604 | if ((a.subject.x1 || a.subject.x2) && !a.x) { 605 | a.x = a.subject.x1 || a.subject.x2 606 | } 607 | } 608 | 609 | mapX(accessors) { 610 | super.mapX(accessors) 611 | const a = this.annotation 612 | if ((a.subject.y1 || a.subject.y2) && a.data && accessors.x) { 613 | a.x = accessors.x(a.data) 614 | } 615 | if ((a.subject.y1 || a.subject.y2) && !a.y) { 616 | a.y = a.subject.y1 || a.subject.y2 617 | } 618 | } 619 | } 620 | 621 | export const d3XYThreshold = customType(ThresholdMap, { 622 | className: "callout xythreshold", 623 | subject: { type: "threshold" } 624 | }) 625 | 626 | export const newWithClass = (a, d, type, className, classID) => { 627 | const group = a.selectAll(`${type}.${classID || className}`).data(d) 628 | group 629 | .enter() 630 | .append(type) 631 | .merge(group) 632 | .attr("class", className) 633 | 634 | group.exit().remove() 635 | return a 636 | } 637 | 638 | const addHandlers = (dispatcher, annotation, { component, name }) => { 639 | if (component) { 640 | component 641 | .on("mouseover.annotations", () => { 642 | dispatcher.call(`${name}over`, component, annotation) 643 | }) 644 | .on("mouseout.annotations", () => 645 | dispatcher.call(`${name}out`, component, annotation) 646 | ) 647 | .on("click.annotations", () => 648 | dispatcher.call(`${name}click`, component, annotation) 649 | ) 650 | } 651 | } 652 | 653 | //Text wrapping code adapted from Mike Bostock 654 | const wrap = (text, width, wrapSplitter, lineHeight = 1.2) => { 655 | text.each(function() { 656 | const text = select(this), 657 | words = text 658 | .text() 659 | .split(wrapSplitter || /[ \t\r\n]+/) 660 | .reverse() 661 | .filter(w => w !== "") 662 | let word, 663 | line = [], 664 | tspan = text 665 | .text(null) 666 | .append("tspan") 667 | .attr("x", 0) 668 | .attr("dy", 0.8 + "em") 669 | 670 | while (word = words.pop()) { 671 | line.push(word) 672 | tspan.text(line.join(" ")) 673 | if (tspan.node().getComputedTextLength() > width && line.length > 1) { 674 | line.pop() 675 | tspan.text(line.join(" ")) 676 | line = [word] 677 | tspan = text 678 | .append("tspan") 679 | .attr("x", 0) 680 | .attr("dy", lineHeight + "em") 681 | .text(word) 682 | } 683 | } 684 | }) 685 | } 686 | 687 | const bboxWithoutHandles = (selection, selector = ":not(.handle)") => { 688 | if (!selection) { 689 | return { x: 0, y: 0, width: 0, height: 0 } 690 | } 691 | 692 | return selection 693 | .selectAll(selector) 694 | .nodes() 695 | .reduce( 696 | (p, c) => { 697 | const bbox = c.getBBox() 698 | p.x = Math.min(p.x, bbox.x) 699 | p.y = Math.min(p.y, bbox.y) 700 | p.width = Math.max(p.width, bbox.width) 701 | 702 | const yOffset = c && c.attributes && c.attributes.y 703 | p.height = Math.max( 704 | p.height, 705 | (yOffset && parseFloat(yOffset.value) || 0) + bbox.height 706 | ) 707 | return p 708 | }, 709 | { x: 0, y: 0, width: 0, height: 0 } 710 | ) 711 | } 712 | 713 | export default { 714 | Type, 715 | d3Label, 716 | d3Callout, 717 | d3CalloutElbow, 718 | d3CalloutCurve, 719 | d3CalloutCircle, 720 | d3CalloutRect, 721 | d3XYThreshold, 722 | d3Badge, 723 | customType 724 | } 725 | -------------------------------------------------------------------------------- /test/connector.test.js: -------------------------------------------------------------------------------- 1 | import endArrow from "../src/Connector/end-arrow" 2 | 3 | describe("Connector-end-arrow", function() { 4 | it("gives back points for an arrow", function() { 5 | const arrow = endArrow({ 6 | annotation: { 7 | x: 331, 8 | y: 186, 9 | dx: 36, 10 | dy: -41, 11 | position: { x: 331, y: 186 } 12 | } 13 | }) 14 | 15 | expect(arrow.components.length).toEqual(1) 16 | 17 | const { data } = arrow.components[0] 18 | expect(data).toEqual([ 19 | [0, 0], 20 | [8.413665914342925, -5.404648543783771], 21 | [4.271165980593775, -9.041965558783025], 22 | [0, 0] 23 | ]) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/note.test.js: -------------------------------------------------------------------------------- 1 | import alignment from "../src/Note/alignment" 2 | import { assign } from "./utils" 3 | 4 | describe("Note-alignment", function() { 5 | const set = { 6 | bbox: { 7 | x: 0, 8 | y: 0, 9 | width: 100, 10 | height: 50 11 | }, 12 | orientation: "top" 13 | } 14 | let a 15 | 16 | it("stays if orientation is top", function() { 17 | a = alignment(set) 18 | expect(a.x).toEqual(-0) 19 | expect(a.y).toEqual(-50) 20 | 21 | a = alignment( 22 | assign(set, { orientation: "topBottom", offset: { x: 0, y: -100 } }) 23 | ) 24 | 25 | expect(a.x).toEqual(-0) 26 | expect(a.y).toEqual(-50) 27 | }) 28 | 29 | it("stays if orientation is left", function() { 30 | a = alignment(assign(set, { orientation: "left" })) 31 | 32 | expect(a.x).toEqual(-100) 33 | expect(a.y).toEqual(0) 34 | }) 35 | 36 | it("honors alignment", function() { 37 | a = alignment(assign(set, { align: "right" })) 38 | expect(a.x).toEqual(-100) 39 | expect(a.y).toEqual(-50) 40 | }) 41 | 42 | it("honors padding", function() { 43 | a = alignment(assign(set, { padding: 20 })) 44 | 45 | expect(a.x).toEqual(-0) 46 | expect(a.y).toEqual(-70) 47 | 48 | a = alignment(assign(set, { padding: 20, orientation: "left" })) 49 | 50 | expect(a.x).toEqual(-120) 51 | expect(a.y).toEqual(0) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /test/subject.badge.test.js: -------------------------------------------------------------------------------- 1 | import badge from "../src/Subject/badge" 2 | 3 | describe("Subject badge", () => { 4 | it("renders", () => { 5 | badge({}) 6 | }) 7 | 8 | it("gives back the positioning", () => { 9 | const position = badge({}) 10 | const badgeObject = { 11 | components: [ 12 | { 13 | attrs: { 14 | d: "M0,0L0,0L0,0L0,0", 15 | fill: undefined, 16 | "stroke-linecap": "round", 17 | "stroke-width": "3px" 18 | }, 19 | classID: undefined, 20 | className: "subject-pointer", 21 | data: [[0, 0], [0, 0], [0, 0], [0, 0]], 22 | type: "path" 23 | }, 24 | { 25 | attrs: { 26 | d: 27 | "M8.572527594031472e-16,-14A14,14,0,1,1,-8.572527594031472e-16,14A14,14,0,1,1,8.572527594031472e-16,-14Z", 28 | fill: undefined, 29 | "stroke-linecap": "round", 30 | "stroke-width": "3px", 31 | transform: "translate(0, 0)" 32 | }, 33 | classID: undefined, 34 | className: "subject", 35 | data: { radius: 14 }, 36 | type: "path" 37 | }, 38 | { 39 | attrs: { 40 | d: 41 | "M8.572527594031472e-16,-14A14,14,0,1,1,-8.572527594031472e-16,14A14,14,0,1,1,8.572527594031472e-16,-14M-1.8002307947466087e-15,-9.799999999999999A9.799999999999999,9.799999999999999,0,1,0,1.8002307947466087e-15,9.799999999999999A9.799999999999999,9.799999999999999,0,1,0,-1.8002307947466087e-15,-9.799999999999999Z", 42 | fill: "white", 43 | "stroke-width": "3px", 44 | transform: "translate(0, 0)" 45 | }, 46 | classID: undefined, 47 | className: "subject-ring", 48 | data: { innerRadius: 9.799999999999999, outerRadius: 14 }, 49 | type: "path" 50 | }, 51 | undefined 52 | ], 53 | handles: [] 54 | } 55 | 56 | expect(position).toEqual(badgeObject) 57 | }) 58 | 59 | it("gives back the positioning, top", () => { 60 | //should abstract out subjectData and noteData 61 | const position = badge({ subjectData: { y: "top" } }) 62 | const badgeObject = { 63 | components: [ 64 | { 65 | attrs: { 66 | d: 67 | "M0,0L9.899494936611665,-9.899494936611664L-9.899494936611665,-9.899494936611664L0,0", 68 | fill: undefined, 69 | "stroke-linecap": "round", 70 | "stroke-width": "3px" 71 | }, 72 | classID: undefined, 73 | className: "subject-pointer", 74 | data: [ 75 | [0, 0], 76 | [9.899494936611665, -9.899494936611664], 77 | [-9.899494936611665, -9.899494936611664], 78 | [0, 0] 79 | ], 80 | type: "path" 81 | }, 82 | { 83 | attrs: { 84 | d: 85 | "M8.572527594031472e-16,-14A14,14,0,1,1,-8.572527594031472e-16,14A14,14,0,1,1,8.572527594031472e-16,-14Z", 86 | fill: undefined, 87 | "stroke-linecap": "round", 88 | "stroke-width": "3px", 89 | transform: "translate(0, -19.79898987322333)" 90 | }, 91 | classID: undefined, 92 | className: "subject", 93 | data: { radius: 14 }, 94 | type: "path" 95 | }, 96 | { 97 | attrs: { 98 | d: 99 | "M8.572527594031472e-16,-14A14,14,0,1,1,-8.572527594031472e-16,14A14,14,0,1,1,8.572527594031472e-16,-14M-1.8002307947466087e-15,-9.799999999999999A9.799999999999999,9.799999999999999,0,1,0,1.8002307947466087e-15,9.799999999999999A9.799999999999999,9.799999999999999,0,1,0,-1.8002307947466087e-15,-9.799999999999999Z", 100 | fill: "white", 101 | "stroke-width": "3px", 102 | transform: "translate(0, -19.79898987322333)" 103 | }, 104 | classID: undefined, 105 | className: "subject-ring", 106 | data: { innerRadius: 9.799999999999999, outerRadius: 14 }, 107 | type: "path" 108 | }, 109 | undefined 110 | ], 111 | handles: [] 112 | } 113 | 114 | expect(position).toEqual(badgeObject) 115 | }) 116 | 117 | it("gives back the positioning, right", () => { 118 | //should abstract out subjectData and noteData 119 | const position = badge({ subjectData: { x: "right" } }) 120 | const badgeObject = { 121 | components: [ 122 | { 123 | attrs: { 124 | d: 125 | "M0,0L9.899494936611664,9.899494936611665L9.899494936611664,-9.899494936611665L0,0", 126 | fill: undefined, 127 | "stroke-linecap": "round", 128 | "stroke-width": "3px" 129 | }, 130 | classID: undefined, 131 | className: "subject-pointer", 132 | data: [ 133 | [0, 0], 134 | [9.899494936611664, 9.899494936611665], 135 | [9.899494936611664, -9.899494936611665], 136 | [0, 0] 137 | ], 138 | type: "path" 139 | }, 140 | { 141 | attrs: { 142 | d: 143 | "M8.572527594031472e-16,-14A14,14,0,1,1,-8.572527594031472e-16,14A14,14,0,1,1,8.572527594031472e-16,-14Z", 144 | fill: undefined, 145 | "stroke-linecap": "round", 146 | "stroke-width": "3px", 147 | transform: "translate(19.79898987322333, 0)" 148 | }, 149 | classID: undefined, 150 | className: "subject", 151 | data: { radius: 14 }, 152 | type: "path" 153 | }, 154 | { 155 | attrs: { 156 | d: 157 | "M8.572527594031472e-16,-14A14,14,0,1,1,-8.572527594031472e-16,14A14,14,0,1,1,8.572527594031472e-16,-14M-1.8002307947466087e-15,-9.799999999999999A9.799999999999999,9.799999999999999,0,1,0,1.8002307947466087e-15,9.799999999999999A9.799999999999999,9.799999999999999,0,1,0,-1.8002307947466087e-15,-9.799999999999999Z", 158 | fill: "white", 159 | "stroke-width": "3px", 160 | transform: "translate(19.79898987322333, 0)" 161 | }, 162 | classID: undefined, 163 | className: "subject-ring", 164 | data: { innerRadius: 9.799999999999999, outerRadius: 14 }, 165 | type: "path" 166 | }, 167 | undefined 168 | ], 169 | handles: [] 170 | } 171 | 172 | expect(position).toEqual(badgeObject) 173 | }) 174 | 175 | it("gives back, top right ", () => { 176 | const position = badge({ subjectData: { x: "right", y: "top" } }) 177 | 178 | const badgeObject = { 179 | components: [ 180 | { 181 | attrs: { 182 | d: "M0,0L14,0L0,-14L0,0", 183 | fill: undefined, 184 | "stroke-linecap": "round", 185 | "stroke-width": "3px" 186 | }, 187 | classID: undefined, 188 | className: "subject-pointer", 189 | data: [[0, 0], [14, 0], [0, -14], [0, 0]], 190 | type: "path" 191 | }, 192 | { 193 | attrs: { 194 | d: 195 | "M8.572527594031472e-16,-14A14,14,0,1,1,-8.572527594031472e-16,14A14,14,0,1,1,8.572527594031472e-16,-14Z", 196 | fill: undefined, 197 | "stroke-linecap": "round", 198 | "stroke-width": "3px", 199 | transform: "translate(14, -14)" 200 | }, 201 | classID: undefined, 202 | className: "subject", 203 | data: { radius: 14 }, 204 | type: "path" 205 | }, 206 | { 207 | attrs: { 208 | d: 209 | "M8.572527594031472e-16,-14A14,14,0,1,1,-8.572527594031472e-16,14A14,14,0,1,1,8.572527594031472e-16,-14M-1.8002307947466087e-15,-9.799999999999999A9.799999999999999,9.799999999999999,0,1,0,1.8002307947466087e-15,9.799999999999999A9.799999999999999,9.799999999999999,0,1,0,-1.8002307947466087e-15,-9.799999999999999Z", 210 | fill: "white", 211 | "stroke-width": "3px", 212 | transform: "translate(14, -14)" 213 | }, 214 | classID: undefined, 215 | className: "subject-ring", 216 | data: { innerRadius: 9.799999999999999, outerRadius: 14 }, 217 | type: "path" 218 | }, 219 | undefined 220 | ], 221 | handles: [] 222 | } 223 | 224 | expect(position).toEqual(badgeObject) 225 | }) 226 | it("gives back, bottom right ", () => { 227 | const position = badge({ subjectData: { x: "right", y: "bottom" } }) 228 | 229 | const badgeObject = { 230 | components: [ 231 | { 232 | attrs: { 233 | d: "M0,0L14,0L0,14L0,0", 234 | fill: undefined, 235 | "stroke-linecap": "round", 236 | "stroke-width": "3px" 237 | }, 238 | classID: undefined, 239 | className: "subject-pointer", 240 | data: [[0, 0], [14, 0], [0, 14], [0, 0]], 241 | type: "path" 242 | }, 243 | { 244 | attrs: { 245 | d: 246 | "M8.572527594031472e-16,-14A14,14,0,1,1,-8.572527594031472e-16,14A14,14,0,1,1,8.572527594031472e-16,-14Z", 247 | fill: undefined, 248 | "stroke-linecap": "round", 249 | "stroke-width": "3px", 250 | transform: "translate(14, 14)" 251 | }, 252 | classID: undefined, 253 | className: "subject", 254 | data: { radius: 14 }, 255 | type: "path" 256 | }, 257 | { 258 | attrs: { 259 | d: 260 | "M8.572527594031472e-16,-14A14,14,0,1,1,-8.572527594031472e-16,14A14,14,0,1,1,8.572527594031472e-16,-14M-1.8002307947466087e-15,-9.799999999999999A9.799999999999999,9.799999999999999,0,1,0,1.8002307947466087e-15,9.799999999999999A9.799999999999999,9.799999999999999,0,1,0,-1.8002307947466087e-15,-9.799999999999999Z", 261 | fill: "white", 262 | "stroke-width": "3px", 263 | transform: "translate(14, 14)" 264 | }, 265 | classID: undefined, 266 | className: "subject-ring", 267 | data: { innerRadius: 9.799999999999999, outerRadius: 14 }, 268 | type: "path" 269 | }, 270 | undefined 271 | ], 272 | handles: [] 273 | } 274 | 275 | expect(position).toEqual(badgeObject) 276 | }) 277 | }) 278 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | export const assign = (base, additional) => { 2 | return Object.assign({}, base, additional) 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "typeRoots": ["./types"], 4 | "allowJs": true 5 | }, 6 | "include": ["types/*"] 7 | } -------------------------------------------------------------------------------- /types/d3-svg-annotation.d.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'd3-dispatch'; 2 | import { BaseType, Selection } from 'd3-selection'; 3 | 4 | type Accessors = { 5 | [k: string]: () => number 6 | }; 7 | 8 | // TODO figure out what these actually are. 9 | type Components = any[]; 10 | 11 | type Orientation = 'topBottom' | 'leftRight' | 'fixed'; 12 | type Align = 'dynamic' | 'left' | 'right' | 'top' | 'bottom' | 'middle'; 13 | type LineType = 'horizontal' | 'vertical' | 'none'; 14 | 15 | type BBox = { 16 | x: number, 17 | y: number, 18 | width: number, 19 | height: number 20 | }; 21 | 22 | export default class Annotation { 23 | 24 | className: string; 25 | x: number; 26 | y: number; 27 | dx: number; 28 | dy: number; 29 | offset: { 30 | x: number; 31 | y: number; 32 | }; 33 | position: { 34 | x: number; 35 | y: number; 36 | }; 37 | readonly translation: { 38 | x: number; 39 | y: number; 40 | }; 41 | readonly json: { 42 | x: number; 43 | y: number; 44 | dx: number; 45 | dy: number; 46 | }; 47 | 48 | constructor({ x, y, dy, dx, data, type, subject, connector, note, disable, id, className }: { 49 | x?: number; 50 | y?: number; 51 | dy?: number; 52 | dx?: number; 53 | data: any; 54 | type: any; 55 | subject: any; 56 | connector: any; 57 | note: any; 58 | disable: any; 59 | id: any; 60 | className: string; 61 | }); 62 | 63 | annotations(anotations: any[]): Annotation; 64 | accessors(accessors: { x?: (datum: T) => any, y?: (datum: T) => any }): Annotation; 65 | accessorsInverse(accessors: any): Annotation; 66 | editMode(editMode: boolean): Annotation; 67 | notePadding(padding: number): Annotation; 68 | type(type: any): Annotation; 69 | updatePosition(): void; 70 | updateOffset(): void; 71 | update(): void; 72 | updatedAccessors(): void; 73 | } 74 | 75 | export class Type { 76 | a: Selection; 77 | note: Selection | false; 78 | noteContent: Selection | false; 79 | connector: Selection | false; 80 | subject: Selection | false; 81 | annotation: Annotation; 82 | editMode: boolean; 83 | notePadding: number; 84 | offsetCornerX: number; 85 | offsetCornerY: number; 86 | 87 | constructor(args: { 88 | a: Selection, 89 | annotation: Annotation, 90 | editMode?: boolean, 91 | dispatcher?: Dispatch, 92 | notePadding?: number, 93 | accessors?: Accessors 94 | }); 95 | 96 | init(accessors: Accessors): void; 97 | 98 | mapY(accessors: Accessors): void; 99 | 100 | mapX(accessors: Accessors): void; 101 | 102 | updateEditMode(): void; 103 | 104 | drawOnSVG( 105 | component: Selection, 106 | builders: Array< 107 | { type: string, className: string, attributes: {}, handles?: any[] }>): 108 | void; 109 | 110 | getNoteBBox(): BBox; 111 | 112 | getNoteBBoxOffset(): BBox; 113 | 114 | drawSubject(context?: { type: string }): Components; 115 | 116 | drawConnector(context?: { type: string, end: string }): Components; 117 | 118 | drawNote(context: { 119 | orientation: Orientation, 120 | align: Align, 121 | lineType: LineType, 122 | bbox: BBox, 123 | }): Components; 124 | 125 | drawNoteContent(context: { 126 | orientation: Orientation, 127 | lineType: LineType, 128 | align: Align, 129 | bbox: BBox, 130 | }): Array; 131 | 132 | drawOnScreen(component: any, drawFunction: () => any): void; 133 | 134 | redrawSubmit(): void; 135 | 136 | redrawConnector(bbox?: BBox): void; 137 | 138 | redrawNote(bbox?: BBox): void; 139 | 140 | setPosition(): void; 141 | 142 | setOffset(): void; 143 | 144 | setPositionWithAccessors(accessors?: Accessors): void; 145 | 146 | setClassName(): void; 147 | 148 | draw(): void; 149 | 150 | dragstarted(): void; 151 | drawended(): void; 152 | dragSubject(): void; 153 | dragNode(): void; 154 | mapHandler(handles: Array<{}>): { start: () => void, end: () => void }; 155 | } 156 | 157 | // TODO define these 158 | export class annotationLabel extends Type { } 159 | export class annotationCallout extends Type { } 160 | export class annotationCalloutElbow extends Type { } 161 | export class annotationCalloutCurve extends Type { } 162 | export class annotationCalloutCircle extends Type { } 163 | export class annotationCalloutRect extends Type { } 164 | export class annotationXYThreshold extends Type { } 165 | export class annotationBadge extends Type { } 166 | 167 | interface CustomAnnotationSettings { 168 | disable?: string[]; 169 | className?: string; 170 | subject?: any; 171 | connector?: any; 172 | note?: any; 173 | } 174 | 175 | export class annotationCustomType extends Type { 176 | constructor(type: typeof Type, settings: CustomAnnotationSettings); 177 | } 178 | 179 | export function annotation(): Annotation; 180 | 181 | export as namespace d3; 182 | --------------------------------------------------------------------------------