├── .gitignore ├── .jshintrc ├── CHANGELOG ├── LICENSE ├── README.md ├── app.js ├── css ├── app.css └── main.css ├── debug.html ├── directives └── quickHelp │ ├── quickHelp.html │ └── quickHelp.js ├── favicon.ico ├── imgs ├── banner.svg └── icons.svg ├── index.html ├── js ├── timeline.js └── wdq-mode.js ├── package.json ├── planning.md ├── sample.svg ├── services ├── urlParamManager.js ├── userSettings.js ├── wdqSamples.js └── wikidata.js ├── toolinfo.json └── views ├── newTimelineView ├── newTimelineView.html └── newTimelineView.js ├── staticSampleView ├── staticSampleView.html └── staticSampleView.js └── timelineView ├── timelineView.html └── timelineView.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "predef": [ "angular", "d3", "$", "console", "CodeMirror" ] 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 1.2.1: Update dependencies 2 | Switch from bower to node for dependency management 3 | 1.2.0: SPARQL url parameter support 4 | 1.0.0: First release 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Drini Cami 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![banner image](https://rawgit.com/cdrini/wikidata-timeline/master/imgs/banner.svg) 2 | View it live here: https://tools.wmflabs.org/wikidata-timeline/ 3 | 4 | Web app for visualizing Wikidata items in the form of a timeline. 5 | 6 | 7 | Developed on/for Chrome 44+. Firefox/IE also tested to be stable, but might have some issues. 8 | 9 | When using WDQ, these properties are used for determining time: 10 | * startTime: 'P577', 'P580', 'P569', 'P571' 11 | * endTime: 'P577', 'P582', 'P570', 'P576' 12 | 13 | If endTime is missing, applies a default value (see the `defaultEndTime` url parameter). 14 | If startTime = endTime, displayed as a point. 15 | Performs rounding when endtime is 'someValue'. 16 | 17 | # URL Params 18 | Valid on the [timeline view](https://tools.wmflabs.org/wikidata-timeline/#/timeline) and the [new view](https://tools.wmflabs.org/wikidata-timeline/#/new). 19 | 20 | Note: Boolean parameters are true for any value which is not "false". 21 | 22 | Name | Type | Default | Description 23 | -------------------- | ----------- | --------------- | ------------- 24 | title | String | Untitled | Timeline's title. Useful so that your browser's history doesn't display the same thing for different timelines. 25 | sparql | String | | The SPARQL Query to use for the visualization. Expects the 1st variable to be items, 2nd to be labels, 3rd to be start time, and 4th to be end time (optional; see `defaultEndTime` parameter)). Try it/learn more at [Wikidata Query Service](https://query.wikidata.org). 26 | wdq | String | | The Wikidata Query from which to get items. See [WDQ's Documentation](https://wdq.wmflabs.org/api_documentation.html) for help. Note this is translated to SPARQL using [WDQ2SPARQL](http://tools.wmflabs.org/wdq2sparql/w2s.php), so some features might not be available. 27 | defaultEndTime | String | now | One of "now" or "start".
(also useful for resolving ``P571(inception)`` ambiguity). 28 | embed | Boolean | false | If true, optimizes view for embedding in an iframe. 29 | widthOfYear | Number | widthOfYear | How many pixels wide a year should be on the timeline 30 | 31 | ## WDQ URL Params 32 | The url parameters work if the `wdq` is present. 33 | 34 | Name | Type | Default | Description 35 | -------------------- | ----------- | --------------- | ------------- 36 | languages | CSV | en,fr | The languages to use, ordered by preference. If no label in the given lang(s), stays blank 37 | sitelink | String | wikidata | What an item links to when clicked. Language determined by ``languages``. Possible values: ``[ 'wikisource', 'commonswiki', 'wikibooks', 'wikiquote', 'wiki', 'wikinews', 'wikidata' ]`` 38 | sitelinkFallback | Boolean | true | If true, when the desired sitelink is not available, links to wikidata. If false, links to nothing. 39 | 40 | ## Deprecated URL Params 41 | Please do not use these parameters. 42 | - `query`: Now an alias for `wdq` 43 | 44 | # Credits 45 | 46 | ## Services/APIs 47 | * [WDQ2SPARQL](http://tools.wmflabs.org/wdq2sparql/w2s.php) 48 | * [Wikidata API](https://www.wikidata.org/w/api.php) 49 | * [Wikidata Query](https://wdq.wmflabs.org/api_documentation.html) 50 | * [Wikidata Query Service SPARQL Endpoint](https://www.mediawiki.org/wiki/Wikidata_query_service/User_Manual#SPARQL_endpoint) 51 | 52 | ## Libraries 53 | * [AngularJS](https://github.com/angular/angular.js) 54 | * [Bootstrap](https://github.com/twbs/bootstrap) 55 | * [Codemirror](https://github.com/codemirror/CodeMirror) 56 | * [D3](https://github.com/mbostock/d3) 57 | * [jQuery](https://github.com/jquery/jquery) 58 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Declare app level module which depends on views, and components 4 | angular.module('wikidataTimeline', [ 5 | 'ngRoute', 6 | 'wikidataTimeline.newTimelineView', 7 | 'wikidataTimeline.timelineView', 8 | 'wikidataTimeline.staticSampleView' 9 | ]) 10 | .config(['$routeProvider', function($routeProvider) { 11 | $routeProvider.otherwise({ 12 | templateUrl: 'views/newTimelineView/newTimelineView.html', 13 | controller: 'NewTimelineViewCtrl' 14 | }); 15 | }]) 16 | 17 | .config(['$compileProvider', function($compileProvider) { 18 | $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|blob):/); 19 | }]) 20 | 21 | .config(['$sceDelegateProvider', function($sceDelegateProvider) { 22 | $sceDelegateProvider.resourceUrlWhitelist([ 23 | 'self', // Allow same origin resource loads. 24 | 'https://www.wikidata.org/w/api.php' // Allow JSONP calls that match this pattern 25 | ]); 26 | }]) 27 | 28 | .config(['$locationProvider', function($locationProvider) { 29 | $locationProvider.hashPrefix(''); 30 | }]) 31 | 32 | .controller('AppController', ['$scope', '$urlParamManager', '$wdqSamples', 33 | function($scope, $urlParamManager, $wdqSamples) { 34 | var paramManager = $urlParamManager({ 35 | embed: false 36 | }); 37 | 38 | $scope.embedded = paramManager.get('embed'); 39 | $scope.samples = $wdqSamples.getSamples(); 40 | }]); 41 | -------------------------------------------------------------------------------- /css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width:100%; 3 | width: -moz-calc(100% - 8px); /* firefox fix :/ */ 4 | height:100%; 5 | 6 | margin: 0; 7 | } 8 | 9 | body { 10 | padding-top: 50px; /* fixed header */ 11 | } 12 | 13 | body.embedded { 14 | padding-top: 0; 15 | } 16 | 17 | #main-content, div[ng-view] { 18 | height: 100%; 19 | overflow: auto; 20 | position: relative; 21 | } 22 | 23 | .alert { 24 | margin: 10px; 25 | } 26 | 27 | .alert-info.dark { 28 | color: #B5B5B5; 29 | background-color: rgba(20, 20, 20, 0.95); 30 | border-color: #3A3A3A; 31 | } 32 | 33 | .alert-info.plain { 34 | background-color: #F7F7F7; 35 | border: 1px solid #DDD; 36 | color: #555; 37 | } 38 | 39 | .notification-container { 40 | position: fixed; 41 | width: 100%; 42 | bottom: 0; 43 | } 44 | 45 | .notification-container > .alert { 46 | text-align: center; 47 | box-shadow: 0 0 4px #000; 48 | 49 | position: absolute; 50 | max-width: 300px; 51 | width: 100%; 52 | transform: translateX(-50%); 53 | margin-bottom: 20px; 54 | padding: 8px; 55 | bottom: 0; 56 | left: 50%; 57 | } 58 | 59 | .notification-container > .alert > .progress { 60 | background-color: rgba(0, 0, 0, 0.24); 61 | margin-bottom: 8px; 62 | 63 | overflow: visible; 64 | height: 6px; 65 | } 66 | 67 | .notification-container > .alert > .progress > .progress-bar { 68 | background-color: #0067BF; 69 | box-shadow: 0 0 10px 0px #338BB7; 70 | } 71 | 72 | .notification-container > .alert > .progress > .progress-bar.hidden-items { 73 | background-color: #5B5B5B; 74 | box-shadow: 0 0 10px 0px #5B5B5B; 75 | } 76 | 77 | .notification-container button { 78 | background: none; 79 | border: none; 80 | text-transform: uppercase; 81 | font-weight: 700; 82 | color: #1D6CAF; 83 | text-shadow: 0 0 10px #338BB7; 84 | padding: 8px; 85 | font-size: 0.8em; 86 | border-radius: 4px; 87 | 88 | transition-property: background-color; 89 | transition-duration: 0.2s; 90 | } 91 | .notification-container button:hover:not([disabled]) { 92 | background-color: rgba(255,255,255,0.1); 93 | } 94 | .notification-container button[disabled] { 95 | color: #797A7B; 96 | text-shadow: none; 97 | } 98 | 99 | .progress-bar:first-child { 100 | border-radius: 4px 0 0 4px; 101 | } 102 | .progress-bar:last-child { 103 | border-radius: 0 4px 4px 0; 104 | } 105 | .progress-bar:first-child:last-child { 106 | border-radius: 4px; 107 | } 108 | 109 | .flex-row { 110 | display: flex; 111 | flex-direction: row; 112 | align-items: center; 113 | } 114 | 115 | .wdt-banner { 116 | position: relative; 117 | margin: 10px auto; 118 | } 119 | .wdt-banner .bg-holder { 120 | background: url("../imgs/banner.svg"); 121 | background-size: auto 100%; 122 | background-repeat: no-repeat; 123 | background-position: center; 124 | max-width: 800px; 125 | margin: 0 auto; 126 | } 127 | .wdt-banner .bg-holder img { 128 | width: 100%; 129 | min-width: 506px; 130 | visibility: hidden; 131 | max-width: inherit; 132 | } 133 | 134 | form { 135 | padding: 14px; 136 | } 137 | 138 | label { 139 | margin-top: 16px; 140 | margin-bottom: 3px; 141 | } 142 | 143 | a:not([disabled]) { 144 | cursor: pointer; 145 | } 146 | 147 | a[disabled] { 148 | color: #888; 149 | } 150 | 151 | a[disabled]:hover { 152 | text-decoration: none; 153 | } 154 | 155 | button[type="submit"] { 156 | display:block; 157 | margin: 10px auto; 158 | } 159 | 160 | .panel-footer { 161 | overflow: auto; 162 | } 163 | 164 | /* NEW VIEW */ 165 | 166 | .query-editor .CodeMirror { 167 | height: auto; 168 | } 169 | 170 | .query-panel .panel-body { 171 | padding: 5px; 172 | } 173 | 174 | .query-panel .query-editor { 175 | margin: 5px; 176 | margin-bottom: -14px; 177 | } 178 | 179 | .query-panel .sample-queries { 180 | text-align: right; 181 | } 182 | 183 | .query-panel .sample-queries .collapse-toggle { 184 | font-size: 80%; 185 | } 186 | 187 | .query-panel .sample-queries .items { 188 | text-align: left; 189 | 190 | border-top: 1px solid #ddd; 191 | } 192 | 193 | .query-panel .sample-queries .items a { 194 | display: block; 195 | color: #555; 196 | 197 | border-bottom: 1px solid #ddd; 198 | padding: 2px 8px; 199 | font-size: 90%; 200 | } 201 | 202 | .query-panel .sample-queries .items a:hover { 203 | text-decoration: none; 204 | background-color: #EEE; 205 | } 206 | 207 | .query-panel .sample-queries .items a code { 208 | color: #555; 209 | display: block; 210 | background: none; 211 | padding: 0; 212 | } 213 | 214 | form.new-view { 215 | width:100%; 216 | max-width: 600px; 217 | margin: 0 auto; 218 | } 219 | 220 | .wdq-docs { 221 | max-height: 280px; 222 | overflow: auto; 223 | margin-top: 6px; 224 | } 225 | .wdq-docs tt { 226 | margin:2px; 227 | padding:1px; 228 | border:dotted 1px #AAAAAA; 229 | background-color:#CEDEF4; 230 | } 231 | span.property { 232 | background-color:#CAFFD8; 233 | font-family:Courier, monospace; 234 | font-size:11pt; 235 | } 236 | span.item { 237 | background-color:#FFCECE; 238 | font-family:Courier, monospace; 239 | font-size:11pt; 240 | } 241 | .wdq-docs .qualifiers { 242 | color:#1FCB4A; 243 | font-style:italic; 244 | } 245 | 246 | .wdq-docs.contextual-only > ul { 247 | padding: 0; 248 | } 249 | .wdq-docs.contextual-only > ul > li { 250 | list-style: none; 251 | } 252 | 253 | /** timeline view **/ 254 | .options-bar { 255 | height: 32px; 256 | line-height: 32px; 257 | background-color: #FFF; 258 | width: 100%; 259 | position: absolute; 260 | top: 0; 261 | } 262 | body.embedded .options-bar { 263 | opacity: 0; 264 | transition: opacity 0.2s; 265 | } 266 | body.embedded:hover .options-bar { 267 | opacity: 1; 268 | } 269 | 270 | .left-right-bar > .left { 271 | float: left; 272 | margin-left: 8px; 273 | } 274 | .left-right-bar > .right { 275 | float: right; 276 | margin-right: 8px; 277 | } 278 | 279 | .embed-modal pre { 280 | max-height: 140px; 281 | } 282 | 283 | .embed-modal .embed-preview { 284 | overflow:auto; 285 | } 286 | 287 | span[data-toggle="tooltip"] { 288 | border-bottom: 1px dotted #999; 289 | transition: border-style 0.2s; 290 | } 291 | 292 | wdt-help { 293 | 294 | } 295 | .help-icon { 296 | display: inline-block; 297 | 298 | padding: 0 4px; 299 | font-size: 14px; 300 | line-height: 15px; 301 | border-radius: 20px; 302 | } 303 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | .timeline-container { 2 | width: 100%; 3 | height: 100%; 4 | overflow: auto; 5 | box-sizing: border-box; 6 | 7 | background-color: #F0F0F0; 8 | } 9 | 10 | .mini-chart { 11 | position: absolute; 12 | bottom: 0; 13 | left: 0; 14 | 15 | background-color: #FCFCFC; 16 | cursor: move; 17 | } 18 | 19 | .timeline-container .main-chart-container { 20 | width: 100%; 21 | height: 100%; 22 | 23 | overflow: auto; 24 | } 25 | 26 | .timeline-container .main-chart { 27 | font-family: Roboto, sans-serif; 28 | } 29 | .timeline-container text, 30 | .timeline-container tspan { 31 | -webkit-touch-callout: none; 32 | -webkit-user-select: none; 33 | -khtml-user-select: none; 34 | -moz-user-select: none; 35 | -ms-user-select: none; 36 | user-select: none; 37 | } 38 | 39 | .timeline-container .main-chart .item rect, 40 | .timeline-container .main-chart .item circle { 41 | fill: #FD5; 42 | /*stroke: #C92;*/ 43 | } 44 | 45 | /*.timeline-container .main-chart .item rect { 46 | shape-rendering: crispEdges; 47 | }*/ 48 | 49 | .timeline-container .main-chart .item circle { 50 | stroke: #C92; 51 | } 52 | 53 | .timeline-container .mini-chart .items-path { 54 | stroke: #FD5; 55 | shape-rendering: crispEdges; 56 | fill: transparent; 57 | } 58 | 59 | .timeline-container .main-chart .item text { 60 | font-size: 12px; 61 | } 62 | 63 | .timeline-container .main-chart .axis .tick line { 64 | stroke-width: 1px; 65 | stroke: #000; 66 | } 67 | 68 | .timeline-container .main-chart .grid .tick line { 69 | stroke-width: 1px; 70 | stroke: #AAA; 71 | stroke-dasharray: 1,1; 72 | shape-rendering: crispEdges; 73 | } 74 | 75 | .timeline-container .mini-chart .viewfield { 76 | fill: #08D; 77 | fill-opacity: 0.1; 78 | stroke: #08E; 79 | stroke-width: 1px; 80 | 81 | shape-rendering: crispEdges; 82 | } 83 | -------------------------------------------------------------------------------- /debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Wikidata Timeline 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 66 | 67 |
68 |
69 |
70 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /directives/quickHelp/quickHelp.html: -------------------------------------------------------------------------------- 1 | 6 |
7 | -------------------------------------------------------------------------------- /directives/quickHelp/quickHelp.js: -------------------------------------------------------------------------------- 1 | angular.module('wikidataTimeline') 2 | 3 | .directive('wdtHelp', [function() { 4 | return { 5 | restrict: 'E', 6 | transclude: true, 7 | scope: {}, 8 | templateUrl: 'directives/quickHelp/quickHelp.html', 9 | link: function($scope, $element, $attrs) { 10 | $scope.quickHelpActive = false; 11 | } 12 | }; 13 | }]); 14 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdrini/wikidata-timeline/d3ff5051b6d5f4af448ce7ac36797d0686755f8a/favicon.ico -------------------------------------------------------------------------------- /imgs/banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 68 | 75 | 88 | 98 | 108 | 118 | 128 | 138 | 148 | 158 | 168 | 178 | 188 | 189 | 203 | 215 | 227 | 239 | 251 | 263 | 275 | 287 | 299 | 311 | 323 | 326 | Wikidata 339 | Timeline 352 | 353 | 360 | 365 | 378 | 388 | 398 | 408 | 418 | 428 | 438 | 448 | 458 | 459 | 460 | 461 | -------------------------------------------------------------------------------- /imgs/icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 46 | 47 | 49 | 50 | 52 | image/svg+xml 53 | 55 | 56 | 57 | 58 | 59 | 64 | 70 | 77 | 79 | 84 | 89 | 94 | 101 | 108 | 115 | 122 | 123 | 124 | 127 | 134 | 136 | 141 | 146 | 151 | 158 | 165 | 172 | 179 | 186 | 193 | 200 | 205 | 212 | 219 | 226 | 233 | 240 | 247 | 248 | 249 | 250 | 251 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Wikidata Timeline 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 66 | 67 |
68 |
69 |
70 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /js/timeline.js: -------------------------------------------------------------------------------- 1 | /****************************************** 2 | ****** Helpers 3 | ******************************************/ 4 | 5 | /** 6 | * @private 7 | * Helper function which will assign principal[key] to secondary[key] if 8 | * defined, otherwise to defaultValue. 9 | */ 10 | function setParam(principal, secondary, key, defaultValue) { 11 | if(typeof(secondary[key]) == 'undefined') { 12 | principal[key] = defaultValue; 13 | } else { 14 | principal[key] = secondary[key]; 15 | } 16 | } 17 | 18 | /** 19 | * @private 20 | * Helper function which converts ms to years 21 | * @param {Integer} ms 22 | * @return {Integer} years 23 | */ 24 | function msToYears(ms) { 25 | return ms / 3.15569e10; 26 | } 27 | 28 | /** 29 | * @private 30 | * Helper function which takes a string with '%%' as placeholders. Replaces 31 | * ith placeholder with (i+1)th param (first being the string) 32 | * @param {String} str with '%%' placeholders 33 | * @param {...*} [items] items to replace placeholders with 34 | * @return {String} 35 | */ 36 | function sprintf(str) { 37 | var args = arguments; 38 | var i = 1; 39 | return str.replace(/%%/g, function() { return args[i++];}); 40 | } 41 | 42 | /****************************************** 43 | ****** Main 44 | ******************************************/ 45 | 46 | /** 47 | * @class 48 | * @param {Array} items items to plot on timeline. Objects must have name, start, and end defined. 49 | * @param {Object} opts config object 50 | * @config {Integer} [widthOfYear] 51 | * @config {Integer} [itemHeight] 52 | * @config {Integer} [itemSpacing] @todo 53 | * @config {Timestamp} [startDate] @todo 54 | * @config {Timestamp} [endDate] @todo 55 | * @config {Integer} [padding] 56 | * @config {Integer} [axisLabelSize] @todo 57 | */ 58 | function Timeline(items, opts) { 59 | this.items = items; 60 | opts = opts || {}; 61 | 62 | // param name // default value 63 | setParam(this, opts, 'widthOfYear', 20); //px 64 | setParam(this, opts, 'itemHeight', 20); //px 65 | setParam(this, opts, 'itemSpacing', 2); //px 66 | setParam(this, opts, 'startDate', 0); 67 | setParam(this, opts, 'endDate', (new Date()).getTime()); //present 68 | setParam(this, opts, 'padding', 10); //px 69 | setParam(this, opts, 'axisLabelSize', 20); //px 70 | setParam(this, opts, 'miniChartHeight', 80); //px 71 | 72 | return this; 73 | } 74 | 75 | /************************* 76 | ****** Public Methods 77 | *************************/ 78 | 79 | /** 80 | * Tells if the timeline has been drawn yet. 81 | * @return {Boolean} 82 | */ 83 | Timeline.prototype.isDrawn = function() { 84 | return !!this.mainChart; 85 | }; 86 | 87 | /** 88 | * Adds the supplied items to the internal array and the chart. Updates chart's 89 | * axes to ensure the items fit. Use if adding a large number of items, to avoid 90 | * updating the axes a lot. Otherwise just add the array of items as per usual. 91 | * @param {array} itemsArr items to add 92 | * @return {Timeline} @this 93 | */ 94 | Timeline.prototype.addItems = function(itemsArr) { 95 | var _this = this; 96 | 97 | if (!this.isDrawn()) { 98 | this.items = this.items.concat(itemsArr); 99 | return this; 100 | } 101 | 102 | var currentDomain = this.mainChart.xScale.domain(); 103 | var newItemsDomain = [ 104 | d3.min(itemsArr, this.itemStart.bind(this)), 105 | d3.max(itemsArr, this.itemEnd.bind(this)) 106 | ]; 107 | 108 | var mustChangeDomain = (newItemsDomain[0] < currentDomain[0]) 109 | || (newItemsDomain[1] > currentDomain[1]); 110 | 111 | if (mustChangeDomain) { 112 | var newDomain = [ 113 | Math.min(newItemsDomain[0], currentDomain[0]), 114 | Math.max(newItemsDomain[1], currentDomain[1]) 115 | ]; 116 | var xTmp = this.mainChart.xScale(0); 117 | this.mainChart.xScale.domain(newDomain); 118 | this.miniChart.xScale.domain(newDomain); 119 | } 120 | 121 | this.items = this.items.concat(itemsArr); 122 | 123 | // update xScale Range 124 | this.mainChart.grid.width = msToYears(this.mainChart.xScale.domain()[1] - this.mainChart.xScale.domain()[0]) * this.widthOfYear; 125 | this.mainChart.xScale.range([0, this.mainChart.grid.width]); 126 | 127 | // update axes 128 | this.mainChart.xAxisGroup.call(this.mainChart.xAxis); 129 | this.mainChart.grid.axis.group.call(this.mainChart.grid.axis); 130 | this.miniChart.xAxisGroup.call(this.miniChart.xAxis); 131 | 132 | if (mustChangeDomain) { 133 | var xChange = this.mainChart.xScale(0) - xTmp; 134 | // move all the existing items over 135 | this.mainChart.svg.itemsGroup.selectAll('g.item') 136 | .each(function(d, i) { 137 | var transform = d3.transform(this.getAttribute('transform')); 138 | transform.translate = [_this.mainChart.xScale(_this.itemStart(d)), transform.translate[1]]; 139 | this.setAttribute('transform', transform.toString()); 140 | }); 141 | 142 | // update ranges in rows so that things stay correct 143 | for (var i = 0; i < this.rows.length; ++i ) { 144 | for (var j = 0; j < this.rows[i].length; ++j) { 145 | this.rows[i][j].start += xChange; 146 | this.rows[i][j].end += xChange; 147 | } 148 | } 149 | } 150 | 151 | // draw the new items 152 | this._drawItems(); 153 | }; 154 | 155 | /************************* 156 | ****** Timeline Items 157 | *************************/ 158 | 159 | Timeline.ItemTypes = { 160 | Invalid: -1, 161 | Range: 1, 162 | Point: 2 163 | }; 164 | 165 | Timeline.prototype.itemType = function(d) { 166 | if (this.getStartTime(d) !== this.getEndTime(d)) { 167 | return Timeline.ItemTypes.Range; 168 | } 169 | else { 170 | return Timeline.ItemTypes.Point; 171 | } 172 | }; 173 | 174 | Timeline.prototype.itemStart = function(d) { 175 | return this.getStartTime(d); 176 | }; 177 | 178 | Timeline.prototype.itemEnd = function(d) { 179 | return this.getEndTime(d); 180 | }; 181 | 182 | /** 183 | * @private 184 | * returns the start timestamp if its defined 185 | * @param {object} d 186 | * @returns {timestamp} 187 | */ 188 | Timeline.prototype.getStartTime = function(d) { 189 | var start; 190 | if (this.customGetStartTime) { 191 | start = this.customGetStartTime(d); 192 | } else { 193 | start = d.start; 194 | } 195 | 196 | if (!start && start !== 0) { 197 | throw "ERR: Unable to find starttime"; 198 | } 199 | 200 | return start.getTime(); 201 | }; 202 | 203 | /** 204 | * @private 205 | * returns the end timestamp 206 | * @param {object} d 207 | * @returns {timestamp} 208 | */ 209 | Timeline.prototype.getEndTime = function(d) { 210 | var end; 211 | if (this.customGetEndTime) { 212 | end = this.customGetEndTime(d); 213 | } else { 214 | end = d.end; 215 | } 216 | 217 | if (!end && end !== 0) { 218 | throw "ERR: Unable to find endtime"; 219 | } 220 | return end.getTime(); 221 | }; 222 | 223 | /** 224 | * @private 225 | * returns the name 226 | * @param {object} d 227 | * @returns {string} 228 | */ 229 | Timeline.prototype.getName = function(d) { 230 | if (this.customGetName) return this.customGetName(d); 231 | else return d.name; 232 | }; 233 | 234 | /** 235 | * @private 236 | * returns the url 237 | * @param {object} d 238 | * @returns {string} 239 | */ 240 | Timeline.prototype.getUrl = function(d) { 241 | if (this.customGetUrl) return this.customGetUrl(d); 242 | else return d.url || d.href; 243 | }; 244 | /************************* 245 | ****** Drawing Methods 246 | *************************/ 247 | 248 | /** Creates the timeline and appends it to HTMLContainer 249 | * @param{HTMLElement} HTMLContainer 250 | */ 251 | Timeline.prototype.draw = function(HTMLContainer) { 252 | var _this = this; 253 | 254 | this.container = HTMLContainer; 255 | this.container.style.paddingBottom = this.miniChartHeight + 'px'; 256 | 257 | // scale setup 258 | var timeMin = d3.min(this.items, this.itemStart.bind(this) ); 259 | var timeMax = d3.max(this.items, this.itemEnd.bind(this) ); 260 | 261 | // {svg, xScale, xAxis, xAxisGroup, grid, itemsGroup, container} 262 | this.mainChart = {}; 263 | 264 | // { axis, width, height } 265 | this.mainChart.grid = {}; 266 | this.mainChart.grid.width = msToYears(timeMax - timeMin) * this.widthOfYear; 267 | this.mainChart.grid.height = 0; 268 | 269 | // chart container 270 | this.mainChart.container = d3.select(this.container).append('div') 271 | .attr('class', 'main-chart-container'); 272 | this.mainChart.container.rect = this.mainChart.container.node().getBoundingClientRect(); 273 | 274 | // the svg 275 | this.mainChart.svg = this.mainChart.container.append('svg') 276 | .attr("version", 1.1) 277 | .attr("xmlns", "http://www.w3.org/2000/svg") 278 | .classed('main-chart', true); 279 | 280 | this.mainChart.xScale = d3.time.scale() 281 | .domain([timeMin, timeMax]) 282 | .range([0, this.mainChart.grid.width]); 283 | 284 | // x axis 285 | this.mainChart.xAxis = d3.svg.axis() 286 | .scale(this.mainChart.xScale) 287 | .ticks(100) 288 | .orient("bottom") 289 | .tickSize(8,4) 290 | .tickFormat(function (d) { return d.getUTCFullYear(); }) // avoid things like -0800 291 | .tickPadding(4); 292 | this.mainChart.xAxisGroup = this.mainChart.svg.append('g') 293 | .classed('x axis', true) 294 | .call(this.mainChart.xAxis); 295 | 296 | // grid 297 | this.mainChart.grid.axis = d3.svg.axis() 298 | .scale(this.mainChart.xScale) 299 | .ticks(100) 300 | .tickFormat('') 301 | .orient("bottom") 302 | .tickSize(-1*this.mainChart.grid.height, 0); 303 | this.mainChart.grid.axis.group = this.mainChart.svg.append('g') 304 | .attr('class', 'grid') 305 | .call(this.mainChart.grid.axis); 306 | 307 | // the items 308 | this.mainChart.svg.itemsGroup = this.mainChart.svg.append('g').classed('items', true); 309 | 310 | // these rows store the items in each row of the timeline, sorted. Used to 311 | // pack events in the _drawItems method. 312 | this.rows = []; 313 | this.nextRow = 0; 314 | 315 | // miniChart (to control main chart viewfield) 316 | this.miniChart = {}; 317 | this.miniChart.svg = d3.select(this.container).append('svg') 318 | .classed('mini-chart', true) 319 | .attr({ 320 | width: '100%', 321 | height: _this.miniChartHeight 322 | }); 323 | 324 | this.miniChart.xScale = d3.time.scale() 325 | .domain([timeMin, timeMax]) 326 | .range([0, this.container.clientWidth]); 327 | this.miniChart.xAxis = d3.svg.axis() 328 | .scale(this.miniChart.xScale) 329 | .ticks(5) 330 | .orient("bottom") 331 | .tickSize(0,0) 332 | .tickFormat(function (d) { return d.getUTCFullYear(); }) // avoid things like -0800 333 | .tickPadding(0); 334 | this.miniChart.xAxisGroup = this.miniChart.svg.append('g') 335 | .classed('x axis', true) 336 | .call(this.miniChart.xAxis) 337 | .attr({ 338 | 'transform': sprintf('translate(0, %%)', this.miniChartHeight / 2) 339 | }); 340 | this.miniChart.viewfieldRect = this.miniChart.svg.append('rect') 341 | .classed('viewfield', true) 342 | .attr({ 343 | width: (this.mainChart.container.rect.width / this.mainChart.container.node().scrollWidth) * this.mainChart.container.rect.width, 344 | height: (this.mainChart.container.rect.height / this.mainChart.container.node().scrollHeight) * this.miniChartHeight 345 | }); 346 | 347 | // events 348 | this.mainChart.container.on('scroll', function() { 349 | _this.miniChart.viewfieldRect.attr({ 350 | transform: sprintf('translate(%%, %%)', 351 | (this.scrollLeft / this.scrollWidth) * _this.mainChart.container.rect.width, 352 | (this.scrollTop / this.scrollHeight) * _this.miniChartHeight) 353 | }); 354 | }); 355 | 356 | d3.select(window).on('resize', Timeline.prototype._resizeHandler.bind(this)); 357 | 358 | this._setupViewfieldRectDrag(); 359 | 360 | this._drawItems(); 361 | }; 362 | 363 | /**Draws the individual items 364 | * @private 365 | */ 366 | Timeline.prototype._drawItems = function(items) { 367 | var _this = this; 368 | 369 | // Group 370 | var groups = this.mainChart.svg.itemsGroup.selectAll('g') 371 | .data(this.items) 372 | .enter() 373 | .append('g') 374 | .attr({ 375 | class: 'item' 376 | }); 377 | 378 | groups.each(function(d, i) { 379 | var type = _this.itemType(d); 380 | switch(type) { 381 | case Timeline.ItemTypes.Range: _this._drawRangeItem(d3.select(this), d, i); break; 382 | case Timeline.ItemTypes.Point: _this._drawPointItem(d3.select(this), d, i); break; 383 | }; 384 | }); 385 | 386 | // position the items 387 | groups.attr('transform', function(d, i) { 388 | var defaultY = - (_this.nextRow+1) * _this.itemHeight; 389 | var finalY = defaultY; 390 | var bbox = this.getBBox(); 391 | 392 | var itemStart = _this.mainChart.xScale(_this.itemStart(d)); 393 | var xRange = { 394 | start: itemStart + bbox.x, 395 | end: itemStart + bbox.x + bbox.width, 396 | item: d 397 | }; 398 | 399 | // first item; just add it 400 | if (_this.nextRow === 0) { 401 | finalY = defaultY; 402 | _this.rows[_this.nextRow] = [ xRange ]; 403 | _this.nextRow++; 404 | } else { 405 | var rowWithRoom = -1; 406 | var indexInRow = -1; 407 | 408 | // starting from row 0, check if there is room. 409 | for(var i = 0; i < _this.nextRow; ++i) { 410 | // check left 411 | if (xRange.end < _this.rows[i][0].start) { 412 | rowWithRoom = i; 413 | indexInRow = 0; 414 | break; 415 | } 416 | // check right 417 | if (xRange.start > _this.rows[i][_this.rows[i].length - 1].end) { 418 | rowWithRoom = i; 419 | indexInRow = _this.rows[i].length; 420 | break; 421 | } 422 | // check middle 423 | for(var j = 0; j < _this.rows[i].length - 1; j++) { 424 | if (_this.rows[i][j].end < xRange.start && _this.rows[i][j+1].start > xRange.end) { 425 | rowWithRoom = i; 426 | indexInRow = j+1; 427 | break; 428 | } 429 | } 430 | 431 | if (rowWithRoom !== -1) break; 432 | } 433 | 434 | if (rowWithRoom != -1) { 435 | // success! put it here 436 | finalY = - (rowWithRoom+1) * _this.itemHeight; 437 | 438 | // add it to row (in correct position) 439 | _this.rows[rowWithRoom] = _this.rows[rowWithRoom].slice(0, indexInRow) 440 | .concat(xRange) 441 | .concat(_this.rows[rowWithRoom].slice(indexInRow)); 442 | } else { 443 | finalY = defaultY; 444 | _this.rows[_this.nextRow] = [ xRange ]; 445 | _this.nextRow++; 446 | } 447 | } 448 | 449 | return sprintf('translate(%%, %%)', itemStart, finalY); 450 | }); 451 | 452 | this._updateMiniChart(); 453 | 454 | // Add anchors (where appropriate) 455 | groups.each(function(d) { 456 | if (_this.getUrl(d)) { 457 | var group = d3.select(this); 458 | // move all the groups children into the anchor 459 | var anchor = group.append('a') 460 | .attr({ 461 | 'class': 'main-link', 462 | 'xlink:href': function(d) { return _this.getUrl(d); }, 463 | 'xlink:show': 'new' 464 | }); 465 | 466 | var a = anchor.node(); 467 | while(this.firstChild != this.lastChild) { 468 | a.appendChild(this.firstChild); 469 | } 470 | } 471 | }); 472 | 473 | // the height has probably changed because of stacking; should shrink doc 474 | var bbox = this.mainChart.svg.itemsGroup.node().getBBox(); 475 | var axisTicks = Math.floor(bbox.width / 100); 476 | this.mainChart.grid.height = bbox.height; 477 | this.mainChart.grid.axis.innerTickSize(-1*this.mainChart.grid.height); // FIXME: put me in better place T_T 478 | this._updateSVGSize(); 479 | this.mainChart.grid.axis.ticks(axisTicks); 480 | this.mainChart.xAxis.ticks(axisTicks); 481 | this.mainChart.grid.axis.group.call(this.mainChart.grid.axis); 482 | this.mainChart.xAxisGroup.call(this.mainChart.xAxis); 483 | 484 | this._resizeHandler(); 485 | }; 486 | 487 | Timeline.prototype._updateMiniChart = function() { 488 | var _this = this; 489 | // mirror mini chart 490 | if (this.miniChart.items) { 491 | this.miniChart.items.remove(); 492 | } 493 | this.miniChart.items = this.miniChart.svg.insert('path', ':first-child'); 494 | var miniItemsD = ""; 495 | 496 | var minItemHeight = 6; 497 | var condensedRows = Math.floor(this.miniChartHeight / minItemHeight); 498 | if (this.rows.length > condensedRows) { 499 | var rowsToMerge = this.rows.length / condensedRows; 500 | // don't want too many rows in the mini chart, so we'll merge them 501 | var miniItemHeight = miniItemHeight; 502 | for(var r = 0; r < this.rows.length; r += rowsToMerge) { 503 | var mergedRow = []; 504 | for(var r2 = Math.floor(r); r2 < Math.floor(r + rowsToMerge) && r2 < this.rows.length; r2++) { 505 | for(var i = 0; i < this.rows[r2].length ; ++i) { 506 | var d = this.rows[r2][i].item; 507 | var toAdd = { 508 | start: this.getStartTime(d), 509 | end: this.getEndTime(d) 510 | }; 511 | 512 | if (r2 == Math.floor(r)) { 513 | // first row to be merge; just place a copy in the mergedRow 514 | mergedRow.push(toAdd); 515 | } else { 516 | // merge with the stuff in merged rows. 517 | var inserted = false; 518 | for(var j = 0; j < mergedRow.length; ++j) { 519 | if (toAdd.end < mergedRow[j].start) { 520 | // insert before current item 521 | mergedRow = mergedRow.splice(j, 0, toAdd); 522 | inserted = true; 523 | break; 524 | } 525 | else if (toAdd.start <= mergedRow[j].end && toAdd.end >= mergedRow[j].start) { 526 | // should be merged with the current item 527 | mergedRow[j] = { 528 | start: Math.min(mergedRow[j].start, toAdd.start), 529 | end: Math.max(mergedRow[j].end, toAdd.end) 530 | }; 531 | inserted = true; 532 | break; 533 | } 534 | } 535 | if (!inserted) { 536 | mergedRow.push(toAdd); 537 | } 538 | } 539 | } 540 | } 541 | 542 | // the merged row has been created! 543 | for(var i = 0; i < mergedRow.length; ++i) { 544 | var miniYPos = Math.floor(r/rowsToMerge) * minItemHeight + minItemHeight / 2; 545 | miniItemsD += sprintf(' M %%,%% H %%', this.miniChart.xScale(mergedRow[i].start), -miniYPos, 546 | this.miniChart.xScale(mergedRow[i].end)); 547 | } 548 | } 549 | this.miniChart.items.attr({ 550 | 'stroke-width': 4.5, 551 | transform: sprintf('translate(0, %%) scale(1,1)', condensedRows * minItemHeight) 552 | }); 553 | } 554 | else { 555 | var miniItemHeight = this.miniChartHeight / this.rows.length; 556 | for(var r = 0; r < this.rows.length; ++r) { 557 | for(var i = 0; i < this.rows[r].length; ++i) { 558 | var d = this.rows[r][i].item; 559 | 560 | var bounds = { 561 | start: this.getStartTime(d), 562 | end: this.getEndTime(d) 563 | }; 564 | 565 | var miniYPos = r * miniItemHeight + miniItemHeight / 2; 566 | miniItemsD += sprintf(' M %%,%% H %%', this.miniChart.xScale(bounds.start), -miniYPos, 567 | this.miniChart.xScale(bounds.end)); 568 | } 569 | } 570 | this.miniChart.items.attr({ 571 | 'stroke-width': miniItemHeight / 2, 572 | transform: sprintf('translate(0, %%) scale(1,1)', this.rows.length * miniItemHeight) 573 | }); 574 | } 575 | 576 | this.miniChart.items.attr({ 577 | d: miniItemsD, 578 | class: 'items-path', 579 | 'stroke-linecap': 'square' 580 | }); 581 | }; 582 | 583 | /** 584 | * @param {d3.selection} group 585 | * @param {object} d datum 586 | * @param {integer} i index 587 | */ 588 | Timeline.prototype._drawRangeItem = function(group, d, i) { 589 | var _this = this; 590 | 591 | // Rect 592 | group.append('rect') 593 | .attr({ 594 | x: 0, 595 | y: 1, 596 | width: _this.mainChart.xScale(_this.getEndTime(d)) - _this.mainChart.xScale(_this.getStartTime(d)), 597 | height: _this.itemHeight -2, 598 | }); 599 | 600 | // Item text 601 | group.append('text') 602 | .attr({ 603 | x: (_this.mainChart.xScale(_this.getEndTime(d)) - _this.mainChart.xScale(_this.getStartTime(d)))/2, 604 | y: _this.itemHeight / 2 605 | }) 606 | .append('tspan') 607 | .text(this.getName(d)) 608 | .style({ 609 | fill: '#000', 610 | 'text-anchor': 'middle', 611 | 'alignment-baseline': 'central' 612 | }); 613 | 614 | }; 615 | 616 | /** 617 | * @param {d3.selection} group 618 | * @param {object} d datum 619 | * @param {integer} i index 620 | */ 621 | Timeline.prototype._drawPointItem = function(group, d, i) { 622 | var _this = this; 623 | 624 | // Rect 625 | group.append('circle') 626 | .attr({ 627 | cx: 0, 628 | cy: _this.itemHeight / 2, 629 | r: _this.itemHeight / 3 - 3 630 | }); 631 | 632 | // Item text 633 | group.append('text') 634 | .attr({ 635 | x: _this.itemHeight / 3, // mind the circle 636 | y: _this.itemHeight / 2 637 | }) 638 | .append('tspan') 639 | .text(this.getName(d)) 640 | .style({ 641 | fill: '#000', 642 | 'text-anchor': 'left', 643 | 'alignment-baseline': 'central' 644 | }); 645 | 646 | }; 647 | 648 | Timeline.prototype._updateSVGSize = function() { 649 | var componentBBoxes = [ 650 | this.mainChart.svg.itemsGroup.node().getBBox(), 651 | this.mainChart.xAxisGroup.node().getBBox() 652 | ]; 653 | 654 | var bounds = { 655 | left: Infinity, 656 | right: -Infinity, 657 | top: Infinity, 658 | bottom: -Infinity 659 | }; 660 | for(var i = 0; i < componentBBoxes.length; ++i) { 661 | var bbox = componentBBoxes[i]; 662 | bounds.left = Math.min(bounds.left, bbox.x); 663 | bounds.right = Math.max(bounds.right, bbox.x + bbox.width); 664 | bounds.top = Math.min(bounds.top, bbox.y); 665 | bounds.bottom = Math.max(bounds.bottom, bbox.y + bbox.height); 666 | } 667 | 668 | var width = bounds.right - bounds.left + 2*this.padding; 669 | var height = bounds.bottom - bounds.top + 2*this.padding; 670 | 671 | this.mainChart.svg.attr({ 672 | width: width, 673 | height: height, 674 | viewBox: sprintf("%% %% %% %%", 675 | bounds.left - this.padding, 676 | bounds.top - this.padding, 677 | width, 678 | height) 679 | }); 680 | }; 681 | 682 | /** 683 | * Sets up viewfieldRect dragging logic 684 | * @private 685 | */ 686 | Timeline.prototype._setupViewfieldRectDrag = function() { 687 | var _this = this; 688 | 689 | var startViewfieldRectDrag = function() { 690 | viewfieldRectDragMain(); 691 | 692 | _this.miniChart.svg.on('mousemove', viewfieldRectDragMain); 693 | _this.miniChart.svg.on('touchmove', viewfieldRectDragMain); 694 | 695 | _this.miniChart.svg.on('mouseup', endViewfieldRectDrag); 696 | _this.miniChart.svg.on('mouseleave', endViewfieldRectDrag); 697 | _this.miniChart.svg.on('touchend', endViewfieldRectDrag); 698 | }; 699 | var endViewfieldRectDrag = function() { 700 | _this.miniChart.svg.on('mousemove', null); 701 | _this.miniChart.svg.on('touchmove', null); 702 | }; 703 | var viewfieldRectDragMain = function() { 704 | var mousePos = d3.mouse(_this.miniChart.svg.node()); 705 | 706 | var boxPos = { 707 | x: Math.max(mousePos[0] - 0.5 * _this.miniChart.viewfieldRect.attr('width'), 0), 708 | y: Math.max(mousePos[1] - 0.5 * _this.miniChart.viewfieldRect.attr('height'), 0), 709 | }; 710 | 711 | boxPos.x = Math.min(boxPos.x, _this.mainChart.container.rect.width - _this.miniChart.viewfieldRect.attr('width')); 712 | boxPos.y = Math.min(boxPos.y, _this.miniChartHeight - _this.miniChart.viewfieldRect.attr('height')); 713 | 714 | // move box 715 | _this.miniChart.viewfieldRect.attr({ 716 | transform: sprintf('translate(%%, %%)', boxPos.x, boxPos.y) 717 | }); 718 | // move view in mainChart 719 | _this.mainChart.container.node().scrollTop = (boxPos.y / _this.miniChartHeight) * _this.mainChart.container.node().scrollHeight; 720 | _this.mainChart.container.node().scrollLeft = (boxPos.x / _this.mainChart.container.rect.width) * _this.mainChart.container.node().scrollWidth; 721 | }; 722 | 723 | this.miniChart.svg.on('mousedown', startViewfieldRectDrag); 724 | this.miniChart.svg.on('touchstart', startViewfieldRectDrag); 725 | }; 726 | 727 | /** 728 | * Call on resize / on scrollWidth/Height changes to keep variables accurate 729 | */ 730 | Timeline.prototype._resizeHandler = function() { 731 | this.mainChart.container.rect = this.mainChart.container.node().getBoundingClientRect(); 732 | this.miniChart.viewfieldRect 733 | .attr({ 734 | width: (this.mainChart.container.rect.width / this.mainChart.container.node().scrollWidth) * this.mainChart.container.rect.width, 735 | height: (this.mainChart.container.rect.height / this.mainChart.container.node().scrollHeight) * this.miniChartHeight 736 | }); 737 | 738 | // resize miniChart 739 | var oldRange = this.miniChart.xScale.range(); 740 | var widthChangeRatio = this.mainChart.container.rect.width / oldRange[1]; //FIXME: padding :/ 741 | 742 | var transform = d3.transform(this.miniChart.items.attr('transform')); 743 | transform.scale = [transform.scale[0] * widthChangeRatio, 1]; 744 | this.miniChart.items.attr('transform', transform.toString()); 745 | 746 | this.miniChart.xScale 747 | .range([0, this.mainChart.container.rect.width]); 748 | this.miniChart.xAxisGroup.call(this.miniChart.xAxis); 749 | }; 750 | 751 | /************************* 752 | ****** Setup Methods 753 | *************************/ 754 | 755 | /** 756 | * Define how to get the startime from a datum 757 | * @param {Function} given a datum, should return a Date object 758 | * @return {Timeline} this 759 | */ 760 | Timeline.prototype.startTime = function(fn) { 761 | this.customGetStartTime = fn; 762 | return this; 763 | }; 764 | 765 | /** 766 | * Define how to get the endtime from a datum 767 | * @param {Function} given a datum, should return a Date object 768 | * @return {Timeline} this 769 | */ 770 | Timeline.prototype.endTime = function(fn) { 771 | this.customGetEndTime = fn; 772 | return this; 773 | }; 774 | 775 | /** 776 | * Define how to get the name from a datum 777 | * @param {Function} given a datum, should return a string 778 | * @return {Timeline} this 779 | */ 780 | Timeline.prototype.name = function(fn) { 781 | this.customGetName = fn; 782 | return this; 783 | }; 784 | 785 | /** 786 | * Define how to get the url from a datum 787 | * @param {Function} given a datum, should return a URL string 788 | * @return {Timeline} this 789 | */ 790 | Timeline.prototype.url = function(fn) { 791 | this.customGetUrl = fn; 792 | return this; 793 | }; 794 | -------------------------------------------------------------------------------- /js/wdq-mode.js: -------------------------------------------------------------------------------- 1 | CodeMirror.defineSimpleMode("simplemode", { 2 | // The start state contains the rules that are intially used 3 | start: [ 4 | {regex: /"(?:[^\\]|\\.)*?"/, token: "string"}, 5 | // You can match multiple tokens at once. Note that the captured 6 | // groups must span the whole string in this case 7 | // {regex: /(function)(\s+)([a-z$][\w$]*)/, token: ["keyword", null, "variable-2"]}, 8 | // Rules are matched in the order in which they appear, so there is 9 | // no ambiguity between this one and the one above 10 | {regex: /(?:noclaim|claim|tree|web|string|around|between|quantity|items|link)\b/i, 11 | token: "keyword"}, 12 | {regex: /0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i, 13 | token: "number"}, 14 | {regex: /(?:and|or)\b/i, token: "operator"}, 15 | ] 16 | }); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wikidata-timeline", 3 | "version": "1.2.1", 4 | "description": "App for viewing live Wikidata info as a timeline", 5 | "main": "index.html", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/cdrini/wikidata-timeline.git" 12 | }, 13 | "keywords": [ 14 | "wikidata", 15 | "visualization" 16 | ], 17 | "author": "Drini Cami ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/cdrini/wikidata-timeline/issues" 21 | }, 22 | "homepage": "https://tools.wmflabs.org/wikidata-timeline", 23 | "dependencies": { 24 | "angular": "^1.6.8", 25 | "angular-route": "^1.6.8", 26 | "bootstrap": "^3.3.7", 27 | "codemirror": "^5.33.0", 28 | "d3": "^3.5.17", 29 | "jquery": "^3.2.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /planning.md: -------------------------------------------------------------------------------- 1 | # MVP 2 | ## Done: 3 | * Available on WD tools for easy access 4 | * Add to the directory of WD tools 5 | * Complete README 6 | * Add license 7 | * Allow users to embed timeline in their own sites iframes/svg 8 | * Allow users to download an SVG copy of the timeline 9 | * Dynamically load content using WDQ which the user can enter 10 | * Display a timeline using data from wikidata 11 | 12 | # Current Sprint 13 | ## Done: 14 | 15 | # Backlog 16 | * Add option for 'interactive SVG' timeline embed 17 | * Add UI page to change timeline settings in-place 18 | * Make progress notification appear as bar instead of hovering overtop. Blocks a considerable chunk of the screen. 19 | * Visual indicator for 'somevalue' (i.e. unknown end date) 20 | * Visual indicator for date precision 21 | * cleanup timeline.js:285 22 | * Let user choose how pick start/end times (somehow) 23 | * example: show US presidents office time :/ 24 | * maybe `claim[39:11696]{claim[580]}` ? Would have to write a parser though... 25 | * maybe `item.claim('39:11696').qualifier(580)` ? This would be easy! Harder for users to pick up, but not *that* hard. 26 | * Add item tooltips 27 | * Add SPARQL support 28 | 29 | ## Done: 30 | * Let user choose where to link 31 | * Stack the items so they take less vertical space 32 | * Add WDQ API examples 33 | * Add Samples to New page 34 | * Smarter item label positioning (not off screen, if possible) 35 | * Add embed option 36 | * Add title URL param 37 | 38 | # Past Sprints 39 | 40 | ## 20150907 41 | * Add point events 42 | * Add brush control 43 | * Timeline example: http://bl.ocks.org/bunkat/2338034 44 | * General example: http://bl.ocks.org/mbostock/1667367 45 | * Docs: https://github.com/mbostock/d3/wiki/SVG-Controls 46 | * Great example on codepen: http://codepen.io/techniq/pen/QbdpmB?editors=001 47 | * Restructure angular routes (home; no samples) 48 | * Smarter Axis appearance 49 | * Add UI page for query entry 50 | * Trim Wikidata items so we use less memory 51 | * Better date formatting for BCE years 52 | * Added toolinfo.json 53 | * bug: fix shrinking API field in newView causing button to be difficult to press. 54 | ## 20150831 55 | * Add visual indicator (with pause/cancel options) as pages loaded from Wikidata 56 | * Use bootstrap 57 | * Add items in realtime as loaded from Wikidata 58 | -------------------------------------------------------------------------------- /services/urlParamManager.js: -------------------------------------------------------------------------------- 1 | angular.module('wikidataTimeline') 2 | 3 | .factory('$urlParamManager', ['$location', '$q', function($location, $q) { 4 | var ParamTypes = { 5 | 'String': 1, 6 | 'Array': 2, 7 | 'JSON': 3, 8 | 'Float': 4, 9 | 'Boolean':5 10 | }; 11 | 12 | /** 13 | * @class 14 | * @constructor 15 | * @param {object} defn mapping of urlParamNames to default values. Arrays will 16 | * be automatically read as CSV 17 | */ 18 | function URLManager(defn) { 19 | this.definedParams = defn; 20 | this.alias2Param = {}; 21 | this.param2Aliases = {}; 22 | this.types = {}; 23 | 24 | // determine types 25 | for (var name in defn) { 26 | var val = defn[name]; 27 | var type; 28 | 29 | if (val instanceof Array) type = ParamTypes.Array; 30 | else if (typeof val == "boolean") type = ParamTypes.Boolean; 31 | else if (val instanceof Object) type = ParamTypes.Object; 32 | else if (!isNaN(parseFloat(val))) type = ParamTypes.Float; 33 | else type = ParamTypes.String; 34 | 35 | this.types[name] = type; 36 | } 37 | } 38 | 39 | /** 40 | * @param {string} val the unparsed value of the urlParam 41 | * @param {ParamTypes} type the type of the param 42 | * @return {string|array|Boolean|Object} 43 | */ 44 | URLManager._parseParam = function(val, type) { 45 | switch(type) { 46 | case ParamTypes.String: return val; 47 | case ParamTypes.Array: return val.split(','); 48 | case ParamTypes.Boolean: return val !== 'false'; 49 | case ParamTypes.Float: return parseFloat(val); 50 | case ParamTypes.Object: { 51 | try { return JSON.parse(val); } 52 | catch(e) { throw "Invalid JSON as URL Param Object"; } 53 | break; 54 | } 55 | default: throw "Unrecognized parameters type"; 56 | } 57 | }; 58 | 59 | URLManager.prototype._getFromPath = function(param) { 60 | var toCheck = [ param ].concat(this.param2Aliases[param] || []); 61 | 62 | for(var i = 0; i < toCheck.length; ++i) { 63 | var val = $location.search()[toCheck[i]]; 64 | if (angular.isDefined(val)) return val; 65 | } 66 | }; 67 | 68 | URLManager.prototype.get = function(name) { 69 | if (angular.isUndefined(this.definedParams[name])) 70 | throw "Unrecognized param name '" + name + "'."; 71 | 72 | var val = this._getFromPath(name); 73 | if (angular.isDefined(val)) { 74 | return URLManager._parseParam(val, this.types[name]); 75 | } 76 | else return this.definedParams[name]; 77 | }; 78 | 79 | URLManager.prototype.getFirst = function() { 80 | for(var i = 0; i < arguments.length; ++i) { 81 | if (angular.isUndefined(this.definedParams[arguments[i]])) 82 | throw "Unrecognized param name '" + arguments[i] + "'."; 83 | 84 | if (this._getFromPath(arguments[i])) return this.get(arguments[i]); 85 | } 86 | }; 87 | 88 | URLManager.prototype.getUserSpecified = function() { 89 | var params = $location.search(); 90 | var result = {}; 91 | for(var p in params) { 92 | if (angular.isDefined(this.definedParams[p])) { 93 | result[p] = URLManager._parseParam(params[p], this.types[p]); 94 | } 95 | else if (this.alias2Param[p]) { 96 | var param = this.alias2Param[p]; 97 | result[param] = URLManager._parseParam(params[p], this.types[param]); 98 | } 99 | } 100 | 101 | return result; 102 | }; 103 | 104 | URLManager.prototype.isUserSpecified = function(name) { 105 | return angular.isDefined(this.getUserSpecified()[name]); 106 | }; 107 | 108 | URLManager.prototype.addAlias = function(name, alias) { 109 | if (angular.isDefined(this.definedParams[alias]) || this.alias2Param[alias]) 110 | throw "Cannot create alias '" + alias + "' for it is already defined."; 111 | if (angular.isUndefined(name)) 112 | throw "Cannot create an alias to '" + name + "'; Unrecognized parameter name."; 113 | 114 | this.alias2Param[alias] = name; 115 | if (this.param2Aliases[name]) this.param2Aliases[name].push(alias); 116 | else this.param2Aliases[name] = [ alias ]; 117 | }; 118 | 119 | var api = function(defn) { 120 | return new URLManager(defn); 121 | }; 122 | 123 | api.isDefined = function(name) { 124 | return angular.isDefined($location.search()[name]); 125 | }; 126 | 127 | return api; 128 | }]); 129 | -------------------------------------------------------------------------------- /services/userSettings.js: -------------------------------------------------------------------------------- 1 | angular.module('wikidataTimeline') 2 | 3 | // TODO: Add cookies fallback for browsers with localStorage disabled 4 | // or with no localStorage 5 | .factory('$userSettings', [function() { 6 | var settings = {}; 7 | 8 | if(localStorage.getItem('wikidata-timeline')) { 9 | settings = JSON.parse(localStorage.getItem('wikidata-timeline')); 10 | } else { 11 | localStorage.setItem('wikidata-timeline', "{}"); 12 | } 13 | 14 | return { 15 | /** 16 | * store an item in the userSettings 17 | * @param {string} name setting name 18 | * @param {*} value the value to store 19 | */ 20 | setItem: function(name, value) { 21 | settings[name] = value; 22 | localStorage.setItem('wikidata-timeline', JSON.stringify(settings)); 23 | }, 24 | /** 25 | * get an item from memory. Return undefined if no item in memory 26 | * @param {string} name setting name 27 | * @return {*} setting value 28 | */ 29 | getItem: function(name) { 30 | settings = JSON.parse(localStorage.getItem('wikidata-timeline')); 31 | return settings[name]; 32 | } 33 | } 34 | }]); 35 | -------------------------------------------------------------------------------- /services/wdqSamples.js: -------------------------------------------------------------------------------- 1 | angular.module('wikidataTimeline') 2 | 3 | .factory('$wdqSamples', [function() { 4 | var samples = [ 5 | { 6 | title: 'Former Countries', 7 | wdq: 'claim[31:(TREE[3024240][][279])] AND CLAIM[571]', 8 | widthOfYear: '1' 9 | }, 10 | { 11 | title: 'American Sitcoms', 12 | wdq: 'claim[31:(tree[5398426][][279])] AND claim[495:30] AND claim[136:170238]' 13 | }, 14 | { 15 | title: 'US Presidents', 16 | wdq: 'claim[39:11696] and claim[31:5]', 17 | widthOfYear: '10' 18 | }, 19 | { 20 | title: 'Wars', 21 | wdq: 'claim[31:(TREE[198][][279])] AND CLAIM[580]', 22 | widthOfYear: '2' 23 | }, 24 | { 25 | title: 'Empires', 26 | wdq: 'claim[31:48349]', 27 | widthOfYear: '1' 28 | }, 29 | { 30 | title: 'Meryl Streep', 31 | wdq: 'claim[161:873] or items[873]' 32 | }, 33 | { 34 | title: 'Charlie Chaplin', 35 | wdq: 'claim[161:882] or items[882]' 36 | }, 37 | { 38 | title: 'Jules Verne', 39 | wdq: 'items[33977] OR claim[50:33977]', 40 | widthOfYear: 10, 41 | sitelink: 'wikisource' 42 | } 43 | ]; 44 | 45 | function toUrl(sample) { 46 | var result = ""; 47 | for(var name in sample) { 48 | result += '&' + name + '=' + encodeURIComponent(sample[name]); 49 | } 50 | 51 | return result.length ? result.slice(1) : ""; 52 | } 53 | 54 | samples = samples.map(function(sample) { 55 | sample.urlComponents = toUrl(sample); 56 | return sample; 57 | }); 58 | 59 | var api = {}; 60 | api.getSamples = function() { 61 | return samples; 62 | }; 63 | 64 | return api; 65 | }]); 66 | -------------------------------------------------------------------------------- /services/wikidata.js: -------------------------------------------------------------------------------- 1 | angular.module('wikidataTimeline') 2 | 3 | .factory('$wikidata', ['$http', '$q', function($http, $q) { 4 | var WD = {}; 5 | WD.languages = ['en', 'fr']; 6 | WD.sitelinks = [ 'wikisource', 'commonswiki', 'wikibooks', 'wikiquote', 'wiki', 'wikinews' ]; 7 | 8 | /** 9 | * converts a wikidata datetime to a JS Date 10 | * @todo: expand to get time part as well 11 | * @param {string} dateTimeStr ex: +1952-03-11T00:00:00Z 12 | * @return {Date} 13 | */ 14 | WD.parseDateTime = function(dateTimeStr) { 15 | var match = dateTimeStr.match(/^([+-]?\d+)-(\d\d)-(\d\d)/); 16 | if (match && match.length == 4) { 17 | var result = new Date(match[1], match[2] - 1, match[3]); 18 | result.setFullYear(match[1]); // 30 != 1930, javascript! 19 | return result; 20 | } else { 21 | return undefined; 22 | } 23 | }; 24 | /** 25 | * Creates a sitelink's url from the given params 26 | * @param {string} lang the lang 27 | * @param {string} sitelink the sitelink suffix 28 | * @return {string} string the url 29 | */ 30 | WD.makeSitelinkUrl = function(lang, sitelink, title) { 31 | switch(sitelink) { 32 | case 'wiki': 33 | return "https://" + lang + ".wikipedia.org/w/index.php?title=" + title; 34 | case 'commonswiki': 35 | return "https://commons.wikimedia.org/w/index.php?title=" + title; 36 | default: 37 | return "https://" + lang + "." + sitelink + ".org/w/index.php?title=" + title; 38 | } 39 | }; 40 | 41 | /** 42 | * @class 43 | */ 44 | WD.Entity = function(entity) { 45 | this.isTrimmed = false; 46 | this.entity = entity; 47 | }; 48 | /** 49 | * Gets the array of property's values. If not found, returns undefined. 50 | * 51 | * @param {String} prop - a property PID 52 | * @return {Array} of values or undefined 53 | */ 54 | WD.Entity.prototype.getClaim = function(prop) { 55 | return this.entity.claims[prop]; 56 | }; 57 | /** 58 | * Returns wikidata url 59 | * 60 | * @return {string} wikidata url 61 | */ 62 | WD.Entity.prototype.url = function(prop) { 63 | return "https://www.wikidata.org/wiki/" + this.entity.id; 64 | }; 65 | /** 66 | * Get's a claim's value. Uses the first statement. 67 | * 68 | * @param {String} prop - a property PID 69 | * @return {String} The value 70 | */ 71 | WD.Entity.prototype.getClaimValue = function(prop) { 72 | var claim = this.entity.claims[prop]; 73 | if(!claim) return undefined; 74 | 75 | // TODO: if there is a preferred statement, use it 76 | // TODO: else pick first normal ranked statement 77 | 78 | var statement = claim[0]; 79 | var type = statement.mainsnak.snaktype; 80 | switch(type) { 81 | case 'value': 82 | return statement.mainsnak.datavalue.value; 83 | default: 84 | // TODO: Add other cases 85 | return statement.mainsnak.datavalue.value; 86 | } 87 | }; 88 | /** 89 | * Returns the first of the arguments that has 1 or more claims. If none 90 | * found, returns undefined. 91 | * 92 | * @param list of string property IDs 93 | * @return {Array} of values or undefined 94 | */ 95 | WD.Entity.prototype.getFirstClaim = function() { 96 | for (var i = 0; i < arguments.length; i++) { 97 | if(this.getClaim(arguments[i])) { 98 | return this.getClaim(arguments[i]); 99 | } 100 | }; 101 | return undefined; 102 | }; 103 | /** 104 | * Returns an entity's label. If langs provided, finds langs, otherwise uses 105 | * APIs defaults. 106 | * @param {array} [langs] langs to look for label. Defaults to WD params 107 | * @param {Boolean} [returnObject=false] if true, returns an object ({language, value}) 108 | * @return {string|object} string if !returnObject, else {language, value} object 109 | */ 110 | WD.Entity.prototype.getLabel = function(langs, returnObject) { 111 | if (typeof langs == 'undefined') { 112 | langs = WD.languages; 113 | } 114 | if(typeof langs == "string") { 115 | langs = [ langs ]; 116 | } 117 | 118 | if (!this.entity.labels) { 119 | return ""; 120 | } 121 | 122 | // iterate through langs until we find one we have 123 | for(var i = 0; i < langs.length; ++i) { 124 | var obj = this.entity.labels[langs[i]]; 125 | if(obj) { 126 | if (returnObject) { 127 | return obj; 128 | } else { 129 | return obj.value; 130 | } 131 | } 132 | } 133 | 134 | // no luck. Try to return any label. 135 | for(var lang in this.entity.labels) { 136 | if(returnObject) { 137 | return this.entity.labels[lang]; 138 | } else { 139 | return this.entity.labels[lang].value; 140 | } 141 | } 142 | 143 | // still nothing?!? return empty string 144 | return ""; 145 | }; 146 | /** 147 | * Returns an entity's sitelink. If langs provided, finds langs, otherwise uses 148 | * APIs defaults. 149 | * @param {string|array} sitelinks the sitelinks to get. See WD.sitelinks for valid options. 150 | * @param {string|array} [langs] langs to look for label. Defaults to WD params 151 | * @return {string} string the url 152 | */ 153 | WD.Entity.prototype.getSitelink = function(sitelinks, langs) { 154 | if (typeof langs == 'undefined') { 155 | langs = WD.languages; 156 | } 157 | if (typeof langs == "string") { 158 | langs = [ langs ]; 159 | } 160 | if (typeof sitelinks == "string") { 161 | sitelinks = [ sitelinks ] 162 | } 163 | 164 | if (!this.entity.sitelinks) { 165 | return null; 166 | } 167 | 168 | // iterate through langs/sitelink pairs 169 | for(var i = 0; i < sitelinks.length; ++i) { 170 | 171 | for(var j = 0; j < langs.length; ++j) { 172 | var sl = this.entity.sitelinks[sitelinks[i]] || this.entity.sitelinks[langs[j].replace(/\-/g, '_') + sitelinks[i]]; 173 | if (sl) { 174 | return WD.makeSitelinkUrl(langs[j], sitelinks[i], sl.title); 175 | } 176 | } 177 | } 178 | 179 | // no luck. Return null 180 | return null; 181 | }; 182 | /** 183 | * Deletes any unneeded items. Define the properties to keep. Everything else 184 | * deleted. 185 | * @param {object} config defines how to trim 186 | * @config {array} claims 187 | * @config {array} descriptions 188 | * @config {*} id 189 | * @config {array} labels 190 | * @config {*} lastrevid 191 | * @config {*} modified 192 | * @config {*} ns 193 | * @config {*} pageid 194 | * @config {array} sitelinks 195 | * @config {*} title 196 | * @config {*} type 197 | */ 198 | WD.Entity.prototype.trimIncludeOnly = function(config) { 199 | var neverRemove = ['id']; 200 | 201 | for (var key in this.entity) { 202 | if(neverRemove.indexOf(key) !== -1) { 203 | continue; 204 | } 205 | 206 | if (typeof config[key] !== 'undefined') { 207 | if (['claims', 'descriptions', 'labels', 'sitelinks'].indexOf(key) !== -1) { 208 | for (var p in this.entity[key]) { 209 | if (config[key].indexOf(p) == -1) { 210 | delete this.entity[key][p]; 211 | } 212 | } 213 | } 214 | } else { 215 | delete this.entity[key]; 216 | } 217 | } 218 | 219 | return this; 220 | }; 221 | 222 | /** 223 | * @param {string} wikidata query 224 | * @returns {Promise} 225 | */ 226 | WD.WDQ = function(query) { 227 | // first convert to a SPARQL query 228 | return $http({ 229 | url: '//tools.wmflabs.org/wdq2sparql/w2s.php', 230 | params: { 231 | wdq: query 232 | } 233 | }).then(function (response) { 234 | var contentType = response.headers()['content-type']; 235 | if (contentType.indexOf('text/plain') != -1) { 236 | // avoid duplicate items; replace the outermost SELECT with DISTINCT 237 | var sparql = response.data 238 | .replace(/^SELECT (\S+)/i, function($0, $1) { 239 | return $1.toLowerCase() == 'distinct' ? $0 : 'SELECT DISTINCT ' + $1; 240 | }); 241 | return WD.wdqs(sparql); 242 | } else { 243 | return $q.reject(); 244 | } 245 | }).then(function (response) { 246 | return response.data.results.bindings.map(function (o) { 247 | return o.item.value.replace('http://www.wikidata.org/entity/', ''); 248 | }); 249 | }); 250 | }; 251 | 252 | /** 253 | * Query the Wikidata Query Service 254 | * @param {string} sparql the SPARQL query 255 | * @return {Promise} 256 | */ 257 | WD.wdqs = function(sparql) { 258 | return $http({ 259 | url: 'https://query.wikidata.org/sparql', 260 | params: { 261 | query: sparql, 262 | format: 'json' 263 | } 264 | }); 265 | }; 266 | 267 | WD.api = {}; 268 | var api = WD.api; 269 | api.baseURL = 'https://www.wikidata.org/w/api.php'; 270 | 271 | /** 272 | * @name QueryState 273 | * @enum {number} 274 | */ 275 | WD.QueryStates = { 276 | Active: 1, 277 | Pausing: 2, 278 | Paused: 3, 279 | Complete: 4 280 | }; 281 | 282 | /** 283 | * See {@link https://www.wikidata.org/w/api.php?action=help&modules=wbgetentities} 284 | * Executes the query in 50-sized chunks 285 | * @async 286 | * @param {array} ids the IDs to get 287 | * @param {array} props props to get back for each item 288 | * @param {object} opts @todo 289 | * @return {Object} publicApi 290 | * @return {Function} publicApi.onChunkComplete 291 | * @return {Function} publicApi.onFullCompletion 292 | * @return {Function} publicApi.pause 293 | * @return {Function} publicApi.resume 294 | * @return {Function} publicApi.getState the state of the query. 295 | * See {@link QueryState} 296 | */ 297 | api.wbgetentities = function(ids, props, opts) { 298 | ids = ids.map(function(id) { return 'Q' + id; }); 299 | 300 | 301 | // split into 50-sized chunks 302 | var idChunks = []; 303 | for(var i = 0; i < ids.length; ++i) { 304 | if (i % 50 == 0) { 305 | idChunks.push([]); 306 | } 307 | idChunks[idChunks.length - 1].push(ids[i]); 308 | } 309 | 310 | var state = WD.QueryStates.Active; 311 | var api = { 312 | onChunkCompletion: function() {}, 313 | onFullCompletion: function() {} 314 | }; 315 | var publicApi = { 316 | onChunkCompletion: function(fn) { 317 | api.onChunkCompletion = fn; 318 | return publicApi; 319 | }, 320 | onFullCompletion: function(fn) { 321 | api.onFullCompletion = fn; 322 | return publicApi; 323 | }, 324 | pause: function() { 325 | state = WD.QueryStates.Pausing; 326 | return publicApi; 327 | }, 328 | resume: function() { 329 | if (state == WD.QueryStates.Paused) { 330 | state = WD.QueryStates.Active; 331 | queryForNextChunk(); 332 | } 333 | return publicApi; 334 | }, 335 | getState: function() { 336 | return state; 337 | } 338 | }; 339 | 340 | function queryForNextChunk() { 341 | if (state == WD.QueryStates.Active) { 342 | $http({ 343 | url: WD.api.baseURL, 344 | method: 'jsonp', 345 | params: { 346 | action: 'wbgetentities', 347 | ids: idChunks.shift().join('|'), 348 | languages: WD.languages.join('|'), 349 | props: props.join('|'), 350 | format: 'json', 351 | cache: true 352 | } 353 | }) 354 | .then(_onChunkCompletion); 355 | } 356 | }; 357 | 358 | function _onChunkCompletion(response) { 359 | if (state == WD.QueryStates.Pausing) { 360 | state = WD.QueryStates.Paused; 361 | } 362 | api.onChunkCompletion(response); 363 | 364 | if (idChunks.length === 0) { 365 | state = WD.QueryStates.Complete; 366 | api.onFullCompletion(); 367 | } else { 368 | queryForNextChunk(); 369 | } 370 | } 371 | 372 | queryForNextChunk(); 373 | 374 | return publicApi; 375 | }; 376 | 377 | return WD; 378 | }]); 379 | -------------------------------------------------------------------------------- /toolinfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wikidata-timeline", 3 | "title": "Wikidata Timeline", 4 | "description": "Build timelines using Wikidata queries and live Wikidata data", 5 | "url": "http://tools.wmflabs.org/wikidata-timeline", 6 | "keywords": "visualization, timeline, viz, wdq", 7 | "author": "Drini Cami (i.e. Hardwigg)", 8 | "repository": "https://github.com/cdrini/wikidata-timeline.git" 9 | } 10 | -------------------------------------------------------------------------------- /views/newTimelineView/newTimelineView.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 |
6 |
7 |
8 | 9 | Adding a title makes it easier to recognize old timelines in your browser's history. 10 | 11 | 12 | 13 |
14 | 34 | 201 |
202 | 203 | 204 | A CSV list of language codes. If first fails, tries next ones. Click here for a list of all languages. 205 | 206 | 207 | 208 | 209 | What to do if no endtime is present. 210 |
    211 |
  • Now: default to current datetime.
  • 212 |
  • Item start: default to the item's start. This will display the item as a point.
  • 213 |
214 |
215 | 219 | 220 | 221 | 222 | What should open when you click on an item. Ex: wiki, wikisource, wikidata, etc. 223 | Languages determined by the above field. 224 | 225 | 226 | 227 | 229 |
230 | 234 |
235 | 236 | 239 | 240 | 248 |
249 | -------------------------------------------------------------------------------- /views/newTimelineView/newTimelineView.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('wikidataTimeline.newTimelineView', ['ngRoute']) 4 | 5 | .config(['$routeProvider', function($routeProvider) { 6 | $routeProvider.when('/new', { 7 | templateUrl: 'views/newTimelineView/newTimelineView.html', 8 | controller: 'NewTimelineViewCtrl' 9 | }); 10 | }]) 11 | 12 | .controller('NewTimelineViewCtrl', ['$scope', '$timeout', '$location', '$wikidata', '$userSettings', '$urlParamManager', 13 | function($scope, $timeout, $location, $wikidata, $userSettings, $urlParamManager) { 14 | // initialize bootstrap tooltips :/ 15 | $('[data-toggle="tooltip"]').tooltip(); 16 | 17 | $scope.$userSettings = $userSettings; 18 | 19 | // URLParam setup 20 | var defaultValues = { 21 | wdq: '', 22 | 23 | languages: ['en', 'fr'], 24 | sitelink: 'wikidata', 25 | sitelinkFallback: true, 26 | 27 | widthOfYear: 20, 28 | defaultEndTime: 'now', 29 | title: '' 30 | }; 31 | var urlManager = $urlParamManager(defaultValues); 32 | urlManager.addAlias('wdq', 'query'); 33 | 34 | $scope.urlManager = urlManager; 35 | $scope.languages = urlManager.get('languages') ? urlManager.get('languages').join(',') : 'en,fr'; 36 | 37 | $scope.title = urlManager.get('title'); 38 | $scope.defaultEndTime = urlManager.get('defaultEndTime'); 39 | $scope.sitelink = urlManager.get('sitelink'); 40 | $scope.validSitelinks = $wikidata.sitelinks.concat('wikidata'); 41 | $scope.sitelinkFallback = urlManager.get('sitelinkFallback'); 42 | $scope.activeToken = ''; 43 | $scope.showAllWDQDocs = false; 44 | $scope.contextualDocsEnabled = true; 45 | $scope.toggleContextualDocs = function() { 46 | $scope.activeToken = ''; 47 | $scope.contextualDocsEnabled = !$scope.contextualDocsEnabled; 48 | }; 49 | $scope.saveButtonStates = { 50 | Def: 1, 51 | ValidatingWDQ: 2, 52 | InvalidWDQ: 3, 53 | PreparingToDraw: 4 54 | }; 55 | $scope.saveButtonState = $scope.saveButtonStates.Def; 56 | $scope.wdqError = false; 57 | 58 | $scope.drawTimeline = function() { 59 | var wdq = queryEditor.getValue(); 60 | $scope.wdqError = false; 61 | 62 | $scope.saveButtonState = $scope.saveButtonStates.ValidatingWDQ; 63 | $wikidata.WDQ(wdq) 64 | .then( 65 | function success(qids) { 66 | $scope.saveButtonState = $scope.saveButtonStates.PreparingToDraw; 67 | $location.path('timeline').search({ 68 | title: $scope.title, 69 | wdq: wdq, 70 | languages: $scope.languages, 71 | defaultEndTime: $scope.defaultEndTime, 72 | sitelink: $scope.sitelink, 73 | sitelinkFallback: $scope.sitelinkFallback 74 | }); 75 | }, 76 | function error() { 77 | $scope.wdqError = true; 78 | $scope.saveButtonState = $scope.saveButtonStates.InvalidWDQ; 79 | $timeout(function() { 80 | $scope.saveButtonState = $scope.saveButtonStates.Def; 81 | }, 1000); 82 | } 83 | ); 84 | }; 85 | 86 | var queryEditor = CodeMirror($('.query-editor')[0], { 87 | viewportMargin: Infinity, 88 | lineWrapping: true, 89 | matchBrackets: true, 90 | value: urlManager.get('wdq') 91 | }); 92 | 93 | var getTokenUnderCursor = function(cm) { 94 | var token = cm.getTokenAt(cm.getCursor()); 95 | 96 | if (token.type == 'keyword') { 97 | $scope.activeToken = token.string.toLowerCase(); 98 | $scope.$digest(); 99 | } else if (token.type == 'operator') { 100 | $scope.activeToken = 'operator'; 101 | $scope.$digest(); 102 | } 103 | }; 104 | 105 | queryEditor.on('change', getTokenUnderCursor); 106 | queryEditor.on('cursorActivity', getTokenUnderCursor); 107 | 108 | $('form.new-view').on('keyup', function(ev) { 109 | // submit on ctrl enter 110 | if(ev.keyCode == 13 && ev.ctrlKey) $(this).find('[type="submit"]').click(); 111 | }); 112 | }]); 113 | -------------------------------------------------------------------------------- /views/staticSampleView/staticSampleView.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /views/staticSampleView/staticSampleView.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('wikidataTimeline.staticSampleView', ['ngRoute']) 4 | 5 | .config(['$routeProvider', function($routeProvider) { 6 | $routeProvider.when('/samples', { 7 | templateUrl: 'views/staticSampleView/staticSampleView.html', 8 | controller: 'StaticSampleViewCtrl' 9 | }); 10 | }]) 11 | 12 | .controller('StaticSampleViewCtrl', [function() { 13 | var format = d3.time.format("%d/%m/%Y"); 14 | var items = [ 15 | { 16 | name: "The Simpsons", 17 | href: "https://en.wikipedia.org/wiki/" + "The Simpsons", 18 | start: format.parse('17/12/1989'), 19 | end: format.parse('') 20 | }, 21 | { 22 | name: "The Simpsons Movie", 23 | href: "https://en.wikipedia.org/wiki/" + "The Simpsons Movie", 24 | time: format.parse('21/07/2007'), 25 | }, 26 | { 27 | name: "Two and a Half Men", 28 | href: "https://en.wikipedia.org/wiki/" + "Two and a Half Men", 29 | start: format.parse('22/09/2003'), 30 | end: format.parse('') 31 | }, 32 | { 33 | name: "2 Broke Girls", 34 | href: "https://en.wikipedia.org/wiki/" + "2 Broke Girls", 35 | start: format.parse('19/09/2011'), 36 | end: format.parse('') 37 | }, 38 | { 39 | name: "Last Man Standing", 40 | href: "https://en.wikipedia.org/wiki/" + "Last Man Standing", 41 | start: format.parse('11/10/2011'), 42 | end: format.parse('') 43 | }, 44 | { 45 | name: "Save Me", 46 | href: "https://en.wikipedia.org/wiki/" + "Save Me", 47 | start: format.parse('23/05/2013'), 48 | end: format.parse('') 49 | }, 50 | { 51 | name: "The Big Bang Theory", 52 | href: "https://en.wikipedia.org/wiki/" + "The Big Bang Theory", 53 | start: format.parse('24/09/2007'), 54 | end: format.parse('') 55 | }, 56 | { 57 | name: "Arrested Development", 58 | href: "https://en.wikipedia.org/wiki/" + "Arrested Development", 59 | start: format.parse('02/11/2003'), 60 | end: format.parse('') 61 | }, 62 | { 63 | name: "Modern Family", 64 | href: "https://en.wikipedia.org/wiki/" + "Modern Family", 65 | start: format.parse('23/09/2009'), 66 | end: format.parse('') 67 | }, 68 | { 69 | name: "Curb Your Enthusiasm", 70 | href: "https://en.wikipedia.org/wiki/" + "Curb Your Enthusiasm", 71 | start: format.parse('15/10/2000'), 72 | end: format.parse('') 73 | }, 74 | { 75 | name: "The Mindy Project", 76 | href: "https://en.wikipedia.org/wiki/" + "The Mindy Project", 77 | start: format.parse('25/09/2012'), 78 | end: format.parse('') 79 | }, 80 | { 81 | name: "Archer", 82 | href: "https://en.wikipedia.org/wiki/" + "Archer", 83 | start: format.parse('14/01/2010'), 84 | end: format.parse('') 85 | }, 86 | { 87 | name: "Community", 88 | href: "https://en.wikipedia.org/wiki/" + "Community", 89 | start: format.parse('17/09/2009'), 90 | end: format.parse('') 91 | }, 92 | { 93 | name: "Austin & All", 94 | href: "https://en.wikipedia.org/wiki/" + "Austin & All", 95 | start: format.parse('02/12/2011'), 96 | end: format.parse('') 97 | }, 98 | { 99 | name: "Baby Daddy", 100 | href: "https://en.wikipedia.org/wiki/" + "Baby Daddy", 101 | start: format.parse('20/06/2012'), 102 | end: format.parse('') 103 | }, 104 | { 105 | name: "New Girl", 106 | href: "https://en.wikipedia.org/wiki/" + "New Girl", 107 | start: format.parse('20/09/2011'), 108 | end: format.parse('') 109 | }, 110 | { 111 | name: "Arthur", 112 | href: "https://en.wikipedia.org/wiki/" + "Arthur", 113 | start: format.parse('02/09/1996'), 114 | end: format.parse('') 115 | }, 116 | { 117 | name: "Episodes", 118 | href: "https://en.wikipedia.org/wiki/" + "Episodes", 119 | start: format.parse('09/01/2011'), 120 | end: format.parse('') 121 | }, 122 | { 123 | name: "BoJack Horseman", 124 | href: "https://en.wikipedia.org/wiki/" + "BoJack Horseman", 125 | start: format.parse('22/08/2014'), 126 | end: format.parse('') 127 | }, 128 | { 129 | name: "Amos 'n' Andy", 130 | href: "https://en.wikipedia.org/wiki/" + "Amos 'n' Andy", 131 | start: format.parse('19/03/1928'), 132 | end: format.parse('25/11/1960') 133 | }, 134 | { 135 | name: "Futurama", 136 | href: "https://en.wikipedia.org/wiki/" + "Futurama", 137 | start: format.parse('28/03/1999'), 138 | end: format.parse('10/08/2013') 139 | }, 140 | { 141 | name: "Cheers", 142 | href: "https://en.wikipedia.org/wiki/" + "Cheers", 143 | start: format.parse('30/09/1982'), 144 | end: format.parse('20/05/1993') 145 | }, 146 | { 147 | name: "Frasier", 148 | href: "https://en.wikipedia.org/wiki/" + "Frasier", 149 | start: format.parse('16/09/1993'), 150 | end: format.parse('13/05/2004') 151 | }, 152 | { 153 | name: "Married... with Children", 154 | href: "https://en.wikipedia.org/wiki/" + "Married... with Children", 155 | start: format.parse('05/04/1987'), 156 | end: format.parse('09/06/1997') 157 | }, 158 | { 159 | name: "Friends", 160 | href: "https://en.wikipedia.org/wiki/" + "Friends", 161 | start: format.parse('22/09/1994'), 162 | end: format.parse('06/05/2004') 163 | }, 164 | { 165 | name: "Seinfeld", 166 | href: "https://en.wikipedia.org/wiki/" + "Seinfeld", 167 | start: format.parse('05/07/1989'), 168 | end: format.parse('14/05/1998') 169 | }, 170 | { 171 | name: "How I Met Your Mother", 172 | href: "https://en.wikipedia.org/wiki/" + "How I Met Your Mother", 173 | start: format.parse('19/09/2005'), 174 | end: format.parse('31/03/2014') 175 | }, 176 | { 177 | name: "Scrubs", 178 | href: "https://en.wikipedia.org/wiki/" + "Scrubs", 179 | start: format.parse('02/10/2001'), 180 | end: format.parse('17/03/2010') 181 | }, 182 | { 183 | name: "The George Burns and Gracie Allen Show", 184 | href: "https://en.wikipedia.org/wiki/" + "The George Burns and Gracie Allen Show", 185 | start: format.parse('12/10/1950'), 186 | end: format.parse('15/09/1958') 187 | }, 188 | { 189 | name: "Home Improvement", 190 | href: "https://en.wikipedia.org/wiki/" + "Home Improvement", 191 | start: format.parse('17/09/1991'), 192 | end: format.parse('25/05/1999') 193 | }, 194 | { 195 | name: "The Cosby Show", 196 | href: "https://en.wikipedia.org/wiki/" + "The Cosby Show", 197 | start: format.parse('20/09/1984'), 198 | end: format.parse('30/04/1992') 199 | }, 200 | { 201 | name: "According to Jim", 202 | href: "https://en.wikipedia.org/wiki/" + "According to Jim", 203 | start: format.parse('03/10/2001'), 204 | end: format.parse('02/06/2009') 205 | }, 206 | { 207 | name: "Bewitched", 208 | href: "https://en.wikipedia.org/wiki/" + "Bewitched", 209 | start: format.parse('17/09/1964'), 210 | end: format.parse('25/03/1972') 211 | }, 212 | { 213 | name: "The Golden Girls", 214 | href: "https://en.wikipedia.org/wiki/" + "The Golden Girls", 215 | start: format.parse('14/09/1985'), 216 | end: format.parse('09/05/1992') 217 | }, 218 | { 219 | name: "Boy Meets World", 220 | href: "https://en.wikipedia.org/wiki/" + "Boy Meets World", 221 | start: format.parse('24/09/1993'), 222 | end: format.parse('05/05/2000') 223 | }, 224 | { 225 | name: "30 Rock", 226 | href: "https://en.wikipedia.org/wiki/" + "30 Rock", 227 | start: format.parse('11/10/2006'), 228 | end: format.parse('31/01/2013') 229 | }, 230 | { 231 | name: "1st & Ten", 232 | href: "https://en.wikipedia.org/wiki/" + "1st & Ten", 233 | start: format.parse('02/12/1984'), 234 | end: format.parse('23/01/1991') 235 | }, 236 | { 237 | name: "A Different World", 238 | href: "https://en.wikipedia.org/wiki/" + "A Different World", 239 | start: format.parse('24/09/1987'), 240 | end: format.parse('10/07/1993') 241 | }, 242 | { 243 | name: "Cougar Town", 244 | href: "https://en.wikipedia.org/wiki/" + "Cougar Town", 245 | start: format.parse('23/09/2009'), 246 | end: format.parse('31/03/2015') 247 | }, 248 | { 249 | name: "3rd Rock from the Sun", 250 | href: "https://en.wikipedia.org/wiki/" + "3rd Rock from the Sun", 251 | start: format.parse('09/01/1996'), 252 | end: format.parse('22/05/2001') 253 | }, 254 | { 255 | name: "iCarly", 256 | href: "https://en.wikipedia.org/wiki/" + "iCarly", 257 | start: format.parse('08/09/2007'), 258 | end: format.parse('23/11/2012') 259 | }, 260 | { 261 | name: "Becker", 262 | href: "https://en.wikipedia.org/wiki/" + "Becker", 263 | start: format.parse('02/11/1998'), 264 | end: format.parse('28/01/2004') 265 | }, 266 | { 267 | name: "Dharma & Greg", 268 | href: "https://en.wikipedia.org/wiki/" + "Dharma & Greg", 269 | start: format.parse('24/09/1997'), 270 | end: format.parse('30/04/2002') 271 | }, 272 | { 273 | name: "Ellen", 274 | href: "https://en.wikipedia.org/wiki/" + "Ellen", 275 | start: format.parse('29/03/1994'), 276 | end: format.parse('22/07/1998') 277 | }, 278 | { 279 | name: "Til Death", 280 | href: "https://en.wikipedia.org/wiki/" + "Til Death", 281 | start: format.parse('07/09/2006'), 282 | end: format.parse('20/06/2010') 283 | }, 284 | { 285 | name: "Mork & Mindy", 286 | href: "https://en.wikipedia.org/wiki/" + "Mork & Mindy", 287 | start: format.parse('14/09/1978'), 288 | end: format.parse('27/05/1982') 289 | }, 290 | { 291 | name: "Small Wonder", 292 | href: "https://en.wikipedia.org/wiki/" + "Small Wonder", 293 | start: format.parse('07/09/1985'), 294 | end: format.parse('20/05/1989') 295 | }, 296 | { 297 | name: "The Cleveland Show", 298 | href: "https://en.wikipedia.org/wiki/" + "The Cleveland Show", 299 | start: format.parse('27/09/2009'), 300 | end: format.parse('19/05/2013') 301 | }, 302 | { 303 | name: "Big Time Rush", 304 | href: "https://en.wikipedia.org/wiki/" + "Big Time Rush", 305 | start: format.parse('28/11/2009'), 306 | end: format.parse('25/07/2013') 307 | }, 308 | { 309 | name: "ALF", 310 | href: "https://en.wikipedia.org/wiki/" + "ALF", 311 | start: format.parse('22/09/1986'), 312 | end: format.parse('24/03/1990') 313 | }, 314 | { 315 | name: "The Looney Tunes Show", 316 | href: "https://en.wikipedia.org/wiki/" + "The Looney Tunes Show", 317 | start: format.parse('03/05/2011'), 318 | end: format.parse('31/08/2014') 319 | }, 320 | { 321 | name: "Lizzie McGuire", 322 | href: "https://en.wikipedia.org/wiki/" + "Lizzie McGuire", 323 | start: format.parse('12/01/2001'), 324 | end: format.parse('14/02/2004') 325 | }, 326 | { 327 | name: "Victorious", 328 | href: "https://en.wikipedia.org/wiki/" + "Victorious", 329 | start: format.parse('27/03/2010'), 330 | end: format.parse('02/02/2013') 331 | }, 332 | { 333 | name: "A.N.T. Farm", 334 | href: "https://en.wikipedia.org/wiki/" + "A.N.T. Farm", 335 | start: format.parse('06/05/2011'), 336 | end: format.parse('21/03/2014') 337 | }, 338 | { 339 | name: "Clueless", 340 | href: "https://en.wikipedia.org/wiki/" + "Clueless", 341 | start: format.parse('20/09/1996'), 342 | end: format.parse('25/05/1999') 343 | }, 344 | { 345 | name: "Are We There Yet?", 346 | href: "https://en.wikipedia.org/wiki/" + "Are We There Yet?", 347 | start: format.parse('02/06/2010'), 348 | end: format.parse('01/03/2013') 349 | }, 350 | { 351 | name: "My Favorite Martian", 352 | href: "https://en.wikipedia.org/wiki/" + "My Favorite Martian", 353 | start: format.parse('29/09/1963'), 354 | end: format.parse('01/05/1966') 355 | }, 356 | { 357 | name: "100 Deeds for Eddie McDowd", 358 | href: "https://en.wikipedia.org/wiki/" + "100 Deeds for Eddie McDowd", 359 | start: format.parse('16/10/1999'), 360 | end: format.parse('21/04/2002') 361 | }, 362 | { 363 | name: "8 Simple Rules", 364 | href: "https://en.wikipedia.org/wiki/" + "8 Simple Rules", 365 | start: format.parse('17/09/2002'), 366 | end: format.parse('15/04/2005') 367 | }, 368 | { 369 | name: "Anger Management", 370 | href: "https://en.wikipedia.org/wiki/" + "Anger Management", 371 | start: format.parse('28/06/2012'), 372 | end: format.parse('22/12/2014') 373 | }, 374 | { 375 | name: "I'm in the Band", 376 | href: "https://en.wikipedia.org/wiki/" + "I'm in the Band", 377 | start: format.parse('27/11/2009'), 378 | end: format.parse('09/12/2011') 379 | }, 380 | { 381 | name: "Blue Mountain State", 382 | href: "https://en.wikipedia.org/wiki/" + "Blue Mountain State", 383 | start: format.parse('11/01/2010'), 384 | end: format.parse('30/11/2011') 385 | }, 386 | { 387 | name: "Doctor Doctor", 388 | href: "https://en.wikipedia.org/wiki/" + "Doctor Doctor", 389 | start: format.parse('12/06/1989'), 390 | end: format.parse('06/04/1991') 391 | }, 392 | { 393 | name: "The Munsters", 394 | href: "https://en.wikipedia.org/wiki/" + "The Munsters", 395 | start: format.parse('24/09/1964'), 396 | end: format.parse('12/05/1966') 397 | }, 398 | { 399 | name: "Dilbert", 400 | href: "https://en.wikipedia.org/wiki/" + "Dilbert", 401 | start: format.parse('25/01/1999'), 402 | end: format.parse('25/07/2000') 403 | }, 404 | { 405 | name: "The Neighbors", 406 | href: "https://en.wikipedia.org/wiki/" + "The Neighbors", 407 | start: format.parse('26/09/2012'), 408 | end: format.parse('11/04/2014') 409 | }, 410 | { 411 | name: "Better Off Ted", 412 | href: "https://en.wikipedia.org/wiki/" + "Better Off Ted", 413 | start: format.parse('18/03/2009'), 414 | end: format.parse('24/08/2010') 415 | }, 416 | { 417 | name: "Baby Bob", 418 | href: "https://en.wikipedia.org/wiki/" + "Baby Bob", 419 | start: format.parse('18/03/2002'), 420 | end: format.parse('04/07/2003') 421 | }, 422 | { 423 | name: "Bagdad Cafe", 424 | href: "https://en.wikipedia.org/wiki/" + "Bagdad Cafe", 425 | start: format.parse('30/03/1990'), 426 | end: format.parse('27/07/1991') 427 | }, 428 | { 429 | name: "Harper Valley PTA", 430 | href: "https://en.wikipedia.org/wiki/" + "Harper Valley PTA", 431 | start: format.parse('16/01/1981'), 432 | end: format.parse('01/05/1982') 433 | }, 434 | { 435 | name: "Don't Trust the B---- in Apartment 23", 436 | href: "https://en.wikipedia.org/wiki/" + "Don't Trust the B---- in Apartment 23", 437 | start: format.parse('11/04/2012'), 438 | end: format.parse('13/05/2013') 439 | }, 440 | { 441 | name: "Sam & Cat", 442 | href: "https://en.wikipedia.org/wiki/" + "Sam & Cat", 443 | start: format.parse('08/06/2013'), 444 | end: format.parse('17/07/2014') 445 | }, 446 | { 447 | name: "Almost Perfect", 448 | href: "https://en.wikipedia.org/wiki/" + "Almost Perfect", 449 | start: format.parse('17/09/1995'), 450 | end: format.parse('30/10/1996') 451 | }, 452 | { 453 | name: "Off Centre", 454 | href: "https://en.wikipedia.org/wiki/" + "Off Centre", 455 | start: format.parse('14/10/2001'), 456 | end: format.parse('31/10/2002') 457 | }, 458 | { 459 | name: "The Hard Times of RJ Berger", 460 | href: "https://en.wikipedia.org/wiki/" + "The Hard Times of RJ Berger", 461 | start: format.parse('06/06/2010'), 462 | end: format.parse('30/05/2011') 463 | }, 464 | { 465 | name: "Breaking In", 466 | href: "https://en.wikipedia.org/wiki/" + "Breaking In", 467 | start: format.parse('06/04/2011'), 468 | end: format.parse('03/04/2012') 469 | }, 470 | { 471 | name: "Bakersfield P.D.", 472 | href: "https://en.wikipedia.org/wiki/" + "Bakersfield P.D.", 473 | start: format.parse('14/09/1993'), 474 | end: format.parse('18/08/1994') 475 | }, 476 | { 477 | name: "10 Things I Hate About You", 478 | href: "https://en.wikipedia.org/wiki/" + "10 Things I Hate About You", 479 | start: format.parse('07/07/2009'), 480 | end: format.parse('24/05/2010') 481 | }, 482 | { 483 | name: "Andy Richter Controls the Universe", 484 | href: "https://en.wikipedia.org/wiki/" + "Andy Richter Controls the Universe", 485 | start: format.parse('19/03/2002'), 486 | end: format.parse('12/01/2003') 487 | }, 488 | { 489 | name: "Ann Jillian", 490 | href: "https://en.wikipedia.org/wiki/" + "Ann Jillian", 491 | start: format.parse('30/11/1989'), 492 | end: format.parse('01/09/1990') 493 | }, 494 | { 495 | name: "Angel", 496 | href: "https://en.wikipedia.org/wiki/" + "Angel", 497 | start: format.parse('06/10/1960'), 498 | end: format.parse('14/06/1961') 499 | }, 500 | { 501 | name: "Bailey Kipper's P.O.V.", 502 | href: "https://en.wikipedia.org/wiki/" + "Bailey Kipper's P.O.V.", 503 | start: format.parse('14/09/1996'), 504 | end: format.parse('31/05/1997') 505 | }, 506 | { 507 | name: "Aliens in America", 508 | href: "https://en.wikipedia.org/wiki/" + "Aliens in America", 509 | start: format.parse('01/10/2007'), 510 | end: format.parse('18/05/2008') 511 | }, 512 | { 513 | name: "Better with You", 514 | href: "https://en.wikipedia.org/wiki/" + "Better with You", 515 | start: format.parse('22/09/2010'), 516 | end: format.parse('11/05/2011') 517 | }, 518 | { 519 | name: "Trophy Wife", 520 | href: "https://en.wikipedia.org/wiki/" + "Trophy Wife", 521 | start: format.parse('24/09/2013'), 522 | end: format.parse('13/05/2014') 523 | }, 524 | { 525 | name: "Brian O'Brian", 526 | href: "https://en.wikipedia.org/wiki/" + "Brian O'Brian", 527 | start: format.parse('03/10/2008'), 528 | end: format.parse('03/04/2009') 529 | }, 530 | { 531 | name: "All American Girl", 532 | href: "https://en.wikipedia.org/wiki/" + "All American Girl", 533 | start: format.parse('14/09/1994'), 534 | end: format.parse('15/03/1995') 535 | }, 536 | { 537 | name: "Bonnie", 538 | href: "https://en.wikipedia.org/wiki/" + "Bonnie", 539 | start: format.parse('22/09/1995'), 540 | end: format.parse('07/04/1996') 541 | }, 542 | { 543 | name: "Aliens in the Family", 544 | href: "https://en.wikipedia.org/wiki/" + "Aliens in the Family", 545 | start: format.parse('15/03/1996'), 546 | end: format.parse('31/08/1996') 547 | }, 548 | { 549 | name: "$h*! My Dad Says", 550 | href: "https://en.wikipedia.org/wiki/" + "$h*! My Dad Says", 551 | start: format.parse('23/09/2010'), 552 | end: format.parse('17/02/2011') 553 | }, 554 | { 555 | name: "A Family for Joe", 556 | href: "https://en.wikipedia.org/wiki/" + "A Family for Joe", 557 | start: format.parse('24/03/1990'), 558 | end: format.parse('19/08/1990') 559 | }, 560 | { 561 | name: "A League of Their Own", 562 | href: "https://en.wikipedia.org/wiki/" + "A League of Their Own", 563 | start: format.parse('10/04/1993'), 564 | end: format.parse('13/08/1993') 565 | }, 566 | { 567 | name: "Bette", 568 | href: "https://en.wikipedia.org/wiki/" + "Bette", 569 | start: format.parse('11/10/2000'), 570 | end: format.parse('07/03/2001') 571 | }, 572 | { 573 | name: "Selfie", 574 | href: "https://en.wikipedia.org/wiki/" + "Selfie", 575 | start: format.parse('30/09/2014'), 576 | end: format.parse('30/12/2014') 577 | }, 578 | { 579 | name: "1600 Penn", 580 | href: "https://en.wikipedia.org/wiki/" + "1600 Penn", 581 | start: format.parse('17/12/2012'), 582 | end: format.parse('28/03/2013') 583 | }, 584 | { 585 | name: "Ben and Kate", 586 | href: "https://en.wikipedia.org/wiki/" + "Ben and Kate", 587 | start: format.parse('25/09/2012'), 588 | end: format.parse('22/01/2013') 589 | }, 590 | { 591 | name: "Here and Now", 592 | href: "https://en.wikipedia.org/wiki/" + "Here and Now", 593 | start: format.parse('19/09/1992'), 594 | end: format.parse('02/01/1993') 595 | }, 596 | { 597 | name: "Hot l Baltimore", 598 | href: "https://en.wikipedia.org/wiki/" + "Hot l Baltimore", 599 | start: format.parse('24/01/1975'), 600 | end: format.parse('25/04/1975') 601 | }, 602 | { 603 | name: "A to Z", 604 | href: "https://en.wikipedia.org/wiki/" + "A to Z", 605 | start: format.parse('02/10/2014'), 606 | end: format.parse('22/01/2015') 607 | }, 608 | { 609 | name: "Family Tools", 610 | href: "https://en.wikipedia.org/wiki/" + "Family Tools", 611 | start: format.parse('01/05/2013'), 612 | end: format.parse('10/07/2013') 613 | }, 614 | { 615 | name: "Are You There, Chelsea?", 616 | href: "https://en.wikipedia.org/wiki/" + "Are You There, Chelsea?", 617 | start: format.parse('11/01/2012'), 618 | end: format.parse('28/03/2012') 619 | }, 620 | { 621 | name: "Allen Gregory", 622 | href: "https://en.wikipedia.org/wiki/" + "Allen Gregory", 623 | start: format.parse('30/10/2011'), 624 | end: format.parse('18/12/2011') 625 | }, 626 | { 627 | name: "100 Questions", 628 | href: "https://en.wikipedia.org/wiki/" + "100 Questions", 629 | start: format.parse('27/05/2010'), 630 | end: format.parse('01/07/2010') 631 | }, 632 | { 633 | name: "Arsenio", 634 | href: "https://en.wikipedia.org/wiki/" + "Arsenio", 635 | start: format.parse('05/03/1997'), 636 | end: format.parse('23/04/1997') 637 | }, 638 | { 639 | name: "Big Wave Dave's", 640 | href: "https://en.wikipedia.org/wiki/" + "Big Wave Dave's", 641 | start: format.parse('09/08/1993'), 642 | end: format.parse('13/09/1993') 643 | }, 644 | { 645 | name: "Bent", 646 | href: "https://en.wikipedia.org/wiki/" + "Bent", 647 | start: format.parse('21/03/2012'), 648 | end: format.parse('04/04/2012') 649 | }, 650 | { 651 | name: "704 Hauser", 652 | href: "https://en.wikipedia.org/wiki/" + "704 Hauser", 653 | start: format.parse('11/04/1994'), 654 | end: format.parse('09/05/1994') 655 | }, 656 | { 657 | name: "Flesh 'n' Blood", 658 | href: "https://en.wikipedia.org/wiki/" + "Flesh 'n' Blood", 659 | start: format.parse('19/09/1991'), 660 | end: format.parse('15/10/1991') 661 | }, 662 | { 663 | name: "As If", 664 | href: "https://en.wikipedia.org/wiki/" + "As If", 665 | start: format.parse('05/03/2002'), 666 | end: format.parse('12/03/2002') 667 | }, 668 | { 669 | name: "Ask Harriet", 670 | href: "https://en.wikipedia.org/wiki/" + "Ask Harriet", 671 | start: format.parse('04/01/1998'), 672 | end: format.parse('29/01/1998') 673 | }, 674 | { 675 | name: "Battery Park", 676 | href: "https://en.wikipedia.org/wiki/" + "Battery Park", 677 | start: format.parse('23/03/2000'), 678 | end: format.parse('13/04/2000') 679 | }, 680 | { 681 | name: "Big Lake", 682 | href: "https://en.wikipedia.org/wiki/" + "Big Lake", 683 | start: format.parse('17/08/2010'), 684 | end: format.parse('14/09/2010') 685 | }, 686 | { 687 | name: "Bringing up Jack", 688 | href: "https://en.wikipedia.org/wiki/" + "Bringing up Jack", 689 | start: format.parse('27/05/1995'), 690 | end: format.parse('24/06/1995') 691 | } 692 | ]; 693 | 694 | var tl = new Timeline(items, { 695 | widthOfYear: 40 696 | }); 697 | tl.draw(d3.select('.timeline-container')[0][0]); 698 | }]); 699 | -------------------------------------------------------------------------------- /views/timelineView/timelineView.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Wikidata Timeline 8 | 9 |
10 |
11 | Embed 15 | | 16 | Download 24 |
25 |
26 |
27 | 28 |
29 | 30 | 75 | 76 | 80 |
81 | 82 | 83 | 108 | -------------------------------------------------------------------------------- /views/timelineView/timelineView.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('wikidataTimeline.timelineView', []) 4 | 5 | .config(['$routeProvider', function($routeProvider) { 6 | $routeProvider.when('/timeline', { 7 | templateUrl: 'views/timelineView/timelineView.html', 8 | controller: 'TimelineViewCtrl' 9 | }); 10 | }]) 11 | 12 | .controller('TimelineViewCtrl', ['$scope', '$http', '$wikidata', '$urlParamManager', 13 | function($scope, $http, $wikidata, $urlParamManager) { 14 | var defaultOpts = { 15 | wdq: 'claim[31:(tree[5398426][][279])] AND claim[495:30] AND claim[136:170238]', 16 | 17 | sparql: '', 18 | itemVar: '', 19 | labelVar: '', 20 | startVar: '', 21 | endVar: '', 22 | 23 | languages: ['en', 'fr'], 24 | sitelink: 'wikidata', 25 | sitelinkFallback: true, 26 | widthOfYear: 20, 27 | defaultEndTime: 'now', 28 | title: 'Untitled' 29 | }; 30 | var urlManager = $urlParamManager(defaultOpts); 31 | urlManager.addAlias('wdq', 'query'); 32 | $scope.title = urlManager.get('title') + ' Timeline'; 33 | document.title = $scope.title; 34 | 35 | $wikidata.languages = urlManager.get('languages'); 36 | $scope.unembeddedUrl = function() { 37 | return location.href.replace(/([\?\&])embed/, function(match, $1) { 38 | return $1 + 'noembed'; // just in case someone did embed=true 39 | }); 40 | }; 41 | 42 | // scope variables 43 | $scope.queryStates = { 44 | WDQ: 1, 45 | Wikidata: 2, 46 | wdqs: 3 47 | }; 48 | $scope.queryState = null; 49 | 50 | $scope.wdQueryStates = $wikidata.QueryStates; 51 | 52 | $scope.totalItemsToLoad = 0; 53 | $scope.itemsLoaded = 0; 54 | $scope.percentLoaded = function(type) { 55 | switch(type) { 56 | case 'shown': return Math.round(100*$scope.shownEntities.length / $scope.totalItemsToLoad); 57 | case 'hidden': return Math.round(100*$scope.hiddenEntities.length / $scope.totalItemsToLoad); 58 | default: return Math.round(100*$scope.itemsLoaded / $scope.totalItemsToLoad); 59 | } 60 | }; 61 | 62 | var timelineStyle; 63 | $http({ 64 | url: 'css/main.css' 65 | }).then(function(response) { 66 | timelineStyle = $(''); 67 | }); 68 | $scope.downloadURL = ''; 69 | $scope.getSVGCode = function() { 70 | var svgEl = $('svg.main-chart'); 71 | if (svgEl.length === 0) return; 72 | 73 | $('svg.main-chart') 74 | .prepend(timelineStyle); 75 | var svg = $('.main-chart-container').html(); // get the 'outer' html 76 | timelineStyle.remove(); 77 | 78 | return svg; 79 | }; 80 | $scope.createDownloadURL = function() { 81 | var blob = new Blob([$scope.getSVGCode()], {type: 'octet/stream'}); 82 | if ($scope.downloadURL !== '') { 83 | window.URL.revokeObjectURL($scope.downloadURL); 84 | } 85 | $scope.downloadURL = window.URL.createObjectURL(blob); 86 | }; 87 | 88 | if (urlManager.isUserSpecified('sparql')) _buildFromSPARQL(urlManager.get('sparql')); 89 | else if (urlManager.isUserSpecified('wdq')) _buildFromWDQ(urlManager.get('wdq')); 90 | 91 | $scope.shownEntities = []; 92 | $scope.hiddenEntities = []; 93 | 94 | var items, tl; 95 | var tlOpts = { 96 | widthOfYear: urlManager.get('widthOfYear') 97 | }; 98 | 99 | function _buildFromSPARQL(query) { 100 | $scope.queryState = $scope.queryStates.wdqs; 101 | $wikidata.wdqs(query) 102 | .then(function(response) { 103 | var data = response.data; 104 | var accessors = { 105 | item: urlManager.get('itemVar') || data.head.vars[0], 106 | label: urlManager.get('labelVar') || data.head.vars[1], 107 | start: urlManager.get('startVar') || data.head.vars[2], 108 | end: urlManager.get('endVar') || data.head.vars[3] 109 | }; 110 | 111 | items = data.results.bindings; 112 | tl = new Timeline(items, tlOpts) 113 | .startTime(function(b) { return $wikidata.parseDateTime(b[accessors.start].value); }) 114 | .endTime(function(b) { 115 | return b[accessors.end] ? 116 | $wikidata.parseDateTime(b[accessors.end].value) : 117 | (urlManager.get('defaultEndTime') == 'start' ? 118 | $wikidata.parseDateTime(b[accessors.start].value) : 119 | new Date()); 120 | }) 121 | .name(function(b) { return b[accessors.label].value; }) 122 | .url(function(b) { return b[accessors.item].value; }) 123 | .draw(d3.select('.timeline-container')[0][0]); 124 | }); 125 | } 126 | 127 | function _buildFromWDQ(wdq) { 128 | items = []; 129 | tl = new Timeline(items, tlOpts); 130 | 131 | $scope.queryState = $scope.queryStates.WDQ; 132 | $wikidata.WDQ(wdq) 133 | .then(function(qids) { 134 | $scope.queryState = null; 135 | 136 | var ids = qids.map(function(qid) { return parseInt(qid.slice(1), 10); }); 137 | $scope.totalItemsToLoad = ids.length; 138 | 139 | $scope.queryState = $scope.queryStates.Wikidata; 140 | $scope.wikidataQuery = $wikidata.api.wbgetentities(ids, ['labels', 'sitelinks', 'claims']) 141 | .onChunkCompletion(function(response) { 142 | console.log('chunk!'); 143 | 144 | if (response.error) throw response.error.info; 145 | 146 | var itemsChunk = []; 147 | var entities = response.data.entities; 148 | for (var id in entities) { 149 | $scope.itemsLoaded++; 150 | 151 | var ent = new $wikidata.Entity(entities[id]); 152 | 153 | var link; 154 | if(urlManager.get('sitelink') == 'wikidata') { 155 | link = ent.url(); 156 | } else { 157 | link = ent.getSitelink(urlManager.get('sitelink')); 158 | } 159 | if (!link && urlManager.get('sitelinkFallback')) { 160 | link = ent.url(); 161 | } 162 | 163 | var tmpItem = { 164 | start: ent.getFirstClaim('P577', 'P580', 'P569', 'P571'), 165 | end: ent.getFirstClaim('P577', 'P582', 'P570', 'P576') 166 | }; 167 | 168 | var item = { 169 | name: ent.getLabel(), 170 | href: link 171 | }; 172 | 173 | if (tmpItem.start) { 174 | if (tmpItem.start[0].mainsnak.snaktype == 'value') { 175 | item.start = $wikidata.parseDateTime(tmpItem.start[0].mainsnak.datavalue.value.time); 176 | } 177 | } 178 | 179 | if (tmpItem.end) { 180 | var snaktype = tmpItem.end[0].mainsnak.snaktype; 181 | if (snaktype == 'value') { 182 | item.end = $wikidata.parseDateTime(tmpItem.end[0].mainsnak.datavalue.value.time); 183 | } else if (snaktype == 'somevalue' && item.start) { 184 | // average lifespan is like 80, right?! 185 | // TODO: Add visual indicator (gradient? wavy line?) 186 | item.end = new Date(item.start.getTime() + 3.15569e10 * 80); 187 | } 188 | } 189 | 190 | if(item.start && !item.end) { 191 | // set to current date 192 | item.end = urlManager.get('defaultEndTime') !== 'start' ? new Date() : item.start; 193 | } 194 | 195 | ent.trimIncludeOnly({ 196 | claims: ['P580', 'P569', 'P571', 'P582', 'P570', 'P576', 'P577'], 197 | labels: urlManager.get('languages'), 198 | descriptions: urlManager.get('languages'), 199 | sitelinks: urlManager.get('languages').map(function(l) { return l + "wiki"; }) 200 | }); 201 | 202 | if (!item.start || !item.end) { 203 | $scope.hiddenEntities.push(ent); 204 | } else { 205 | $scope.shownEntities.push(ent); 206 | itemsChunk.push(item); 207 | } 208 | } 209 | 210 | if (!tl.isDrawn()) { 211 | Array.prototype.push.apply(items, itemsChunk); 212 | tl.draw(d3.select('.timeline-container')[0][0]); 213 | } else { 214 | tl.addItems(itemsChunk); 215 | } 216 | }) 217 | .onFullCompletion(function() { 218 | $scope.queryState = null; 219 | console.log('done!'); 220 | }); 221 | }); 222 | } 223 | }]) 224 | 225 | .controller('EmbedCtrl', ['$scope', '$element', 226 | function($scope, $element) { 227 | var embedPreview = $('.embed-preview'); 228 | var embedCode = $('.embed-code code'); 229 | 230 | $scope.embedTypes = [ 231 | { 232 | name: 'Live '; } 234 | }, 235 | { 236 | name: 'Static SVG', 237 | code: function() { return '
' + $scope.$parent.getSVGCode() + '
'; } 238 | } 239 | // }, 240 | // { 241 | // name: 'Interactive SVG', 242 | // code: function() { return 'ho!'; } 243 | // } 244 | ]; 245 | 246 | $scope.activeEmbedType = -1; 247 | $scope.setEmbedType = function (newEmbedType) { 248 | if (newEmbedType !== $scope.activeEmbedType) { 249 | $scope.activeEmbedType = newEmbedType; 250 | 251 | var code = $scope.embedTypes[$scope.activeEmbedType].code(); 252 | embedCode.text(code); 253 | embedPreview.html(code); 254 | } 255 | }; 256 | $scope.isActive = function (embedIndex) { 257 | return $scope.activeEmbedType == embedIndex; 258 | }; 259 | 260 | $('.embed-modal').on('shown.bs.modal', function () { 261 | if ($scope.activeEmbedType == -1) { 262 | $scope.setEmbedType(0); 263 | } 264 | }); 265 | }]); 266 | --------------------------------------------------------------------------------