├── assets ├── icon.png ├── img │ ├── q.png │ ├── csv.png │ ├── gif.png │ ├── min.png │ ├── pin.png │ ├── png.png │ ├── svg.png │ ├── vl.png │ ├── check.png │ ├── clear.png │ ├── close.png │ ├── delete.png │ ├── draw.png │ ├── expand.png │ ├── export.png │ ├── facets.png │ ├── filter.png │ ├── gdocs.png │ ├── hide.png │ ├── image.png │ ├── info.png │ ├── json.png │ ├── l-fac.png │ ├── l-seg.png │ ├── l-uni.png │ ├── mail.png │ ├── next.png │ ├── open.png │ ├── play.png │ ├── prev.png │ ├── r-arb.png │ ├── r-cal.png │ ├── r-grid.png │ ├── r-lin.png │ ├── r-rad.png │ ├── r-spi.png │ ├── record.png │ ├── reset.png │ ├── s-log.png │ ├── s-rel.png │ ├── s-seq.png │ ├── story.png │ ├── caption.png │ ├── s-chron.png │ ├── s-intdur.png │ ├── segments.png │ ├── timeline.png │ ├── categories.png │ ├── highlight.png │ ├── resetBasic.png │ └── ms-logo.svg └── css │ └── lib │ └── colorpicker.css ├── privacy_terms.md ├── style └── font-awesome │ ├── _fixed-width.scss │ ├── _larger.scss │ ├── _list.scss │ ├── font-awesome.scss │ ├── _core.scss │ ├── _stacked.scss │ ├── _bordered-pulled.scss │ ├── _rotated-flipped.scss │ ├── _animated.scss │ └── _mixins.scss ├── .gitignore ├── tslint.json ├── src ├── core │ ├── templates │ │ ├── toElement.ts │ │ └── addImagePopup.html.ts │ ├── annotations.ts │ ├── colors.ts │ ├── colorPickerPopup.ts │ ├── imageUrls.ts │ ├── lib │ │ ├── time.min.js │ │ ├── colorpicker.js │ │ ├── gif.js │ │ ├── saveSvgAsPng.ts │ │ └── gif.worker.js │ ├── gridAxis.ts │ ├── dialogs │ │ └── addImageDialog.ts │ ├── calendarAxis.ts │ ├── addCaption.ts │ ├── addImage.ts │ ├── utils.ts │ ├── globals.ts │ └── radialAxis.ts ├── utils.ts ├── settings.ts ├── interfaces.ts ├── lib │ ├── UpdateType.ts │ └── calcUpdateType.ts ├── dataConversion.ts └── visual.ts ├── tsconfig.json ├── CHANGELOG.md ├── package.json ├── certs ├── PowerBICustomVisualTest_public.crt └── PowerBICustomVisualTest_private.key ├── LICENSE ├── pbiviz.json ├── README.md ├── SECURITY.md └── capabilities.json /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/img/q.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/q.png -------------------------------------------------------------------------------- /assets/img/csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/csv.png -------------------------------------------------------------------------------- /assets/img/gif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/gif.png -------------------------------------------------------------------------------- /assets/img/min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/min.png -------------------------------------------------------------------------------- /assets/img/pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/pin.png -------------------------------------------------------------------------------- /assets/img/png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/png.png -------------------------------------------------------------------------------- /assets/img/svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/svg.png -------------------------------------------------------------------------------- /assets/img/vl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/vl.png -------------------------------------------------------------------------------- /assets/img/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/check.png -------------------------------------------------------------------------------- /assets/img/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/clear.png -------------------------------------------------------------------------------- /assets/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/close.png -------------------------------------------------------------------------------- /assets/img/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/delete.png -------------------------------------------------------------------------------- /assets/img/draw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/draw.png -------------------------------------------------------------------------------- /assets/img/expand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/expand.png -------------------------------------------------------------------------------- /assets/img/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/export.png -------------------------------------------------------------------------------- /assets/img/facets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/facets.png -------------------------------------------------------------------------------- /assets/img/filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/filter.png -------------------------------------------------------------------------------- /assets/img/gdocs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/gdocs.png -------------------------------------------------------------------------------- /assets/img/hide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/hide.png -------------------------------------------------------------------------------- /assets/img/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/image.png -------------------------------------------------------------------------------- /assets/img/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/info.png -------------------------------------------------------------------------------- /assets/img/json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/json.png -------------------------------------------------------------------------------- /assets/img/l-fac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/l-fac.png -------------------------------------------------------------------------------- /assets/img/l-seg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/l-seg.png -------------------------------------------------------------------------------- /assets/img/l-uni.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/l-uni.png -------------------------------------------------------------------------------- /assets/img/mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/mail.png -------------------------------------------------------------------------------- /assets/img/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/next.png -------------------------------------------------------------------------------- /assets/img/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/open.png -------------------------------------------------------------------------------- /assets/img/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/play.png -------------------------------------------------------------------------------- /assets/img/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/prev.png -------------------------------------------------------------------------------- /assets/img/r-arb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/r-arb.png -------------------------------------------------------------------------------- /assets/img/r-cal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/r-cal.png -------------------------------------------------------------------------------- /assets/img/r-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/r-grid.png -------------------------------------------------------------------------------- /assets/img/r-lin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/r-lin.png -------------------------------------------------------------------------------- /assets/img/r-rad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/r-rad.png -------------------------------------------------------------------------------- /assets/img/r-spi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/r-spi.png -------------------------------------------------------------------------------- /assets/img/record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/record.png -------------------------------------------------------------------------------- /assets/img/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/reset.png -------------------------------------------------------------------------------- /assets/img/s-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/s-log.png -------------------------------------------------------------------------------- /assets/img/s-rel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/s-rel.png -------------------------------------------------------------------------------- /assets/img/s-seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/s-seq.png -------------------------------------------------------------------------------- /assets/img/story.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/story.png -------------------------------------------------------------------------------- /assets/img/caption.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/caption.png -------------------------------------------------------------------------------- /assets/img/s-chron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/s-chron.png -------------------------------------------------------------------------------- /assets/img/s-intdur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/s-intdur.png -------------------------------------------------------------------------------- /assets/img/segments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/segments.png -------------------------------------------------------------------------------- /assets/img/timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/timeline.png -------------------------------------------------------------------------------- /privacy_terms.md: -------------------------------------------------------------------------------- 1 | [https://privacy.microsoft.com/en-US/privacystatement/](https://privacy.microsoft.com/en-US/privacystatement/) -------------------------------------------------------------------------------- /assets/img/categories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/categories.png -------------------------------------------------------------------------------- /assets/img/highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/highlight.png -------------------------------------------------------------------------------- /assets/img/resetBasic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TimelineStoryteller/HEAD/assets/img/resetBasic.png -------------------------------------------------------------------------------- /style/font-awesome/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .tmp 4 | stats.json 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | webpack.statistics.dev.html 13 | webpack.statistics.prod.html 14 | 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-microsoft-contrib/recommended", 3 | "rulesDirectory": [ 4 | "node_modules/tslint-microsoft-contrib" 5 | ], 6 | "rules": { 7 | "no-relative-imports": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/core/templates/toElement.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates an html element from the given html 3 | * @param {string} html The html for the element 4 | * @returns {HTMLElement} The element that was created from the html 5 | */ 6 | export default function (html) { 7 | var tmpEle = document.createElement("div"); 8 | tmpEle.innerHTML = html.trim().replace(/\n/g, ""); 9 | return tmpEle.firstChild; 10 | }; -------------------------------------------------------------------------------- /style/font-awesome/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /style/font-awesome/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "target": "es6", 7 | "sourceMap": true, 8 | "outDir": "./.tmp/build/", 9 | "moduleResolution": "node", 10 | "declaration": true, 11 | "lib": [ 12 | "es2015", 13 | "dom" 14 | ] 15 | }, 16 | "files": [ 17 | "src/visual.ts" 18 | ] 19 | } -------------------------------------------------------------------------------- /style/font-awesome/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | -------------------------------------------------------------------------------- /style/font-awesome/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a number whose value is limited to the given range. 3 | * 4 | * Example: limit the output of this computation to between 0 and 255 5 | * (x * 255).clamp(0, 255) 6 | * 7 | * @param {Number} value The value to clamp 8 | * @param {Number} min The lower boundary of the output range 9 | * @param {Number} max The upper boundary of the output range 10 | * @returns A number in the range [min, max] 11 | * @type Number 12 | */ 13 | export function clamp(value, min, max) { 14 | return Math.min(Math.max(value, min), max); 15 | }; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.5 2 | * Cosmetic changes 3 | 4 | ## 2.0.4 5 | * Filter applying fix 6 | 7 | ## 2.0.3 8 | * Additional items for context menu 9 | 10 | ## 2.0.2 11 | * Switched to API 3.4.0 12 | * Support of contextmenu 13 | 14 | ## 2.0.1 15 | * New library core-js to support modern JS features in IE11 16 | 17 | ## 2.0.0 18 | * Switched to API 3.2.0 19 | * Switched to webpack builder 20 | * Packages were updated 21 | * Code from timelinestoryteller library was migrated to the visual 22 | 23 | ## 1.0.8 24 | * Packages update 25 | * A new support e-mail 26 | -------------------------------------------------------------------------------- /style/font-awesome/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /src/core/annotations.ts: -------------------------------------------------------------------------------- 1 | import globals from "./globals"; 2 | 3 | export default { 4 | 5 | /** 6 | * Returns the next available z-index for an annotation 7 | * @returns {number} The next z-index 8 | */ 9 | getNextZIndex: function () { 10 | var nextIndex = 0; 11 | (globals.annotation_list || []) 12 | .concat(globals.caption_list || []) 13 | .concat(globals.image_list || []) 14 | .forEach(function (item) { 15 | if (item.z_index >= nextIndex) { 16 | nextIndex = item.z_index + 1; 17 | } 18 | }); 19 | return nextIndex; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /style/font-awesome/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /style/font-awesome/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /style/font-awesome/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timelinestoryteller-powerbi", 3 | "version": "2.0.5", 4 | "description": "A PowerBI visual that provies similar functionality as timelinestoryteller (https://timelinestoryteller.com)", 5 | "scripts": { 6 | "pbiviz": "pbiviz", 7 | "start": "pbiviz start", 8 | "package": "pbiviz package", 9 | "lint": "tslint -c tslint.json -p tsconfig.json" 10 | }, 11 | "author": "Microsoft", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "powerbi-visuals-tools": "^3.1.15", 15 | "typescript": "4.0.5" 16 | }, 17 | "dependencies": { 18 | "core-js": "^3.8.1", 19 | "d3": "^3.5.17", 20 | "ellipsize": "^0.1.0", 21 | "intro.js": "^3.1.0", 22 | "lodash.assignin": "^4.2.0", 23 | "lodash.isequal": "^4.5.0", 24 | "moment": "^2.29.1", 25 | "powerbi-visuals-api": "^3.4.0", 26 | "powerbi-visuals-utils-dataviewutils": "^2.4.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /style/font-awesome/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | -------------------------------------------------------------------------------- /certs/PowerBICustomVisualTest_public.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICwzCCAaugAwIBAgIJALUT957lyUi3MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMTCWxvY2FsaG9zdDAeFw0xODA5MjcxOTI3NDlaFw0yODA5MjQxOTI3NDlaMBQx 4 | EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBANqnmPqw/bmKH+KwGfrfWmcDTjBgGmY6I+D1D5nuPmY2sxf2iGPZ+lWvzCgi 6 | DMna+f1t1kA7dlWdF8xKi6rWmLtAIgPglfmRI0w6zxDEY445Adg+m71/ay6Z2T26 7 | LR3ssM3z/UC8cjBnDdX9aYxdEEq/EOzSo/83s3swioD24xMOe5eNoMUSjcbziTNJ 8 | eYyR7uwCYcV/s3xBYgGAnxW25R9/6kALMV2AXGsbLwikYufK4GEonuI1XN+pErSr 9 | h4DqVzcKpyZs1uI3Us4rWXIEVuOvQ/ASb4nLfq79Y0MXfJiGU9XjUq8U3XYjArUw 10 | OrHnJiLwLaC1yoljSfkYEMEgm/ECAwEAAaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxo 11 | b3N0MA0GCSqGSIb3DQEBCwUAA4IBAQAL5XiElTxbXR4ef9ngTwuEulHhhSyiZcMl 12 | cZw0Gt4if20Qr5re+4EulWbgQ5gEOKjbvfDGZ32B8Gq0eN1UIkwoCIAuSrSAF8w1 13 | IY1YbsyzRxZ2Uf6N8W7VP25UHnZ8YPPTyhkvkPzGJmHqf1O80ZHZBUj/aw9Vgmb0 14 | FlzN3Du9eDgVVgMDO1ALymKScIHgPjWo3e4eB0B5BMFpA2bVMA7GhLgib+DgYXe5 15 | kvfWgvu+DJzxG2VWd37AG1nA2UXiDBkwdghAzUz5RYmmHpzEGfQcCf5x/4H3fMVh 16 | I8JSVrnWuHCBCLlCBdHzoT6XOq2QTzUzGGGPAli7Qsi5wJzSaLP5 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { dataViewObjectsParser } from "powerbi-visuals-utils-dataviewutils"; 2 | 3 | /** 4 | * Represents the settings for timeline story teller 5 | */ 6 | export default class TimelineStorytellerSettings extends dataViewObjectsParser.DataViewObjectsParser { 7 | 8 | /** 9 | * Represents the story related settings 10 | */ 11 | public story: StorySettings = new StorySettings(); 12 | 13 | /** 14 | * The settings used for display 15 | */ 16 | public display: DisplaySettings = new DisplaySettings(); 17 | } 18 | 19 | /** 20 | * Represents the story related settings 21 | */ 22 | class StorySettings { 23 | 24 | /** 25 | * The saved story 26 | */ 27 | public savedStory: string = ''; // Needs to be an empty string, otherwise PBI will not pick it up 28 | 29 | /** 30 | * Boolean indicating whether or not to auto load the story that was saved 31 | */ 32 | public autoLoad: boolean = false; 33 | } 34 | 35 | /** 36 | * Represents the display settings 37 | */ 38 | class DisplaySettings { 39 | /** 40 | * The scale of the UI 41 | */ 42 | public uiScale: number = 0.7; 43 | } 44 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 Uncharted Software Inc. 3 | * http://www.uncharted.software/ 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | * this software and associated documentation files (the "Software"), to deal in 7 | * the Software without restriction, including without limitation the rights to 8 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | * of the Software, and to permit persons to whom the Software is furnished to do 10 | * so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | -------------------------------------------------------------------------------- /pbiviz.json: -------------------------------------------------------------------------------- 1 | { 2 | "visual": { 3 | "name": "TimelineStoryteller", 4 | "displayName": "Timeline Storyteller ver.2.0.5", 5 | "guid": "TimelineStoryteller1652434605730", 6 | "visualClassName": "TimelineStoryteller", 7 | "description": "Timeline Storyteller is an expressive visual storytelling environment for presenting timelines. You can use Timeline Storyteller to present different aspects of your data using a palette of timeline representations, scales, and layouts, as well as controls for filtering, highlighting, and annotation. To learn more about the research performed at Microsoft Research that informed this project, see timelinesrevisited.github.io. This is an open source project released under a MIT license at https://github.com/Microsoft/timelinestoryteller.", 8 | "supportUrl": "http://community.powerbi.com", 9 | "gitHubUrl": "https://github.com/microsoft/PowerBI-visuals-TimelineStoryteller.git", 10 | "version": "2.0.5" 11 | }, 12 | "author": { 13 | "name": "Microsoft Corporation", 14 | "email": "pbicvsupport@microsoft.com" 15 | }, 16 | "apiVersion": "3.4.0", 17 | "assets": { 18 | "icon": "assets/icon.png" 19 | }, 20 | "style": "style/visual.scss", 21 | "capabilities": "capabilities.json" 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Timeline Storyteller -- PowerBI 2 | 3 | ## Getting Started 4 | * Install Python 2.7 5 | * Install [yarn](https://yarnpkg.com) 6 | * Install [node 6](https://nodejs.org/en/) 7 | * Run `yarn` to install the dependencies 8 | * Build `timelinestoryteller` in another directory: (https://github.com/Microsoft/timelinestoryteller#setup--testing) 9 | * Run `yarn link` in the timelinestoryteller directory 10 | * Run `yarn link timeline_storyteller` in the PowerBI-visuals-TimelineStoryteller directory 11 | * Run `yarn package` to package the visual. 12 | * The `.pbiviz` file will be generated in the `dist` folder 13 | > You must change the `guid` property in pbiviz.json for Power BI to recognize your custom build 14 | 15 | ## Testing 16 | 17 | * Run `yarn test` 18 | 19 | # Contributing 20 | 21 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 22 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 23 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 24 | 25 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 26 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 27 | provided by the bot. You will only need to do this once across all repos using our CLA. 28 | 29 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 30 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 31 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 32 | -------------------------------------------------------------------------------- /certs/PowerBICustomVisualTest_private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDap5j6sP25ih/i 3 | sBn631pnA04wYBpmOiPg9Q+Z7j5mNrMX9ohj2fpVr8woIgzJ2vn9bdZAO3ZVnRfM 4 | Souq1pi7QCID4JX5kSNMOs8QxGOOOQHYPpu9f2sumdk9ui0d7LDN8/1AvHIwZw3V 5 | /WmMXRBKvxDs0qP/N7N7MIqA9uMTDnuXjaDFEo3G84kzSXmMke7sAmHFf7N8QWIB 6 | gJ8VtuUff+pACzFdgFxrGy8IpGLnyuBhKJ7iNVzfqRK0q4eA6lc3CqcmbNbiN1LO 7 | K1lyBFbjr0PwEm+Jy36u/WNDF3yYhlPV41KvFN12IwK1MDqx5yYi8C2gtcqJY0n5 8 | GBDBIJvxAgMBAAECggEABwa/ub6aSK4RnD1AeHlY9Fp81r0f3A0ePgph9/ZZ9vrn 9 | AmwfseW9T6eYjemaM5pf0i8HZYUDpnin3R7AIjtHKS1Eao97AkwRsE9rfARzloxq 10 | bMKGSq6fiR3Uh1FGReHnRJcEmmHz4W5OWYQNMj+DWIOPTazvLDJkCB9lNAO7BE+x 11 | mtC2GhxmW61j/X/UUNBIa5EKOI6ygqHeDWYoymqmx3PbZB+QijI0E0qGqjo0ipjy 12 | AGHaZwVYUqL7mjFca1yzJwcjSI+zQyr4hsJeZCh1TnyZcRlIu8xImVPQQzyCBGvO 13 | tbmsPgSCDK0m8kgEjw55Hsd5Kgx6PHShjjKHkkKJKQKBgQD5k0xJOyII+fOZ41CU 14 | DEJyz4VAzhddV3Ee2GFa49J3MjmGR4j/S4k1mfvD7Js9sX3SrUs5/EG1nXjFUN5h 15 | tUhrFuaNTULcsavGdc2Ok6YQgRgB+GWR5ROJ6EEZFXBw1EDlVn+wl5AhV7T0evQn 16 | xwfhpfn5JEtQvTsr1WOZBcwmtwKBgQDgSIg4UfZL5wsolE85inLUYVseV1OKvPUh 17 | 8WxPjSDR1Z2Q8ws7Vm75anSDkpHs9Hqvsuj34GQNXnlzan3RWL8ImmR9E2RYp+Ld 18 | F5bI3BO0KH9w7OPUhEK5/WLfkP4r/PA0NPMTLYz+GiagKlCi9DpqBMpzgsfMGZrd 19 | 2z8SQcJqlwKBgQDbGElnHGdTlAIbfWfWoDCRU8Z6TrJKOiJPXsuQm+G+zDOdwxzs 20 | aaZpjOVtaUAbV77KfWFF5ULWKgjEx6qCAkAx6ue48jOZRMw0rGQpH0swv/OQfzzD 21 | aGPPtDm5yI1uL5dM/bOZFmTsMG/mFC5U4S/1Et0wr4ECvyy73VfTFTjUXwKBgDqt 22 | f0YM2308M0UomBmbyTMbTXWAr1Cq4w9AVG/zolSDqLeVfqjFkj8axOTvYdI2nCp/ 23 | ikffow2EXA4AHG664y/jBMtcWXKAafAiDcb7HQSTW9Q1hd3BxJtYWZfYHUdw438l 24 | IzsvPaX1PYnFyFb5wpaeLkFOQ+t3/3Zvt+6U2cJ9AoGAVwnQmey6Bku8fcNbm/SO 25 | qp1czF6tP1wTKVDFGSKMJVJVPiYFd/UkG/85V1TND6tRnCm4bX7gt/9NaS5UY7Wy 26 | kRFqKB+gNfoHuN9GRGXKfkLfcTLynDXsUgDVImbg8HhjODMef8cDneJoab0DYrs0 27 | Vkj6DKt5CuF6HEYMoNRUIro= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /src/core/colors.ts: -------------------------------------------------------------------------------- 1 | const shemas = { 2 | schema1: function () { 3 | return [ 4 | "#775566", 5 | "#6688bb", 6 | "#556677", 7 | "#88aa88", 8 | "#88bb33", 9 | "#cc7744", 10 | "#003366", 11 | "#994422", 12 | "#331111" 13 | ]; 14 | }, 15 | schema2: function () { 16 | return [ 17 | "#44b3c2", 18 | "#f1a94e", 19 | "#e45641", 20 | "#5d4c46", 21 | "#7b8d8e", 22 | "#2ca02c", 23 | "#003366", 24 | "#9467bd", 25 | "#bcbd22", 26 | "#e377c2" 27 | ]; 28 | }, 29 | schema3: function () { 30 | return [ 31 | "#001166", 32 | "#0055aa", 33 | "#1199cc", 34 | "#99ccdd", 35 | "#002222", 36 | "#ddffff", 37 | "#446655", 38 | "#779988", 39 | "#115522" 40 | ]; 41 | }, 42 | schema4: function () { 43 | return [ 44 | "#1f77b4", 45 | "#ff7f0e", 46 | "#2ca02c", 47 | "#d62728", 48 | "#9467bd", 49 | "#8c564b", 50 | "#e377c2", 51 | "#7f7f7f", 52 | "#bcbd22", 53 | "#17becf" 54 | ]; 55 | }, 56 | schema5: function () { 57 | return [ 58 | "#1f77b4", 59 | "#aec7e8", 60 | "#ff7f0e", 61 | "#ffbb78", 62 | "#2ca02c", 63 | "#98df8a", 64 | "#d62728", 65 | "#ff9896", 66 | "#9467bd", 67 | "#c5b0d5", 68 | "#8c564b", 69 | "#c49c94", 70 | "#e377c2", 71 | "#f7b6d2", 72 | "#7f7f7f", 73 | "#c7c7c7", 74 | "#bcbd22", 75 | "#dbdb8d", 76 | "#17becf", 77 | "#9edae5" 78 | ]; 79 | }, 80 | // colorbrewer categorical 12 81 | schema6: function () { 82 | return [ 83 | "#a6cee3", 84 | "#1f78b4", 85 | "#b2df8a", 86 | "#33a02c", 87 | "#fb9a99", 88 | "#e31a1c", 89 | "#fdbf6f", 90 | "#ff7f00", 91 | "#cab2d6", 92 | "#6a3d9a", 93 | "#ffff99", 94 | "#b15928" 95 | ]; 96 | } 97 | }; 98 | 99 | export default shemas; 100 | -------------------------------------------------------------------------------- /src/lib/UpdateType.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * Copyright (c) 2016 Microsoft 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | /** 25 | * Represents an update type for a visual 26 | */ 27 | enum UpdateType { 28 | 29 | /** 30 | * This is an unknown update type 31 | */ 32 | Unknown = 0, 33 | 34 | /** 35 | * This is a data update 36 | */ 37 | Data = 1 << 0, 38 | 39 | /** 40 | * This is a resize operation 41 | */ 42 | Resize = 1 << 1, 43 | 44 | /** 45 | * This has some settings that have been changed 46 | */ 47 | Settings = 1 << 2, 48 | 49 | /** 50 | * This is the initial update 51 | */ 52 | Initial = 1 << 3, 53 | 54 | // Some utility keys for debugging 55 | DataAndResize = UpdateType.Data | UpdateType.Resize, 56 | DataAndSettings = UpdateType.Data | UpdateType.Settings, 57 | SettingsAndResize = UpdateType.Settings | UpdateType.Resize, 58 | All = UpdateType.Data | UpdateType.Resize | UpdateType.Settings 59 | } 60 | export default UpdateType; -------------------------------------------------------------------------------- /src/core/templates/addImagePopup.html.ts: -------------------------------------------------------------------------------- 1 | import toElement from "./toElement"; 2 | 3 | /** 4 | * Template for the add image popup 5 | * @returns {HTMLElement} The add image popup element 6 | */ 7 | export default function () { 8 | return toElement("\n
\n
\n
\n
\n

Add from image URL

\n \n
\n \n
\n
\n
\n
\n

Add from your computer

\n
\n
Drop files here
\n
OR
\n \n
\n
\n No files selected\n
\n
\n
\n
\n

Options

\n
\n \n \n x\n \n
\n
\n
\n \n
\n
\n
\n"); 9 | }; -------------------------------------------------------------------------------- /src/dataConversion.ts: -------------------------------------------------------------------------------- 1 | import powerbiVisualsApi from "powerbi-visuals-api"; 2 | import IVisualHost = powerbiVisualsApi.extensibility.visual.IVisualHost; 3 | import ISelectionId = powerbiVisualsApi.extensibility.ISelectionId; 4 | 5 | /** 6 | * Converts the powerbi dataview to the data format required by TimelineStoryteller 7 | * @param {powerbiVisualsApi.DataView} dataView The dataview to convert 8 | * @returns {object} The data compatible with TimelineStoryteller 9 | */ 10 | export default function (dataView: powerbiVisualsApi.DataView, host: IVisualHost) { 11 | const cols = [ 12 | 'facet', 13 | 'content_text', 14 | 'start_date', 15 | 'end_date', 16 | 'category' 17 | ]; 18 | 19 | if (dataView && dataView.table && dataView.table.columns) { 20 | const newMappings: any = {}; 21 | dataView.table.columns.forEach((column, index) => { 22 | Object.keys(column.roles).sort().forEach(role => { 23 | newMappings[role] = { 24 | index, 25 | parent: column.queryName + ':' + column.groupName 26 | }; 27 | }); 28 | }); 29 | 30 | // We need both dates for it to work properly 31 | return dataView.table.rows.map((row, rowIndex) => { 32 | const item = { 33 | selectionId: null 34 | }; 35 | cols.forEach(column => { 36 | let value = row[(newMappings[column] || {}).index]; 37 | if (value && (column === 'start_date' || column === 'end_date')) { 38 | if (!(value instanceof Date)) { 39 | // TimelineStoryteller likes strings 40 | value = value + ""; 41 | } 42 | } 43 | item[column] = value; 44 | }); 45 | 46 | const selection: ISelectionId = host.createSelectionIdBuilder() 47 | .withTable(dataView.table, rowIndex) 48 | .createSelectionId(); 49 | 50 | item.selectionId = selection; 51 | 52 | return item; 53 | }); 54 | } 55 | } -------------------------------------------------------------------------------- /src/core/colorPickerPopup.ts: -------------------------------------------------------------------------------- 1 | // Include the flexi-color-picker css 2 | import "../../assets/css/lib/colorpicker.css"; 3 | 4 | // Include the color picker js 5 | require("./lib/colorpicker"); 6 | var colorPicker = (window).ColorPicker; 7 | 8 | /** 9 | * Creates a color picker popup 10 | * @param {HTMLElement} parentElement The parent element. 11 | * @returns {Object} The color picker popup instance 12 | */ 13 | export default function (parentElement) { 14 | /* eslint-disable */ 15 | var element = document.createElement("div"); 16 | element.className = "color-picker-popup"; 17 | element.innerHTML = '
' + '
' + '' + ''; 18 | '
'; 19 | /* eslint-enable */ 20 | 21 | element.style.cssText = "text-align:right;border:1px solid #ccc;display:none;position:absolute;outline:none;z-index:10000000;background-color:white"; 22 | parentElement.appendChild(element); 23 | 24 | // The currently selected color 25 | var selectedColor; 26 | var listener; 27 | 28 | // Our color picker 29 | var cp = colorPicker(element.querySelector(".cp-small"), function (hex) { 30 | selectedColor = hex; 31 | }); 32 | 33 | element.querySelector(".color-picker-ok").addEventListener("click", function () { 34 | me.hide(); 35 | if (listener) { 36 | listener(selectedColor); 37 | } 38 | }); 39 | 40 | element.querySelector(".color-picker-cancel").addEventListener("click", function () { 41 | me.hide(); 42 | }); 43 | 44 | var me = { 45 | 46 | /** 47 | * Shows the color picker 48 | * @param {HTMLElement} relativeTo The element to show the color picker relative to 49 | * @param {string} color The starting color for the color picker 50 | * @param {Function} onChanged The function which will be called when a color is picked. 51 | * @returns {void} 52 | */ 53 | show: function show(relativeTo, color, onChanged) { 54 | selectedColor = color; 55 | 56 | cp.setHex(color); 57 | listener = onChanged; 58 | 59 | element.style.display = "block"; 60 | 61 | var rect = relativeTo.getBoundingClientRect(); 62 | element.style.left = rect.right + "px"; 63 | element.style.top = rect.bottom + "px"; 64 | }, 65 | 66 | /** 67 | * Hides the color picker 68 | * @returns {void} 69 | */ 70 | hide: function hide() { 71 | element.style.display = "none"; 72 | } 73 | }; 74 | return me; 75 | }; -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/core/imageUrls.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Forms the url for the given image name 3 | * @param {string} name The name of the image to get the url for 4 | * @returns {string} The final url for the given image 5 | */ 6 | 7 | function formUrl(name) { 8 | if (name.indexOf("demo") >= 0) { 9 | return "img/" + name; 10 | } 11 | 12 | var raw = require("../../assets/img/" + name); 13 | return raw; 14 | 15 | // var imageContents = toArrayBuffer(raw); 16 | // var blob = new Blob([imageContents], { 17 | // type: name.indexOf(".png") >= 0 ? "image/png" : "image/svg+xml" 18 | // }); 19 | 20 | //return URL.createObjectURL(blob); 21 | } 22 | 23 | function toArrayBuffer(str) { 24 | var buffer = new ArrayBuffer(str.length); 25 | var array = new Uint8Array(buffer); 26 | for (var i = 0; i < str.length; i++) { 27 | array[i] = str.charCodeAt(i); 28 | } 29 | return buffer; 30 | } 31 | 32 | var imageUrlMapping = { 33 | "caption.png": formUrl("caption.png"), 34 | "categories.png": formUrl("categories.png"), 35 | "check.png": formUrl("check.png"), 36 | "clear.png": formUrl("clear.png"), 37 | "close.png": formUrl("close.png"), 38 | "csv.png": formUrl("csv.png"), 39 | "delete.png": formUrl("delete.png"), 40 | 41 | /** 42 | * Demo images 43 | */ 44 | "demo.png": formUrl("demo.png"), 45 | "demo_story.png": formUrl("demo_story.png"), 46 | 47 | "draw.png": formUrl("draw.png"), 48 | "expand.png": formUrl("expand.png"), 49 | "export.png": formUrl("export.png"), 50 | "facets.png": formUrl("facets.png"), 51 | "filter.png": formUrl("filter.png"), 52 | "gdocs.png": formUrl("gdocs.png"), 53 | "gif.png": formUrl("gif.png"), 54 | "hide.png": formUrl("hide.png"), 55 | "highlight.png": formUrl("highlight.png"), 56 | "image.png": formUrl("image.png"), 57 | "info.png": formUrl("info.png"), 58 | "json.png": formUrl("json.png"), 59 | "l-fac.png": formUrl("l-fac.png"), 60 | "l-seg.png": formUrl("l-seg.png"), 61 | "l-uni.png": formUrl("l-uni.png"), 62 | "mail.png": formUrl("mail.png"), 63 | "min.png": formUrl("min.png"), 64 | "ms-logo.svg": formUrl("ms-logo.svg"), 65 | "next.png": formUrl("next.png"), 66 | "open.png": formUrl("open.png"), 67 | "pin.png": formUrl("pin.png"), 68 | "play.png": formUrl("play.png"), 69 | "png.png": formUrl("png.png"), 70 | "prev.png": formUrl("prev.png"), 71 | "q.png": formUrl("q.png"), 72 | "r-arb.png": formUrl("r-arb.png"), 73 | "r-cal.png": formUrl("r-cal.png"), 74 | "r-grid.png": formUrl("r-grid.png"), 75 | "r-lin.png": formUrl("r-lin.png"), 76 | "r-rad.png": formUrl("r-rad.png"), 77 | "r-spi.png": formUrl("r-spi.png"), 78 | "record.png": formUrl("record.png"), 79 | "reset.png": formUrl("reset.png"), 80 | "resetBasic.png": formUrl("resetBasic.png"), 81 | "s-chron.png": formUrl("s-chron.png"), 82 | "s-intdur.png": formUrl("s-intdur.png"), 83 | "s-log.png": formUrl("s-log.png"), 84 | "s-rel.png": formUrl("s-rel.png"), 85 | "s-seq.png": formUrl("s-seq.png"), 86 | "segments.png": formUrl("segments.png"), 87 | "story.png": formUrl("story.png"), 88 | "svg.png": formUrl("svg.png"), 89 | "timeline.png": formUrl("timeline.png"), 90 | "vl.png": formUrl("vl.png") 91 | }; 92 | export default function (imageName) { 93 | return imageUrlMapping[imageName]; 94 | }; 95 | -------------------------------------------------------------------------------- /src/core/lib/time.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(e.time={})}(this,function(e){"use strict";function t(e,n,u){function r(t){return e(t=new Date(+t)),t}return r.floor=r,r.round=function(t){var u=new Date(+t),r=new Date(t-1);return e(u),e(r),n(r,1),r-t>t-u?u:r},r.ceil=function(t){return e(t=new Date(t-1)),n(t,1),t},r.offset=function(e,t){return n(e=new Date(+e),null==t?1:Math.floor(t)),e},r.range=function(t,u,r){var a=[];if(t=new Date(t-1),u=new Date(+u),r=null==r?1:Math.floor(r),!(u>t))return a;for(e(t),n(t,1),u>t&&a.push(new Date(+t));n(t,r),u>t;)a.push(new Date(+t));return a},r.filter=function(u){return t(function(t){for(;e(t),!u(t);)t.setTime(t-1)},function(e,t){for(;--t>=0;)for(;n(e,1),!u(e););})},u&&(r.count=function(t,n){return t=new Date(+t),n=new Date(+n),e(t),e(n),Math.floor(u(t,n))}),r}function n(e){return t(function(t){t.setHours(0,0,0,0),t.setDate(t.getDate()-(t.getDay()+7-e)%7)},function(e,t){e.setDate(e.getDate()+7*t)},function(e,t){return(t-e-6e4*(t.getTimezoneOffset()-e.getTimezoneOffset()))/6048e5})}function u(e){return t(function(t){t.setUTCHours(0,0,0,0),t.setUTCDate(t.getUTCDate()-(t.getUTCDay()+7-e)%7)},function(e,t){e.setUTCDate(e.getUTCDate()+7*t)},function(e,t){return(t-e)/6048e5})}var r=t(function(e){e.setMilliseconds(0)},function(e,t){e.setTime(+e+1e3*t)},function(e,t){return(t-e)/1e3});e.seconds=r.range;var a=t(function(e){e.setSeconds(0,0)},function(e,t){e.setTime(+e+6e4*t)},function(e,t){return(t-e)/6e4});e.minutes=a.range;var o=t(function(e){e.setMinutes(0,0,0)},function(e,t){e.setTime(+e+36e5*t)},function(e,t){return(t-e)/36e5});e.hours=o.range;var s=t(function(e){e.setHours(0,0,0,0)},function(e,t){e.setDate(e.getDate()+t)},function(e,t){return(t-e-6e4*(t.getTimezoneOffset()-e.getTimezoneOffset()))/864e5});e.days=s.range,e.sunday=n(0),e.sundays=e.sunday.range,e.monday=n(1),e.mondays=e.monday.range,e.tuesday=n(2),e.tuesdays=e.tuesday.range,e.wednesday=n(3),e.wednesdays=e.wednesday.range,e.thursday=n(4),e.thursdays=e.thursday.range,e.friday=n(5),e.fridays=e.friday.range,e.saturday=n(6),e.saturdays=e.saturday.range;var c=e.sunday;e.weeks=c.range;var i=t(function(e){e.setHours(0,0,0,0),e.setDate(1)},function(e,t){e.setMonth(e.getMonth()+t)},function(e,t){return t.getMonth()-e.getMonth()+12*(t.getFullYear()-e.getFullYear())});e.months=i.range;var f=t(function(e){e.setHours(0,0,0,0),e.setMonth(0,1)},function(e,t){e.setFullYear(e.getFullYear()+t)},function(e,t){return t.getFullYear()-e.getFullYear()});e.years=f.range;var d=t(function(e){e.setUTCMilliseconds(0)},function(e,t){e.setTime(+e+1e3*t)},function(e,t){return(t-e)/1e3});e.utcSeconds=d.range;var g=t(function(e){e.setUTCSeconds(0,0)},function(e,t){e.setTime(+e+6e4*t)},function(e,t){return(t-e)/6e4});e.utcMinutes=g.range;var y=t(function(e){e.setUTCMinutes(0,0,0)},function(e,t){e.setTime(+e+36e5*t)},function(e,t){return(t-e)/36e5});e.utcHours=y.range;var T=t(function(e){e.setUTCHours(0,0,0,0)},function(e,t){e.setUTCDate(e.getUTCDate()+t)},function(e,t){return(t-e)/864e5});e.utcDays=T.range,e.utcSunday=u(0),e.utcSundays=e.utcSunday.range,e.utcMonday=u(1),e.utcMondays=e.utcMonday.range,e.utcTuesday=u(2),e.utcTuesdays=e.utcTuesday.range,e.utcWednesday=u(3),e.utcWednesdays=e.utcWednesday.range,e.utcThursday=u(4),e.utcThursdays=e.utcThursday.range,e.utcFriday=u(5),e.utcFridays=e.utcFriday.range,e.utcSaturday=u(6),e.utcSaturdays=e.utcSaturday.range;var l=e.utcSunday;e.utcWeeks=l.range;var D=t(function(e){e.setUTCHours(0,0,0,0),e.setUTCDate(1)},function(e,t){e.setUTCMonth(e.getUTCMonth()+t)},function(e,t){return t.getUTCMonth()-e.getUTCMonth()+12*(t.getUTCFullYear()-e.getUTCFullYear())});e.utcMonths=D.range;var h=t(function(e){e.setUTCHours(0,0,0,0),e.setUTCMonth(0,1)},function(e,t){e.setUTCFullYear(e.getUTCFullYear()+t)},function(e,t){return t.getUTCFullYear()-e.getUTCFullYear()});e.utcYears=h.range,e.second=r,e.minute=a,e.hour=o,e.day=s,e.week=c,e.month=i,e.year=f,e.utcSecond=d,e.utcMinute=g,e.utcHour=y,e.utcDay=T,e.utcWeek=l,e.utcMonth=D,e.utcYear=h}); -------------------------------------------------------------------------------- /src/core/gridAxis.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import d3 from "d3"; 4 | 5 | /** 6 | 7 | gridAxis: //a reusable grid axis 8 | 9 | **/ 10 | 11 | d3.gridAxis = function (unit_width) { 12 | var cell_size = 50, 13 | century_height = cell_size * unit_width, 14 | duration = 1000, 15 | min_year = 1900, 16 | max_year = 2000; 17 | 18 | function gridAxis(selection) { 19 | selection.each(function (data) { 20 | var g = d3.select(this); 21 | 22 | // grid container items for each year 23 | var grid = g.selectAll(".grid") 24 | .data(data); 25 | 26 | var grid_enter = grid.enter() 27 | .append("g") 28 | .attr("class", "grid"); 29 | 30 | grid_enter.append("rect") 31 | .attr("class", "year_cell") 32 | .attr("width", cell_size) 33 | .attr("height", cell_size) 34 | .attr("x", function (d) { 35 | return getXGridPosition(d); 36 | }) 37 | .attr("y", 0); 38 | 39 | grid_enter.append("text") 40 | .attr("class", "year_label") 41 | .attr("x", function (d) { 42 | return getXGridPosition(d); 43 | }) 44 | .attr("y", 0) 45 | .attr("dy", "-0.3em") 46 | .attr("dx", "0.3em") 47 | .text(""); 48 | 49 | var grid_exit = grid.exit() 50 | .transition() 51 | .duration(duration) 52 | .remove(); 53 | 54 | grid_exit.select(".year_cell") 55 | .attr("height", 0) 56 | .attr("y", 0); 57 | 58 | grid_exit.select(".year_label") 59 | .attr("y", 0) 60 | .text(""); 61 | 62 | var grid_update = grid.transition() 63 | .duration(duration); 64 | 65 | grid_update.select(".year_cell") 66 | .attr("x", function (d) { 67 | return getXGridPosition(d); 68 | }) 69 | .attr("y", function (d) { 70 | return getYGridPosition(d, min_year); 71 | }); 72 | 73 | grid_update.select(".year_label") 74 | .attr("x", function (d) { 75 | return getXGridPosition(d); 76 | }) 77 | .attr("y", function (d) { 78 | return getYGridPosition(d, min_year) + cell_size; 79 | }) 80 | .text(function (d) { 81 | return d; 82 | }); 83 | }); 84 | d3.timer.flush(); 85 | } 86 | 87 | // place an element in correct x position on grid axis 88 | function getXGridPosition(year) { 89 | if (year < 0 && year % 10 !== 0) { 90 | return (year % 10 + 10) * cell_size; // negative decade year correction adds 10 91 | } 92 | return year % 10 * cell_size; 93 | } 94 | 95 | // place an element in correct y position on grid axis 96 | function getYGridPosition(year, min) { 97 | var decade_of_century = 0, 98 | century_offset = Math.floor(year / 100) - Math.floor(min / 100), 99 | y_offset = 0; 100 | 101 | century_height = cell_size * unit_width; 102 | 103 | if (year < 0) { 104 | century_offset++; // handle BC dates 105 | if (year % 100 === 0) { 106 | decade_of_century = 0; 107 | century_offset--; 108 | y_offset = -unit_width; 109 | } else { 110 | decade_of_century = Math.floor(year % 100 / 10) - 1; 111 | } 112 | } else { 113 | decade_of_century = Math.floor(year % 100 / 10); 114 | } 115 | 116 | return decade_of_century * 1.25 * cell_size + century_offset * (century_height + cell_size) + y_offset; 117 | } 118 | 119 | gridAxis.min_year = function (x) { 120 | if (!arguments.length) { 121 | return min_year; 122 | } 123 | min_year = x; 124 | return gridAxis; 125 | }; 126 | 127 | gridAxis.max_year = function (x) { 128 | if (!arguments.length) { 129 | return max_year; 130 | } 131 | max_year = x; 132 | return gridAxis; 133 | }; 134 | 135 | gridAxis.duration = function (x) { 136 | if (!arguments.length) { 137 | return duration; 138 | } 139 | duration = x; 140 | return gridAxis; 141 | }; 142 | 143 | return gridAxis; 144 | }; 145 | 146 | export default d3.gridAxis; 147 | -------------------------------------------------------------------------------- /capabilities.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataRoles": [ 3 | { 4 | "name": "facet", 5 | "kind": "Grouping", 6 | "displayName": "Facet" 7 | }, 8 | { 9 | "name": "content_text", 10 | "kind": "Grouping", 11 | "displayName": "Content Text" 12 | }, 13 | { 14 | "name": "start_date", 15 | "kind": "Grouping", 16 | "displayName": "Start Date" 17 | }, 18 | { 19 | "name": "end_date", 20 | "kind": "Grouping", 21 | "displayName": "End Date" 22 | }, 23 | { 24 | "name": "category", 25 | "kind": "Grouping", 26 | "displayName": "Category" 27 | } 28 | ], 29 | "dataViewMappings": [ 30 | { 31 | "conditions": [ 32 | { 33 | "facet": { 34 | "max": 1 35 | }, 36 | "content_text": { 37 | "max": 1 38 | }, 39 | "start_date": { 40 | "max": 1 41 | }, 42 | "end_date": { 43 | "max": 1 44 | }, 45 | "category": { 46 | "max": 1 47 | } 48 | } 49 | ], 50 | "categorical": { 51 | "categories": { 52 | "select": [ 53 | { 54 | "for": { 55 | "in": "facet" 56 | } 57 | }, 58 | { 59 | "for": { 60 | "in": "content_text" 61 | } 62 | }, 63 | { 64 | "for": { 65 | "in": "start_date" 66 | } 67 | }, 68 | { 69 | "for": { 70 | "in": "end_date" 71 | } 72 | }, 73 | { 74 | "for": { 75 | "in": "category" 76 | } 77 | } 78 | ], 79 | "dataReductionAlgorithm": { 80 | "window": { 81 | "count": 2000 82 | } 83 | } 84 | } 85 | }, 86 | "table": { 87 | "rows": { 88 | "select": [ 89 | { 90 | "for": { 91 | "in": "facet" 92 | } 93 | }, 94 | { 95 | "for": { 96 | "in": "content_text" 97 | } 98 | }, 99 | { 100 | "for": { 101 | "in": "start_date" 102 | } 103 | }, 104 | { 105 | "for": { 106 | "in": "end_date" 107 | } 108 | }, 109 | { 110 | "for": { 111 | "in": "category" 112 | } 113 | } 114 | ], 115 | "dataReductionAlgorithm": { 116 | "window": { 117 | "count": 2000 118 | } 119 | } 120 | } 121 | } 122 | } 123 | ], 124 | "objects": { 125 | "story": { 126 | "displayName": "Story", 127 | "properties": { 128 | "savedStory": { 129 | "type": { 130 | "text": true 131 | } 132 | }, 133 | "autoLoad": { 134 | "displayName": "Auto Load", 135 | "description": "If true, then the saved story will auto load when TimelineStoryteller is loaded", 136 | "type": { 137 | "bool": true 138 | } 139 | } 140 | } 141 | }, 142 | "display": { 143 | "displayName": "Display", 144 | "properties": { 145 | "uiScale": { 146 | "displayName": "UI Scale", 147 | "description": "The scale of the UI elements", 148 | "type": { 149 | "numeric": true 150 | } 151 | }, 152 | "autoLoad": { 153 | "displayName": "Auto Load", 154 | "description": "If true, then the saved story will auto load when TimelineStoryteller is loaded", 155 | "type": { 156 | "bool": true 157 | } 158 | } 159 | } 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /assets/css/lib/colorpicker.css: -------------------------------------------------------------------------------- 1 | /* Common stuff */ 2 | .picker-wrapper, 3 | .slide-wrapper { 4 | position: relative; 5 | float: left; 6 | } 7 | .picker-indicator, 8 | .slide-indicator { 9 | position: absolute; 10 | left: 0; 11 | top: 0; 12 | pointer-events: none; 13 | } 14 | .picker, 15 | .slide { 16 | cursor: crosshair; 17 | float: left; 18 | } 19 | 20 | /* Default skin */ 21 | 22 | .cp-default { 23 | background-color: gray; 24 | padding: 12px; 25 | box-shadow: 0 0 40px #000; 26 | border-radius: 15px; 27 | float: left; 28 | } 29 | .cp-default .picker { 30 | width: 200px; 31 | height: 200px; 32 | } 33 | .cp-default .slide { 34 | width: 30px; 35 | height: 200px; 36 | } 37 | .cp-default .slide-wrapper { 38 | margin-left: 10px; 39 | } 40 | .cp-default .picker-indicator { 41 | width: 5px; 42 | height: 5px; 43 | border: 2px solid darkblue; 44 | -moz-border-radius: 4px; 45 | -o-border-radius: 4px; 46 | -webkit-border-radius: 4px; 47 | border-radius: 4px; 48 | opacity: .5; 49 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; 50 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=50); 51 | filter: alpha(opacity=50); 52 | background-color: white; 53 | } 54 | .cp-default .slide-indicator { 55 | width: 100%; 56 | height: 10px; 57 | left: -4px; 58 | opacity: .6; 59 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=60)"; 60 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=60); 61 | filter: alpha(opacity=60); 62 | border: 4px solid lightblue; 63 | -moz-border-radius: 4px; 64 | -o-border-radius: 4px; 65 | -webkit-border-radius: 4px; 66 | border-radius: 4px; 67 | background-color: white; 68 | } 69 | 70 | /* Small skin */ 71 | 72 | .cp-small { 73 | padding: 5px; 74 | background-color: white; 75 | /*float: left;*/ 76 | border-radius: 5px; 77 | } 78 | .cp-small .picker { 79 | width: 100px; 80 | height: 100px; 81 | } 82 | .cp-small .slide { 83 | width: 15px; 84 | height: 100px; 85 | } 86 | .cp-small .slide-wrapper { 87 | margin-left: 5px; 88 | } 89 | .cp-small .picker-indicator { 90 | width: 1px; 91 | height: 1px; 92 | border: 1px solid black; 93 | background-color: white; 94 | } 95 | .cp-small .slide-indicator { 96 | width: 100%; 97 | height: 2px; 98 | left: 0px; 99 | background-color: black; 100 | } 101 | 102 | /* Fancy skin */ 103 | 104 | .cp-fancy { 105 | padding: 10px; 106 | /* background-color: #C5F7EA; */ 107 | background: -webkit-linear-gradient(top, #aaa 0%, #222 100%); 108 | float: left; 109 | border: 1px solid #999; 110 | box-shadow: inset 0 0 10px white; 111 | } 112 | .cp-fancy .picker { 113 | width: 200px; 114 | height: 200px; 115 | } 116 | .cp-fancy .slide { 117 | width: 30px; 118 | height: 200px; 119 | } 120 | .cp-fancy .slide-wrapper { 121 | margin-left: 10px; 122 | } 123 | .cp-fancy .picker-indicator { 124 | width: 24px; 125 | height: 24px; 126 | background-image: url(http://cdn1.iconfinder.com/data/icons/fugue/bonus/icons-24/target.png); 127 | } 128 | .cp-fancy .slide-indicator { 129 | width: 30px; 130 | height: 31px; 131 | left: 30px; 132 | background-image: url(http://cdn1.iconfinder.com/data/icons/bluecoral/Left.png); 133 | } 134 | 135 | /* Normal skin */ 136 | 137 | .cp-normal { 138 | padding: 10px; 139 | background-color: white; 140 | float: left; 141 | border: 4px solid #d6d6d6; 142 | box-shadow: inset 0 0 10px white; 143 | } 144 | .cp-normal .picker { 145 | width: 200px; 146 | height: 200px; 147 | } 148 | .cp-normal .slide { 149 | width: 30px; 150 | height: 200px; 151 | } 152 | .cp-normal .slide-wrapper { 153 | margin-left: 10px; 154 | } 155 | .cp-normal .picker-indicator { 156 | width: 5px; 157 | height: 5px; 158 | border: 1px solid gray; 159 | opacity: .5; 160 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; 161 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=50); 162 | filter: alpha(opacity=50); 163 | background-color: white; 164 | pointer-events: none; 165 | } 166 | .cp-normal .slide-indicator { 167 | width: 100%; 168 | height: 10px; 169 | left: -4px; 170 | opacity: .6; 171 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=60)"; 172 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=60); 173 | filter: alpha(opacity=60); 174 | border: 4px solid gray; 175 | background-color: white; 176 | pointer-events: none; 177 | } -------------------------------------------------------------------------------- /assets/img/ms-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 12 | 16 | 20 | 24 | 28 | 32 | -------------------------------------------------------------------------------- /src/core/dialogs/addImageDialog.ts: -------------------------------------------------------------------------------- 1 | import d3 from "d3"; 2 | 3 | import template from "../templates/addImagePopup.html"; 4 | import imageUrls from "../imageUrls"; 5 | import utils from "../utils"; 6 | 7 | /** 8 | * An add image dialog 9 | * @constructor 10 | */ 11 | function AddImageDialog() { 12 | this.element = d3.select(template()); 13 | this._dispatcher = d3.dispatch("imageSelected"); 14 | this.on = this._dispatcher.on.bind(this._dispatcher); 15 | this._addImageUrl = this.element.select(".add_image_link"); 16 | this._addImageButton = this.element.select(".add_image_btn"); 17 | this._addImageDropZone = this.element.select(".image_local_add_drop_zone"); 18 | this._addImageFileChooser = this.element.select(".add_image_file_chooser"); 19 | this._addFilesContainer = this.element.select(".file_selection_container"); 20 | this._selectedFilesContainer = this.element.select(".selected_files_container"); 21 | this._resizeEnabled = this.element.select(".resize_enabled_cb"); 22 | this._resizeWidth = this.element.select(".resize_width"); 23 | this._resizeHeight = this.element.select(".resize_height"); 24 | this._errorElement = this.element.select(".image_div_error"); 25 | this._offlineEnabled = this.element.select(".offline_enabled_cb"); 26 | this._selectedFiles = []; 27 | 28 | this._addImageButton.on("click", this._addImageButtonClicked.bind(this)); 29 | this._addImageDropZone.on("dragover", this._addImageDropZoneDragOver.bind(this)).on("drop", this._addImageDropZoneDrop.bind(this)).on("dragleave", this._addImageDropZoneDragLeave.bind(this)); 30 | this._addImageFileChooser.on("change", this._addImageFileChooserChange.bind(this)); 31 | } 32 | 33 | /** 34 | * Shows the add image dialog 35 | * @returns {void} 36 | */ 37 | AddImageDialog.prototype.show = function () { 38 | this.element.style("display", ""); 39 | }; 40 | 41 | /** 42 | * Hides the add image dialog 43 | * @returns {void} 44 | */ 45 | AddImageDialog.prototype.hide = function () { 46 | this.element.style("display", "none"); 47 | }; 48 | 49 | /** 50 | * Returns true if the image dialog is hidden 51 | * @returns {boolean} true if hidden 52 | */ 53 | AddImageDialog.prototype.hidden = function () { 54 | return this.element.style("display") === "none"; 55 | }; 56 | 57 | /** 58 | * Resets the dialog to the default state 59 | * @param {boolean} [hide=true] If the dialog should be hidden 60 | * @returns {void} 61 | */ 62 | AddImageDialog.prototype.reset = function (hide) { 63 | this._selectedFiles.length = 0; 64 | this._addImageUrl.property("value", ""); 65 | this._addImageFileChooser.property("value", ""); 66 | this._addFilesContainer.style("display", ""); 67 | this._errorElement.style("display", "none"); 68 | this._selectedFilesContainer.style("display", "none").html("No files selected"); 69 | if (hide !== false) { 70 | this.hide(); 71 | } 72 | }; 73 | 74 | /** 75 | * Adds a selected file to the list of selected files 76 | * @param {File} file The file that was selected 77 | * @returns {void} 78 | */ 79 | AddImageDialog.prototype._addSelectedFile = function (file) { 80 | var _this = this; 81 | 82 | this._selectedFiles.push(file); 83 | var fileContainer = this._selectedFilesContainer.html("").append("div").attr("role", "button").attr("tabIndex", 0).attr("class", "add_image_selected_file"); 84 | fileContainer.append("span").text(file.name); 85 | fileContainer.append("img").attr("class", "selected_file_remove_btn").attr("src", imageUrls("close.png")); 86 | fileContainer.on("click", function () { 87 | _this.reset(false); 88 | }); 89 | this._addFilesContainer.style("display", "none"); 90 | this._selectedFilesContainer.style("display", ""); 91 | }; 92 | 93 | /** 94 | * Listener for the Add image button being clicked 95 | * @returns {void} 96 | */ 97 | AddImageDialog.prototype._addImageButtonClicked = function () { 98 | var _this2 = this; 99 | 100 | var imageUrl = this._addImageUrl.property("value"); 101 | var finalizeImage = function finalizeImage(url) { 102 | var waitForImagePromise = function waitForImagePromise(p) { 103 | p.then(function (dataURL) { 104 | _this2._dispatcher.imageSelected(dataURL); 105 | _this2.reset(); 106 | }, function (e) { 107 | var error = "Could not save image. "; 108 | var message = (e && e.message ? e.message : e) || ""; 109 | 110 | // This occurs if the server does not have CORS set up properly, or does not allow canvas saving 111 | if (message.indexOf("tainted") >= 0) { 112 | error += "
The image server is not set up correctly.
Try disabling \"Keep Offline\" and \"Resize To\" options."; 113 | } else if (message.indexOf("CORS") >= 0 || message.indexOf("Cross")) { 114 | error += "
The image server does not allow for the saving of images.
Try disabling \"Keep Offline\" and \"Resize To\" options."; 115 | } else { 116 | error += "
" + message; 117 | } 118 | _this2._errorElement.node().innerHTML = error; 119 | _this2._errorElement.style("display", ""); 120 | }); 121 | }; 122 | 123 | // If we are resizing it 124 | if (_this2._resizeEnabled.property("checked")) { 125 | var width = _this2._resizeWidth.property("value"); 126 | var height = _this2._resizeHeight.property("value"); 127 | waitForImagePromise(utils.resizeImage(url, width, height, true)); 128 | } else if (_this2._offlineEnabled.property("checked")) { 129 | waitForImagePromise(utils.imageUrlToDataURL(url)); 130 | } else { 131 | _this2._dispatcher.imageSelected(url); 132 | _this2.reset(); 133 | } 134 | }; 135 | 136 | if (this._selectedFiles.length) { 137 | var fileReader = new FileReader(); 138 | fileReader.onloadend = function (fileEvent) { 139 | finalizeImage(fileEvent.target.result); 140 | }; 141 | fileReader.readAsDataURL(this._selectedFiles[0]); 142 | } else if (imageUrl) { 143 | finalizeImage(imageUrl); 144 | } 145 | }; 146 | 147 | /** 148 | * Drag over listener for the drag/drop zone for files 149 | * @returns {void} 150 | */ 151 | AddImageDialog.prototype._addImageDropZoneDragOver = function () { 152 | stopEvent(); 153 | 154 | var e = d3.event; 155 | e.dataTransfer.dropEffect = "copy"; 156 | this._addImageDropZone.classed("dragging", true); 157 | }; 158 | 159 | /** 160 | * Drag over listener for the drag/drop zone for files 161 | * @returns {void} 162 | */ 163 | AddImageDialog.prototype._addImageDropZoneDragLeave = function () { 164 | stopEvent(); 165 | 166 | this._addImageDropZone.classed("dragging", false); 167 | }; 168 | 169 | /** 170 | * Drop listener for the drag/drop zone for files 171 | * @returns {void} 172 | */ 173 | AddImageDialog.prototype._addImageDropZoneDrop = function () { 174 | stopEvent(); 175 | 176 | var e = d3.event; 177 | var files = e.dataTransfer.files; 178 | this._addSelectedFile(files[0]); 179 | }; 180 | 181 | /** 182 | * Listener for when the file chooser changes 183 | * @returns {void} 184 | */ 185 | AddImageDialog.prototype._addImageFileChooserChange = function () { 186 | this._addSelectedFile(this._addImageFileChooser.node().files[0]); 187 | }; 188 | 189 | /** 190 | * Quick helper to completely stop an d3 event 191 | * @returns {void} 192 | */ 193 | function stopEvent() { 194 | var e = d3.event; 195 | e.stopPropagation(); 196 | e.preventDefault(); 197 | } 198 | 199 | const imageDialog = () => new AddImageDialog(); 200 | 201 | export default imageDialog; -------------------------------------------------------------------------------- /src/core/calendarAxis.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import d3 from "d3"; 4 | import * as moment from "moment"; 5 | 6 | /** 7 | 8 | calendarAxis: //a reusable calendar axis 9 | 10 | **/ 11 | 12 | d3.calendarAxis = function () { 13 | var cell_size = 20, 14 | year_height = cell_size * 8, // 7 days of week + buffer 15 | year_width = cell_size * 54, // 53 weeks of the year + buffer 16 | duration = 1000; 17 | 18 | function calendarAxis(selection) { 19 | selection.each(function (data) { 20 | var g = d3.select(this); 21 | 22 | // grid container items for each year 23 | var year_grid = g.selectAll(".year_grid") 24 | .data(data); 25 | 26 | var min_year = data[0]; 27 | 28 | // var year_number = -1; 29 | 30 | var year_grid_enter = year_grid.enter() 31 | .append("g") 32 | .attr("class", "year_grid"); 33 | 34 | var year_grid_exit = year_grid.exit() 35 | .transition() 36 | .duration(duration) 37 | .remove(); 38 | 39 | var year_grid_update = year_grid.transition() 40 | .duration(duration); 41 | 42 | year_grid_enter.append("text") 43 | .attr("class", "segment_title") 44 | .style("text-anchor", "middle") 45 | .text(function (d) { return d; }) 46 | .attr("transform", function (d) { 47 | var year_offset = d - min_year; 48 | return "translate(-5," + (cell_size * 3.5 + year_offset * year_height) + ")rotate(-90)"; 49 | }); 50 | 51 | var day_cell = year_grid.selectAll(".day_cell") 52 | .data(function (d) { 53 | return d3.time.days(new Date(d, 0, 1), new Date(d + 1, 0, 1)); 54 | }); 55 | 56 | day_cell.enter() 57 | .append("rect") 58 | .attr("class", "day_cell") 59 | .attr("width", cell_size) 60 | .attr("height", cell_size) 61 | .attr("x", function (d) { 62 | return d3.time.weekOfYear(d) * cell_size; 63 | }) 64 | .attr("y", function (d) { 65 | var year_offset = d.getUTCFullYear() - min_year; 66 | return d.getDay() * cell_size + year_offset * year_height; 67 | }) 68 | .append("title") 69 | .text(function (d) { 70 | return moment(d).format("dddd, MMMM Do, YYYY"); 71 | }); 72 | 73 | var day_cell_label = year_grid.selectAll(".day_cell_label") 74 | .data(function (d) { 75 | return d3.time.days(new Date(d, 0, 1), new Date(d + 1, 0, 1)); 76 | }); 77 | 78 | day_cell_label.enter() 79 | .append("text") 80 | .attr("class", "day_cell_label") 81 | .attr("x", function (d) { 82 | return d3.time.weekOfYear(d) * cell_size + 0.5 * cell_size; 83 | }) 84 | .attr("y", function (d) { 85 | var year_offset = d.getUTCFullYear() - min_year; 86 | return d.getDay() * cell_size + cell_size + year_offset * year_height; 87 | }) 88 | .attr("dy", "-0.5em") 89 | .text(function (d) { 90 | return moment(d).format("DD"); 91 | }); 92 | 93 | // draw the month paths 94 | var month_path = year_grid.selectAll(".month") 95 | .data(function (d) { 96 | return d3.time.months(new Date(d, 0, 1), new Date(d + 1, 0, 1)); 97 | }); 98 | 99 | month_path.enter() 100 | .append("path") 101 | .attr("class", "month") 102 | .attr("d", monthPath) 103 | .attr("transform", function (d) { 104 | var year_offset = d.getUTCFullYear() - min_year; 105 | return "translate(0," + (year_offset * year_height) + ")"; 106 | }); 107 | 108 | // draw the month paths 109 | var weekday_label = year_grid.selectAll(".weekday_label") 110 | .data(d3.range(0, 7)); 111 | 112 | weekday_label.enter() 113 | .append("text") 114 | .attr("class", "weekday_label") 115 | .attr("x", year_width) 116 | .attr("dy", "-0.5em") 117 | .attr("dx", "-1.3em") 118 | .attr("y", function (d) { 119 | var year_offset = d3.select(this.parentNode)[0][0].__data__ - min_year; 120 | return (d + 1) * cell_size + year_offset * year_height; 121 | }) 122 | .text(function (d) { 123 | return moment().day(d).format("ddd"); 124 | }); 125 | 126 | year_grid_update.select(".segment_title") 127 | .text(function (d) { return d; }) 128 | .attr("transform", function (d) { 129 | var year_offset = d - min_year; 130 | return "translate(-5," + (cell_size * 3.5 + year_offset * year_height) + ")rotate(-90)"; 131 | }); 132 | 133 | year_grid_exit.select(".segment_title") 134 | .text(""); 135 | 136 | year_grid_update.selectAll(".day_cell") 137 | .attr("width", cell_size) 138 | .attr("height", cell_size) 139 | .attr("x", function (d) { 140 | return d3.time.weekOfYear(d) * cell_size; 141 | }) 142 | .attr("y", function (d) { 143 | var year_offset = d.getUTCFullYear() - min_year; 144 | return d.getDay() * cell_size + year_offset * year_height; 145 | }); 146 | 147 | year_grid_exit.selectAll(".day_cell") 148 | .attr("width", 0) 149 | .attr("height", 0) 150 | .attr("x", 0) 151 | .attr("y", 0); 152 | 153 | year_grid_update.selectAll(".day_cell_label") 154 | .attr("x", function (d) { 155 | return d3.time.weekOfYear(d) * cell_size + 0.5 * cell_size; 156 | }) 157 | .attr("y", function (d) { 158 | var year_offset = d.getUTCFullYear() - min_year; 159 | return d.getDay() * cell_size + cell_size + year_offset * year_height; 160 | }) 161 | .attr("dy", "-0.5em") 162 | .text(function (d) { 163 | return moment(d).format("DD"); 164 | }); 165 | 166 | year_grid_exit.selectAll(".day_cell_label") 167 | .attr("x", 0) 168 | .attr("y", 0) 169 | .text(""); 170 | 171 | year_grid_update.selectAll(".month") 172 | .attr("d", monthPath) 173 | .attr("transform", function (d) { 174 | var year_offset = d.getUTCFullYear() - min_year; 175 | return "translate(0," + (year_offset * year_height) + ")"; 176 | }); 177 | 178 | year_grid_exit.selectAll(".month") 179 | .attr("y", 0) 180 | .attr("d", monthPath); 181 | 182 | year_grid_update.selectAll(".weekday_label") 183 | .attr("x", year_width) 184 | .attr("y", function (d) { 185 | var year_offset = d3.select(this.parentNode)[0][0].__data__ - min_year; 186 | return (d + 1) * cell_size + year_offset * year_height; 187 | }) 188 | .attr("dy", "-0.5em") 189 | .attr("dx", "-1.3em") 190 | .text(function (d) { 191 | return moment().day(d).format("ddd"); 192 | }); 193 | 194 | year_grid_exit.selectAll(".weekday_label") 195 | .attr("x", 0) 196 | .attr("y", 0) 197 | .text(""); 198 | }); 199 | d3.timer.flush(); 200 | } 201 | 202 | function monthPath(t0) { 203 | var t1 = new Date(t0.getFullYear(), t0.getMonth() + 1, 0), 204 | d0 = t0.getDay(), w0 = d3.time.weekOfYear(t0), 205 | d1 = t1.getDay(), w1 = d3.time.weekOfYear(t1); 206 | return "M" + (w0 + 1) * cell_size + "," + d0 * cell_size 207 | + "H" + w0 * cell_size + "V" + 7 * cell_size 208 | + "H" + w1 * cell_size + "V" + (d1 + 1) * cell_size 209 | + "H" + (w1 + 1) * cell_size + "V" + 0 210 | + "H" + (w0 + 1) * cell_size + "Z"; 211 | } 212 | 213 | calendarAxis.duration = function (x) { 214 | if (!arguments.length) { 215 | return duration; 216 | } 217 | duration = x; 218 | return calendarAxis; 219 | }; 220 | 221 | return calendarAxis; 222 | }; 223 | 224 | export default d3.calendarAxis; 225 | -------------------------------------------------------------------------------- /src/core/addCaption.ts: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | addCaption: //on-demand captions for a timeline 4 | 5 | **/ 6 | import d3 from "d3"; 7 | import ellipsize from "ellipsize"; 8 | 9 | import imageUrls from "./imageUrls"; 10 | import globals from "./globals"; 11 | import utils from "./utils"; 12 | 13 | export default function (caption, caption_width, x_rel_pos, y_rel_pos, captionObj) { 14 | "use strict"; 15 | 16 | var x_pos = x_rel_pos * globals.width, 17 | y_pos = y_rel_pos * globals.height; 18 | 19 | var min_caption_width = d3.min([caption.length * 10, 200]); 20 | 21 | var timeline_caption = utils.selectWithParent("#main_svg").append("g") 22 | .attr("id", "caption" + captionObj.id) 23 | .attr("data-id", captionObj.id) 24 | .attr("data-type", "caption") 25 | .attr("class", "timeline_caption"); 26 | 27 | timeline_caption.on("mouseover", function () { 28 | d3.select(this).selectAll(".annotation_control") 29 | .transition() 30 | .duration(250) 31 | .style("opacity", 1); 32 | d3.select(this).select(".caption_frame") 33 | .transition() 34 | .duration(250) 35 | .style("stroke", "#999") 36 | .attr("filter", "url(#drop-shadow)"); 37 | }) 38 | .on("mouseout", function () { 39 | d3.select(this).selectAll(".annotation_control") 40 | .transition() 41 | .duration(250) 42 | .style("opacity", 0); 43 | d3.select(this).select(".caption_frame") 44 | .transition() 45 | .duration(250) 46 | .style("stroke", "white") 47 | .attr("filter", "none"); 48 | }); 49 | 50 | var drag = d3.behavior.drag() 51 | .origin(function () { 52 | var t = d3.select(this); 53 | 54 | return { 55 | x: t.attr("x"), 56 | y: t.attr("y") 57 | }; 58 | }) 59 | .on("drag", function () { 60 | x_pos = d3.event.x; 61 | y_pos = d3.event.y; 62 | 63 | captionObj.x_rel_pos = x_pos / globals.width; 64 | captionObj.y_rel_pos = y_pos / globals.height; 65 | 66 | d3.select(this) 67 | .attr("x", x_pos) 68 | .attr("y", y_pos); 69 | 70 | d3.select(this.parentNode).select(".caption_frame") 71 | .attr("x", x_pos) 72 | .attr("y", y_pos); 73 | 74 | d3.select(this.parentNode).select(".caption_label").selectAll("tspan") 75 | .attr("x", x_pos + 7.5) 76 | .attr("y", y_pos + globals.unit_width); 77 | 78 | d3.select(this.parentNode).selectAll(".frame_resizer") 79 | .attr("x", x_pos + caption_width + 7.5) 80 | .attr("y", y_pos); 81 | 82 | d3.select(this.parentNode).selectAll(".annotation_delete") 83 | .attr("x", x_pos + caption_width + 7.5 + 20) 84 | .attr("y", y_pos); 85 | }); 86 | // .on("dragend", function () { 87 | // logEvent("caption " + captionObj.id + " moved to [" + x_pos + "," + y_pos + "]"); 88 | // }); 89 | 90 | var resize = d3.behavior.drag() 91 | .origin(function () { 92 | var t = d3.select(this); 93 | y_pos = +t.attr("y"); 94 | 95 | return { 96 | x: t.attr("x"), 97 | y: t.attr("y") 98 | }; 99 | }) 100 | .on("drag", function () { 101 | d3.select(this).attr("x", d3.max([x_pos + caption_width + 7.5, x_pos + 7.5 + (d3.event.x - x_pos)])); 102 | 103 | caption_width = d3.max([min_caption_width, d3.event.x - x_pos]); 104 | 105 | captionObj.caption_width = caption_width; 106 | 107 | d3.select(this.parentNode).selectAll(".frame_resizer") 108 | .attr("x", x_pos + caption_width + 7.5) 109 | .attr("y", y_pos); 110 | 111 | d3.select(this.parentNode).select(".caption_frame") 112 | .attr("width", caption_width + 7.5); 113 | 114 | d3.select(this.parentNode).selectAll(".annotation_delete") 115 | .attr("x", x_pos + caption_width + 7.5 + 20) 116 | .attr("y", y_pos); 117 | 118 | d3.select(this.parentNode).select(".caption_drag_area") 119 | .attr("width", caption_width + 7.5); 120 | 121 | d3.select(this.parentNode).select(".caption_label") 122 | .attr("x", x_pos + 7.5) 123 | .attr("y", y_pos + globals.unit_width) 124 | .text(caption) 125 | .call(wrap, caption_width - 7.5); 126 | }); 127 | // .on("dragend", function () { 128 | // logEvent("caption " + captionObj.id + " resized to " + caption_width + "px"); 129 | // }); 130 | 131 | var caption_frame = timeline_caption.append("rect") 132 | .attr("class", "caption_frame") 133 | .attr("x", x_pos) 134 | .attr("y", y_pos) 135 | .attr("width", caption_width + 7.5); 136 | 137 | timeline_caption.append("svg:image") 138 | .attr("class", "annotation_control frame_resizer") 139 | .attr("title", "resize caption") 140 | .attr("x", x_pos + caption_width + 7.5) 141 | .attr("y", y_pos) 142 | .attr("width", 20) 143 | .attr("height", 20) 144 | .attr("xlink:href", imageUrls("expand.png")) 145 | .attr("filter", "url(#drop-shadow)") 146 | .style("opacity", 0); 147 | 148 | var caption_resizer = timeline_caption.append("rect") 149 | .attr("class", "annotation_control frame_resizer") 150 | .attr("x", x_pos + caption_width + 7.5) 151 | .attr("y", y_pos) 152 | .attr("width", 20) 153 | .attr("height", 20) 154 | .style("opacity", 0) 155 | .on("mouseover", function () { 156 | d3.select(this).style("stroke", "#f00"); 157 | }) 158 | .on("mouseout", function () { 159 | d3.select(this).style("stroke", "#ccc"); 160 | }) 161 | .call(resize); 162 | 163 | caption_resizer.append("title") 164 | .text("Resize caption"); 165 | 166 | timeline_caption.append("svg:image") 167 | .attr("class", "annotation_control annotation_delete") 168 | .attr("title", "remove caption") 169 | .attr("x", x_pos + caption_width + 7.5 + 20) 170 | .attr("y", y_pos) 171 | .attr("width", 20) 172 | .attr("height", 20) 173 | .attr("xlink:href", imageUrls("delete.png")) 174 | .attr("filter", "url(#drop-shadow)") 175 | .style("opacity", 0); 176 | 177 | timeline_caption.append("rect") 178 | .attr("class", "annotation_control annotation_delete") 179 | .attr("x", x_pos + caption_width + 7.5 + 20) 180 | .attr("y", y_pos) 181 | .attr("width", 20) 182 | .attr("height", 20) 183 | .style("opacity", 0) 184 | .on("mouseover", function () { 185 | d3.select(this).style("stroke", "#f00"); 186 | }) 187 | .on("mouseout", function () { 188 | d3.select(this).style("stroke", "#ccc"); 189 | }) 190 | .on("click", function () { 191 | // logEvent("caption " + captionObj.id + " removed"); 192 | 193 | d3.select(this.parentNode).remove(); 194 | }) 195 | .append("title") 196 | .text("Remove caption"); 197 | 198 | var caption_label = timeline_caption.append("text") 199 | .attr("class", "caption_label") 200 | .attr("x", x_pos + 7.5) 201 | .attr("y", y_pos + globals.unit_width) 202 | .attr("dy", 1) 203 | .text(caption) 204 | .call(wrap, caption_width - 7.5); 205 | 206 | var caption_drag_area = timeline_caption.append("rect") 207 | .attr("class", "caption_drag_area") 208 | .attr("x", x_pos) 209 | .attr("y", y_pos) 210 | .attr("width", caption_width + 7.5) 211 | .call(drag); 212 | 213 | caption_label.attr("dy", 1 + "em") 214 | .text(caption) 215 | .call(wrap, caption_width - 7.5); 216 | 217 | function wrap(text, width) { 218 | var words = text.text().split(/\s+/).reverse(), 219 | word, 220 | line = [], 221 | line_number = 0, 222 | letter_width = 8, 223 | max_letters = Math.floor(width / letter_width) - 2, 224 | dy = parseFloat(text.attr("dy")), 225 | tspan = text.text(null).append("tspan") 226 | .attr("dy", dy + "em") 227 | .attr("x", x_pos + 7.5) 228 | .attr("y", y_pos + globals.unit_width); 229 | while (word = words.pop()) { // eslint-disable-line no-cond-assign 230 | word = ellipsize(word, max_letters); 231 | line.push(word); 232 | tspan.text(line.join(" ")); 233 | if (tspan.node().getComputedTextLength() > width) { 234 | line.pop(); 235 | tspan.text(line.join(" ")); 236 | line = [word]; 237 | tspan = text.append("tspan") 238 | .attr("x", x_pos + 7.5) 239 | .attr("y", y_pos + globals.unit_width) 240 | .attr("dy", ++line_number + dy + "em").text(word); 241 | } 242 | } 243 | caption_frame.attr("height", ((line_number + 2.5) * 18) + "px"); 244 | if (caption_drag_area !== undefined) { 245 | caption_drag_area.attr("height", ((line_number + 2.5) * 18) + "px"); 246 | } 247 | } 248 | 249 | return true; 250 | }; 251 | -------------------------------------------------------------------------------- /src/core/addImage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | addImage: //on-demand image for a timeline 4 | 5 | **/ 6 | import d3 from "d3"; 7 | 8 | import imageUrls from "./imageUrls"; 9 | import globals from "./globals"; 10 | import utils from "./utils"; 11 | 12 | export default function (timeline_vis, image_url, x_rel_pos, y_rel_pos, image_width, image_height, imageObj) { 13 | "use strict"; 14 | 15 | var x_pos = x_rel_pos * globals.width, 16 | y_pos = y_rel_pos * globals.height; 17 | 18 | var orig_image_weight = image_width, 19 | min_image_width = 10; 20 | 21 | var scaling_ratio = 1; 22 | 23 | var timeline_image = utils.selectWithParent("#main_svg").append("g") 24 | .attr("id", "image" + imageObj.id) 25 | .attr("data-id", imageObj.id) 26 | .attr("data-type", "image") 27 | .attr("class", "timeline_image"); 28 | 29 | d3.selection.prototype.moveToBack = function () { 30 | return this.each(function () { 31 | var firstChild = this.parentNode.firstChild; 32 | if (firstChild) { 33 | this.parentNode.insertBefore(this, firstChild); 34 | } 35 | }); 36 | }; 37 | 38 | d3.selection.prototype.moveToFront = function () { 39 | return this.each(function () { 40 | this.parentNode.appendChild(this); 41 | }); 42 | }; 43 | 44 | timeline_image.on("click", function () { 45 | if (d3.event.shiftKey) { 46 | d3.select(this) 47 | .style("opacity", 0.3) 48 | .moveToBack(); 49 | } 50 | if (d3.event.ctrlKey) { 51 | d3.select(this) 52 | .style("opacity", 1) 53 | .moveToFront(); 54 | } 55 | }) 56 | .on("mouseover", function () { 57 | d3.select(this).selectAll(".annotation_control") 58 | .transition() 59 | .duration(250) 60 | .style("opacity", 1); 61 | d3.select(this).select(".image_frame") 62 | .transition() 63 | .duration(250) 64 | .style("stroke", "#999") 65 | .attr("filter", "url(#drop-shadow)"); 66 | }) 67 | .on("mouseout", function () { 68 | d3.select(this).selectAll(".annotation_control") 69 | .transition() 70 | .duration(250) 71 | .style("opacity", 0); 72 | d3.select(this).select(".image_frame") 73 | .transition() 74 | .duration(250) 75 | .style("stroke", "none") 76 | .attr("filter", "none"); 77 | }); 78 | 79 | var drag = d3.behavior.drag() 80 | .origin(function () { 81 | var t = d3.select(this); 82 | 83 | return { 84 | x: t.attr("x"), 85 | y: t.attr("y") 86 | }; 87 | }) 88 | .on("drag", function () { 89 | x_pos = d3.event.x; 90 | y_pos = d3.event.y; 91 | 92 | imageObj.x_rel_pos = x_pos / globals.width; 93 | imageObj.y_rel_pos = y_pos / globals.height; 94 | 95 | d3.select(this) 96 | .attr("x", x_pos) 97 | .attr("y", y_pos); 98 | 99 | d3.select(this.parentNode).select("clipPath").select("circle") 100 | .attr("cx", x_pos + image_width / 2) 101 | .attr("cy", y_pos + (image_height * scaling_ratio) / 2) 102 | .attr("r", image_width / 2); 103 | 104 | d3.select(this.parentNode).select(".image_frame") 105 | .attr("x", x_pos) 106 | .attr("y", y_pos); 107 | 108 | d3.select(this.parentNode).selectAll(".frame_resizer") 109 | .attr("x", x_pos + image_width) 110 | .attr("y", y_pos); 111 | 112 | d3.select(this.parentNode).selectAll(".annotation_delete") 113 | .attr("x", x_pos + image_width + 20) 114 | .attr("y", y_pos); 115 | }); 116 | // .on("dragend", function () { 117 | // logEvent("image " + imageObj.id + " moved to [" + x_pos + "," + y_pos + "]"); 118 | // }); 119 | 120 | var resize = d3.behavior.drag() 121 | .origin(function () { 122 | var t = d3.select(this); 123 | y_pos = +t.attr("y"); 124 | 125 | return { 126 | x: t.attr("x"), 127 | y: t.attr("y") 128 | }; 129 | }) 130 | .on("drag", function () { 131 | d3.select(this).attr("x", d3.max([x_pos + image_width, x_pos + (d3.event.x - x_pos)])); 132 | 133 | image_width = d3.max([min_image_width, d3.event.x - x_pos]); 134 | 135 | scaling_ratio = image_width / orig_image_weight; 136 | 137 | imageObj.i_width = image_width; 138 | imageObj.i_height = image_height * scaling_ratio; 139 | 140 | d3.select(this.parentNode).select("clipPath").select("circle") 141 | .attr("cx", x_pos + image_width / 2) 142 | .attr("cy", y_pos + (image_height * scaling_ratio) / 2) 143 | .attr("r", image_width / 2); 144 | 145 | d3.select(this.parentNode).select(".image_frame") 146 | .attr("width", image_width) 147 | .attr("height", image_height * scaling_ratio); 148 | 149 | d3.select(this.parentNode).selectAll(".frame_resizer") 150 | .attr("x", x_pos + image_width) 151 | .attr("y", y_pos); 152 | 153 | d3.select(this.parentNode).selectAll(".annotation_delete") 154 | .attr("x", x_pos + image_width + 20) 155 | .attr("y", y_pos); 156 | 157 | d3.select(this.parentNode).select(".image_drag_area") 158 | .attr("width", image_width) 159 | .attr("height", image_height * scaling_ratio); 160 | }); 161 | // .on("dragend", function () { 162 | // logEvent("image " + imageObj.id + " resized to " + image_width + "px"); 163 | // }); 164 | 165 | var image_defs = timeline_image.append("defs"); 166 | 167 | var clipPathId = utils.nextId(); 168 | image_defs.append("clipPath") 169 | .attr("id", "circlepath" + clipPathId) 170 | .attr("class", "image-clip-path") 171 | .append("circle") 172 | .attr("cx", x_pos + image_width / 2) 173 | .attr("cy", y_pos + image_height / 2) 174 | .attr("r", image_width / 2); 175 | 176 | timeline_image.append("svg:image") 177 | .attr("xlink:href", image_url) 178 | .attr("class", "image_frame") 179 | .attr("clip-path", function () { 180 | if (timeline_vis.tl_representation() === "Radial") { 181 | return "url(#circlepath" + clipPathId + ")"; 182 | } 183 | 184 | return "none"; 185 | }) 186 | .style("clip-path", function () { 187 | if (timeline_vis.tl_representation() === "Radial") { 188 | return "circle()"; 189 | } 190 | 191 | return "none"; 192 | }) 193 | .style("-webkit-clip-path", function () { 194 | if (timeline_vis.tl_representation() === "Radial") { 195 | return "circle()"; 196 | } 197 | 198 | return "none"; 199 | }) 200 | .attr("x", x_pos) 201 | .attr("y", y_pos) 202 | .attr("width", image_width) 203 | .attr("height", image_height); 204 | 205 | timeline_image.append("svg:image") 206 | .attr("class", "annotation_control frame_resizer") 207 | .attr("title", "resize image") 208 | .attr("x", x_pos + image_width) 209 | .attr("y", y_pos) 210 | .attr("width", 20) 211 | .attr("height", 20) 212 | .attr("xlink:href", imageUrls("expand.png")) 213 | .attr("filter", "url(#drop-shadow)") 214 | .style("opacity", 0); 215 | 216 | var image_resizer = timeline_image.append("rect") 217 | .attr("class", "annotation_control frame_resizer") 218 | .attr("x", x_pos + image_width) 219 | .attr("y", y_pos) 220 | .attr("width", 20) 221 | .attr("height", 20) 222 | .style("opacity", 0) 223 | .on("mouseover", function () { 224 | d3.select(this).style("stroke", "#f00"); 225 | }) 226 | .on("mouseout", function () { 227 | d3.select(this).style("stroke", "#ccc"); 228 | }) 229 | .call(resize); 230 | 231 | image_resizer.append("title") 232 | .text("Resize image"); 233 | 234 | timeline_image.append("svg:image") 235 | .attr("class", "annotation_control annotation_delete") 236 | .attr("title", "remove image") 237 | .attr("x", x_pos + image_width + 20) 238 | .attr("y", y_pos) 239 | .attr("width", 20) 240 | .attr("height", 20) 241 | .attr("xlink:href", imageUrls("delete.png")) 242 | .attr("filter", "url(#drop-shadow)") 243 | .style("opacity", 0); 244 | 245 | timeline_image.append("rect") 246 | .attr("class", "annotation_control annotation_delete") 247 | .attr("x", x_pos + image_width + 20) 248 | .attr("y", y_pos) 249 | .attr("width", 20) 250 | .attr("height", 20) 251 | .style("opacity", 0) 252 | .on("mouseover", function () { 253 | d3.select(this).style("stroke", "#f00"); 254 | }) 255 | .on("mouseout", function () { 256 | d3.select(this).style("stroke", "#ccc"); 257 | }) 258 | .on("click", function () { 259 | // logEvent("image " + imageObj.id + " removed"); 260 | 261 | d3.select(this.parentNode).remove(); 262 | }) 263 | .append("title") 264 | .text("Remove image"); 265 | 266 | timeline_image.append("rect") 267 | .attr("class", "image_drag_area") 268 | .attr("x", x_pos) 269 | .attr("y", y_pos) 270 | .attr("width", image_width) 271 | .attr("height", image_width) 272 | .call(drag); 273 | 274 | return true; 275 | }; 276 | -------------------------------------------------------------------------------- /src/core/lib/colorpicker.js: -------------------------------------------------------------------------------- 1 | (function (s, t, u) { 2 | var v = s.SVGAngle || t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1") ? "SVG" : "VML", 3 | picker, 4 | slide, 5 | hueOffset = 15, 6 | svgNS = 'http://www.w3.org/2000/svg';var w = ['
', '
', '
', '
', '
', '
', '
', '
'].join('');function mousePosition(a) { 7 | if (s.event && s.event.contentOverflow !== u) { 8 | return { x: s.event.offsetX, y: s.event.offsetY }; 9 | }if (a.offsetX !== u && a.offsetY !== u) { 10 | return { x: a.offsetX, y: a.offsetY }; 11 | }var b = a.target.parentNode.parentNode;return { x: a.layerX - b.offsetLeft, y: a.layerY - b.offsetTop }; 12 | }function $(a, b, c) { 13 | a = t.createElementNS(svgNS, a);for (var d in b) { 14 | a.setAttribute(d, b[d]); 15 | }if (Object.prototype.toString.call(c) != '[object Array]') c = [c];var i = 0, 16 | len = c[0] && c.length || 0;for (; i < len; i++) { 17 | a.appendChild(c[i]); 18 | }return a; 19 | }if (v == 'SVG') { 20 | slide = $('svg', { xmlns: 'http://www.w3.org/2000/svg', version: '1.1', width: '100%', height: '100%' }, [$('defs', {}, $('linearGradient', { id: 'gradient-hsv', x1: '0%', y1: '100%', x2: '0%', y2: '0%' }, [$('stop', { offset: '0%', 'stop-color': '#FF0000', 'stop-opacity': '1' }), $('stop', { offset: '13%', 'stop-color': '#FF00FF', 'stop-opacity': '1' }), $('stop', { offset: '25%', 'stop-color': '#8000FF', 'stop-opacity': '1' }), $('stop', { offset: '38%', 'stop-color': '#0040FF', 'stop-opacity': '1' }), $('stop', { offset: '50%', 'stop-color': '#00FFFF', 'stop-opacity': '1' }), $('stop', { offset: '63%', 'stop-color': '#00FF40', 'stop-opacity': '1' }), $('stop', { offset: '75%', 'stop-color': '#0BED00', 'stop-opacity': '1' }), $('stop', { offset: '88%', 'stop-color': '#FFFF00', 'stop-opacity': '1' }), $('stop', { offset: '100%', 'stop-color': '#FF0000', 'stop-opacity': '1' })])), $('rect', { x: '0', y: '0', width: '100%', height: '100%', fill: 'url(#gradient-hsv)' })]);picker = $('svg', { xmlns: 'http://www.w3.org/2000/svg', version: '1.1', width: '100%', height: '100%' }, [$('defs', {}, [$('linearGradient', { id: 'gradient-black', x1: '0%', y1: '100%', x2: '0%', y2: '0%' }, [$('stop', { offset: '0%', 'stop-color': '#000000', 'stop-opacity': '1' }), $('stop', { offset: '100%', 'stop-color': '#CC9A81', 'stop-opacity': '0' })]), $('linearGradient', { id: 'gradient-white', x1: '0%', y1: '100%', x2: '100%', y2: '100%' }, [$('stop', { offset: '0%', 'stop-color': '#FFFFFF', 'stop-opacity': '1' }), $('stop', { offset: '100%', 'stop-color': '#CC9A81', 'stop-opacity': '0' })])]), $('rect', { x: '0', y: '0', width: '100%', height: '100%', fill: 'url(#gradient-white)' }), $('rect', { x: '0', y: '0', width: '100%', height: '100%', fill: 'url(#gradient-black)' })]); 21 | } else if (v == 'VML') { 22 | slide = ['
', '', '', '', '
'].join('');picker = ['
', '', '', '', '', '', '', '
'].join('');if (!t.namespaces['v']) t.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML'); 23 | }function hsv2rgb(a) { 24 | var R, G, B, X, C;var h = a.h % 360 / 60;C = a.v * a.s;X = C * (1 - Math.abs(h % 2 - 1));R = G = B = a.v - C;h = ~~h;R += [C, X, 0, 0, X, C][h];G += [X, C, C, X, 0, 0][h];B += [0, 0, X, C, C, X][h];var r = Math.floor(R * 255);var g = Math.floor(G * 255);var b = Math.floor(B * 255);return { r: r, g: g, b: b, hex: "#" + (16777216 | b | g << 8 | r << 16).toString(16).slice(1) }; 25 | }function rgb2hsv(a) { 26 | var r = a.r;var g = a.g;var b = a.b;if (a.r > 1 || a.g > 1 || a.b > 1) { 27 | r /= 255;g /= 255;b /= 255; 28 | }var H, S, V, C;V = Math.max(r, g, b);C = V - Math.min(r, g, b);H = C == 0 ? null : V == r ? (g - b) / C + (g < b ? 6 : 0) : V == g ? (b - r) / C + 2 : (r - g) / C + 4;H = H % 6 * 60;S = C == 0 ? 0 : C / V;return { h: H, s: S, v: V }; 29 | }function slideListener(d, e, f) { 30 | return function (a) { 31 | a = a || s.event;var b = mousePosition(a);d.h = b.y / e.offsetHeight * 360 + hueOffset;d.s = d.v = 1;var c = hsv2rgb({ h: d.h, s: 1, v: 1 });f.style.backgroundColor = c.hex;d.callback && d.callback(c.hex, { h: d.h - hueOffset, s: d.s, v: d.v }, { r: c.r, g: c.g, b: c.b }, u, b); 32 | }; 33 | };function pickerListener(d, e) { 34 | return function (a) { 35 | a = a || s.event;var b = mousePosition(a), 36 | width = e.offsetWidth, 37 | height = e.offsetHeight;d.s = b.x / width;d.v = (height - b.y) / height;var c = hsv2rgb(d);d.callback && d.callback(c.hex, { h: d.h - hueOffset, s: d.s, v: d.v }, { r: c.r, g: c.g, b: c.b }, b); 38 | }; 39 | };var x = 0;function ColorPicker(f, g, h) { 40 | if (!(this instanceof ColorPicker)) return new ColorPicker(f, g, h);this.h = 0;this.s = 1;this.v = 1;if (!h) { 41 | var i = f;i.innerHTML = w;this.slideElement = i.getElementsByClassName('slide')[0];this.pickerElement = i.getElementsByClassName('picker')[0];var j = i.getElementsByClassName('slide-indicator')[0];var k = i.getElementsByClassName('picker-indicator')[0];ColorPicker.fixIndicators(j, k);this.callback = function (a, b, c, d, e) { 42 | ColorPicker.positionIndicators(j, k, e, d);g(a, b, c); 43 | }; 44 | } else { 45 | this.callback = h;this.pickerElement = g;this.slideElement = f; 46 | }if (v == 'SVG') { 47 | var l = slide.cloneNode(true);var m = picker.cloneNode(true);var n = l.getElementsByTagName('linearGradient')[0];var o = l.getElementsByTagName('rect')[0];n.id = 'gradient-hsv-' + x;o.setAttribute('fill', 'url(#' + n.id + ')');var p = [m.getElementsByTagName('linearGradient')[0], m.getElementsByTagName('linearGradient')[1]];var q = m.getElementsByTagName('rect');p[0].id = 'gradient-black-' + x;p[1].id = 'gradient-white-' + x;q[0].setAttribute('fill', 'url(#' + p[1].id + ')');q[1].setAttribute('fill', 'url(#' + p[0].id + ')');this.slideElement.appendChild(l);this.pickerElement.appendChild(m);x++; 48 | } else { 49 | this.slideElement.innerHTML = slide;this.pickerElement.innerHTML = picker; 50 | }addEventListener(this.slideElement, 'click', slideListener(this, this.slideElement, this.pickerElement));addEventListener(this.pickerElement, 'click', pickerListener(this, this.pickerElement));enableDragging(this, this.slideElement, slideListener(this, this.slideElement, this.pickerElement));enableDragging(this, this.pickerElement, pickerListener(this, this.pickerElement)); 51 | };function addEventListener(a, b, c) { 52 | if (a.attachEvent) { 53 | a.attachEvent('on' + b, c); 54 | } else if (a.addEventListener) { 55 | a.addEventListener(b, c, false); 56 | } 57 | }function enableDragging(b, c, d) { 58 | var e = false;addEventListener(c, 'mousedown', function (a) { 59 | e = true; 60 | });addEventListener(c, 'mouseup', function (a) { 61 | e = false; 62 | });addEventListener(c, 'mouseout', function (a) { 63 | e = false; 64 | });addEventListener(c, 'mousemove', function (a) { 65 | if (e) { 66 | d(a); 67 | } 68 | }); 69 | }ColorPicker.hsv2rgb = function (a) { 70 | var b = hsv2rgb(a);delete b.hex;return b; 71 | };ColorPicker.hsv2hex = function (a) { 72 | return hsv2rgb(a).hex; 73 | };ColorPicker.rgb2hsv = rgb2hsv;ColorPicker.rgb2hex = function (a) { 74 | return hsv2rgb(rgb2hsv(a)).hex; 75 | };ColorPicker.hex2hsv = function (a) { 76 | return rgb2hsv(ColorPicker.hex2rgb(a)); 77 | };ColorPicker.hex2rgb = function (a) { 78 | return { r: parseInt(a.substr(1, 2), 16), g: parseInt(a.substr(3, 2), 16), b: parseInt(a.substr(5, 2), 16) }; 79 | };function setColor(a, b, d, e) { 80 | a.h = b.h % 360;a.s = b.s;a.v = b.v;var c = hsv2rgb(a);var f = { y: a.h * a.slideElement.offsetHeight / 360, x: 0 };var g = a.pickerElement.offsetHeight;var h = { x: a.s * a.pickerElement.offsetWidth, y: g - a.v * g };a.pickerElement.style.backgroundColor = hsv2rgb({ h: a.h, s: 1, v: 1 }).hex;a.callback && a.callback(e || c.hex, { h: a.h, s: a.s, v: a.v }, d || { r: c.r, g: c.g, b: c.b }, h, f);return a; 81 | };ColorPicker.prototype.setHsv = function (a) { 82 | return setColor(this, a); 83 | };ColorPicker.prototype.setRgb = function (a) { 84 | return setColor(this, rgb2hsv(a), a); 85 | };ColorPicker.prototype.setHex = function (a) { 86 | return setColor(this, ColorPicker.hex2hsv(a), u, a); 87 | };ColorPicker.positionIndicators = function (a, b, c, d) { 88 | if (c) { 89 | b.style.left = 'auto';b.style.right = '0px';b.style.top = '0px';a.style.top = c.y - a.offsetHeight / 2 + 'px'; 90 | }if (d) { 91 | b.style.top = d.y - b.offsetHeight / 2 + 'px';b.style.left = d.x - b.offsetWidth / 2 + 'px'; 92 | } 93 | };ColorPicker.fixIndicators = function (a, b) { 94 | b.style.pointerEvents = 'none';a.style.pointerEvents = 'none'; 95 | };s.ColorPicker = ColorPicker; 96 | })(window, window.document); -------------------------------------------------------------------------------- /src/lib/calcUpdateType.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * Copyright (c) 2016 Microsoft 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the 'Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | import powerbi from "powerbi-visuals-api"; 25 | import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions; 26 | import UpdateType from './UpdateType'; 27 | import ldIsEqual from "lodash.isequal"; 28 | import assignIn from "lodash.assignin"; 29 | 30 | declare var _: any; 31 | 32 | export const DEFAULT_CALCULATE_SETTINGS: ICalcUpdateTypeOptions = { 33 | checkHighlights: false, 34 | defaultUnkownToData: false, 35 | ignoreCategoryOrder: true, 36 | }; 37 | 38 | Object.freeze(DEFAULT_CALCULATE_SETTINGS); 39 | 40 | /** 41 | * Calculates the type of update that has occurred between two visual update options, this gives greater granularity than what 42 | * powerbi has. 43 | * @param oldOpts The old options 44 | * @param newOpts The new options 45 | * @param addlOptions The additional options to use when calculating the update type. 46 | */ 47 | export default function calcUpdateType( 48 | oldOpts: VisualUpdateOptions, 49 | newOpts: VisualUpdateOptions, 50 | addlOptions?: ICalcUpdateTypeOptions | boolean) { 51 | 'use strict'; 52 | let updateType = UpdateType.Unknown; 53 | const options = assignIn({}, 54 | DEFAULT_CALCULATE_SETTINGS, 55 | typeof addlOptions === 'boolean' ? 56 | { defaultUnkownToData: addlOptions } : (addlOptions || {})); 57 | 58 | if (hasResized(oldOpts, newOpts, options)) { 59 | updateType ^= UpdateType.Resize; 60 | } 61 | 62 | if (hasDataChanged2(oldOpts, newOpts, options)) { 63 | updateType ^= UpdateType.Data; 64 | } 65 | 66 | if (hasSettingsChanged(oldOpts, newOpts, options)) { 67 | updateType ^= UpdateType.Settings; 68 | } 69 | 70 | if (!oldOpts) { 71 | updateType ^= UpdateType.Initial; 72 | } 73 | 74 | if (options.defaultUnkownToData && updateType === UpdateType.Unknown) { 75 | updateType = UpdateType.Data; 76 | } 77 | 78 | return updateType; 79 | } 80 | 81 | function hasDataChanged2(oldOptions: VisualUpdateOptions, newOptions: VisualUpdateOptions, options: ICalcUpdateTypeOptions) { 82 | 'use strict'; 83 | const oldDvs = (oldOptions && oldOptions.dataViews) || []; 84 | const dvs = newOptions.dataViews || []; 85 | if (oldDvs.length !== dvs.length) { 86 | dvs.forEach(dv => markDataViewState(dv)); 87 | return true; 88 | } 89 | for (let i = 0; i < oldDvs.length; i++) { 90 | if (hasDataViewChanged(oldDvs[i], dvs[i], options)) { 91 | dvs.forEach(dv => markDataViewState(dv)); 92 | return true; 93 | } 94 | } 95 | dvs.forEach(dv => markDataViewState(dv)); 96 | return false; 97 | } 98 | 99 | 100 | function hasSettingsChanged(oldOptions: VisualUpdateOptions, newOptions: VisualUpdateOptions, options: ICalcUpdateTypeOptions) { 101 | 'use strict'; 102 | const oldDvs = (oldOptions && oldOptions.dataViews) || []; 103 | const dvs = newOptions.dataViews || []; 104 | 105 | // Is this correct? 106 | if (oldDvs.length !== dvs.length) { 107 | return true; 108 | } 109 | 110 | for (let i = 0; i < oldDvs.length; i++) { 111 | const oM: any = oldDvs[i].metadata || {}; 112 | const nM: any = dvs[i].metadata || {}; 113 | if (!ldIsEqual(oM.objects, nM.objects)) { 114 | return true; 115 | } 116 | } 117 | } 118 | 119 | function hasResized(oldOptions: VisualUpdateOptions, newOptions: VisualUpdateOptions, options: ICalcUpdateTypeOptions) { 120 | 'use strict'; 121 | return !oldOptions || newOptions['resizeMode']; 122 | } 123 | 124 | function markDataViewState(dv: powerbi.DataView) { 125 | 'use strict'; 126 | if (dv) { 127 | let cats2 = (dv.categorical && dv.categorical.categories) || []; 128 | // set the length, so next go around, hasCategoryChanged can properly compare 129 | cats2.forEach(dc => { 130 | if (dc.identity) { 131 | dc.identity['$prevLength'] = dc.identity.length; 132 | } 133 | }); 134 | } 135 | } 136 | 137 | function hasArrayChanged(a1: T[], a2: T[], isEqual: (a: T, b: T) => boolean) { 138 | 'use strict'; 139 | // If the same array, shortcut (also works for undefined/null) 140 | if (a1 === a2) { 141 | return false; 142 | 143 | // If one of them is null and the other one isn't 144 | } else if (!a1 || !a2) { 145 | return true; 146 | } 147 | 148 | if (a1.length !== a2.length) { 149 | return true; 150 | } 151 | 152 | if (a1.length > 0) { 153 | const last = a1.length - 1; 154 | 155 | // check first and last, initially, as it should find 99.95% of changed cases 156 | return (!isEqual(a1[0], a2[0])) || 157 | (!isEqual(a1[last], a2[last])) || 158 | 159 | // Check everything 160 | (a1.some((n: any, i: number) => !isEqual(n, a2[i]))); 161 | } 162 | return false; 163 | } 164 | 165 | function hasCategoryChanged(dc1: powerbi.DataViewCategoryColumn, dc2: powerbi.DataViewCategoryColumn) { 166 | 'use strict'; 167 | let changed = hasArrayChanged(dc1.identity, dc2.identity, (a, b) => a.key === b.key); 168 | // Samesees array, they reuse the array for appending items 169 | if (dc1.identity && dc2.identity && dc1.identity === dc2.identity) { 170 | // TODO: This will not catch the case they reuse the array, ie clear the array, add new items with the same amount as the old one. 171 | let prevLength = dc1.identity['$prevLength']; 172 | let newLength = dc1.identity.length; 173 | dc1.identity['$prevLength'] = newLength; 174 | return prevLength !== newLength; 175 | } 176 | return changed; 177 | } 178 | 179 | function pickProps(obj) { 180 | const { queryName, roles, sort, aggregates } = obj; 181 | return { queryName, roles, sort, aggregates }; 182 | } 183 | 184 | function hasDataViewChanged(dv1: powerbi.DataView, dv2: powerbi.DataView, options: ICalcUpdateTypeOptions) { 185 | 'use strict'; 186 | let cats1 = (dv1.categorical && dv1.categorical.categories) || []; 187 | let cats2 = (dv2.categorical && dv2.categorical.categories) || []; 188 | let vals1 = (dv1.categorical && dv1.categorical.values) || []; 189 | let vals2 = (dv2.categorical && dv2.categorical.values) || []; 190 | let cols1 = (dv1.metadata && dv1.metadata.columns) || []; 191 | let cols2 = (dv2.metadata && dv2.metadata.columns) || []; 192 | if (cats1.length !== cats2.length || 193 | cols1.length !== cols2.length || 194 | vals1.length !== vals2.length) { 195 | return true; 196 | } 197 | 198 | if (options.ignoreCategoryOrder) { 199 | cols1 = cols1.sort((a, b) => a.queryName.localeCompare(b.queryName)); 200 | cols2 = cols2.sort((a, b) => a.queryName.localeCompare(b.queryName)); 201 | } 202 | 203 | for (let i = 0; i < cols1.length; i++) { 204 | // The underlying column has changed, or if the roles have changed 205 | if (!ldIsEqual(pickProps(cols1[i]), pickProps(cols2[i]))) { 206 | return true; 207 | } 208 | } 209 | 210 | for (let i = 0; i < cats1.length; i++) { 211 | if (hasCategoryChanged(cats1[i], cats2[i])) { 212 | return true; 213 | } 214 | } 215 | 216 | if (options.checkHighlights) { 217 | for (let i = 0; i < vals1.length; i++) { 218 | if (hasHighlightsChanged(vals1[i], vals2[i])) { 219 | return true; 220 | } 221 | } 222 | } 223 | return false; 224 | } 225 | 226 | function hasHighlightsChanged(val1: powerbi.DataViewValueColumn, val2: powerbi.DataViewValueColumn) { 227 | 'use strict'; 228 | if (val1 && val2) { 229 | const h1 = val1.highlights || []; 230 | const h2 = val2.highlights || []; 231 | if (h1 === h2) { 232 | // TODO: This will not catch the case they reuse the array, 233 | // ie clear the array, add new items with the same amount as the old one. 234 | let prevLength = h1['$prevLength']; 235 | let newLength = h1.length; 236 | h1['$prevLength'] = newLength; 237 | return prevLength !== newLength; 238 | } 239 | if (h1.length !== h2.length) { 240 | return true; 241 | } 242 | 243 | // Check any highlights have changed. 244 | return h1.some((h, i) => h !== h2[i]); 245 | } 246 | return false; 247 | } 248 | 249 | export interface ICalcUpdateTypeOptions { 250 | checkHighlights?: boolean; 251 | ignoreCategoryOrder?: boolean; 252 | defaultUnkownToData?: boolean; 253 | } -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import d3 from "d3"; 2 | var _nextId = 0; 3 | 4 | /** 5 | * Provides a set of utility functions 6 | */ 7 | var utils = { 8 | 9 | /** 10 | * Creates a d3 selection with the correct parent selector 11 | * @param {d3.Selection} selector The selector to select 12 | * @returns {d3.Selection} d3 selection bound to the parent 13 | */ 14 | selectWithParent: function (selector = null) { 15 | return d3.select(".timeline_storyteller" + (selector ? " " + selector : "")); 16 | }, 17 | 18 | /** 19 | * Creates a d3 selection with the correct parent selector 20 | * @param {d3.Selection} selector The selector to select 21 | * @returns {d3.Selection} d3 selection bound to the parent 22 | */ 23 | selectAllWithParent: function (selector) { 24 | return d3.selectAll(".timeline_storyteller" + (selector ? " " + selector : "")); 25 | }, 26 | 27 | /** 28 | * A function which will return the next unique id within a session. 29 | * @returns {number} The next unique id 30 | */ 31 | nextId: function () { 32 | return _nextId++; 33 | }, 34 | 35 | /** 36 | * Gets the highest id being used in the list of items 37 | * @param {{ id: number }[]} list The list of items to look through 38 | * @returns {number} The highest id 39 | */ 40 | getHighestId: function (list) { 41 | let highestId = 0; 42 | (list || []).forEach(n => { 43 | if (n.id > highestId) { 44 | highestId = n.id; 45 | } 46 | }); 47 | return highestId; 48 | }, 49 | 50 | /** 51 | * Creates a tweening function for an arc 52 | * @param {d3.svg.arc} arc The arc to create the tweening function for 53 | * @returns {function} A tweening function. 54 | */ 55 | arcTween: function (arc) { 56 | var cmdRegEx = /[mlhvcsqtaz][^mlhvcsqtaz]*/ig; 57 | 58 | // The gist is, basically if we are just transitioning between two states, then we 59 | // update the arc flags to match between the start and end states so the interpolate function doesn't try to interpolate the flags 60 | // problem being, if it interpolates the flags, sometimes it can generate values that aren't 0 or 1, but like .2398 61 | return function (d, idx, a) { 62 | var finalValue = arc.call(this, d, idx, a); 63 | var finalMatches = (finalValue).match(cmdRegEx) || []; 64 | var initialMatches = (d3.select(this).attr("d") || "").match(cmdRegEx) || []; 65 | if (finalMatches.length === initialMatches.length) { 66 | for (var i = 0; i < finalMatches.length; i++) { 67 | var finalMatch = finalMatches[i]; 68 | var startMatch = initialMatches[i]; 69 | var command = finalMatch[0]; 70 | // We've got an arc command 71 | if ((command === "A" || command === "a") && finalMatch[i] === startMatch[i]) { 72 | var finalParts = finalMatch.substring(1).split(/[\s\,]/); 73 | var startParts = startMatch.substring(1).split(/[\s\,]/); 74 | 75 | // Large arc flag 76 | startParts[3] = finalParts[3]; 77 | 78 | // sweep flag 79 | startParts[4] = finalParts[4]; 80 | 81 | initialMatches[i] = command + startParts.join(" "); 82 | finalMatches[i] = command + finalParts.join(" "); 83 | } 84 | } 85 | } else { 86 | return function () { 87 | return finalValue; 88 | }; 89 | } 90 | return d3.interpolate(initialMatches.join(""), finalMatches.join("")); 91 | }; 92 | }, 93 | 94 | /** 95 | * Creates a debounced function 96 | * @param {function} fn The function to debounce 97 | * @param {number} [delay=100] The debounce delay 98 | * @returns {function} The debounced function 99 | */ 100 | debounce: function (fn, delay) { 101 | var timeout; 102 | return function () { 103 | var args = Array.prototype.slice.call(arguments, 0); 104 | var that = this; 105 | clearTimeout(timeout); 106 | timeout = setTimeout(function () { 107 | fn.apply(that, args); 108 | }, delay || 500); 109 | }; 110 | }, 111 | 112 | /** 113 | * Provides a very basic deep clone of an object, functions get directly copied over 114 | * @param {Object} obj The object to clone 115 | * @returns {Object} The clone 116 | */ 117 | clone: function (obj) { 118 | var copy; 119 | 120 | // Handle the 3 simple types, and null or undefined 121 | if (obj === null || typeof obj !== "object") return obj; 122 | 123 | // Handle Date 124 | if (obj instanceof Date) { 125 | copy = new Date(); 126 | copy.setTime(obj.getTime()); 127 | return copy; 128 | } 129 | 130 | // Handle Array 131 | if (obj instanceof Array) { 132 | copy = []; 133 | for (var i = 0, len = obj.length; i < len; i++) { 134 | copy[i] = utils.clone(obj[i]); 135 | } 136 | return copy; 137 | } 138 | 139 | // Handle Object 140 | if (obj instanceof Object) { 141 | copy = {}; 142 | for (var attr in obj) { 143 | if (obj.hasOwnProperty(attr)) { 144 | copy[attr] = utils.clone(obj[attr]); 145 | } 146 | } 147 | return copy; 148 | } 149 | }, 150 | 151 | /** 152 | * Sets the scale value for the given category 153 | * @param {d3.Scale} scale The scale to change 154 | * @param {string} category The category to change 155 | * @param {object} value The value to set the category to 156 | * @returns {void} 157 | */ 158 | setScaleValue: function (scale, category, value) { 159 | var temp_palette = scale.range(); 160 | var target = temp_palette.indexOf(scale(category)); 161 | temp_palette[target] = value; 162 | scale.range(temp_palette); 163 | }, 164 | 165 | /** 166 | * Adds a listener on a transition for when the transition is complete 167 | * @param {d3.Transition} transition The transition to listen for completion 168 | * @param {Function} callback The callback for when the transition is complete 169 | * @returns {void} 170 | */ 171 | onTransitionComplete: function (transition) { 172 | // if (typeof callback !== "function") throw new Error("Wrong callback in onTransitionComplete"); 173 | return new Promise((resolve) => { 174 | if (transition.size() === 0) { 175 | resolve(); 176 | } 177 | var n = 0; 178 | transition 179 | .each(() => ++n) 180 | .each("end", () => { 181 | if (!--n) { 182 | resolve(); 183 | } 184 | }); 185 | }); 186 | }, 187 | 188 | /** 189 | * Converts a data url into a object url 190 | * @param {string} dataURL The data url to convert 191 | * @returns {string} The object url 192 | */ 193 | dataURLtoObjectURL: function (dataURL) { 194 | // convert base64/URLEncoded data component to raw binary data held in a string 195 | const dataURLParts = dataURL.split(","); 196 | const byteString = dataURLParts[0].indexOf("base64") >= 0 ? atob(dataURLParts[1]) : decodeURIComponent(dataURLParts[1]); 197 | 198 | // separate out the mime component 199 | const type = dataURLParts[0].split(":")[1].split(";")[0]; 200 | 201 | // write the bytes of the string to a typed array 202 | const ia = new Uint8Array(byteString.length); 203 | for (var i = 0; i < byteString.length; i++) { 204 | ia[i] = byteString.charCodeAt(i); 205 | } 206 | 207 | const blob = new Blob([ia], { type }); 208 | return URL.createObjectURL(blob); 209 | }, 210 | 211 | /** 212 | * Converts an image url to a data URL 213 | * @param {string} url The url of the image 214 | * @returns {Promise} A dataurl containing the image 215 | */ 216 | imageUrlToDataURL: function (url) { 217 | const img = new Image(); 218 | const me = this; 219 | img.crossOrigin = "anonymous"; 220 | return new Promise((resolve, reject) => { 221 | img.onload = function () { 222 | const canvas = document.createElement("canvas"); 223 | const ctx = canvas.getContext("2d"); 224 | 225 | canvas.width = me.width; 226 | canvas.height = me.height; 227 | 228 | // step 3, resize to final size 229 | ctx.drawImage(img, 0, 0); 230 | 231 | try { 232 | resolve(canvas.toDataURL()); 233 | } catch (e) { 234 | reject(e); 235 | } 236 | }; 237 | img.onerror = function () { 238 | reject(); 239 | }; 240 | img.src = url; 241 | }); 242 | }, 243 | 244 | /** 245 | * Resizes the given image to the given size 246 | * @param {string} url The url of the image 247 | * @param {number} width The final width of the image 248 | * @param {number} height The final height of the image 249 | * @param {boolean} [preserve=true] True if the aspect ratio should be preserved 250 | * @returns {Promise} A dataurl containing the image 251 | */ 252 | resizeImage: function (url, width, height, preserve) { 253 | const img = new Image(); 254 | img.crossOrigin = "anonymous"; 255 | preserve = preserve === undefined ? true : preserve; 256 | 257 | return new Promise((resolve, reject) => { 258 | img.onload = function () { 259 | const canvas = document.createElement("canvas"); 260 | const ctx = canvas.getContext("2d"); 261 | 262 | // https://stackoverflow.com/questions/19262141/resize-image-with-javascript-canvas-smoothly 263 | // Perform the resize in two steps to produce a higher quality resized image 264 | 265 | // set size proportional to image if there is no height passed to it, otherwise just use the height 266 | if (preserve) { 267 | if (width >= height) { 268 | height = width * (img.height / img.width); 269 | } else { 270 | width = height * (img.width / img.height); 271 | } 272 | } 273 | 274 | canvas.width = width; 275 | canvas.height = height; 276 | 277 | // step 1 - resize to 50% 278 | const oc = document.createElement("canvas"); 279 | const octx = oc.getContext("2d"); 280 | 281 | oc.width = img.width * 0.5; 282 | oc.height = img.height * 0.5; 283 | octx.drawImage(img, 0, 0, oc.width, oc.height); 284 | 285 | // step 2 - resize 50% of step 1 286 | octx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5); 287 | 288 | // step 3, resize to final size 289 | ctx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5, 0, 0, width, height); 290 | 291 | try { 292 | resolve(canvas.toDataURL()); 293 | } catch (e) { 294 | reject(e); 295 | } 296 | }; 297 | img.onerror = function () { 298 | reject(); 299 | }; 300 | img.src = url; 301 | }); 302 | } 303 | }; 304 | 305 | export default utils; 306 | -------------------------------------------------------------------------------- /src/core/globals.ts: -------------------------------------------------------------------------------- 1 | import imageUrls from "./imageUrls"; 2 | import d3 from "d3"; 3 | var u; 4 | let globals = { 5 | reset: null, 6 | formatAbbreviation: null, 7 | formatNumber: null, 8 | isNumber: null, 9 | segment_granularity: null, 10 | centre_radius: null, 11 | track_height: null, 12 | date_granularity: null, 13 | max_end_age: null, 14 | leader_line_style: null, 15 | unit_width: null, 16 | padding: null, 17 | annotation_list: null, 18 | caption_list: null, 19 | image_list: null, 20 | global_min_start_date: null, 21 | global_max_end_date: null, 22 | dirty_curve: null, 23 | max_item_index: null, 24 | playback_mode: null, 25 | max_seq_index: null, 26 | segments: null, 27 | num_segments: null, 28 | num_segment_cols: null, 29 | num_segment_rows: null, 30 | facets: null, 31 | buffer: null, 32 | num_facets: null, 33 | num_facet_cols: null, 34 | num_facet_rows: null, 35 | spiral_dim: null, 36 | active_event_list: null, 37 | prev_active_event_list: null, 38 | filter_type: null, 39 | categories: null, 40 | num_tracks: null, 41 | max_num_tracks: null, 42 | width: null, 43 | height: null, 44 | serverless: null, 45 | margin: null, 46 | legend_x: null, 47 | legend_y: null, 48 | effective_filter_width: null, 49 | effective_filter_height: null, 50 | scenes: null, 51 | opt_out: null, 52 | timeline_json_data: null, 53 | usage_log: null, 54 | email_address: null, 55 | timeline_story: null, 56 | gdoc_key: null, 57 | gdoc_worksheet: null, 58 | active_data: null, 59 | representations: null, 60 | scales: null, 61 | layouts: null, 62 | use_custom_palette: null, 63 | gif_index: null, 64 | legend_panel: null, 65 | max_num_seq_tracks: null, 66 | legend_rect_size: null, 67 | legend_spacing: null, 68 | color_palette: null, 69 | present_segments: null, 70 | num_categories: null, 71 | timeline_facets: null, 72 | earliest_date: null, 73 | latest_start_date: null, 74 | latest_end_date: null, 75 | max_legend_item_width: null, 76 | all_data: null, 77 | selected_categories: null, 78 | selected_facets: null, 79 | selected_segments: null, 80 | dispatch: null, 81 | total_num_facets: null, 82 | range_text: null, 83 | filter_set_length: null, 84 | legend_expanded: null, 85 | legend: null, 86 | all_event_ids: null, 87 | spiral_padding: null, 88 | num_seq_tracks: null, 89 | color_swap_target: null, 90 | source: null, 91 | reader: null, 92 | record_width: null, 93 | record_height: null, 94 | source_format: null, 95 | time_scale: null, 96 | filter_result: null, 97 | leader_line_styles: null, 98 | curve: null, 99 | socket: null 100 | }; 101 | 102 | // global dimensions 103 | function reset() { 104 | Object.assign(globals, { 105 | margin: { top: 100, right: 50, bottom: 105, left: 50 }, 106 | padding: { top: 100, right: 50, bottom: 105, left: 50 }, 107 | effective_filter_width: u, 108 | effective_filter_height: u, 109 | width: u, 110 | height: u, 111 | 112 | // initialize global variables 113 | date_granularity: u, 114 | segment_granularity: u, 115 | usage_log: [], 116 | max_num_tracks: u, 117 | max_num_seq_tracks: u, 118 | legend_panel: u, 119 | legend: u, 120 | legend_rect_size: u, 121 | legend_spacing: u, 122 | legend_expanded: true, 123 | legend_x: 100, 124 | legend_y: 100, 125 | source: u, 126 | source_format: u, 127 | earliest_date: u, 128 | latest_start_date: u, 129 | latest_end_date: u, 130 | categories: u, // scale for event types 131 | selected_categories: [], 132 | num_categories: u, 133 | max_legend_item_width: 0, 134 | facets: u, // scale for facets (timelines) 135 | num_facets: u, 136 | selected_facets: [], 137 | total_num_facets: u, 138 | num_facet_rows: u, 139 | num_facet_cols: u, 140 | segments: u, // scale for segments 141 | present_segments: u, 142 | 143 | /** 144 | * The selected date granularities used for filtering 145 | */ 146 | selected_segments: [], 147 | num_segments: u, 148 | num_segment_cols: u, 149 | num_segment_rows: u, 150 | buffer: 25, 151 | time_scale: u, // scale for time (years) 152 | timeline_facets: u, 153 | num_tracks: u, 154 | num_seq_tracks: u, 155 | global_min_start_date: u, 156 | global_max_end_date: u, 157 | max_end_age: u, 158 | max_seq_index: u, 159 | dispatch: d3.dispatch("Emphasize", "remove"), 160 | filter_result: u, 161 | scales: [ 162 | { "name": "Chronological", "icon": imageUrls("s-chron.png"), "hint": "A CHRONOLOGICAL scale is useful for showing absolute dates and times, like 2017, or 1999-12-31, or 6:37 PM." }, 163 | { "name": "Relative", "icon": imageUrls("s-rel.png"), "hint": "A RELATIVE scale is useful when comparing Faceted timelines with a common baseline at time 'zero'.For example, consider a timeline of person 'A' who lived between 1940 to 2010 and person 'B' who lived between 1720 and 1790. A Relative scale in this case would span from 0 to 70 years." }, 164 | { "name": "Log", "icon": imageUrls("s-log.png"), "hint": "A base-10 LOGARITHMIC scale is useful for long-spanning timelines and a skewed distributions of events. This scale is compatible with a Linear representation." }, 165 | { "name": "Sequential", "icon": imageUrls("s-seq.png"), "hint": "A SEQUENTIAL scale is useful for showing simply the order and number of events." }, 166 | { "name": "Collapsed", "icon": imageUrls("s-intdur.png"), "hint": "A COLLAPSED scale is a hybrid between Sequential and Chronological, and is useful for showing uneven distributions of events. It is compatible with a Linear representation and Unified layout. The duration between events is encoded as the length of bars." }], 167 | layouts: [ 168 | { "name": "Unified", "icon": imageUrls("l-uni.png"), "hint": "A UNIFIED layout is a single uninterrupted timeline and is useful when your data contains no facets." }, 169 | { "name": "Faceted", "icon": imageUrls("l-fac.png"), "hint": "A FACETED layout is useful when you have multiple timelines to compare." }, 170 | { "name": "Segmented", "icon": imageUrls("l-seg.png"), "hint": "A SEGMENTED layout splits a timeline into meaningful segments like centuries or days, depending on the extent of your timeline.It is compatible with a Chronological scale and is useful for showing patterns or differences across segments, such as periodicity." }], 171 | representations: [ 172 | { "name": "Linear", "icon": imageUrls("r-lin.png"), "hint": "A LINEAR representation is read left-to-right and is the most familiar timeline representation." }, 173 | { "name": "Radial", "icon": imageUrls("r-rad.png"), "hint": "A RADIAL representation is useful for showing cyclical patterns. It has the added benefit of a square aspect ratio." }, 174 | { "name": "Spiral", "icon": imageUrls("r-spi.png"), "hint": "A SPIRAL is a compact and playful way to show a sequence of events. It has a square aspect ratio and is only compatible with a Sequential scale." }, 175 | { "name": "Curve", "icon": imageUrls("r-arb.png"), "hint": "A CURVE is a playful way to show a sequence of events. It is only compatible with a Sequential scale and a Unified layout.Drag to draw a curve on the canvas; double click the canvas to reset the curve." }, 176 | { "name": "Calendar", "icon": imageUrls("r-cal.png"), "hint": "A month-week-day CALENDAR is a familiar representation that is compatible with a Chronological scale and a Segmented layout. This representation does not currently support timelines spanning decades or longer." }, 177 | { "name": "Grid", "icon": imageUrls("r-grid.png"), "hint": "A 10x10 GRID representation is compatible with a Chronological scale and a Segmented layout. This representation is ideal for timelines spanning decades or centuries." }], 178 | unit_width: 15, 179 | track_height: 15 * 1.5, 180 | spiral_padding: 15 * 1.25, 181 | spiral_dim: 0, 182 | centre_radius: 50, 183 | max_item_index: 0, 184 | filter_type: "Emphasize", 185 | active_data: [], 186 | all_data: [], 187 | active_event_list: [], 188 | prev_active_event_list: [], 189 | all_event_ids: [], 190 | scenes: [], 191 | caption_list: [], 192 | image_list: [], 193 | annotation_list: [], 194 | gif_index: 0, 195 | filter_set_length: 0, 196 | leader_line_styles: ["Rectangular", "Octoline", "Curved"], 197 | leader_line_style: 1, // 0=OCTO, 1=RECT, 2=CURVE 198 | curve: false, 199 | dirty_curve: false, 200 | record_width: u, 201 | record_height: u, 202 | reader: new FileReader(), 203 | timeline_json_data: [], 204 | gdoc_key: "1x8N7Z9RUrA9Jmc38Rvw1VkHslp8rgV2Ws3h_5iM-I8M", 205 | gdoc_worksheet: "dailyroutines", 206 | timeline_story: {}, 207 | opt_out: false, 208 | email_address: "", 209 | formatNumber: d3.format(".0f"), 210 | range_text: "", 211 | color_palette: [], 212 | color_swap_target: 0, 213 | use_custom_palette: false, 214 | serverless: false, 215 | socket: u, 216 | playback_mode: u 217 | }); // Defined in main.js 218 | } 219 | 220 | globals.reset = reset; 221 | 222 | reset(); // Set the initial values 223 | 224 | globals.formatAbbreviation = function (x) { 225 | "use strict"; 226 | 227 | var v = Math.abs(x); 228 | if (v >= 0.9995e9) { 229 | return globals.formatNumber(x / 1e9) + "B"; 230 | } else if (v >= 0.9995e6) { 231 | return globals.formatNumber(x / 1e6) + "M"; 232 | } else if (v >= 0.9995e3) { 233 | return globals.formatNumber(x / 1e3) + "k"; 234 | } 235 | return globals.formatNumber(x); 236 | }; 237 | 238 | // function for checking if string is a number 239 | globals.isNumber = function (n) { 240 | "use strict"; 241 | return !isNaN(parseFloat(n)) && isFinite(n); 242 | }; 243 | 244 | export default globals; 245 | -------------------------------------------------------------------------------- /src/visual.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017 Microsoft 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | * of the Software, and to permit persons to whom the Software is furnished to do 9 | * so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import 'core-js/stable/object/assign'; 24 | 25 | // TODO: Fix alignment of navigation frame hover popup 26 | require("intro.js/introjs.css"); // Loads the intro.js css 27 | 28 | import d3 from "d3"; 29 | import powerbiVisualsApi from "powerbi-visuals-api"; 30 | import ISelectionManager = powerbiVisualsApi.extensibility.ISelectionManager; 31 | import IVisual = powerbiVisualsApi.extensibility.IVisual; 32 | import IVisualHost = powerbiVisualsApi.extensibility.visual.IVisualHost; 33 | import VisualConstructorOptions = powerbiVisualsApi.extensibility.visual.VisualConstructorOptions; 34 | import EnumerateVisualObjectInstancesOptions = powerbiVisualsApi.EnumerateVisualObjectInstancesOptions; 35 | import VisualObjectInstanceEnumeration = powerbiVisualsApi.VisualObjectInstanceEnumeration; 36 | import VisualUpdateOptions = powerbiVisualsApi.extensibility.visual.VisualUpdateOptions; 37 | import Settings from './settings'; 38 | import convert from './dataConversion'; 39 | import { clamp } from './utils'; 40 | import calcUpdateType from './lib/calcUpdateType'; 41 | import UpdateType from './lib/UpdateType'; 42 | import TimelineStorytellerDefinition from "./core/main"; 43 | import utils from "./core/utils"; 44 | import images from "./core/imageUrls"; 45 | 46 | const isSafari = !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/); 47 | 48 | 49 | /** 50 | * Timeline story teller PowerBI visual class. 51 | * @class TimelineStoryteller 52 | */ 53 | export class TimelineStoryteller implements IVisual { 54 | private teller: any; 55 | private columnMappings: { [bucket: string]: any }; 56 | private element: HTMLElement; 57 | private settings: Settings = new Settings(); 58 | private host: IVisualHost; 59 | private firstUpdate = true; 60 | private dataView: powerbiVisualsApi.DataView; 61 | private options: powerbiVisualsApi.extensibility.visual.VisualUpdateOptions; 62 | private selectionManager: ISelectionManager; 63 | 64 | /** 65 | * TimelineStoryteller class constructor. 66 | * 67 | * @constructor 68 | * @param {VisualConstructorOptions} options - The initialization options as provided by PowerBI. 69 | */ 70 | constructor(options: VisualConstructorOptions) { 71 | this.element = options.element; 72 | this.element.className += ' timelinestoryteller-powerbi'; 73 | this.element.style.visibility = 'hidden'; 74 | this.host = options.host; 75 | this.teller = new TimelineStorytellerDefinition(true, false, options.element); 76 | this.teller.setUIScale(.7); 77 | this.teller.setOptions(this.buildTimelineOptions()); 78 | this.teller.on("stateChanged", () => this.saveStory()); 79 | 80 | const toHide = this.element.querySelectorAll(".file_selection_container .image_local_add_drop_zone, .file_selection_container h5" + (isSafari ? ", .offline_option_container, .options_container, .image_local_add_container " : "")); 81 | Array.prototype.forEach.call(toHide, n => { 82 | n.style.display = "none"; 83 | }); 84 | 85 | if (isSafari) { 86 | const toDisable = this.element.querySelectorAll(".resize_enabled_cb, .offline_enabled_cb"); 87 | Array.prototype.forEach.call(toDisable, n => { 88 | n.checked = false; 89 | }); 90 | } 91 | 92 | this.selectionManager = this.host.createSelectionManager(); 93 | 94 | const visualSelection = d3.select(this.element); 95 | visualSelection.on("contextmenu", () => { 96 | const mouseEvent: MouseEvent = d3.event as MouseEvent; 97 | const eventTarget: EventTarget = mouseEvent.target; 98 | let dataPoint = d3.select(eventTarget).datum(); 99 | this.selectionManager.showContextMenu(dataPoint ? dataPoint.selectionId : {}, { 100 | x: mouseEvent.clientX, 101 | y: mouseEvent.clientY 102 | }); 103 | mouseEvent.preventDefault(); 104 | }); 105 | } 106 | 107 | /** 108 | * TimelineStoryteller's visualization destroy method. Called by PowerBI. 109 | * 110 | * @method destroy 111 | */ 112 | public destroy(): void { 113 | } 114 | 115 | /** 116 | * Update function called by PowerBI when the visual or its data need to be updated. 117 | * 118 | * @method update 119 | * @param {VisualUpdateOptions} options - Update options object as provided by PowerBI. 120 | */ 121 | public update(options: VisualUpdateOptions): void { 122 | const dv = this.dataView = options.dataViews && options.dataViews[0]; 123 | 124 | if (dv && dv.categorical) { 125 | const isFirstUpdate = this.firstUpdate; 126 | const updateType = calcUpdateType(this.options, options); 127 | 128 | // Let the timeline storyteller know it was resized 129 | if (!this.options || 130 | this.options.viewport.width !== options.viewport.width || 131 | this.options.viewport.height !== options.viewport.height) { 132 | this.teller._onResized(); 133 | } 134 | 135 | // This needs to happen after the updateType calc 136 | this.options = options; 137 | 138 | if ((updateType & UpdateType.Settings) === UpdateType.Settings) { 139 | this.loadSettings(); 140 | } 141 | 142 | if ((updateType & UpdateType.Data) === UpdateType.Data) { 143 | this.firstUpdate = false; 144 | this.loadData(isFirstUpdate); 145 | } 146 | } 147 | } 148 | 149 | /** 150 | * This method will be executed only if the formatting panel is open. 151 | */ 152 | public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstanceEnumeration { 153 | // This should not be visible 154 | const savedStory = this.settings.story.savedStory; 155 | delete this.settings.story.savedStory; 156 | 157 | const objs = Settings.enumerateObjectInstances(this.settings, options); 158 | 159 | this.settings.story.savedStory = savedStory; 160 | 161 | return objs; 162 | } 163 | 164 | /** 165 | * Builds the options for TimelineStoryteller 166 | */ 167 | private buildTimelineOptions() { 168 | const importStoryMenu = utils.clone(TimelineStorytellerDefinition.DEFAULT_OPTIONS.import.storyMenu); 169 | const menu = utils.clone(TimelineStorytellerDefinition.DEFAULT_OPTIONS.menu); 170 | menu.export = {}; 171 | menu.open = { 172 | label: 'Data', 173 | items: { 174 | open: menu.open.items[0], 175 | reset: { 176 | text: 'Reset', 177 | image: images("resetBasic.png"), 178 | click: this.reset.bind(this) 179 | } 180 | } 181 | }; 182 | this.teller.setOptions({ 183 | showAbout: false, 184 | showLogo: false, 185 | // showImportOptions: true, 186 | showIntro: false, 187 | showHints: false, 188 | export: { 189 | images: false 190 | }, 191 | import: { 192 | storyMenu: { 193 | items: { 194 | file: importStoryMenu.items.file, 195 | } 196 | } 197 | }, 198 | menu 199 | }); 200 | } 201 | 202 | /** 203 | * Loads settings from PowerBI 204 | */ 205 | private loadSettings() { 206 | const dv = this.dataView; 207 | const oldSettings = this.settings; 208 | this.settings = dv ? Settings.parse(dv) : new Settings(); 209 | 210 | // Clamp the UI Scale 211 | let newScale = this.settings.display.uiScale; 212 | this.settings.display.uiScale = newScale ? clamp(newScale, 0.1, 2) : 0.7; 213 | 214 | newScale = this.settings.display.uiScale; 215 | if (oldSettings.display.uiScale !== newScale) { 216 | this.teller.setUIScale(newScale); 217 | } 218 | } 219 | 220 | /** 221 | * Resets the current state of timeline storyteller 222 | */ 223 | private reset() { 224 | this.settings.story.savedStory = ""; 225 | this.host.persistProperties({ 226 | replace: [{ 227 | objectName: 'story', 228 | selector: null, 229 | properties: { 230 | savedStory: '' 231 | } 232 | }] 233 | }); 234 | this.loadData(false); 235 | } 236 | 237 | /** 238 | * Loads data from PowerBI into TimelineStoryteller 239 | * @param isFirstUpdate If this is the load call from the fist update 240 | */ 241 | private loadData(isFirstUpdate: boolean) { 242 | const data = convert(this.dataView, this.host); 243 | let display = 'hidden'; 244 | if (data) { 245 | display = ''; 246 | // Disable the update calls until we can nail down the filtering, it looks like when .update is called for the first time with filtered 247 | // data, it applies some transparency that it shouldn't 248 | // We are initially loading 249 | // if (!this.columnMappings || cols.filter(n => (newMappings[n] || {}).parent === (this.columnMappings[n] || {}).parent).length !== cols.length) { 250 | // this.columnMappings = newMappings; 251 | // this.teller.load(data); 252 | // } else { 253 | // this.teller.update(data); 254 | // } 255 | 256 | const savedStory = this.settings.story.savedStory; 257 | const timelineState = savedStory ? JSON.parse(savedStory) : {}; 258 | timelineState.timeline_json_data = data; 259 | 260 | if (isFirstUpdate && this.settings.story.autoLoad) { 261 | // Give it time to load the data first 262 | setTimeout(() => this.teller.load(timelineState, true), 1000); 263 | } else { 264 | this.teller.load(timelineState, false, true); 265 | } 266 | } 267 | 268 | const elesToHide = document.querySelectorAll('.introjs-hints, .timelinestoryteller-powerbi'); 269 | for (let i = 0; i < elesToHide.length; i++) { 270 | elesToHide[i]['style'].visibility = display; 271 | } 272 | } 273 | 274 | /** 275 | * Saves the current story to powerbi 276 | */ 277 | private saveStory() { 278 | const savedStory = JSON.stringify(this.teller.saveState()); 279 | this.host.persistProperties({ 280 | replace: [{ 281 | objectName: 'story', 282 | selector: null, 283 | properties: { 284 | savedStory 285 | } 286 | }] 287 | }); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/core/lib/gif.js: -------------------------------------------------------------------------------- 1 | (function (c) { 2 | function a(b, d) { 3 | if ({}.hasOwnProperty.call(a.cache, b)) return a.cache[b];var e = a.resolve(b);if (!e) throw new Error('Failed to resolve module ' + b);var c = { id: b, require: a, filename: b, exports: {}, loaded: !1, parent: d, children: [] };d && d.children.push(c);var f = b.slice(0, b.lastIndexOf('/') + 1);return a.cache[b] = c.exports, e.call(c.exports, c, c.exports, f, b), c.loaded = !0, a.cache[b] = c.exports; 4 | }a.modules = {}, a.cache = {}, a.resolve = function (b) { 5 | return {}.hasOwnProperty.call(a.modules, b) ? a.modules[b] : void 0; 6 | }, a.define = function (b, c) { 7 | a.modules[b] = c; 8 | };var b = function (a) { 9 | return a = '/', { title: 'browser', version: 'v0.10.26', browser: !0, env: {}, argv: [], nextTick: c.setImmediate || function (a) { 10 | setTimeout(a, 0); 11 | }, cwd: function cwd() { 12 | return a; 13 | }, chdir: function chdir(b) { 14 | a = b; 15 | } }; 16 | }();a.define('/gif.coffee', function (d, m, l, k) { 17 | function g(a, b) { 18 | return {}.hasOwnProperty.call(a, b); 19 | }function j(d, b) { 20 | for (var a = 0, c = b.length; a < c; ++a) { 21 | if (a in b && b[a] === d) return !0; 22 | }return !1; 23 | }function i(a, b) { 24 | function d() { 25 | this.constructor = a; 26 | }for (var c in b) { 27 | g(b, c) && (a[c] = b[c]); 28 | }return d.prototype = b.prototype, a.prototype = new d(), a.__super__ = b.prototype, a; 29 | }var h, c, f, b, e;f = a('events', d).EventEmitter, h = a('/browser.coffee', d), e = function (d) { 30 | function a(d) { 31 | var a, b;this.running = !1, this.options = {}, this.frames = [], this.freeWorkers = [], this.activeWorkers = [], this.setOptions(d);for (a in c) { 32 | b = c[a], null != this.options[a] ? this.options[a] : this.options[a] = b; 33 | } 34 | }return i(a, d), c = { workerScript: 'gif.worker.js', workers: 2, repeat: 0, background: '#fff', quality: 10, width: null, height: null, transparent: null }, b = { delay: 500, copy: !1 }, a.prototype.setOption = function (a, b) { 35 | return this.options[a] = b, null != this._canvas && (a === 'width' || a === 'height') ? this._canvas[a] = b : void 0; 36 | }, a.prototype.setOptions = function (b) { 37 | var a, c;return function (d) { 38 | for (a in b) { 39 | if (!g(b, a)) continue;c = b[a], d.push(this.setOption(a, c)); 40 | }return d; 41 | }.call(this, []); 42 | }, a.prototype.addFrame = function (a, d) { 43 | var c, e;null == d && (d = {}), c = {}, c.transparent = this.options.transparent;for (e in b) { 44 | c[e] = d[e] || b[e]; 45 | }if (null != this.options.width || this.setOption('width', a.width), null != this.options.height || this.setOption('height', a.height), 'undefined' !== typeof ImageData && null != ImageData && a instanceof ImageData) c.data = a.data;else if ('undefined' !== typeof CanvasRenderingContext2D && null != CanvasRenderingContext2D && a instanceof CanvasRenderingContext2D || 'undefined' !== typeof WebGLRenderingContext && null != WebGLRenderingContext && a instanceof WebGLRenderingContext) d.copy ? c.data = this.getContextData(a) : c.context = a;else if (null != a.childNodes) d.copy ? c.data = this.getImageData(a) : c.image = a;else throw new Error('Invalid image');return this.frames.push(c); 46 | }, a.prototype.render = function () { 47 | var d, a;if (this.running) throw new Error('Already running');if (!(null != this.options.width && null != this.options.height)) throw new Error('Width and height must be set prior to rendering');this.running = !0, this.nextFrame = 0, this.finishedFrames = 0, this.imageParts = function (c) { 48 | for (var b = function () { 49 | var b;b = [];for (var a = 0; 0 <= this.frames.length ? a < this.frames.length : a > this.frames.length; 0 <= this.frames.length ? ++a : --a) { 50 | b.push(a); 51 | }return b; 52 | }.apply(this, arguments), a = 0, e = b.length; a < e; ++a) { 53 | d = b[a], c.push(null); 54 | }return c; 55 | }.call(this, []), a = this.spawnWorkers();for (var c = function () { 56 | var c;c = [];for (var b = 0; 0 <= a ? b < a : b > a; 0 <= a ? ++b : --b) { 57 | c.push(b); 58 | }return c; 59 | }.apply(this, arguments), b = 0, e = c.length; b < e; ++b) { 60 | d = c[b], this.renderNextFrame(); 61 | }return this.emit('start'), this.emit('progress', 0); 62 | }, a.prototype.abort = function () { 63 | var a;while (!0) { 64 | if (a = this.activeWorkers.shift(), !(null != a)) break;console.log('killing active worker'), a.terminate(); 65 | }return this.running = !1, this.emit('abort'); 66 | }, a.prototype.spawnWorkers = function () { 67 | var a;return a = Math.min(this.options.workers, this.frames.length), function () { 68 | var c;c = [];for (var b = this.freeWorkers.length; this.freeWorkers.length <= a ? b < a : b > a; this.freeWorkers.length <= a ? ++b : --b) { 69 | c.push(b); 70 | }return c; 71 | }.apply(this, arguments).forEach(function (a) { 72 | return function (c) { 73 | var b;return console.log('spawning worker ' + c), b = new Worker(a.options.workerScript), b.onmessage = function (a) { 74 | return function (c) { 75 | return a.activeWorkers.splice(a.activeWorkers.indexOf(b), 1), a.freeWorkers.push(b), a.frameFinished(c.data); 76 | }; 77 | }(a), a.freeWorkers.push(b); 78 | }; 79 | }(this)), a; 80 | }, a.prototype.frameFinished = function (a) { 81 | return console.log('frame ' + a.index + ' finished - ' + this.activeWorkers.length + ' active'), this.finishedFrames++, this.emit('progress', this.finishedFrames / this.frames.length), this.imageParts[a.index] = a, j(null, this.imageParts) ? this.renderNextFrame() : this.finishRendering(); 82 | }, a.prototype.finishRendering = function () { 83 | var e, a, k, m, b, d, h;b = 0;for (var f = 0, j = this.imageParts.length; f < j; ++f) { 84 | a = this.imageParts[f], b += (a.data.length - 1) * a.pageSize + a.cursor; 85 | }b += a.pageSize - a.cursor, console.log('rendering finished - filesize ' + Math.round(b / 1e3) + 'kb'), e = new Uint8Array(b), d = 0;for (var g = 0, l = this.imageParts.length; g < l; ++g) { 86 | a = this.imageParts[g];for (var c = 0, i = a.data.length; c < i; ++c) { 87 | h = a.data[c], k = c, e.set(h, d), k === a.data.length - 1 ? d += a.cursor : d += a.pageSize; 88 | } 89 | }return m = new Blob([e], { type: 'image/gif' }), this.emit('finished', m, e); 90 | }, a.prototype.renderNextFrame = function () { 91 | var c, a, b;if (this.freeWorkers.length === 0) throw new Error('No free workers');return this.nextFrame >= this.frames.length ? void 0 : (c = this.frames[this.nextFrame++], b = this.freeWorkers.shift(), a = this.getTask(c), console.log('starting frame ' + (a.index + 1) + ' of ' + this.frames.length), this.activeWorkers.push(b), b.postMessage(a)); 92 | }, a.prototype.getContextData = function (a) { 93 | return a.getImageData(0, 0, this.options.width, this.options.height).data; 94 | }, a.prototype.getImageData = function (b) { 95 | var a;return null != this._canvas || (this._canvas = document.createElement('canvas'), this._canvas.width = this.options.width, this._canvas.height = this.options.height), a = this._canvas.getContext('2d'), a.setFill = this.options.background, a.fillRect(0, 0, this.options.width, this.options.height), a.drawImage(b, 0, 0), this.getContextData(a); 96 | }, a.prototype.getTask = function (a) { 97 | var c, b;if (c = this.frames.indexOf(a), b = { index: c, last: c === this.frames.length - 1, delay: a.delay, transparent: a.transparent, width: this.options.width, height: this.options.height, quality: this.options.quality, repeat: this.options.repeat, canTransfer: h.name === 'chrome' }, null != a.data) b.data = a.data;else if (null != a.context) b.data = this.getContextData(a.context);else if (null != a.image) b.data = this.getImageData(a.image);else throw new Error('Invalid frame');return b; 98 | }, a; 99 | }(f), d.exports = e; 100 | }), a.define('/browser.coffee', function (f, g, h, i) { 101 | var a, d, e, c, b;c = navigator.userAgent.toLowerCase(), e = navigator.platform.toLowerCase(), b = c.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/) || [null, 'unknown', 0], d = b[1] === 'ie' && document.documentMode, a = { name: b[1] === 'version' ? b[3] : b[1], version: d || parseFloat(b[1] === 'opera' && b[4] ? b[4] : b[2]), platform: { name: c.match(/ip(?:ad|od|hone)/) ? 'ios' : (c.match(/(?:webos|android)/) || e.match(/mac|win|linux/) || ['other'])[0] } }, a[a.name] = !0, a[a.name + parseInt(a.version, 10)] = !0, a.platform[a.platform.name] = !0, f.exports = a; 102 | }), a.define('events', function (f, e, g, h) { 103 | b.EventEmitter || (b.EventEmitter = function () {});var a = e.EventEmitter = b.EventEmitter, 104 | c = typeof Array.isArray === 'function' ? Array.isArray : function (a) { 105 | return Object.prototype.toString.call(a) === '[object Array]'; 106 | }, 107 | d = 10;a.prototype.setMaxListeners = function (a) { 108 | this._events || (this._events = {}), this._events.maxListeners = a; 109 | }, a.prototype.emit = function (f) { 110 | if (f === 'error' && (!(this._events && this._events.error) || c(this._events.error) && !this._events.error.length)) throw arguments[1] instanceof Error ? arguments[1] : new Error("Uncaught, unspecified 'error' event.");if (!this._events) return !1;var a = this._events[f];if (!a) return !1;if (!(typeof a == 'function')) if (c(a)) { 111 | var b = Array.prototype.slice.call(arguments, 1), 112 | e = a.slice();for (var d = 0, g = e.length; d < g; d++) { 113 | e[d].apply(this, b); 114 | }return !0; 115 | } else return !1;switch (arguments.length) {case 1: 116 | a.call(this);break;case 2: 117 | a.call(this, arguments[1]);break;case 3: 118 | a.call(this, arguments[1], arguments[2]);break;default: 119 | var b = Array.prototype.slice.call(arguments, 1);a.apply(this, b);}return !0; 120 | }, a.prototype.addListener = function (a, b) { 121 | if ('function' !== typeof b) throw new Error('addListener only takes instances of Function');if (this._events || (this._events = {}), this.emit('newListener', a, b), !this._events[a]) this._events[a] = b;else if (c(this._events[a])) { 122 | if (!this._events[a].warned) { 123 | var e;this._events.maxListeners !== undefined ? e = this._events.maxListeners : e = d, e && e > 0 && this._events[a].length > e && (this._events[a].warned = !0, console.error('(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.', this._events[a].length), console.trace()); 124 | }this._events[a].push(b); 125 | } else this._events[a] = [this._events[a], b];return this; 126 | }, a.prototype.on = a.prototype.addListener, a.prototype.once = function (b, c) { 127 | var a = this;return a.on(b, function d() { 128 | a.removeListener(b, d), c.apply(this, arguments); 129 | }), this; 130 | }, a.prototype.removeListener = function (a, d) { 131 | if ('function' !== typeof d) throw new Error('removeListener only takes instances of Function');if (!(this._events && this._events[a])) return this;var b = this._events[a];if (c(b)) { 132 | var e = b.indexOf(d);if (e < 0) return this;b.splice(e, 1), b.length == 0 && delete this._events[a]; 133 | } else this._events[a] === d && delete this._events[a];return this; 134 | }, a.prototype.removeAllListeners = function (a) { 135 | return a && this._events && this._events[a] && (this._events[a] = null), this; 136 | }, a.prototype.listeners = function (a) { 137 | return this._events || (this._events = {}), this._events[a] || (this._events[a] = []), c(this._events[a]) || (this._events[a] = [this._events[a]]), this._events[a]; 138 | }; 139 | }), c.GIF = a('/gif.coffee'); 140 | }).call(this, this); 141 | //# sourceMappingURL=gif.js.map 142 | // gif.js 0.1.6 - https://github.com/jnordberg/gif.js -------------------------------------------------------------------------------- /src/core/radialAxis.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import d3 from "d3"; 4 | import * as moment from "moment"; 5 | import globals from "./globals"; 6 | import utils from "./utils"; 7 | const arcTween = utils.arcTween; 8 | 9 | const time = require("./lib/time.min.js"); 10 | 11 | /** 12 | 13 | radialAxis: //a reusable radial axis 14 | 15 | **/ 16 | 17 | d3.radialAxis = function (unit_width) { 18 | var radial_axis_scale = d3.scale.linear().range([0, 2 * Math.PI]), 19 | radial_axis_units = "Chronological", 20 | x_pos = 0, 21 | y_pos = 0, 22 | duration = 1000, 23 | final_quantile = 0, 24 | track_bounds = 0, 25 | bc_origin = false, 26 | longer_than_a_day = true, 27 | num_ticks = 0; 28 | 29 | function radialAxis(selection) { 30 | selection.each(function (data) { 31 | var g = d3.select(this); 32 | 33 | if (moment(data[0]).year() <= 0) { 34 | bc_origin = true; 35 | } else { 36 | bc_origin = false; 37 | } 38 | 39 | num_ticks = data.length; 40 | 41 | if (globals.segment_granularity === "days" && time.hour.count(time.day.floor(data[0]), time.day.ceil(data[num_ticks - 1])) > 24) { 42 | longer_than_a_day = true; 43 | } else { 44 | longer_than_a_day = false; 45 | } 46 | 47 | // stash the new scale and quantiles 48 | this.__chart__ = radial_axis_scale; 49 | 50 | // concentric track circles 51 | var radial_axis_tracks = g.selectAll(".radial_tracks") 52 | .data(d3.range(-1, track_bounds)); 53 | 54 | var radial_axis_tracks_enter = radial_axis_tracks.enter() 55 | .append("g") 56 | .attr("class", "radial_tracks"); 57 | 58 | radial_axis_tracks_enter.append("path") 59 | .attr("class", "rad_track") 60 | .attr("id", function (d, i) { 61 | return "rad_track" + i; 62 | }) 63 | .attr("transform", function () { 64 | return "translate(" + x_pos + " ," + y_pos + ")"; 65 | }) 66 | .style("opacity", 0) 67 | .attr("d", d3.svg.arc() 68 | .innerRadius(function () { 69 | return globals.centre_radius; 70 | }) 71 | .outerRadius(function () { 72 | return globals.centre_radius; 73 | }) 74 | .startAngle(0) 75 | .endAngle(radial_axis_scale(final_quantile))); 76 | 77 | var radial_axis_tracks_update = radial_axis_tracks.transition() 78 | .duration(duration); 79 | 80 | var radial_axis_tracks_delayed_update = radial_axis_tracks.transition() 81 | .delay(function (d, i) { 82 | return duration + i / track_bounds * duration; 83 | }) 84 | .duration(duration); 85 | 86 | var radial_axis_tracks_exit = radial_axis_tracks.exit().transition() 87 | .delay(duration) 88 | .duration(duration) 89 | .remove(); 90 | 91 | radial_axis_tracks_update.selectAll(".rad_track") 92 | .attr("transform", function () { 93 | return "translate(" + x_pos + " ," + y_pos + ")"; 94 | }); 95 | 96 | radial_axis_tracks_delayed_update.selectAll(".rad_track") 97 | .style("opacity", 1) 98 | .attrTween("d", arcTween(d3.svg.arc() 99 | .innerRadius(function (d) { 100 | return globals.centre_radius + d * globals.track_height; 101 | }) 102 | .outerRadius(function (d) { 103 | return globals.centre_radius + d * globals.track_height; 104 | }) 105 | .startAngle(0) 106 | .endAngle(radial_axis_scale(final_quantile)))); 107 | 108 | radial_axis_tracks_exit.selectAll(".rad_track") 109 | .attr("transform", function () { 110 | return "translate(" + x_pos + " ," + y_pos + ")"; 111 | }) 112 | .attrTween("d", arcTween(d3.svg.arc() 113 | .innerRadius(function () { 114 | return globals.centre_radius; 115 | }) 116 | .outerRadius(function () { 117 | return globals.centre_radius; 118 | }) 119 | .startAngle(0) 120 | .endAngle(radial_axis_scale(final_quantile)))); 121 | 122 | // radial ticks 123 | var radial_axis_tick = g.selectAll(".radial_axis_tick") 124 | .data(data); 125 | 126 | var radial_axis_tick_enter = radial_axis_tick.enter() 127 | .append("g") 128 | .attr("class", "radial_axis_tick"); 129 | 130 | var radial_axis_tick_exit = radial_axis_tick.exit().transition() 131 | .duration(duration) 132 | .remove(); 133 | 134 | radial_axis_tick_enter.append("path") 135 | .attr("class", "radial_axis_tick_path") 136 | .style("opacity", 0) 137 | .attr("transform", function () { 138 | return "translate(" + x_pos + " ," + y_pos + ")"; 139 | }); 140 | 141 | radial_axis_tick_enter.append("text") 142 | .attr("class", "radial_axis_tick_label") 143 | .attr("text-anchor", "middle") 144 | .attr("dominant-baseline", "central") 145 | .style("opacity", 0) 146 | .text(function () { 147 | return ""; 148 | }) 149 | .attr("transform", function () { 150 | return "translate(" + x_pos + " ," + y_pos + ")"; 151 | }); 152 | 153 | var radial_axis_tick_update = radial_axis_tick.transition() 154 | .duration(duration); 155 | 156 | var radial_axis_tick_delayed_update = radial_axis_tick.transition() 157 | .delay(function (d, i) { 158 | return duration + i / data.length * duration; 159 | }) 160 | .duration(duration); 161 | 162 | radial_axis_tick_update.select("path") 163 | .attr("transform", function () { 164 | return "translate(" + x_pos + " ," + y_pos + ")"; 165 | }); 166 | 167 | radial_axis_tick_delayed_update.select("path") 168 | .style("opacity", 1) 169 | .attrTween("d", arcTween(d3.svg.arc() 170 | .innerRadius(globals.centre_radius - globals.track_height) 171 | .outerRadius(globals.centre_radius + track_bounds * globals.track_height - 0.25 * unit_width) 172 | .startAngle(function (d) { 173 | return radial_axis_scale(d); 174 | }) 175 | .endAngle(function (d) { 176 | return radial_axis_scale(d); 177 | }))); 178 | 179 | radial_axis_tick_update.select("text") 180 | .style("opacity", 0) 181 | .text("") 182 | .attr("transform", function () { 183 | return "translate(" + x_pos + " ," + y_pos + ")"; 184 | }); 185 | 186 | radial_axis_tick_delayed_update.select("text") 187 | .style("opacity", 1) 188 | .attr("x", function (d) { 189 | return (globals.centre_radius + track_bounds * globals.track_height + 0.5 * unit_width) * Math.sin(radial_axis_scale(d)); 190 | }) 191 | .attr("y", function (d) { 192 | return -1 * (globals.centre_radius + track_bounds * globals.track_height + 0.5 * unit_width) * Math.cos(radial_axis_scale(d)); 193 | }) 194 | .text(function (d, i) { 195 | return formatTick(d, i); 196 | }) 197 | .attr("transform", function (d) { 198 | var angle = radial_axis_scale(d) * (180 / Math.PI); 199 | if (angle > 90 && angle <= 180) { 200 | angle = angle + 180; 201 | } else if (angle < 270 && angle > 180) { 202 | angle = angle - 180; 203 | } 204 | return "translate(" + x_pos + " ," + y_pos + ")rotate(" + angle + "," + (globals.centre_radius + track_bounds * globals.track_height + 0.5 * unit_width) * Math.sin(radial_axis_scale(d)) + " ," + (-1 * (globals.centre_radius + track_bounds * globals.track_height + 0.5 * unit_width) * Math.cos(radial_axis_scale(d))) + ")"; 205 | }); 206 | 207 | radial_axis_tick_exit.select("path") 208 | .attrTween("d", arcTween(d3.svg.arc() 209 | .innerRadius(globals.centre_radius) 210 | .outerRadius(globals.centre_radius) 211 | .startAngle(function (d) { 212 | return radial_axis_scale(d); 213 | }) 214 | .endAngle(function (d) { 215 | return radial_axis_scale(d); 216 | }))); 217 | 218 | radial_axis_tick_exit.select("text") 219 | .text(function () { 220 | return ""; 221 | }); 222 | }); 223 | d3.timer.flush(); 224 | } 225 | 226 | function formatTick(d, i) { 227 | var radial_axis_tick_label = d; 228 | 229 | if (radial_axis_units === "Sequential") { 230 | radial_axis_tick_label = d; 231 | } else if (radial_axis_units === "Chronological") { 232 | switch (globals.segment_granularity) { 233 | case "days": 234 | if (i === num_ticks - 1) { 235 | radial_axis_tick_label = ""; 236 | } else if (longer_than_a_day) { 237 | radial_axis_tick_label = moment(d).format("MMM D hA"); 238 | } else { 239 | radial_axis_tick_label = moment(d).format("hA"); 240 | } 241 | break; 242 | case "weeks": 243 | radial_axis_tick_label = moment(d).format("MMM D"); 244 | break; 245 | case "months": 246 | radial_axis_tick_label = moment(d).format("MMM 'YY"); 247 | break; 248 | case "years": 249 | if (moment(d).year() < 0) { 250 | radial_axis_tick_label = (-1 * moment(d).year()) + " BC"; 251 | } else { 252 | radial_axis_tick_label = +moment(d).year(); 253 | if (bc_origin) { 254 | radial_axis_tick_label += " AD"; 255 | } 256 | } 257 | break; 258 | case "decades": 259 | if (moment(d).year() < 0) { 260 | radial_axis_tick_label = (-1 * moment(d).year()) + " BC"; 261 | } else { 262 | radial_axis_tick_label = +moment(d).year(); 263 | if (bc_origin) { 264 | radial_axis_tick_label += " AD"; 265 | } 266 | } 267 | break; 268 | case "centuries": 269 | if (moment(d).year() < 0) { 270 | radial_axis_tick_label = (-1 * moment(d).year()) + " BC"; 271 | } else { 272 | radial_axis_tick_label = +moment(d).year(); 273 | if (bc_origin) { 274 | radial_axis_tick_label += " AD"; 275 | } 276 | } 277 | break; 278 | case "millenia": 279 | if (moment(d).year() < 0) { 280 | radial_axis_tick_label = (-1 * moment(d).year()) + " BC"; 281 | } else { 282 | radial_axis_tick_label = +moment(d).year(); 283 | if (bc_origin) { 284 | radial_axis_tick_label += " AD"; 285 | } 286 | } 287 | break; 288 | case "epochs": 289 | radial_axis_tick_label = globals.formatAbbreviation(d); 290 | break; 291 | default: 292 | break; 293 | } 294 | } else if (radial_axis_units === "Segments") { 295 | switch (globals.segment_granularity) { 296 | case "days": 297 | radial_axis_tick_label = moment().hour(d).format("hA"); 298 | break; 299 | case "weeks": 300 | radial_axis_tick_label = moment().weekday(d).format("ddd"); 301 | break; 302 | case "months": 303 | if ((d - 1) % 7 !== 0) { 304 | radial_axis_tick_label = ""; 305 | } else { 306 | radial_axis_tick_label = moment().date(d).format("Do"); 307 | } 308 | break; 309 | case "years": 310 | if ((d - 1) % 4 === 0) { 311 | radial_axis_tick_label = ""; 312 | } else { 313 | radial_axis_tick_label = moment().week(d + 1).format("MMM"); 314 | } 315 | break; 316 | case "decades": 317 | radial_axis_tick_label = (d / 12) + " years"; 318 | break; 319 | case "centuries": 320 | radial_axis_tick_label = d + " years"; 321 | break; 322 | case "millenia": 323 | radial_axis_tick_label = d + " years"; 324 | break; 325 | case "epochs": 326 | radial_axis_tick_label = ""; 327 | break; 328 | default: 329 | break; 330 | } 331 | if (i === num_ticks - 1) { 332 | radial_axis_tick_label = ""; 333 | } 334 | } else if (radial_axis_units === "Relative") { 335 | if (globals.date_granularity === "epochs") { 336 | radial_axis_tick_label = globals.formatAbbreviation(d); 337 | } else if ((globals.max_end_age / 86400000) > 1000) { 338 | radial_axis_tick_label = Math.round(d / 31536000730) + " years"; 339 | } else if ((globals.max_end_age / 86400000) > 120) { 340 | radial_axis_tick_label = Math.round(d / 2628000000) + " months"; 341 | } else if ((globals.max_end_age / 86400000) > 2) { 342 | radial_axis_tick_label = Math.round(d / 86400000) + " days"; 343 | } else { 344 | radial_axis_tick_label = Math.round(d / 3600000) + " hours"; 345 | } 346 | } 347 | return radial_axis_tick_label; 348 | } 349 | 350 | radialAxis.x_pos = function (x) { 351 | if (!arguments.length) { 352 | return x_pos; 353 | } 354 | x_pos = x; 355 | return radialAxis; 356 | }; 357 | 358 | radialAxis.y_pos = function (x) { 359 | if (!arguments.length) { 360 | return y_pos; 361 | } 362 | y_pos = x; 363 | return radialAxis; 364 | }; 365 | 366 | radialAxis.duration = function (x) { 367 | if (!arguments.length) { 368 | return duration; 369 | } 370 | duration = x; 371 | return radialAxis; 372 | }; 373 | 374 | radialAxis.radial_axis_scale = function (x) { 375 | if (!arguments.length) { 376 | return radial_axis_scale; 377 | } 378 | radial_axis_scale = x; 379 | return radialAxis; 380 | }; 381 | 382 | radialAxis.radial_axis_units = function (x) { 383 | if (!arguments.length) { 384 | return radial_axis_units; 385 | } 386 | radial_axis_units = x; 387 | return radialAxis; 388 | }; 389 | 390 | radialAxis.final_quantile = function (x) { 391 | if (!arguments.length) { 392 | return final_quantile; 393 | } 394 | final_quantile = x; 395 | return radialAxis; 396 | }; 397 | 398 | radialAxis.track_bounds = function (x) { 399 | if (!arguments.length) { 400 | return track_bounds; 401 | } 402 | track_bounds = x; 403 | return radialAxis; 404 | }; 405 | 406 | radialAxis.bc_origin = function (x) { 407 | if (!arguments.length) { 408 | return bc_origin; 409 | } 410 | bc_origin = x; 411 | return radialAxis; 412 | }; 413 | 414 | return radialAxis; 415 | }; 416 | 417 | export default d3.radialAxis; 418 | -------------------------------------------------------------------------------- /src/core/lib/saveSvgAsPng.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2014 Eric Shull 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | */ 26 | 27 | /* 28 | 29 | BEGIN Timeline Storyteller Modification - March 2017 30 | 31 | */ 32 | import d3 from "d3"; 33 | 34 | import globals from "../globals"; 35 | 36 | //var log = require("debug")("TimelineStoryteller:saveSvgAsPng"); 37 | /* 38 | 39 | END Timeline Storyteller Modification 40 | 41 | */ 42 | 43 | function isElement(obj) { 44 | return obj instanceof HTMLElement || obj instanceof SVGElement; 45 | } 46 | 47 | function requireDomNode(el) { 48 | if (!isElement(el)) { 49 | throw new Error('an HTMLElement or SVGElement is required; got ' + el); 50 | } 51 | } 52 | 53 | function isExternal(url) { 54 | return url && url.lastIndexOf('http', 0) == 0 && url.lastIndexOf(window.location.host) == -1; 55 | } 56 | 57 | function inlineImages(el, callback) { 58 | requireDomNode(el); 59 | 60 | var images = el.querySelectorAll('image'), 61 | left = images.length, 62 | checkDone = function checkDone() { 63 | if (left === 0) { 64 | callback(); 65 | } 66 | }; 67 | 68 | checkDone(); 69 | for (var i = 0; i < images.length; i++) { 70 | (function (image) { 71 | var href = image.getAttributeNS("http://www.w3.org/1999/xlink", "href"); 72 | if (href) { 73 | if (isExternal(href.value)) { 74 | console.warn("Cannot render embedded images linking to external hosts: " + href.value); 75 | return; 76 | } 77 | } 78 | var canvas = document.createElement('canvas'); 79 | var ctx = canvas.getContext('2d'); 80 | var img = new Image(); 81 | img.crossOrigin = "anonymous"; 82 | href = href || image.getAttribute('href'); 83 | if (href) { 84 | img.src = href; 85 | img.onload = function () { 86 | canvas.width = img.width; 87 | canvas.height = img.height; 88 | ctx.drawImage(img, 0, 0); 89 | image.setAttributeNS("http://www.w3.org/1999/xlink", "href", canvas.toDataURL('image/png')); 90 | left--; 91 | checkDone(); 92 | }; 93 | img.onerror = function () { 94 | //log("Could not load " + href); 95 | left--; 96 | checkDone(); 97 | }; 98 | } else { 99 | left--; 100 | checkDone(); 101 | } 102 | })(images[i]); 103 | } 104 | } 105 | 106 | function styles(el, selectorRemap, modifyStyle) { 107 | var css = ""; 108 | var sheets = document.styleSheets; 109 | for (var i = 0; i < sheets.length; i++) { 110 | try { 111 | var rules = sheets[i].cssRules; 112 | } catch (e) { 113 | console.warn("Stylesheet could not be loaded: " + sheets[i].href); 114 | continue; 115 | } 116 | 117 | if (rules != null) { 118 | for (var j = 0, match; j < rules.length; j++, match = null) { 119 | var rule = rules[j]; 120 | if (typeof (rule).style != "undefined") { 121 | var selectorText; 122 | 123 | try { 124 | selectorText = (rule).selectorText; 125 | } catch (err) { 126 | console.warn('The following CSS rule has an invalid selector: "' + rule + '"', err); 127 | } 128 | 129 | try { 130 | if (selectorText) { 131 | match = el.querySelector(selectorText); 132 | } 133 | } catch (err) { 134 | console.warn('Invalid CSS selector "' + selectorText + '"', err); 135 | } 136 | 137 | if (match) { 138 | var selector = selectorRemap ? selectorRemap((rule).selectorText) : (rule).selectorText; 139 | var cssText = modifyStyle ? modifyStyle((rule).style.cssText) : (rule).style.cssText; 140 | css += selector + " { " + cssText + " }\n"; 141 | } else if (rule.cssText.match(/^@font-face/)) { 142 | css += rule.cssText + '\n'; 143 | } 144 | } 145 | } 146 | } 147 | } 148 | return css; 149 | } 150 | 151 | function getDimension(el, clone, dim) { 152 | var v = el.viewBox && el.viewBox.baseVal && el.viewBox.baseVal[dim] || clone.getAttribute(dim) !== null && !clone.getAttribute(dim).match(/%$/) && parseInt(clone.getAttribute(dim)) || el.getBoundingClientRect()[dim] || parseInt(clone.style[dim]) || parseInt(window.getComputedStyle(el).getPropertyValue(dim)); 153 | return typeof v === 'undefined' || v === null || isNaN(parseFloat(v)) ? 0 : v; 154 | } 155 | 156 | function reEncode(data) { 157 | data = encodeURIComponent(data); 158 | data = data.replace(/%([0-9A-F]{2})/g, function (match, p1) { 159 | var c = String.fromCharCode(`0x${p1}`); 160 | return c === '%' ? '%25' : c; 161 | }); 162 | return decodeURIComponent(data); 163 | } 164 | 165 | function uriToBlob(uri) { 166 | var byteString = window.atob(uri.split(',')[1]); 167 | var mimeString = uri.split(',')[0].split(':')[1].split(';')[0]; 168 | var buffer = new ArrayBuffer(byteString.length); 169 | var intArray = new Uint8Array(buffer); 170 | for (var i = 0; i < byteString.length; i++) { 171 | intArray[i] = byteString.charCodeAt(i); 172 | } 173 | return new Blob([buffer], { type: mimeString }); 174 | } 175 | 176 | const doctype = ']>'; 177 | 178 | const out = { 179 | prepareSvg: (el, options, cb) => { 180 | requireDomNode(el); 181 | 182 | options = options || {}; 183 | options.scale = options.scale || 1; 184 | options.responsive = options.responsive || false; 185 | var xmlns = "http://www.w3.org/2000/xmlns/"; 186 | 187 | inlineImages(el, function () { 188 | var outer = document.createElement("div"); 189 | var clone = el.cloneNode(true); 190 | var width, height; 191 | if (el.tagName == 'svg') { 192 | width = options.width || getDimension(el, clone, 'width'); 193 | height = options.height || getDimension(el, clone, 'height'); 194 | } else if (el.getBBox) { 195 | var box = el.getBBox(); 196 | width = box.x + box.width; 197 | height = box.y + box.height; 198 | clone.setAttribute('transform', clone.getAttribute('transform').replace(/translate\(.*?\)/, '')); 199 | 200 | var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 201 | svg.appendChild(clone); 202 | clone = svg; 203 | } else { 204 | console.error('Attempted to render non-SVG element', el); 205 | return; 206 | } 207 | 208 | clone.setAttribute("version", "1.1"); 209 | if (!clone.getAttribute('xmlns')) { 210 | clone.setAttributeNS(xmlns, "xmlns", "http://www.w3.org/2000/svg"); 211 | } 212 | if (!clone.getAttribute('xmlns:xlink')) { 213 | clone.setAttributeNS(xmlns, "xmlns:xlink", "http://www.w3.org/1999/xlink"); 214 | } 215 | 216 | if (options.responsive) { 217 | clone.removeAttribute('width'); 218 | clone.removeAttribute('height'); 219 | clone.setAttribute('preserveAspectRatio', 'xMinYMin meet'); 220 | } else { 221 | clone.setAttribute("width", width * options.scale); 222 | clone.setAttribute("height", height * options.scale); 223 | } 224 | 225 | clone.setAttribute("viewBox", [options.left || 0, options.top || 0, width, height].join(" ")); 226 | 227 | var fos = clone.querySelectorAll('foreignObject > *'); 228 | for (var i = 0; i < fos.length; i++) { 229 | if (!fos[i].getAttribute('xmlns')) { 230 | fos[i].setAttributeNS(xmlns, "xmlns", "http://www.w3.org/1999/xhtml"); 231 | } 232 | } 233 | 234 | outer.appendChild(clone); 235 | 236 | var css = styles(el, options.selectorRemap, options.modifyStyle); 237 | var s = document.createElement('style'); 238 | s.setAttribute('type', 'text/css'); 239 | s.innerHTML = ""; 240 | var defs = document.createElement('defs'); 241 | defs.appendChild(s); 242 | clone.insertBefore(defs, clone.firstChild); 243 | 244 | if (cb) { 245 | cb(outer.innerHTML, width, height); 246 | } 247 | }); 248 | }, 249 | 250 | svgAsDataUri: (el, options, cb) => { 251 | out.prepareSvg(el, options, (svg) => { 252 | // svg.replace - https://stackoverflow.com/questions/30273775/namespace-prefix-ns1-for-href-on-tagelement-is-not-defined-setattributens 253 | var uri = 'data:image/svg+xml;base64,' + window.btoa(reEncode(doctype + svg.replace(/NS\d+:href/g, 'xlink:href'))); 254 | if (cb) { 255 | cb(uri); 256 | } 257 | }); 258 | }, 259 | 260 | svgAsPngUri: (el, options, cb) => { 261 | requireDomNode(el); 262 | 263 | options = options || {}; 264 | options.encoderType = options.encoderType || 'image/png'; 265 | options.encoderOptions = options.encoderOptions || 0.8; 266 | 267 | var convertToPng = function convertToPng(src, w, h) { 268 | var canvas = document.createElement('canvas'); 269 | var context = canvas.getContext('2d'); 270 | canvas.width = w; 271 | canvas.height = h; 272 | 273 | if (options.canvg) { 274 | options.canvg(canvas, src); 275 | } else { 276 | context.drawImage(src, -1 * parseInt(d3.select(el).style('margin-left')), -1 * parseInt(d3.select(el).style('margin-top'))); 277 | } 278 | 279 | if (options.backgroundColor) { 280 | context.globalCompositeOperation = 'destination-over'; 281 | context.fillStyle = options.backgroundColor; 282 | context.fillRect(0, 0, canvas.width, canvas.height); 283 | } 284 | 285 | var png; 286 | try { 287 | png = canvas.toDataURL(options.encoderType, options.encoderOptions); 288 | } catch (e) { 289 | // if (typeof SecurityError !== 'undefined' && e instanceof SecurityError || e.name == "SecurityError") { 290 | console.error("Rendered SVG images cannot be downloaded in this browser."); 291 | return; 292 | // } else { 293 | // throw e; 294 | //} 295 | } 296 | cb(png); 297 | }; 298 | 299 | if (options.canvg) { 300 | out.prepareSvg(el, options, convertToPng); 301 | } else { 302 | out.svgAsDataUri(el, options, function (uri) { 303 | var image = new Image(); 304 | 305 | image.onload = function () { 306 | convertToPng(image, image.width, image.height); 307 | }; 308 | 309 | image.onerror = function () { 310 | console.error('There was an error loading the data URI as an image on the following SVG\n', window.atob(uri.slice(26)), '\n', "Open the following link to see browser's diagnosis\n", uri); 311 | }; 312 | 313 | image.src = uri; 314 | }); 315 | } 316 | }, 317 | 318 | download: (name, uri) => { 319 | if (navigator.msSaveOrOpenBlob) { 320 | navigator.msSaveOrOpenBlob(uriToBlob(uri), name); 321 | } else { 322 | var saveLink = document.createElement('a'); 323 | var downloadSupported = 'download' in saveLink; 324 | if (downloadSupported) { 325 | saveLink.download = name; 326 | saveLink.href = uri; 327 | saveLink.style.display = 'none'; 328 | document.querySelector(".timeline_storyteller").appendChild(saveLink); 329 | saveLink.click(); 330 | document.querySelector(".timeline_storyteller").removeChild(saveLink); 331 | } else { 332 | window.open(uri, '_temp', 'menubar=no,toolbar=no,status=no'); 333 | } 334 | } 335 | 336 | /* 337 | BEGIN Timeline Storyteller Modification - Dec 2016 338 | */ 339 | var research_copy = {}; 340 | if (!globals.opt_out) { 341 | research_copy = { 342 | 'timeline_json_data': globals.timeline_json_data, 343 | 'usage_log': globals.usage_log, 344 | 'name': name, 345 | 'image': uri, 346 | 'email_address': globals.email_address, 347 | 'timestamp': new Date().valueOf() 348 | }; 349 | } else { 350 | research_copy = { 351 | 'usage_log': globals.usage_log, 352 | 'email_address': globals.email_address, 353 | 'timestamp': new Date().valueOf() 354 | }; 355 | } 356 | var research_copy_json = JSON.stringify(research_copy); 357 | var research_blob = new Blob([research_copy_json], { type: "application/json" }); 358 | 359 | //log(research_copy_json); 360 | 361 | // if (globals.socket) { 362 | // globals.socket.emit('export_event', research_copy_json); // raise an event on the server 363 | // } 364 | 365 | /* 366 | END Timeline Storyteller Modification 367 | */ 368 | }, 369 | 370 | saveSvg: (el, name, options) => { 371 | requireDomNode(el); 372 | 373 | options = options || {}; 374 | out.svgAsDataUri(el, options, function (uri) { 375 | out.download(name, uri); 376 | }); 377 | }, 378 | 379 | saveSvgAsPng: (el, name, options) => { 380 | requireDomNode(el); 381 | 382 | options = options || {}; 383 | out.svgAsPngUri(el, options, function (uri) { 384 | out.download(name, uri); 385 | }); 386 | }, 387 | 388 | svgAsPNG: (el, id, options) => { 389 | requireDomNode(el); 390 | 391 | options = options || {}; 392 | out.svgAsPngUri(el, options, function (uri) { 393 | var img = document.createElement('img'); 394 | img.style.display = "none"; 395 | img.id = "gif_frame" + id; 396 | img.src = uri; 397 | document.querySelector(".timeline_storyteller").appendChild(img); 398 | d3.select("#gif_frame" + id).attr('class', 'gif_frame'); 399 | }); 400 | } 401 | }; 402 | 403 | export default out; -------------------------------------------------------------------------------- /src/core/lib/gif.worker.js: -------------------------------------------------------------------------------- 1 | (function (b) { 2 | function a(b, d) { 3 | if ({}.hasOwnProperty.call(a.cache, b)) return a.cache[b];var e = a.resolve(b);if (!e) throw new Error('Failed to resolve module ' + b);var c = { id: b, require: a, filename: b, exports: {}, loaded: !1, parent: d, children: [] };d && d.children.push(c);var f = b.slice(0, b.lastIndexOf('/') + 1);return a.cache[b] = c.exports, e.call(c.exports, c, c.exports, f, b), c.loaded = !0, a.cache[b] = c.exports; 4 | }a.modules = {}, a.cache = {}, a.resolve = function (b) { 5 | return {}.hasOwnProperty.call(a.modules, b) ? a.modules[b] : void 0; 6 | }, a.define = function (b, c) { 7 | a.modules[b] = c; 8 | }, a.define('/gif.worker.coffee', function (d, e, f, g) { 9 | var b, c;b = a('/GIFEncoder.js', d), c = function c(a) { 10 | var c, e, d, f;return c = new b(a.width, a.height), a.index === 0 ? c.writeHeader() : c.firstFrame = !1, c.setTransparent(a.transparent), c.setRepeat(a.repeat), c.setDelay(a.delay), c.setQuality(a.quality), c.addFrame(a.data), a.last && c.finish(), d = c.stream(), a.data = d.pages, a.cursor = d.cursor, a.pageSize = d.constructor.pageSize, a.canTransfer ? (f = function (c) { 11 | for (var b = 0, d = a.data.length; b < d; ++b) { 12 | e = a.data[b], c.push(e.buffer); 13 | }return c; 14 | }.call(this, []), self.postMessage(a, f)) : self.postMessage(a); 15 | }, self.onmessage = function (a) { 16 | return c(a.data); 17 | }; 18 | }), a.define('/GIFEncoder.js', function (e, h, i, j) { 19 | function c() { 20 | this.page = -1, this.pages = [], this.newPage(); 21 | }function b(a, b) { 22 | this.width = ~~a, this.height = ~~b, this.transparent = null, this.transIndex = 0, this.repeat = -1, this.delay = 0, this.image = null, this.pixels = null, this.indexedPixels = null, this.colorDepth = null, this.colorTab = null, this.usedEntry = new Array(), this.palSize = 7, this.dispose = -1, this.firstFrame = !0, this.sample = 10, this.out = new c(); 23 | }var f = a('/TypedNeuQuant.js', e), 24 | g = a('/LZWEncoder.js', e);c.pageSize = 4096, c.charMap = {};for (var d = 0; d < 256; d++) { 25 | c.charMap[d] = String.fromCharCode(d); 26 | }c.prototype.newPage = function () { 27 | this.pages[++this.page] = new Uint8Array(c.pageSize), this.cursor = 0; 28 | }, c.prototype.getData = function () { 29 | var d = '';for (var a = 0; a < this.pages.length; a++) { 30 | for (var b = 0; b < c.pageSize; b++) { 31 | d += c.charMap[this.pages[a][b]]; 32 | } 33 | }return d; 34 | }, c.prototype.writeByte = function (a) { 35 | this.cursor >= c.pageSize && this.newPage(), this.pages[this.page][this.cursor++] = a; 36 | }, c.prototype.writeUTFBytes = function (b) { 37 | for (var c = b.length, a = 0; a < c; a++) { 38 | this.writeByte(b.charCodeAt(a)); 39 | } 40 | }, c.prototype.writeBytes = function (b, d, e) { 41 | for (var c = e || b.length, a = d || 0; a < c; a++) { 42 | this.writeByte(b[a]); 43 | } 44 | }, b.prototype.setDelay = function (a) { 45 | this.delay = Math.round(a / 10); 46 | }, b.prototype.setFrameRate = function (a) { 47 | this.delay = Math.round(100 / a); 48 | }, b.prototype.setDispose = function (a) { 49 | a >= 0 && (this.dispose = a); 50 | }, b.prototype.setRepeat = function (a) { 51 | this.repeat = a; 52 | }, b.prototype.setTransparent = function (a) { 53 | this.transparent = a; 54 | }, b.prototype.addFrame = function (a) { 55 | this.image = a, this.getImagePixels(), this.analyzePixels(), this.firstFrame && (this.writeLSD(), this.writePalette(), this.repeat >= 0 && this.writeNetscapeExt()), this.writeGraphicCtrlExt(), this.writeImageDesc(), this.firstFrame || this.writePalette(), this.writePixels(), this.firstFrame = !1; 56 | }, b.prototype.finish = function () { 57 | this.out.writeByte(59); 58 | }, b.prototype.setQuality = function (a) { 59 | a < 1 && (a = 1), this.sample = a; 60 | }, b.prototype.writeHeader = function () { 61 | this.out.writeUTFBytes('GIF89a'); 62 | }, b.prototype.analyzePixels = function () { 63 | var g = this.pixels.length, 64 | d = g / 3;this.indexedPixels = new Uint8Array(d);var a = new f(this.pixels, this.sample);a.buildColormap(), this.colorTab = a.getColormap();var b = 0;for (var c = 0; c < d; c++) { 65 | var e = a.lookupRGB(this.pixels[b++] & 255, this.pixels[b++] & 255, this.pixels[b++] & 255);this.usedEntry[e] = !0, this.indexedPixels[c] = e; 66 | }this.pixels = null, this.colorDepth = 8, this.palSize = 7, this.transparent !== null && (this.transIndex = this.findClosest(this.transparent)); 67 | }, b.prototype.findClosest = function (e) { 68 | if (this.colorTab === null) return -1;var k = (e & 16711680) >> 16, 69 | l = (e & 65280) >> 8, 70 | m = e & 255, 71 | c = 0, 72 | d = 16777216, 73 | j = this.colorTab.length;for (var a = 0; a < j;) { 74 | var f = k - (this.colorTab[a++] & 255), 75 | g = l - (this.colorTab[a++] & 255), 76 | h = m - (this.colorTab[a] & 255), 77 | i = f * f + g * g + h * h, 78 | b = parseInt(a / 3);this.usedEntry[b] && i < d && (d = i, c = b), a++; 79 | }return c; 80 | }, b.prototype.getImagePixels = function () { 81 | var a = this.width, 82 | g = this.height;this.pixels = new Uint8Array(a * g * 3);var b = this.image, 83 | c = 0;for (var d = 0; d < g; d++) { 84 | for (var e = 0; e < a; e++) { 85 | var f = d * a * 4 + e * 4;this.pixels[c++] = b[f], this.pixels[c++] = b[f + 1], this.pixels[c++] = b[f + 2]; 86 | } 87 | } 88 | }, b.prototype.writeGraphicCtrlExt = function () { 89 | this.out.writeByte(33), this.out.writeByte(249), this.out.writeByte(4);var b, a;this.transparent === null ? (b = 0, a = 0) : (b = 1, a = 2), this.dispose >= 0 && (a = dispose & 7), a <<= 2, this.out.writeByte(0 | a | 0 | b), this.writeShort(this.delay), this.out.writeByte(this.transIndex), this.out.writeByte(0); 90 | }, b.prototype.writeImageDesc = function () { 91 | this.out.writeByte(44), this.writeShort(0), this.writeShort(0), this.writeShort(this.width), this.writeShort(this.height), this.firstFrame ? this.out.writeByte(0) : this.out.writeByte(128 | this.palSize); 92 | }, b.prototype.writeLSD = function () { 93 | this.writeShort(this.width), this.writeShort(this.height), this.out.writeByte(240 | this.palSize), this.out.writeByte(0), this.out.writeByte(0); 94 | }, b.prototype.writeNetscapeExt = function () { 95 | this.out.writeByte(33), this.out.writeByte(255), this.out.writeByte(11), this.out.writeUTFBytes('NETSCAPE2.0'), this.out.writeByte(3), this.out.writeByte(1), this.writeShort(this.repeat), this.out.writeByte(0); 96 | }, b.prototype.writePalette = function () { 97 | this.out.writeBytes(this.colorTab);var b = 768 - this.colorTab.length;for (var a = 0; a < b; a++) { 98 | this.out.writeByte(0); 99 | } 100 | }, b.prototype.writeShort = function (a) { 101 | this.out.writeByte(a & 255), this.out.writeByte(a >> 8 & 255); 102 | }, b.prototype.writePixels = function () { 103 | var a = new g(this.width, this.height, this.indexedPixels, this.colorDepth);a.encode(this.out); 104 | }, b.prototype.stream = function () { 105 | return this.out; 106 | }, e.exports = b; 107 | }), a.define('/LZWEncoder.js', function (e, g, h, i) { 108 | function f(y, D, C, B) { 109 | function w(a, b) { 110 | r[f++] = a, f >= 254 && t(b); 111 | }function x(b) { 112 | u(a), k = i + 2, j = !0, l(i, b); 113 | }function u(b) { 114 | for (var a = 0; a < b; ++a) { 115 | h[a] = -1; 116 | } 117 | }function A(z, r) { 118 | var g, t, d, e, y, w, s;for (q = z, j = !1, n_bits = q, m = p(n_bits), i = 1 << z - 1, o = i + 1, k = i + 2, f = 0, e = v(), s = 0, g = a; g < 65536; g *= 2) { 119 | ++s; 120 | }s = 8 - s, w = a, u(w), l(i, r);a: while ((t = v()) != c) { 121 | if (g = (t << b) + e, d = t << s ^ e, h[d] === g) { 122 | e = n[d];continue; 123 | }if (h[d] >= 0) { 124 | y = w - d, d === 0 && (y = 1);do { 125 | if ((d -= y) < 0 && (d += w), h[d] === g) { 126 | e = n[d];continue a; 127 | } 128 | } while (h[d] >= 0); 129 | }l(e, r), e = t, k < 1 << b ? (n[d] = k++, h[d] = g) : x(r); 130 | }l(e, r), l(o, r); 131 | }function z(a) { 132 | a.writeByte(s), remaining = y * D, curPixel = 0, A(s + 1, a), a.writeByte(0); 133 | }function t(a) { 134 | f > 0 && (a.writeByte(f), a.writeBytes(r, 0, f), f = 0); 135 | }function p(a) { 136 | return (1 << a) - 1; 137 | }function v() { 138 | if (remaining === 0) return c;--remaining;var a = C[curPixel++];return a & 255; 139 | }function l(a, c) { 140 | g &= d[e], e > 0 ? g |= a << e : g = a, e += n_bits;while (e >= 8) { 141 | w(g & 255, c), g >>= 8, e -= 8; 142 | }if ((k > m || j) && (j ? (m = p(n_bits = q), j = !1) : (++n_bits, n_bits == b ? m = 1 << b : m = p(n_bits))), a == o) { 143 | while (e > 0) { 144 | w(g & 255, c), g >>= 8, e -= 8; 145 | }t(c); 146 | } 147 | }var s = Math.max(2, B), 148 | r = new Uint8Array(256), 149 | h = new Int32Array(a), 150 | n = new Int32Array(a), 151 | g, 152 | e = 0, 153 | f, 154 | k = 0, 155 | m, 156 | j = !1, 157 | q, 158 | i, 159 | o;this.encode = z; 160 | }var c = -1, 161 | b = 12, 162 | a = 5003, 163 | d = [0, 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383, 32767, 65535];e.exports = f; 164 | }), a.define('/TypedNeuQuant.js', function (A, F, E, D) { 165 | function C(A, B) { 166 | function I() { 167 | o = [], q = new Int32Array(256), t = new Int32Array(a), y = new Int32Array(a), z = new Int32Array(a >> 3);var c, d;for (c = 0; c < a; c++) { 168 | d = (c << b + 8) / a, o[c] = new Float64Array([d, d, d, 0]), y[c] = e / a, t[c] = 0; 169 | } 170 | }function J() { 171 | for (var c = 0; c < a; c++) { 172 | o[c][0] >>= b, o[c][1] >>= b, o[c][2] >>= b, o[c][3] = c; 173 | } 174 | }function K(b, a, c, e, f) { 175 | o[a][0] -= b * (o[a][0] - c) / d, o[a][1] -= b * (o[a][1] - e) / d, o[a][2] -= b * (o[a][2] - f) / d; 176 | }function L(j, e, n, l, k) { 177 | var h = Math.abs(e - j), 178 | i = Math.min(e + j, a), 179 | g = e + 1, 180 | f = e - 1, 181 | m = 1, 182 | b, 183 | d;while (g < i || f > h) { 184 | d = z[m++], g < i && (b = o[g++], b[0] -= d * (b[0] - n) / c, b[1] -= d * (b[1] - l) / c, b[2] -= d * (b[2] - k) / c), f > h && (b = o[f--], b[0] -= d * (b[0] - n) / c, b[1] -= d * (b[1] - l) / c, b[2] -= d * (b[2] - k) / c); 185 | } 186 | }function C(p, s, q) { 187 | var h = 2147483647, 188 | k = h, 189 | d = -1, 190 | m = d, 191 | c, 192 | j, 193 | e, 194 | n, 195 | l;for (c = 0; c < a; c++) { 196 | j = o[c], e = Math.abs(j[0] - p) + Math.abs(j[1] - s) + Math.abs(j[2] - q), e < h && (h = e, d = c), n = e - (t[c] >> i - b), n < k && (k = n, m = c), l = y[c] >> g, y[c] -= l, t[c] += l << f; 197 | }return y[d] += x, t[d] -= r, m; 198 | }function D() { 199 | var d, 200 | b, 201 | e, 202 | c, 203 | h, 204 | g, 205 | f = 0, 206 | i = 0;for (d = 0; d < a; d++) { 207 | for (e = o[d], h = d, g = e[1], b = d + 1; b < a; b++) { 208 | c = o[b], c[1] < g && (h = b, g = c[1]); 209 | }if (c = o[h], d != h && (b = c[0], c[0] = e[0], e[0] = b, b = c[1], c[1] = e[1], e[1] = b, b = c[2], c[2] = e[2], e[2] = b, b = c[3], c[3] = e[3], e[3] = b), g != f) { 210 | for (q[f] = i + d >> 1, b = f + 1; b < g; b++) { 211 | q[b] = d; 212 | }f = g, i = d; 213 | } 214 | }for (q[f] = i + n >> 1, b = f + 1; b < 256; b++) { 215 | q[b] = n; 216 | } 217 | }function E(j, i, k) { 218 | var b, 219 | d, 220 | c, 221 | e = 1e3, 222 | h = -1, 223 | f = q[i], 224 | g = f - 1;while (f < a || g >= 0) { 225 | f < a && (d = o[f], c = d[1] - i, c >= e ? f = a : (f++, c < 0 && (c = -c), b = d[0] - j, b < 0 && (b = -b), c += b, c < e && (b = d[2] - k, b < 0 && (b = -b), c += b, c < e && (e = c, h = d[3])))), g >= 0 && (d = o[g], c = i - d[1], c >= e ? g = -1 : (g--, c < 0 && (c = -c), b = d[0] - j, b < 0 && (b = -b), c += b, c < e && (b = d[2] - k, b < 0 && (b = -b), c += b, c < e && (e = c, h = d[3])))); 226 | }return h; 227 | }function F() { 228 | var c, 229 | f = A.length, 230 | D = 30 + (B - 1) / 3, 231 | y = f / (3 * B), 232 | q = ~~(y / w), 233 | n = d, 234 | o = u, 235 | a = o >> h;for (a <= 1 && (a = 0), c = 0; c < a; c++) { 236 | z[c] = n * ((a * a - c * c) * m / (a * a)); 237 | }var i;f < s ? (B = 1, i = 3) : f % l !== 0 ? i = 3 * l : f % k !== 0 ? i = 3 * k : f % p !== 0 ? i = 3 * p : i = 3 * j;var r, 238 | t, 239 | x, 240 | e, 241 | g = 0;c = 0;while (c < y) { 242 | if (r = (A[g] & 255) << b, t = (A[g + 1] & 255) << b, x = (A[g + 2] & 255) << b, e = C(r, t, x), K(n, e, r, t, x), a !== 0 && L(a, e, r, t, x), g += i, g >= f && (g -= f), c++, q === 0 && (q = 1), c % q === 0) for (n -= n / D, o -= o / v, a = o >> h, a <= 1 && (a = 0), e = 0; e < a; e++) { 243 | z[e] = n * ((a * a - e * e) * m / (a * a)); 244 | } 245 | } 246 | }function G() { 247 | I(), F(), J(), D(); 248 | }function H() { 249 | var b = [], 250 | g = [];for (var c = 0; c < a; c++) { 251 | g[o[c][3]] = c; 252 | }var d = 0;for (var e = 0; e < a; e++) { 253 | var f = g[e];b[d++] = o[f][0], b[d++] = o[f][1], b[d++] = o[f][2]; 254 | }return b; 255 | }var o, q, t, y, z;this.buildColormap = G, this.getColormap = H, this.lookupRGB = E; 256 | }var w = 100, 257 | a = 256, 258 | n = a - 1, 259 | b = 4, 260 | i = 16, 261 | e = 1 << i, 262 | f = 10, 263 | B = 1 << f, 264 | g = 10, 265 | x = e >> g, 266 | r = e << f - g, 267 | z = a >> 3, 268 | h = 6, 269 | t = 1 << h, 270 | u = z * t, 271 | v = 30, 272 | o = 10, 273 | d = 1 << o, 274 | q = 8, 275 | m = 1 << q, 276 | y = o + q, 277 | c = 1 << y, 278 | l = 499, 279 | k = 491, 280 | p = 487, 281 | j = 503, 282 | s = 3 * j;A.exports = C; 283 | }), a('/gif.worker.coffee'); 284 | }).call(this, this); 285 | //# sourceMappingURL=gif.worker.js.map 286 | // gif.worker.js 0.1.6 - https://github.com/jnordberg/gif.js --------------------------------------------------------------------------------