├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── dist ├── js-gantt.js └── js-gantt.min.js ├── examples ├── README.md └── exportSvgWithNodeJs │ ├── .gitignore │ ├── example.js │ └── package.json ├── gruntfile.js ├── js-gantt.iml ├── package.json ├── public ├── css │ ├── format.css │ └── reset.css ├── index.html ├── testDistributionNoJQuery.html └── testDistributionWithJQuery.html ├── resources ├── color-example.png ├── json-array-example.png ├── sample-data-array.json ├── sample-data.csv ├── sample-data.json └── simple-example.png ├── server ├── build.xml ├── pom.xml └── resources │ ├── data.js │ ├── log4j.properties │ ├── sample.csv │ └── serverSettings.xml └── src ├── ExampleApp.js ├── Optimizer.js ├── jquery-private.js └── net └── meisen └── ui └── gantt ├── GanttChart.js └── svg ├── IntervalView.js ├── Scrollbar.js ├── SvgIllustrator.js └── TimeAxis.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /bower_components 3 | /_* 4 | /www-* 5 | /override.properties 6 | /.classpath 7 | /.project 8 | /server/lib 9 | /server/testLib -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.9.1" 4 | before_script: 5 | - npm install -g grunt-cli 6 | - npm install -g bower 7 | - bower install -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sascha Eiteneuer und Philipp Meisen 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-gantt 2 | [![Bower version](https://badge.fury.io/bo/js-gantt.svg)](https://badge.fury.io/bo/js-gantt) 3 | [![npm version](https://badge.fury.io/js/js-gantt.svg)](https://badge.fury.io/js/js-gantt) 4 | [![Build Status](https://travis-ci.org/pmeisen/js-gantt.svg?branch=master)](https://travis-ci.org/pmeisen/js-gantt) 5 | 6 | Library to create time interval data charts, easily, simple and highly configurable. The created charts can be used to visualize large time interval datasets. 7 | The rich configuration allows among other things: 8 | - customizable axes labeling, 9 | - modifiable sizing for all visual components (i.e., intervals, swim-lanes, chart, axes) 10 | - rule-based coloring, 11 | - tooltip configuration, and 12 | - many many more. 13 | 14 | ## How to Install 15 | 16 | The library can be used with `bower`, `requireJs` or as individual `JavaScript Import`. The following paragraphs 17 | explain how to use the library in the different scenarios. 18 | 19 | ### Using js-gantt with `bower` 20 | 21 | ``` 22 | bower install --save js-gantt 23 | ``` 24 | 25 | The library will be added to your `bower-components`. By default the `js-gantt.js` is selected as single main file, which is the 26 | not minified version of the library (the minified/uglified version is `js-gantt.min.hs`). Examples on how to use the library can 27 | be found [here](#usage-examples). 28 | 29 | ### Using js-gantt with `requireJs` 30 | 31 | If you are building larger web-applications and you want to enjoy the advantage of [requireJs](http://requirejs.org/), you 32 | need to include the sources (and not the optimized libraries). To do so, you may download the tarball or a zip-archive from 33 | GitHub and place it into your `scripts` folder. You can also utilize `npm` or `bower` to download the sources automatically 34 | and override the `main` configuration (see [here](#advanced-bower-and-requirejs)). You can then require the needed library as following: 35 | 36 | ```javascript 37 | require(['net/meisen/ui/gantt/GanttChart'], function (SvgLibrary) { 38 | var gantt = new GanttChart(); 39 | }); 40 | ``` 41 | 42 | ### Using js-gantt with `JavaScript Import` 43 | 44 | If you simple want to use the library within your web-site, you can easily do so by downloading it, deploying it on your 45 | server and adding `` tags: 46 | 47 | ```html 48 | 49 | ``` 50 | 51 | The library is bound to the `window` instance and thus is directly available for any other script: 52 | 53 | ```html 54 |
55 | 56 | 57 | 77 | ``` 78 | 79 | If you'd like to have this library available through a CDN, please **Star** the project. 80 | 81 | ## Usage Examples 82 | 83 | Here are some [jsFiddle](https://jsfiddle.net/) examples utilizing the library. All examples are purely based 84 | on this library, no additional dependencies needed. 85 | 86 | ### A First Example: Showing some sample time intervals 87 | 88 |

89 | Simple Example 90 |

91 | 92 | https://jsfiddle.net/pmeisen/pfg7t1uw/ 93 | 94 | This example demonstrates how easy it is to use the library and config some different aspects like: 95 | 96 | - data: 97 | - loading of data 98 | - mapping 99 | - time-axis 100 | - illustrator: 101 | - scrollbars 102 | - axis 103 | - theme (interval size) 104 | 105 | ### Rule-based Coloring: Example on how to utilize rule-based coloring for time intervals 106 | 107 |

108 | Coloring Example 109 |

110 | 111 | https://jsfiddle.net/pmeisen/sL7dckbs/ 112 | 113 | The example shows how to use the `colorizer` to specify rule-based colors for the intervals. You simple have to override the default-configuration 114 | of the `colorizer`, which is done in the example (see [Configuration](#configuration) for further information): 115 | 116 | ```javascript 117 | var config = { 118 | illustrator: { 119 | config: { 120 | view: { 121 | coloring: { 122 | colorizer: function (interval, map, defaultColor) { 123 | var record = interval.get(IntervalView.gRawAttr); 124 | var value = map.val('label', record); 125 | var n = parseInt(value); 126 | 127 | if (n > 0 && n <= 25000) { 128 | return '#7E8F7C'; 129 | } else if (n > 25000 && n <= 50000) { 130 | return '#3B3738'; 131 | } else if (n > 50000 && n <= 90000) { 132 | return '#C63D0F'; 133 | } else { 134 | return defaultColor; 135 | } 136 | } 137 | } 138 | } 139 | } 140 | } 141 | }; 142 | ``` 143 | 144 | ### External DataSource: Example on how to use an external data-source 145 | 146 |

147 | JSON Array Source Example 148 |

149 | 150 | https://jsfiddle.net/pmeisen/r16qfrnx/ 151 | 152 | Most often an external data source is used to visualize data. The library expects the data to be in JSON Array form, i.e.: 153 | 154 | ```json 155 | [ 156 | ["Clara","11.06.2014 00:15:32","11.06.2014 00:39:32",24,"Tristan","Maryland","Tajikistan",0.22,5.28,"United States","Asia","Female","Male" ], 157 | ["Niklas","11.06.2014 00:15:35","11.06.2014 01:48:35",93,"Jesko","Florida","Tuvalu",1.84,171.12,"United States","Oceania","Male","Male" ], 158 | ["Niklas","11.06.2014 00:17:25","11.06.2014 00:37:25",20,"Anni","Florida","Cayman Islands",0.24,4.8,"United States","Oceania","Male","Female" ], 159 | ] 160 | ``` 161 | 162 | There are several good online tools to convert different formats into a JSON data form, e.g., for CSV [convertcsv.com](http://www.convertcsv.com/csv-to-json.htm). 163 | Nevertheless, sometimes the data that you receive, e.g., from an url, is not in JSON form. To solve this problem, the library provides a configuration, which 164 | allows you to post-process `JSON` data, e.g., retrieved from a web-service. In the jsFiddle, the loaded data is `post-processed` via the following `function`: 165 | 166 | ```javascript 167 | var config = { 168 | data: { 169 | postProcessor: function (data) { 170 | var f = 'dd.MM.yyyy HH:mm:ss'; 171 | for (var i = 0; i < data.length; i++) { 172 | var record = data[i]; 173 | record[1] = GanttChart.DateUtil.parseString(record[1], f); 174 | record[2] = GanttChart.DateUtil.parseString(record[2], f); 175 | } 176 | 177 | return { 178 | names:['caller', 'start', 'end', 179 | 'duration', 'recipient', 'origin', 180 | 'destination', 'ratepermin', 'costs', 181 | 'origincontinent', 'destinationcontinent', 182 | 'callergender','recipientgender'], 183 | records: data 184 | }; 185 | } 186 | } 187 | }; 188 | ``` 189 | 190 | The library needs an `object` defining the `names` and `records`, i.e., `{ names: [], records: [] }`. Thus, the `postProcessor` function must return such an object. The `names` are an array naming the different 191 | values of each record; it can be understood as the header of a CSV-file. The records contain the actual data, which have to ensure, that the values representing the start and end value of the interval, must be a `Date` type. 192 | 193 | If the returned data is not a `JSON`, the library offers a `loader` configuration, which defines how to load data. The loader 194 | must be implemented as: 195 | 196 | ``` 197 | var config = { 198 | data: { 199 | loader: function (success, error) { 200 | // success and error are both functions, with: 201 | // - success: function(data); 202 | // - error: function(msg); 203 | } 204 | } 205 | }; 206 | ``` 207 | 208 | Further examples using different `post-processor` and `loader` definitions can be found: 209 | - mapping JSON objects (e.g., [sample-data.json](https://rawgit.com/pmeisen/js-gantt/master/resources/sample-data.json)) to the required data format: [jsFiddle: post-processing](https://jsfiddle.net/pmeisen/pj9thh4z/) 210 | - using a loader (e.g., loading CSV [sample-data.csv](https://rawgit.com/pmeisen/js-gantt/master/resources/sample-data.csv)) to the required data format: [jsFiddle: loader](https://jsfiddle.net/pmeisen/11z6o8pn/) 211 | 212 | ## Configuration 213 | 214 | The Gantt Chart library provides a rich set of configuration parameters. This paragraph tries to address the none trivial settings. 215 | To get an easy start, it is recommend to look at the [Usage Examples](#usage-examples) and adapt the different configuration 216 | parameters as needed and described here. 217 | 218 | A full configuration (with the default settings) is as follows: 219 | 220 | ```javascript 221 | var config = { 222 | data: { 223 | url: null, 224 | loader: null, 225 | postProcessor: function (data) { 226 | if (!$.isArray(data.names) || !$.isArray(data.records)) { 227 | return null; 228 | } else { 229 | return data; 230 | } 231 | }, 232 | mapper: { 233 | startname: 'start', 234 | endname: 'end', 235 | group: [], 236 | label: [], 237 | tooltip: [] 238 | }, 239 | names: [], 240 | records: [], 241 | timeaxis: { 242 | start: null, 243 | end: null, 244 | granularity: 'days' 245 | } 246 | }, 247 | theme: { 248 | loadingBackgroundColor: '#CCCCCC', 249 | loadingBackgroundPosition: 'center center', 250 | loadingBackgroundRepeat: 'no-repeat', 251 | errorBackgroundColor: '#A30B1D' 252 | }, 253 | illustrator: { 254 | factory: function () { 255 | // the default configuration is based on this usage 256 | return new SvgIllustrator(); 257 | }, 258 | config: { 259 | theme: { 260 | fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', 261 | fontSize: '12px' 262 | }, 263 | general: { 264 | margin: 2 265 | }, 266 | view: { 267 | showGrid: true, 268 | showBorder: true, 269 | showBackground: true, 270 | showPositionMarker: true, 271 | showIntervalMarker: true, 272 | showPositionToolTip: true, 273 | showIntervalToolTip: true, 274 | coloring: { 275 | groupMapping: null, 276 | colorizer: function (interval, map, defaultColor) { 277 | // there is a default implemented, please have 278 | // a look at the source-code (IntervalView.js) 279 | // for further insights 280 | } 281 | }, 282 | tooltip: null, 283 | formatter: { 284 | tooltip: function (interval, map, textFormat, theme) { 285 | // there is a default implemented, please have 286 | // a look at the source-code (IntervalView.js) 287 | // for further insights 288 | } 289 | }, 290 | theme: { 291 | backgroundColor: '#FFFFFF', 292 | laneHeight: null, 293 | intervalPosition: 'middle', 294 | intervalHeight: 20, 295 | intervalColor: '#7CB5EC', 296 | intervalBorderColor: '#99C9F7', 297 | intervalBorderSize: 1, 298 | gridColor: '#D8D8D8', 299 | gridSize: 1, 300 | positionMarkerColor: '#D8D8D8', 301 | positionMarkerSize: 1, 302 | intervalMarkerOpacity: '0.3', 303 | intervalMarkerWidth: null, 304 | borderColor: '#D8D8D8', 305 | borderSize: 1, 306 | tooltipMargin: 2, 307 | tooltipArrow: 6, 308 | tooltipRadius: 3, 309 | tooltipSize: 11, 310 | tooltipTextColor: '#000000', 311 | intervalMarginInPx: null 312 | } 313 | }, 314 | axis: { 315 | tickInterval: null, 316 | viewSize: null, 317 | padding: 100, 318 | formatter: function (value, type, level) { 319 | // there is a default formatter, please have 320 | // a look at the source-code (TimeAxis.js) 321 | // for further insights 322 | }, 323 | theme: { 324 | tickColor: '#C0D0E0', 325 | tickWidth: 1, 326 | labelColor: '#606060', 327 | labelSize: 11 328 | } 329 | }, 330 | scrollbars: { 331 | vertical: { 332 | theme: { 333 | arrowSize: 14, 334 | scrollareaColor: '#EEEEEE', 335 | markerColor: '#BFC8D1', 336 | buttonColorBorder: '#666666', 337 | arrowColor: '#666666', 338 | buttonColor: '#EBE7E8' 339 | }, 340 | hideOnNoScroll: false, 341 | propagateScrollOnNoMove: false, 342 | step: null 343 | }, 344 | horizontal: { 345 | theme: { 346 | arrowSize: 14, 347 | scrollareaColor: '#EEEEEE', 348 | markerColor: '#BFC8D1', 349 | buttonColorBorder: '#666666', 350 | arrowColor: '#666666', 351 | buttonColor: '#EBE7E8' 352 | }, 353 | hideOnNoScroll: true, 354 | propagateScrollOnNoMove: false, 355 | step: null 356 | } 357 | } 358 | } 359 | }, 360 | position: 'center', 361 | throwException: false 362 | }; 363 | ``` 364 | 365 | Most of the configuration parameters should be self explaining (if not please contact me, so that I can enhance the documentation). Nevertheless, one of the 366 | most important things to understand is the `data` section within the configuration. Which is explained in the following paragraph. 367 | 368 | ### Configuration: Data Section 369 | 370 | In general, the library tries to retrieve time interval data in the following order (it is not recommended to mix the different 371 | ways and only utilize one of the ways to retrieve data): 372 | 373 | 1. check if a loader is defined (data must be returned as `JSON`) 374 | 2. check if an url is defined (data must be returned as `JSON`) 375 | 3. check if `records` are set 376 | 377 | If step 1. or 2. are used, the returned `JSON` is passed to the `post-processor`, if one is defined (`data.postProcessor` must be a `function`). 378 | The `post-processor` must return an `JSON` fulfilling the following requirements: 379 | 380 | 1. must be a plain-object (`JSON`) 381 | 2. must have a named `records` attribute, which contains the time-interval data as arrays and each date as UTC-based `Date`, i.e., 382 | 383 | ```javascript 384 | [ 385 | GanttChart.DateUtil.createUTC(1929, 10, 31, 0, 0, 0), 386 | GanttChart.DateUtil.createUTC(2016, 6, 27, 0, 0, 0), 387 | 'actor', 388 | 'Bud Spencer', 389 | 'Carlo Pedersoli' 390 | ] 391 | ``` 392 | 3. must have a named `names` attribute, which returns an array of `names` for the different values in the `records` array, i.e., 393 | 394 | ```javascript 395 | [ 396 | 'birthday', 397 | 'dayOfDeath', 398 | 'type', 399 | 'alias', 400 | 'name' 401 | ] 402 | ``` 403 | 404 | So all together, it must return: 405 | 406 | ```javascript 407 | loader: function(success, error) { 408 | success({ 409 | names: [ 410 | 'birthday', 411 | 'dayOfDeath', 412 | 'type', 413 | 'alias', 414 | 'name' 415 | ], 416 | records: [ 417 | [ 418 | GanttChart.DateUtil.createUTC(1929, 10, 31), 419 | GanttChart.DateUtil.createUTC(2016, 6, 27), 420 | 'actor', 421 | 'Bud Spencer', 422 | 'Carlo Pedersoli' 423 | ], 424 | [ 425 | GanttChart.DateUtil.createUTC(1939, 3, 29), 426 | null, 427 | 'actor', 428 | 'Terence Hill', 429 | 'Mario Girotti' 430 | ] 431 | ] 432 | }) 433 | }; 434 | ``` 435 | 436 | The next important setting within the `data` section of the configuration is the `mapper`. The mapper configuration is used to 437 | define, which values of each record have what semantic meaning, e.g., which value indicates the `startname` of the interval and which 438 | one the `endname`. By default, the library assumes that the `startname` is `start` and the `endname` is `end`. Assuming the `names` 439 | from the previous paragraph, this default setting is incorrect and would need to be modified: 440 | 441 | ```javascript 442 | mapper: { 443 | startname: 'birthday', 444 | endname: 'dayOfDeath' 445 | } 446 | ``` 447 | 448 | Additional mappers can be defined for `groups`, `labels` and `tooltips`, i.e., 449 | 450 | ```javascript 451 | mapper: { 452 | startname: 'birthday', 453 | endname: 'dayOfDeath', 454 | group: ['type'], 455 | label: ['alias'], 456 | tooltip: ['name', 'alias'] 457 | } 458 | ``` 459 | 460 | These values are used by the library to, e.g., offer group-based coloring, showing tool-tips on hover events or add a label 461 | to an interval. 462 | 463 | **Notes**: 464 | - the `GanttChart` provides some utility functions, which makes it easier to parse (e.g., `parseString`) or create (e.g., `createUTC`) UTC dates. 465 | - the `end` date can be `null`, which indicates that the interval has not ended yet (glued to the end of the chart). 466 | - an example using the data of this example can be found here: [jsFiddle](https://jsfiddle.net/pmeisen/boxtwojx/) 467 | 468 | ## Advanced: Bower and RequireJs 469 | 470 | If you utilize the library with `requireJs`, you may want to use the `sources` instead of the minified or combined version 471 | distributed in the `dist`-folder. The library is developed using `requireJs` and ensures an easy usage with any 472 | `Asynchronous Module Definition` (see [AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)). 473 | 474 | If you have a look at the project's `gruntfile.js`, you will notice, that the libraries supporting `AMD` (e.g., `js-misc`, 475 | `js-svglibrary`) are also added to this project using their sources. The following lines in the `gruntfile.js` ensures that 476 | usage: 477 | 478 | ```json 479 | { 480 | "bower": { 481 | "dep": { 482 | "options": { 483 | "includeDev": true, 484 | "checkExistence": true, 485 | "paths": "bower-components", 486 | "overrides": { 487 | "js-misc": {"ignore": true}, 488 | "js-svglibrary": {"ignore": true} 489 | } 490 | }, 491 | "dest": "scripts" 492 | } 493 | }, 494 | 495 | "copy": { 496 | "dep": { 497 | "files": [ 498 | {"expand": true, "flatten": false, "cwd": "bower-components/js-gantt/src", "src": "**/*", "dest": "scripts"} 499 | ] 500 | } 501 | } 502 | } 503 | ``` 504 | 505 | 506 | ## Further Links 507 | 508 | - [Examples](examples/README.md) 509 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-gantt", 3 | "version": "1.0.16", 4 | "description": "JavaScript Gantt-Chart Library", 5 | "authors": [ 6 | "Philipp Meisen " 7 | ], 8 | "main": "dist/js-gantt.js", 9 | "homepage": "https://github.com/pmeisen/js-gantt", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/pmeisen/js-gantt", 13 | "bump": "git://github.com/pmeisen/js-gantt.git" 14 | }, 15 | "keywords": [ 16 | "gantt", 17 | "task", 18 | "performance", 19 | "utility", 20 | "chart", 21 | "svg" 22 | ], 23 | "license": "MIT", 24 | "private": false, 25 | "dependencies": {}, 26 | "devDependencies": { 27 | "js-svglibrary": ">=0.5.0", 28 | "js-misc": ">=0.0.13", 29 | "jquery": ">=1.5.0", 30 | "almond": "0.3.3", 31 | "requirejs": "2.3.3", 32 | "qunit": "2.1.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # js-gantt (Examples) 2 | [![Bower version](https://badge.fury.io/bo/js-gantt.svg)](https://badge.fury.io/bo/js-gantt) 3 | [![npm version](https://badge.fury.io/js/js-gantt.svg)](https://badge.fury.io/js/js-gantt) 4 | [![Build Status](https://travis-ci.org/pmeisen/js-gantt.svg?branch=master)](https://travis-ci.org/pmeisen/js-gantt) 5 | 6 | This folder contains examples created to illustrate some of the possibilities. 7 | 8 | ### Creating SVG from nodeJS (exportSvgWithNodeJs) 9 | 10 | Yes it is possible to create Gantt-Charts, i.e., SVG files (and also convert 11 | these to, e.g., PNG) within nodeJS (see [here](exportSvgWithNodeJs/example.js)). 12 | I personally only use the library within a browser, so the example just shows 13 | what may be possible. The examples utilizes *phantomJs* to render the SVG in 14 | the headless browser, retrieves the created SVG and stores it in a file. There 15 | are some things to be considered to make this example more usable: 16 | 17 | 1. It may be nicer to push the options, as well as the width and height 18 | through nodeJs into the JavaScript. Currently the *options* are also 19 | defined in the JavaScript part. The main reason is that the *js-gantt* 20 | library only supports *Date* instances out of the box. The options coming 21 | from nodeJs are stringified (using *JSON*), which does not support *Date* 22 | types. 23 | 2. The rendering of the SVG may take some milliseconds (or seconds if large 24 | external sources are used). The *js-gantt* chart provides events to know 25 | when the rendering is finished. Nevertheless, in the example the script 26 | just waits a couple of seconds. In a nicer version, nodeJs would utilize 27 | the events provided by the library. 28 | 29 | To test the example, simply check out the [examples](./) folder. Change to 30 | [exportSvgWithNodeJs](./exportSvgWithNodeJs) and run `npm install` (just once to get the dependencies) 31 | and afterwards `npm start`. After the script is executed, there should be 32 | a `ouput` folder, which contains a `sample.svg`. -------------------------------------------------------------------------------- /examples/exportSvgWithNodeJs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | output/**/* -------------------------------------------------------------------------------- /examples/exportSvgWithNodeJs/example.js: -------------------------------------------------------------------------------- 1 | var phantom = require('phantom'); 2 | var fs = require('fs'); 3 | 4 | // we keep the instance to close it at the end of the pipe 5 | var phInstance = null; 6 | var svg = null; 7 | 8 | phantom.create() 9 | .then(instance => { 10 | phInstance = instance; 11 | return instance.createPage(); 12 | }) 13 | 14 | // load an empty page 15 | .then(page => { 16 | return page.open('about:blank').then(status => { 17 | if (status === 'success') { 18 | return page; 19 | } else { 20 | throw new Error('Unable to load blank page.'); 21 | } 22 | }) 23 | }) 24 | 25 | // inject some scripts 26 | .then(page => { 27 | return page.includeJs('https://rawgit.com/pmeisen/js-gantt/master/dist/js-gantt.min.js').then(() => { 28 | return page; 29 | }); 30 | }) 31 | 32 | // add the SVG with the specified options 33 | .then(page => { 34 | return page.evaluate(function () { 35 | var chartEl = document.createElement('div'); 36 | chartEl.id = 'chart'; 37 | document.body.appendChild(chartEl); 38 | 39 | var chart = new GanttChart(); 40 | chart.init(chartEl, { 41 | data: { 42 | url: 'https://rawgit.com/pmeisen/js-gantt/master/resources/sample-data-array.json', 43 | postProcessor: function (data) { 44 | var f = 'dd.MM.yyyy HH:mm:ss'; 45 | for (var i = 0; i < data.length; i++) { 46 | var record = data[i]; 47 | record[1] = GanttChart.DateUtil.parseString(record[1], f); 48 | record[2] = GanttChart.DateUtil.parseString(record[2], f); 49 | } 50 | 51 | return { 52 | names: ['caller', 'start', 'end', 53 | 'duration', 'recipient', 'origin', 54 | 'destination', 'ratepermin', 'costs', 55 | 'origincontinent', 'destinationcontinent', 56 | 'callergender', 'recipientgender'], 57 | records: data 58 | }; 59 | }, 60 | mapper: { 61 | startname: 'start', 62 | endname: 'end', 63 | group: ['callergender', 'recipientgender'], 64 | label: ['callergender', 'start', 'caller'], 65 | tooltip: ['caller', 'recipient', 'start', 'end'] 66 | }, 67 | timeaxis: { 68 | start: GanttChart.DateUtil.createUTC(2014, 6, 11), 69 | end: GanttChart.DateUtil.createUTC(2014, 6, 11, 23, 59, 0), 70 | granularity: 'mi' 71 | } 72 | }, 73 | illustrator: { 74 | config: { 75 | axis: { 76 | viewSize: 1440, 77 | tickInterval: 360 78 | }, 79 | view: { 80 | showBorder: false, 81 | tooltip: '{1} called {2}\nbetween {3|date|HH:mm} - {4|date|HH:mm})', 82 | coloring: { 83 | groupMapping: { 84 | '["Female","Female"]': '#7cb5ec', 85 | '["Male","Female"]': '#434348', 86 | '["Female","Male"]': '#f7a35c', 87 | '["Male","Male"]': '#90ed7d' 88 | } 89 | }, 90 | theme: { 91 | intervalColor: '#444444', 92 | intervalHeight: 10, 93 | intervalBorderSize: 0 94 | } 95 | }, 96 | scrollbars: { 97 | vertical: { 98 | hideOnNoScroll: true 99 | }, 100 | horizontal: { 101 | hideOnNoScroll: true 102 | } 103 | } 104 | } 105 | } 106 | }); 107 | chart.resize(600, 300); 108 | }).then(() => { 109 | return page; 110 | }); 111 | }) 112 | 113 | // wait a little depending on the options that should be changed 114 | .then(page => { 115 | return new Promise(f => setTimeout(f, 2000)).then(() => page); 116 | }) 117 | 118 | // read the page after the full content is loaded 119 | .then(page => { 120 | return page.evaluate(function () { 121 | return document.getElementsByClassName('ganttview')[0].innerHTML; 122 | }).then(chart => { 123 | svg = chart; 124 | return page; 125 | }); 126 | }) 127 | 128 | // clean up when we are done 129 | .then(() => { 130 | phInstance.exit(); 131 | }) 132 | 133 | // write the file 134 | .then(() => { 135 | try { 136 | fs.mkdirSync("./output"); 137 | } catch (err) { 138 | if (err.code !== 'EEXIST') throw err 139 | } 140 | 141 | fs.writeFileSync("./output/sample.svg", svg); 142 | }) 143 | 144 | // in any case of an error log it 145 | .catch(error => { 146 | console.log(error); 147 | phInstance.exit(); 148 | }); -------------------------------------------------------------------------------- /examples/exportSvgWithNodeJs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-gantt-export-svg-example", 3 | "version": "1.0.0", 4 | "description": "This is an example showing how to use nodeJs and js-gantt to export a Gantt-Chart to SVG.", 5 | "main": "example.js", 6 | "scripts": { 7 | "start": "node example.js" 8 | }, 9 | "authors": [ 10 | "Philipp Meisen " 11 | ], 12 | "private": false, 13 | "license": "MIT", 14 | "dependencies": { 15 | "phantom": "^3.2.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | /* 4 | * Configuration settings 5 | */ 6 | var bowerPaths = { 7 | bowerrc: '.bowerrc', 8 | bowerDirectory: 'bower_components', 9 | bowerJson: 'bower.json' 10 | }; 11 | 12 | grunt.loadNpmTasks('grunt-contrib-connect'); 13 | grunt.loadNpmTasks('grunt-contrib-watch'); 14 | grunt.loadNpmTasks('grunt-sync-json'); 15 | grunt.loadNpmTasks('grunt-bower-install-simple'); 16 | grunt.loadNpmTasks('main-bower-files'); 17 | grunt.loadNpmTasks('grunt-contrib-copy'); 18 | grunt.loadNpmTasks('grunt-execute'); 19 | grunt.loadNpmTasks('grunt-publish'); 20 | grunt.loadNpmTasks('grunt-bump'); 21 | 22 | grunt.initConfig({ 23 | pkg: grunt.file.readJSON('package.json'), 24 | 25 | 'sync-json': { 26 | options: { 27 | indent: 2, 28 | include: [ 29 | 'name', 'version', 'description', 'authors', 'main', 'repository', 'keywords', 'license', 'private' 30 | ] 31 | }, 32 | dep: { 33 | files: { 34 | 'bower.json': 'package.json' 35 | } 36 | } 37 | }, 38 | 39 | /* 40 | * Task used to install bower dependencies. 41 | */ 42 | 'bower-install-simple': { 43 | dep: { 44 | options: { 45 | production: false 46 | } 47 | } 48 | }, 49 | 50 | /* 51 | * Modify the bower dependencies and move the needed files to the 52 | * target location. 53 | */ 54 | bower: { 55 | dep: { 56 | options: { 57 | includeDev: true, 58 | checkExistence: true, 59 | paths: bowerPaths, 60 | overrides: { 61 | 'r.js': {main: ['dist/r.js']}, 62 | 'js-misc': {ignore: true}, 63 | 'js-svglibrary': {ignore: true} 64 | } 65 | }, 66 | dest: 'www-root/scripts' 67 | } 68 | }, 69 | 70 | /* 71 | * The watch tasks observes changes on the file-system, which 72 | * allow us to see changes directly in the browser. 73 | * 74 | * Set spawn == false, ee documentation: Setting this option to false speeds 75 | * up the reaction time of the watch (usually 500ms faster 76 | * for most) 77 | */ 78 | watch: { 79 | server: { 80 | options: { 81 | livereload: true, 82 | spawn: false 83 | }, 84 | 85 | // define the task to run when a change happens 86 | tasks: ['01-resolve-dependencies', 'copy:setup'], 87 | 88 | // files to observe, can be an array 89 | files: ['gruntfile.js', 'package.json', 'src/**/*', 'public/**/*', 'test/**/*'] 90 | }, 91 | dist: { 92 | options: { 93 | livereload: true, 94 | spawn: false 95 | }, 96 | 97 | // define the task to run when a change happens 98 | tasks: ['02-compile-sources', 'copy:dist'], 99 | 100 | // files to observe, can be an array 101 | files: ['public/testDistribution*.html', 'src/Optimizer.js'] 102 | } 103 | }, 104 | 105 | /* 106 | * The connect task starts a web server for us to see our results and 107 | * do some testing. 108 | */ 109 | connect: { 110 | server: { 111 | options: { 112 | port: '<%= server.port %>', 113 | base: 'www-root' 114 | } 115 | }, 116 | dist: { 117 | options: { 118 | port: '<%= server.port %>', 119 | base: 'www-dist' 120 | } 121 | } 122 | }, 123 | 124 | /* 125 | * Copies the files into the right location for the web server. 126 | */ 127 | copy: { 128 | dep: { 129 | files: [ 130 | {expand: true, flatten: false, cwd: bowerPaths.bowerDirectory + '/js-misc/src', src: '**/*', dest: 'www-root/scripts'}, 131 | {expand: true, flatten: false, cwd: bowerPaths.bowerDirectory + '/js-svglibrary/src', src: '**/*', dest: 'www-root/scripts'} 132 | ] 133 | }, 134 | setup: { 135 | files: [ 136 | {expand: true, flatten: false, cwd: 'src', src: '**/*', dest: 'www-root/scripts'}, 137 | {expand: true, flatten: false, cwd: 'test', src: '**/*', dest: 'www-root/scripts'}, 138 | {expand: true, flatten: false, cwd: 'public', src: '**/*', dest: 'www-root'} 139 | ] 140 | }, 141 | dist: { 142 | files: [ 143 | {expand: true, flatten: false, cwd: bowerPaths.bowerDirectory + '/jquery/dist', src: 'jquery.min.js', dest: 'www-dist/scripts'}, 144 | {expand: true, flatten: false, cwd: 'dist', src: '**/*', dest: 'www-dist/scripts'}, 145 | {expand: true, flatten: false, cwd: 'public/css', src: '**/*', dest: 'www-dist/css'}, 146 | {expand: true, flatten: false, cwd: 'public', src: 'testDistribution*.html', dest: 'www-dist'} 147 | ] 148 | } 149 | }, 150 | 151 | execute: { 152 | compile: { 153 | call: function (grunt, option, async) { 154 | var requirejs = require('requirejs'); 155 | var extend = require('util')._extend; 156 | var fs = require('fs'); 157 | 158 | var done = async(); 159 | var currentDir = process.cwd(); 160 | var prefixFilename = currentDir + '/dist/' + grunt.config('pkg.name'); 161 | 162 | var baseConfig = { 163 | baseUrl: 'scripts', 164 | name: 'almond', 165 | include: 'Optimizer', 166 | wrap: true 167 | }; 168 | 169 | var optimize = function (config, callback) { 170 | 171 | requirejs.optimize(config, function () { 172 | callback(true); 173 | }, function (err) { 174 | grunt.log.error(err); 175 | callback(false); 176 | }); 177 | }; 178 | 179 | var cleanUp = function () { 180 | process.chdir(currentDir); 181 | done(); 182 | }; 183 | 184 | // run the actual optimization 185 | process.chdir('www-root'); 186 | optimize(extend({ 187 | optimize: 'none', 188 | out: prefixFilename + '.js' 189 | }, baseConfig), function (success) { 190 | 191 | if (success) { 192 | optimize(extend({ 193 | out: prefixFilename + '.min.js' 194 | }, baseConfig), cleanUp); 195 | } else { 196 | cleanUp(); 197 | } 198 | }); 199 | } 200 | } 201 | }, 202 | 203 | publish: { 204 | options: { 205 | ignore: ['node_modules', 'bower_components'] 206 | }, 207 | deploy: { 208 | src: './' 209 | } 210 | }, 211 | 212 | bump: { 213 | options: { 214 | files: ['package.json', 'bower.json'], 215 | commitFiles: ['.'], 216 | pushTo: 'origin' 217 | } 218 | } 219 | }); 220 | 221 | grunt.registerTask('01-resolve-dependencies', 'Resolve all the dependencies', function () { 222 | grunt.task.run('sync-json:dep', 'bower-install-simple:dep', 'bower:dep', 'copy:dep'); 223 | }); 224 | 225 | grunt.registerTask('02-compile-sources', 'Update the current root-directory', function () { 226 | grunt.task.run('01-resolve-dependencies', 'copy:setup', 'execute:compile'); 227 | }); 228 | 229 | grunt.registerTask('04-deploy', 'Update the current root-directory', function () { 230 | grunt.config.set('log.msg', 'Make sure your bower project is registered using: ' + '\n' + 231 | '$ bower register ' + grunt.config('pkg.name') + ' ' + grunt.config('pkg.repository.bump') + '\n' + 232 | 'Make sure your npm users are added: ' + '\n' + 233 | '$ npm adduser'); 234 | 235 | grunt.task.run('02-compile-sources', 'log', 'bump', 'publish:deploy'); 236 | }); 237 | 238 | grunt.registerTask('98-run-server', 'Start the web-server for fast debugging.', function (port) { 239 | port = typeof port === 'undefined' || port === null || isNaN(parseFloat(port)) || !isFinite(port) ? 20000 : parseInt(port); 240 | grunt.config.set('server.port', port); 241 | grunt.config.set('log.msg', 'You may want to start the sample server providing data via JSON, ant 98-run-server (see server)' + '\n' + 242 | 'For an example: http://localhost:' + port + '/index.html'); 243 | 244 | grunt.task.run('01-resolve-dependencies', 'copy:setup', 'connect:server', 'log', 'watch:server'); 245 | }); 246 | 247 | grunt.registerTask('99-run-dist-server', 'Runs a server with the dist-version', function (port) { 248 | port = typeof port === 'undefined' || port === null || isNaN(parseFloat(port)) || !isFinite(port) ? 20000 : parseInt(port); 249 | grunt.config.set('server.port', port); 250 | grunt.config.set('log.msg', 'Test the distribution: http://localhost:' + port + '/testDistributionNoJQuery.html' + '\n' + 251 | ' http://localhost:' + port + '/testDistributionWithJQuery.html'); 252 | 253 | grunt.task.run('02-compile-sources', 'copy:dist', 'connect:dist', 'log', 'watch:dist'); 254 | }); 255 | 256 | grunt.registerTask('log', 'Writes a log messages', function () { 257 | grunt.log.writeln(grunt.config.get('log.msg')); 258 | }); 259 | }; -------------------------------------------------------------------------------- /js-gantt.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-gantt", 3 | "version": "1.0.16", 4 | "description": "JavaScript Gantt-Chart Library", 5 | "authors": [ 6 | "Philipp Meisen " 7 | ], 8 | "scripts": { 9 | "test": "grunt 02-compile-sources" 10 | }, 11 | "main": "dist/js-gantt.js", 12 | "homepage": "https://github.com/pmeisen/js-gantt", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/pmeisen/js-gantt", 16 | "bump": "git://github.com/pmeisen/js-gantt.git" 17 | }, 18 | "keywords": [ 19 | "gantt", 20 | "task", 21 | "performance", 22 | "utility", 23 | "chart", 24 | "svg" 25 | ], 26 | "license": "MIT", 27 | "private": false, 28 | "dependencies": {}, 29 | "devDependencies": { 30 | "grunt": "1.0.1", 31 | "grunt-bower-install-simple": "1.2.3", 32 | "grunt-bump": "0.8.0", 33 | "grunt-contrib-connect": "1.0.2", 34 | "grunt-contrib-copy": "1.0.0", 35 | "grunt-contrib-watch": "1.0.0", 36 | "grunt-execute": "0.2.2", 37 | "grunt-publish": "1.0.0", 38 | "grunt-sync-json": "0.4.0", 39 | "main-bower-files": "2.13.1", 40 | "requirejs": "2.3.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/css/format.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmeisen/js-gantt/6aff3eb02aae43ded30b11ee8e000e383dbd419d/public/css/format.css -------------------------------------------------------------------------------- /public/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample GanttChart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | -------------------------------------------------------------------------------- /public/testDistributionNoJQuery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample GanttChart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 58 | 59 | -------------------------------------------------------------------------------- /public/testDistributionWithJQuery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample GanttChart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 64 | 65 | -------------------------------------------------------------------------------- /resources/color-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmeisen/js-gantt/6aff3eb02aae43ded30b11ee8e000e383dbd419d/resources/color-example.png -------------------------------------------------------------------------------- /resources/json-array-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmeisen/js-gantt/6aff3eb02aae43ded30b11ee8e000e383dbd419d/resources/json-array-example.png -------------------------------------------------------------------------------- /resources/sample-data-array.json: -------------------------------------------------------------------------------- 1 | [ 2 | ["Clara","11.06.2014 00:15:32","11.06.2014 00:39:32",24,"Tristan","Maryland","Tajikistan",0.22,5.28,"United States","Asia","Female","Male" ], 3 | ["Niklas","11.06.2014 00:15:35","11.06.2014 01:48:35",93,"Jesko","Florida","Tuvalu",1.84,171.12,"United States","Oceania","Male","Male" ], 4 | ["Niklas","11.06.2014 00:17:25","11.06.2014 00:37:25",20,"Anni","Florida","Cayman Islands",0.24,4.8,"United States","Oceania","Male","Female" ], 5 | ["Niklas","11.06.2014 00:17:38","11.06.2014 01:04:38",47,"Julian","Florida","Indonesia",0.29,13.63,"United States","Asia","Male","Male" ], 6 | ["Niklas","11.06.2014 00:17:39","11.06.2014 01:19:39",62,"Julian","Florida","Indonesia",0.29,17.98,"United States","Asia","Male","Male" ], 7 | ["Niklas","11.06.2014 00:18:10","11.06.2014 00:24:10",6,"Anni","Florida","Cayman Islands",0.24,1.44,"United States","Oceania","Male","Female" ], 8 | ["Clara","11.06.2014 00:19:54","11.06.2014 00:55:54",36,"Jule","Maryland","Faeroe Islands",0.52,18.72,"United States","Europe","Female","Female" ], 9 | ["Niklas","11.06.2014 00:20:05","11.06.2014 01:40:05",80,"Tamino","Florida","Vietnam",0.65,52,"United States","Asia","Male","Male" ], 10 | ["Clara","11.06.2014 00:20:35","11.06.2014 01:50:35",90,"Jule","Maryland","Faeroe Islands",0.52,46.8,"United States","Europe","Female","Female" ], 11 | ["Emily","11.06.2014 00:26:31","11.06.2014 00:49:31",23,"Leonardo","Rhode Island","Comoro Islands",2.25,51.75,"United States","Africa","Male","Male" ], 12 | ["Ida","11.06.2014 00:43:01","11.06.2014 02:02:01",79,"Colin","Maine","Gabon Republic",1.05,82.95,"United States","Africa","Female","Male" ], 13 | ["Ida","11.06.2014 00:44:56","11.06.2014 00:56:56",12,"Colin","Maine","Gabon Republic",1.05,12.6,"United States","Africa","Female","Male" ], 14 | ["Emily","11.06.2014 00:46:37","11.06.2014 02:12:37",86,"Anne","Rhode Island","Eritrea",0.88,75.68,"United States","Africa","Male","Female" ], 15 | ["Ida","11.06.2014 00:50:07","11.06.2014 01:17:07",27,"Alea","Maine","Ivory Coast",0.68,18.36,"United States","Africa","Female","Female" ], 16 | ["Niklas","11.06.2014 00:52:57","11.06.2014 01:31:57",39,"Tamino","Florida","Vietnam",0.65,25.35,"United States","Asia","Male","Male" ], 17 | ["Ida","11.06.2014 01:01:12","11.06.2014 01:59:12",58,"Colin","Maine","Gabon Republic",1.05,60.9,"United States","Africa","Female","Male" ], 18 | ["Henry","11.06.2014 01:02:15","11.06.2014 01:07:15",5,"Felix","Connecticut","New Caledonia",1.41,7.05,"United States","Oceania","Male","Male" ], 19 | ["Henry","11.06.2014 01:02:26","11.06.2014 01:45:26",43,"Felix","Connecticut","New Caledonia",1.41,60.63,"United States","Oceania","Male","Male" ], 20 | ["Henry","11.06.2014 01:03:15","11.06.2014 02:59:15",116,"Felix","Connecticut","New Caledonia",1.41,163.56,"United States","Oceania","Male","Male" ], 21 | ["Lara","11.06.2014 01:04:59","11.06.2014 01:42:59",38,"Anni","Wisconsin","Cayman Islands",0.24,9.12,"United States","Oceania","Female","Female" ], 22 | ["Clara","11.06.2014 01:19:30","11.06.2014 02:02:30",43,"Tristan","Maryland","Tajikistan",0.22,9.46,"United States","Asia","Female","Male" ], 23 | ["Simon","11.06.2014 01:35:19","11.06.2014 01:38:19",3,"Ursel","Nevada","Denmark",0.08,0.24,"United States","Europe","Male","Female" ], 24 | ["Simon","11.06.2014 01:35:43","11.06.2014 01:39:43",4,"Ursel","Nevada","Denmark",0.08,0.32,"United States","Europe","Male","Female" ], 25 | ["Henry","11.06.2014 01:38:47","11.06.2014 02:06:47",28,"Felix","Connecticut","New Caledonia",1.41,39.48,"United States","Oceania","Male","Male" ], 26 | ["Simon","11.06.2014 01:56:00","11.06.2014 01:58:00",2,"Anni","Nevada","Cayman Islands",0.24,0.48,"United States","Oceania","Male","Female" ], 27 | ["Simon","11.06.2014 01:56:13","11.06.2014 02:58:13",62,"Anni","Nevada","Cayman Islands",0.24,14.88,"United States","Oceania","Male","Female" ], 28 | ["Ida","11.06.2014 02:08:08","11.06.2014 03:12:08",64,"Luan","Maine","Niger Republic",0.91,58.24,"United States","Africa","Female","Male" ], 29 | ["Ida","11.06.2014 02:09:31","11.06.2014 02:15:31",6,"Luan","Maine","Niger Republic",0.91,5.46,"United States","Africa","Female","Male" ], 30 | ["Niklas","11.06.2014 02:13:36","11.06.2014 02:45:36",32,"Jesko","Florida","Tuvalu",1.84,58.88,"United States","Oceania","Male","Male" ], 31 | ["Charlotte","11.06.2014 02:52:37","11.06.2014 03:19:37",27,"Romeo","Alaska","Gabon Republic",1.05,28.35,"United States","Africa","Female","Male" ], 32 | ["Charlotte","11.06.2014 02:53:11","11.06.2014 03:01:11",8,"Romeo","Alaska","Gabon Republic",1.05,8.4,"United States","Africa","Female","Male" ], 33 | ["Charlotte","11.06.2014 02:53:21","11.06.2014 03:19:21",26,"Romeo","Alaska","Gabon Republic",1.05,27.3,"United States","Africa","Female","Male" ], 34 | ["Clara","11.06.2014 03:32:49","11.06.2014 04:46:49",74,"Tristan","Maryland","Tajikistan",0.22,16.28,"United States","Asia","Female","Male" ], 35 | ["Niklas","11.06.2014 04:15:46","11.06.2014 05:26:46",71,"Tamino","Florida","Vietnam",0.65,46.15,"United States","Asia","Male","Male" ], 36 | ["Ida","11.06.2014 04:30:17","11.06.2014 05:05:17",35,"Luan","Maine","Niger Republic",0.91,31.85,"United States","Africa","Female","Male" ], 37 | ["Frieda","11.06.2014 04:33:35","11.06.2014 04:54:35",21,"Milian","Vermont","San Marino",0.89,18.69,"United States","Europe","Female","Female" ], 38 | ["Frieda","11.06.2014 04:39:23","11.06.2014 05:55:23",76,"Tjark","Vermont","Malta",0.71,53.96,"United States","Europe","Female","Male" ], 39 | ["Frieda","11.06.2014 04:40:52","11.06.2014 04:58:52",18,"Damien","Vermont","Comoro Islands",2.25,40.5,"United States","Africa","Female","Male" ], 40 | ["Frieda","11.06.2014 04:41:31","11.06.2014 04:57:31",16,"Tjark","Vermont","Malta",0.71,11.36,"United States","Europe","Female","Male" ], 41 | ["Frieda","11.06.2014 04:45:38","11.06.2014 05:51:38",66,"Melinda","Vermont","Taiwan",0.07,4.62,"United States","Asia","Female","Female" ], 42 | ["Frieda","11.06.2014 05:07:37","11.06.2014 06:17:37",70,"Tjark","Vermont","Malta",0.71,49.7,"United States","Europe","Female","Male" ], 43 | ["Frieda","11.06.2014 05:11:01","11.06.2014 06:43:01",92,"Milian","Vermont","San Marino",0.89,81.88,"United States","Europe","Female","Female" ], 44 | ["Frieda","11.06.2014 05:28:45","11.06.2014 07:28:45",120,"Tjark","Vermont","Malta",0.71,85.2,"United States","Europe","Female","Male" ], 45 | ["Henry","11.06.2014 12:12:30","11.06.2014 12:15:30",3,"Felix","Connecticut","New Caledonia",1.41,4.23,"United States","Oceania","Male","Male" ], 46 | ["Niklas","11.06.2014 13:00:51","11.06.2014 14:19:51",79,"Tamino","Florida","Vietnam",0.65,51.35,"United States","Asia","Male","Male" ], 47 | ["Henry","11.06.2014 13:08:34","11.06.2014 13:25:34",17,"Felix","Connecticut","New Caledonia",1.41,23.97,"United States","Oceania","Male","Male" ], 48 | ["Henry","11.06.2014 13:31:01","11.06.2014 13:59:01",28,"Felix","Connecticut","New Caledonia",1.41,39.48,"United States","Oceania","Male","Male" ], 49 | ["Niklas","11.06.2014 13:31:32","11.06.2014 14:38:32",67,"Jesko","Florida","Tuvalu",1.84,123.28,"United States","Oceania","Male","Male" ], 50 | ["Henry","11.06.2014 13:31:34","11.06.2014 15:14:34",103,"Felix","Connecticut","New Caledonia",1.41,145.23,"United States","Oceania","Male","Male" ], 51 | ["Henry","11.06.2014 13:31:56","11.06.2014 13:43:56",12,"Felix","Connecticut","New Caledonia",1.41,16.92,"United States","Oceania","Male","Male" ], 52 | ["Henry","11.06.2014 13:32:30","11.06.2014 14:54:30",82,"Felix","Connecticut","New Caledonia",1.41,115.62,"United States","Oceania","Male","Male" ], 53 | ["Henry","11.06.2014 13:44:36","11.06.2014 15:10:36",86,"Felix","Connecticut","New Caledonia",1.41,121.26,"United States","Oceania","Male","Male" ], 54 | ["Emily","11.06.2014 14:21:02","11.06.2014 15:09:02",48,"Anne","Rhode Island","Eritrea",0.88,42.24,"United States","Africa","Male","Female" ], 55 | ["Niklas","11.06.2014 14:43:01","11.06.2014 15:01:01",18,"Jesko","Florida","Tuvalu",1.84,33.12,"United States","Oceania","Male","Male" ], 56 | ["Niklas","11.06.2014 14:43:26","11.06.2014 16:04:26",81,"Jesko","Florida","Tuvalu",1.84,149.04,"United States","Oceania","Male","Male" ], 57 | ["Niklas","11.06.2014 14:50:48","11.06.2014 15:21:48",31,"Anni","Florida","Cayman Islands",0.24,7.44,"United States","Oceania","Male","Female" ], 58 | ["Henry","11.06.2014 15:16:36","11.06.2014 15:51:36",35,"Felix","Connecticut","New Caledonia",1.41,49.35,"United States","Oceania","Male","Male" ], 59 | ["Charlotte","11.06.2014 15:43:16","11.06.2014 16:56:16",73,"Romeo","Alaska","Gabon Republic",1.05,76.65,"United States","Africa","Female","Male" ], 60 | ["Charlotte","11.06.2014 16:11:44","11.06.2014 17:55:44",104,"Romeo","Alaska","Gabon Republic",1.05,109.2,"United States","Africa","Female","Male" ], 61 | ["Charlotte","11.06.2014 16:15:16","11.06.2014 16:36:16",21,"Romeo","Alaska","Gabon Republic",1.05,22.05,"United States","Africa","Female","Male" ], 62 | ["Mika","11.06.2014 16:31:08","11.06.2014 16:40:08",9,"Felix","Vermont","New Caledonia",1.41,12.69,"United States","Oceania","Male","Male" ], 63 | ["Mika","11.06.2014 16:33:21","11.06.2014 17:38:21",65,"Anni","Vermont","Cayman Islands",0.24,15.6,"United States","Oceania","Male","Female" ], 64 | ["Mika","11.06.2014 16:34:37","11.06.2014 17:44:37",70,"Felix","Vermont","New Caledonia",1.41,98.7,"United States","Oceania","Male","Male" ], 65 | ["Emily","11.06.2014 17:36:40","11.06.2014 17:50:40",14,"Anne","Rhode Island","Eritrea",0.88,12.32,"United States","Africa","Male","Female" ], 66 | ["Emilia","11.06.2014 17:56:51","11.06.2014 18:46:51",50,"Adelina","Oklahoma","Mauritania",0.98,49,"United States","Africa","Female","Female" ], 67 | ["Sofia","11.06.2014 17:57:00","11.06.2014 18:28:00",31,"Jolie","Massachusetts","Bahamas",0.23,7.13,"United States","North America","Female","Female" ], 68 | ["Niklas","11.06.2014 18:07:28","11.06.2014 19:13:28",66,"Jesko","Florida","Tuvalu",1.84,121.44,"United States","Oceania","Male","Male" ], 69 | ["Henry","11.06.2014 18:27:32","11.06.2014 20:00:32",93,"Felix","Connecticut","New Caledonia",1.41,131.13,"United States","Oceania","Male","Male" ], 70 | ["Niklas","11.06.2014 18:38:05","11.06.2014 19:22:05",44,"Jesko","Florida","Tuvalu",1.84,80.96,"United States","Oceania","Male","Male" ], 71 | ["Niklas","11.06.2014 18:39:03","11.06.2014 19:23:03",44,"Jesko","Florida","Tuvalu",1.84,80.96,"United States","Oceania","Male","Male" ], 72 | ["Niklas","11.06.2014 18:40:20","11.06.2014 20:16:20",96,"Tamino","Florida","Vietnam",0.65,62.4,"United States","Asia","Male","Male" ], 73 | ["Niklas","11.06.2014 18:42:06","11.06.2014 20:20:06",98,"Tamino","Florida","Vietnam",0.65,63.7,"United States","Asia","Male","Male" ], 74 | ["Niklas","11.06.2014 18:58:12","11.06.2014 20:00:12",62,"Jesko","Florida","Tuvalu",1.84,114.08,"United States","Oceania","Male","Male" ], 75 | ["Clara","11.06.2014 19:01:05","11.06.2014 19:57:05",56,"Tristan","Maryland","Tajikistan",0.22,12.32,"United States","Asia","Female","Male" ], 76 | ["Henry","11.06.2014 19:02:44","11.06.2014 19:34:44",32,"Felix","Connecticut","New Caledonia",1.41,45.12,"United States","Oceania","Male","Male" ], 77 | ["Niklas","11.06.2014 19:03:40","11.06.2014 19:43:40",40,"Jesko","Florida","Tuvalu",1.84,73.6,"United States","Oceania","Male","Male" ], 78 | ["Niklas","11.06.2014 19:12:13","11.06.2014 20:00:13",48,"Jesko","Florida","Tuvalu",1.84,88.32,"United States","Oceania","Male","Male" ], 79 | ["Niklas","11.06.2014 19:27:38","11.06.2014 20:18:38",51,"Jesko","Florida","Tuvalu",1.84,93.84,"United States","Oceania","Male","Male" ], 80 | ["Niklas","11.06.2014 19:46:48","11.06.2014 21:13:48",87,"Jesko","Florida","Tuvalu",1.84,160.08,"United States","Oceania","Male","Male" ], 81 | ["Niklas","11.06.2014 19:49:04","11.06.2014 21:29:04",100,"Jesko","Florida","Tuvalu",1.84,184,"United States","Oceania","Male","Male" ], 82 | ["Niklas","11.06.2014 19:51:05","11.06.2014 21:13:05",82,"Jesko","Florida","Tuvalu",1.84,150.88,"United States","Oceania","Male","Male" ], 83 | ["Frieda","11.06.2014 20:02:10","11.06.2014 21:07:10",65,"Tjark","Vermont","Malta",0.71,46.15,"United States","Europe","Female","Male" ], 84 | ["Simon","11.06.2014 20:02:43","11.06.2014 21:06:43",64,"Ursel","Nevada","Denmark",0.08,5.12,"United States","Europe","Male","Female" ], 85 | ["Mika","11.06.2014 20:06:51","11.06.2014 20:52:51",46,"Felix","Vermont","New Caledonia",1.41,64.86,"United States","Oceania","Male","Male" ], 86 | ["Niklas","11.06.2014 20:06:58","11.06.2014 21:24:58",78,"Jesko","Florida","Tuvalu",1.84,143.52,"United States","Oceania","Male","Male" ], 87 | ["Simon","11.06.2014 20:24:52","11.06.2014 21:24:52",60,"Ursel","Nevada","Denmark",0.08,4.8,"United States","Europe","Male","Female" ], 88 | ["Niklas","11.06.2014 20:34:02","11.06.2014 21:25:02",51,"Jesko","Florida","Tuvalu",1.84,93.84,"United States","Oceania","Male","Male" ], 89 | ["Henry","11.06.2014 20:48:11","11.06.2014 22:13:11",85,"Felix","Connecticut","New Caledonia",1.41,119.85,"United States","Oceania","Male","Male" ], 90 | ["Niklas","11.06.2014 20:58:31","11.06.2014 22:24:31",86,"Jesko","Florida","Tuvalu",1.84,158.24,"United States","Oceania","Male","Male" ], 91 | ["Frieda","11.06.2014 21:03:12","11.06.2014 21:45:12",42,"Tjark","Vermont","Malta",0.71,29.82,"United States","Europe","Female","Male" ], 92 | ["Charlotte","11.06.2014 21:08:36","11.06.2014 21:38:36",30,"Ursel","Alaska","Denmark",0.08,2.4,"United States","Europe","Female","Female" ], 93 | ["Clara","11.06.2014 21:10:59","11.06.2014 22:07:59",57,"Tristan","Maryland","Tajikistan",0.22,12.54,"United States","Asia","Female","Male" ], 94 | ["Simon","11.06.2014 21:27:01","11.06.2014 21:54:01",27,"Ursel","Nevada","Denmark",0.08,2.16,"United States","Europe","Male","Female" ], 95 | ["Ida","11.06.2014 21:42:55","11.06.2014 21:57:55",15,"Ahmet","Maine","Mexico",0.07,1.05,"United States","North America","Female","Male" ], 96 | ["Ida","11.06.2014 21:44:56","11.06.2014 22:06:56",22,"Malea","Maine","Latvia",0.34,7.48,"United States","Europe","Female","Female" ], 97 | ["Simon","11.06.2014 21:46:33","11.06.2014 22:36:33",50,"Ursel","Nevada","Denmark",0.08,4,"United States","Europe","Male","Female" ], 98 | ["Emily","11.06.2014 21:47:38","11.06.2014 22:54:38",67,"Nadine","Rhode Island","Lebanon",0.54,36.18,"United States","Asia","Male","Female" ], 99 | ["Mats","11.06.2014 22:00:48","11.06.2014 22:40:48",40,"Felix","Virginia","New Caledonia",1.41,56.4,"United States","Oceania","Male","Male" ], 100 | ["Mika","11.06.2014 22:06:33","11.06.2014 23:14:33",68,"Miro","Vermont","Netherlands Antilles",0.4,27.2,"United States","North America","Male","Male" ], 101 | ["Niklas","11.06.2014 22:07:47","11.06.2014 22:57:47",50,"Tamino","Florida","Vietnam",0.65,32.5,"United States","Asia","Male","Male" ], 102 | ["Mika","11.06.2014 22:10:56","11.06.2014 22:12:56",2,"Miro","Vermont","Netherlands Antilles",0.4,0.8,"United States","North America","Male","Male" ], 103 | ["Mats","11.06.2014 22:11:30","11.06.2014 22:58:30",47,"Felix","Virginia","New Caledonia",1.41,66.27,"United States","Oceania","Male","Male" ], 104 | ["Niklas","11.06.2014 22:27:56","11.06.2014 23:04:56",37,"Tamino","Florida","Vietnam",0.65,24.05,"United States","Asia","Male","Male" ], 105 | ["Emilia","11.06.2014 22:31:30","11.06.2014 22:55:30",24,"Cinar","Oklahoma","Jamaica",0.43,10.32,"United States","North America","Female","Male" ], 106 | ["Clara","11.06.2014 22:33:43","11.06.2014 23:08:43",35,"Jule","Maryland","Faeroe Islands",0.52,18.2,"United States","Europe","Female","Female" ], 107 | ["Charlotte","11.06.2014 22:34:20","11.06.2014 23:27:20",53,"Ursel","Alaska","Denmark",0.08,4.24,"United States","Europe","Female","Female" ], 108 | ["Emilia","11.06.2014 22:34:35","11.06.2014 23:33:35",59,"Merlin","Oklahoma","Pakistan",0.36,21.24,"United States","Asia","Female","Male" ], 109 | ["Henry","11.06.2014 22:35:16","11.06.2014 22:43:16",8,"Viola","Connecticut","Angola",1.44,11.52,"United States","Africa","Male","Female" ], 110 | ["Emilia","11.06.2014 22:36:46","11.06.2014 22:47:46",11,"Benny","Oklahoma","Spain",0.07,0.77,"United States","Europe","Female","Male" ], 111 | ["Emilia","11.06.2014 22:38:58","11.06.2014 23:42:58",64,"Cora","Oklahoma","Bangladesh",0.51,32.64,"United States","Asia","Female","Female" ], 112 | ["Niklas","11.06.2014 22:41:32","11.06.2014 23:33:32",52,"Tamino","Florida","Vietnam",0.65,33.8,"United States","Asia","Male","Male" ], 113 | ["Lilly","11.06.2014 23:27:12","12.06.2014 00:43:12",76,"Kaan","Alaska","Reunion Island",1,76,"United States","Africa","Female","Male" ], 114 | ["Clara","11.06.2014 23:27:41","12.06.2014 00:39:41",72,"Tristan","Maryland","Tajikistan",0.22,15.84,"United States","Asia","Female","Male" ], 115 | ["Emily","11.06.2014 23:34:51","12.06.2014 00:09:51",35,"Nadine","Rhode Island","Lebanon",0.54,18.9,"United States","Asia","Male","Female" ], 116 | ["Sofia","11.06.2014 23:40:26","12.06.2014 00:11:26",31,"Jolie","Massachusetts","Bahamas",0.23,7.13,"United States","North America","Female","Female" ], 117 | ["Emilia","11.06.2014 23:40:33","12.06.2014 00:14:33",34,"Adelina","Oklahoma","Mauritania",0.98,33.32,"United States","Africa","Female","Female" ], 118 | ["Henry","11.06.2014 23:48:43","12.06.2014 00:32:43",44,"Felix","Connecticut","New Caledonia",1.41,62.04,"United States","Oceania","Male","Male" ], 119 | ["Emilia","11.06.2014 23:57:22","12.06.2014 00:29:22",32,"Cora","Oklahoma","Bangladesh",0.51,16.32,"United States","Asia","Female","Female" ] 120 | ] -------------------------------------------------------------------------------- /resources/sample-data.csv: -------------------------------------------------------------------------------- 1 | caller;start;end;duration;recipient;origin;destination;ratepermin;costs;origincontinent;destinationcontinent;callergender;recipientgender 2 | Ben;27.09.2013 02:00:19;27.09.2013 02:44:19;44;Kimi;South Dakota;United Kingdom;0.07;3.08;United States;Europe;Male;Female 3 | Ben;27.09.2013 02:02:36;27.09.2013 02:07:36;5;Diana;South Dakota;Falkland Islands;0.45;2.25;United States;South America;Male;Female 4 | Ben;27.09.2013 02:07:40;27.09.2013 02:30:40;23;Diana;South Dakota;Falkland Islands;0.45;10.35;United States;South America;Male;Female 5 | Leni;27.09.2013 02:11:39;27.09.2013 02:46:39;35;Clemens;Missouri;Namibia;0.79;27.65;United States;Africa;Male;Male 6 | Max;27.09.2013 02:13:26;27.09.2013 02:41:26;28;Jasmin;Ohio;Mauritius;1.22;34.16;United States;Africa;Male;Female 7 | Max;27.09.2013 02:14:06;27.09.2013 02:22:06;8;Felina;Ohio;Suriname;1.22;9.76;United States;South America;Male;Female 8 | Max;27.09.2013 02:14:26;27.09.2013 02:48:26;34;Melinda;Ohio;Taiwan;0.07;2.38;United States;Asia;Male;Female 9 | Ida;27.09.2013 02:14:47;27.09.2013 02:20:47;6;Sidney;Maine;Mayotte Island;1.00;6.00;United States;Africa;Female;Male 10 | Ida;27.09.2013 02:14:47;27.09.2013 02:27:47;13;Sidney;Maine;Mayotte Island;1.00;13.00;United States;Africa;Female;Male 11 | Ida;27.09.2013 02:14:58;27.09.2013 02:56:58;42;Sidney;Maine;Mayotte Island;1.00;42.00;United States;Africa;Female;Male 12 | Ida;27.09.2013 02:14:58;27.09.2013 03:21:58;67;Sidney;Maine;Mayotte Island;1.00;67.00;United States;Africa;Female;Male 13 | Ida;27.09.2013 02:15:44;27.09.2013 02:43:44;28;Sidney;Maine;Mayotte Island;1.00;28.00;United States;Africa;Female;Male 14 | Ida;27.09.2013 02:15:44;27.09.2013 02:24:44;9;Sidney;Maine;Mayotte Island;1.00;9.00;United States;Africa;Female;Male 15 | Clara;27.09.2013 02:16:07;27.09.2013 02:22:07;6;Malia;Maryland;Monaco;0.14;0.84;United States;Europe;Female;Female 16 | Jonathan;27.09.2013 02:16:07;27.09.2013 02:22:07;6;Malia;West Virginia;Monaco;0.14;0.84;United States;Europe;Male;Female 17 | Leni;27.09.2013 02:19:29;27.09.2013 04:08:29;109;Matthias;Missouri;Mali Republic;1.03;112.27;United States;Africa;Male;Male 18 | Paul;27.09.2013 02:21:34;27.09.2013 02:49:34;28;Jasmin;Montana;Mauritius;1.22;34.16;United States;Africa;Male;Female 19 | Leni;27.09.2013 02:23:15;27.09.2013 02:47:15;24;Henriette;Missouri;Zimbabwe;0.48;11.52;United States;Africa;Male;Female 20 | Ben;27.09.2013 02:23:37;27.09.2013 02:37:37;14;Jasmin;South Dakota;Mauritius;1.22;17.08;United States;Africa;Male;Female 21 | Ben;27.09.2013 02:24:15;27.09.2013 02:43:15;19;Hugo;South Dakota;Zimbabwe;0.48;9.12;United States;Africa;Male;Male 22 | Ben;27.09.2013 02:25:12;27.09.2013 03:12:12;47;Diana;South Dakota;Falkland Islands;0.45;21.15;United States;South America;Male;Female 23 | Ben;27.09.2013 02:26:58;27.09.2013 04:15:58;109;Diana;South Dakota;Falkland Islands;0.45;49.05;United States;South America;Male;Female 24 | Max;27.09.2013 02:31:48;27.09.2013 03:04:48;33;Diana;Ohio;Falkland Islands;0.45;14.85;United States;South America;Male;Female 25 | Clara;27.09.2013 02:36:52;27.09.2013 03:30:52;54;Jasmin;Maryland;Mauritius;1.22;65.88;United States;Africa;Female;Female 26 | Jonathan;27.09.2013 02:36:52;27.09.2013 03:30:52;54;Jasmin;West Virginia;Mauritius;1.22;65.88;United States;Africa;Male;Female 27 | Emma;27.09.2013 02:37:56;27.09.2013 02:38:56;1;Jasmin;New Jersey;Mauritius;1.22;1.22;United States;Africa;Female;Female 28 | Clara;27.09.2013 02:38:38;27.09.2013 03:32:38;54;Jordan;Maryland;Bosnia;0.34;18.36;United States;Europe;Female;Male 29 | Jonathan;27.09.2013 02:38:38;27.09.2013 04:31:38;113;Diana;West Virginia;Falkland Islands;0.45;50.85;United States;South America;Male;Female 30 | Paul;27.09.2013 02:38:50;27.09.2013 02:53:50;15;Frauke;Montana;French Antilles;0.62;9.30;United States;South America;Male;Female 31 | Emma;27.09.2013 02:38:50;27.09.2013 02:50:50;12;Jasmin;New Jersey;Mauritius;1.22;14.64;United States;Africa;Female;Female 32 | Emma;27.09.2013 02:39:26;27.09.2013 03:05:26;26;Jasmin;New Jersey;Mauritius;1.22;31.72;United States;Africa;Female;Female 33 | Ida;27.09.2013 02:39:49;27.09.2013 03:39:49;60;Jayden;Maine;Kiribati;1.16;69.60;United States;Oceania;Female;Male 34 | Paul;27.09.2013 02:40:06;27.09.2013 03:44:06;64;Frauke;Montana;French Antilles;0.62;39.68;United States;South America;Male;Female 35 | Clara;27.09.2013 02:42:13;27.09.2013 02:48:13;6;Jordan;Maryland;Bosnia;0.34;2.04;United States;Europe;Female;Male 36 | Jonathan;27.09.2013 02:42:13;27.09.2013 04:14:13;92;Diana;West Virginia;Falkland Islands;0.45;41.40;United States;South America;Male;Female 37 | Ida;27.09.2013 02:47:35;27.09.2013 03:54:35;67;Ylvi;Maine;Anguilla;0.57;38.19;United States;South America;Female;Female 38 | Ida;27.09.2013 02:47:35;27.09.2013 04:23:35;96;Ylvi;Maine;Anguilla;0.57;54.72;United States;South America;Female;Female 39 | Leni;27.09.2013 02:49:37;27.09.2013 04:19:37;90;Henriette;Missouri;Zimbabwe;0.48;43.20;United States;Africa;Male;Female 40 | Leni;27.09.2013 02:51:42;27.09.2013 04:14:42;83;Anni;Missouri;Cayman Islands;0.24;19.92;United States;Oceania;Male;Female 41 | Emma;27.09.2013 02:53:54;27.09.2013 04:14:54;81;Diana;New Jersey;Falkland Islands;0.45;36.45;United States;South America;Female;Female 42 | Clara;27.09.2013 02:59:43;27.09.2013 04:02:43;63;Jordan;Maryland;Bosnia;0.34;21.42;United States;Europe;Female;Male 43 | Jonathan;27.09.2013 02:59:43;27.09.2013 04:02:43;63;Diana;West Virginia;Falkland Islands;0.45;28.35;United States;South America;Male;Female 44 | Paul;27.09.2013 03:07:57;27.09.2013 03:27:57;20;Patrick;Montana;Colombia;0.19;3.80;United States;South America;Male;Male 45 | Paul;27.09.2013 03:08:45;27.09.2013 03:45:45;37;Frauke;Montana;French Antilles;0.62;22.94;United States;South America;Male;Female 46 | Clara;27.09.2013 03:11:50;27.09.2013 04:50:50;99;Jordan;Maryland;Bosnia;0.34;33.66;United States;Europe;Female;Male 47 | Jonathan;27.09.2013 03:11:50;27.09.2013 04:50:50;99;Diana;West Virginia;Falkland Islands;0.45;44.55;United States;South America;Male;Female 48 | Paul;27.09.2013 03:16:09;27.09.2013 03:45:09;29;Elif;Montana;Burundi;1.39;40.31;United States;Africa;Male;Female 49 | Ben;27.09.2013 03:16:17;27.09.2013 04:09:17;53;Talea;South Dakota;Denmark;0.08;4.24;United States;Europe;Male;Female 50 | Paul;27.09.2013 03:47:32;27.09.2013 04:11:32;24;Stina;Montana;Mexico;0.07;1.68;United States;North America;Male;Female 51 | Ben;27.09.2013 05:08:06;27.09.2013 06:28:06;80;Hugo;South Dakota;Zimbabwe;0.48;38.40;United States;Africa;Male;Male 52 | Ben;27.09.2013 05:24:38;27.09.2013 05:35:38;11;Diana;South Dakota;Falkland Islands;0.45;4.95;United States;South America;Male;Female 53 | Ben;27.09.2013 11:41:36;27.09.2013 12:17:36;36;Diana;South Dakota;Falkland Islands;0.45;16.20;United States;South America;Male;Female 54 | Ben;27.09.2013 11:44:26;27.09.2013 12:17:26;33;Diana;South Dakota;Falkland Islands;0.45;14.85;United States;South America;Male;Female 55 | Ben;27.09.2013 11:46:02;27.09.2013 12:00:02;14;Diana;South Dakota;Falkland Islands;0.45;6.30;United States;South America;Male;Female 56 | Leni;27.09.2013 13:35:27;27.09.2013 13:58:27;23;Medina;Missouri;Hong Kong;0.09;2.07;United States;Asia;Male;Female 57 | Ida;27.09.2013 13:37:29;27.09.2013 14:09:29;32;Valentin;Maine;Pakistan;0.36;11.52;United States;Asia;Female;Male 58 | Ida;27.09.2013 13:37:29;27.09.2013 14:53:29;76;Valentin;Maine;Pakistan;0.36;27.36;United States;Asia;Female;Male 59 | Leni;27.09.2013 13:44:47;27.09.2013 13:59:47;15;Medina;Missouri;Hong Kong;0.09;1.35;United States;Asia;Male;Female 60 | Leni;27.09.2013 13:58:06;27.09.2013 14:50:06;52;Christopher;Missouri;Germany;0.07;3.64;United States;Europe;Male;Male 61 | Clara;27.09.2013 14:06:40;27.09.2013 14:53:40;47;Jordan;Maryland;Bosnia;0.34;15.98;United States;Europe;Female;Male 62 | Jonathan;27.09.2013 14:06:40;27.09.2013 14:53:40;47;Diana;West Virginia;Falkland Islands;0.45;21.15;United States;South America;Male;Female 63 | Leni;27.09.2013 14:08:34;27.09.2013 14:55:34;47;Christopher;Missouri;Germany;0.07;3.29;United States;Europe;Male;Male 64 | Leni;27.09.2013 14:08:42;27.09.2013 14:33:42;25;Medina;Missouri;Hong Kong;0.09;2.25;United States;Asia;Male;Female 65 | Leni;27.09.2013 14:13:24;27.09.2013 14:44:24;31;Medina;Missouri;Hong Kong;0.09;2.79;United States;Asia;Male;Female 66 | Leni;27.09.2013 14:15:51;27.09.2013 14:25:51;10;Diana;Missouri;Falkland Islands;0.45;4.50;United States;South America;Male;Female 67 | Clara;27.09.2013 15:16:24;27.09.2013 17:00:24;104;Jordan;Maryland;Bosnia;0.34;35.36;United States;Europe;Female;Male 68 | Jonathan;27.09.2013 15:16:24;27.09.2013 15:36:24;20;Diana;West Virginia;Falkland Islands;0.45;9.00;United States;South America;Male;Female 69 | Ben;27.09.2013 16:45:27;27.09.2013 17:16:27;31;Diana;South Dakota;Falkland Islands;0.45;13.95;United States;South America;Male;Female 70 | Leni;27.09.2013 16:56:35;27.09.2013 17:04:35;8;Medina;Missouri;Hong Kong;0.09;0.72;United States;Asia;Male;Female 71 | Leni;27.09.2013 16:57:51;27.09.2013 17:04:51;7;Medina;Missouri;Hong Kong;0.09;0.63;United States;Asia;Male;Female 72 | Leni;27.09.2013 17:17:28;27.09.2013 18:54:28;97;Medina;Missouri;Hong Kong;0.09;8.73;United States;Asia;Male;Female 73 | Clara;27.09.2013 17:27:58;27.09.2013 18:07:58;40;Jordan;Maryland;Bosnia;0.34;13.60;United States;Europe;Female;Male 74 | Jonathan;27.09.2013 17:27:58;27.09.2013 19:18:58;111;Diana;West Virginia;Falkland Islands;0.45;49.95;United States;South America;Male;Female 75 | Leni;27.09.2013 17:48:25;27.09.2013 18:18:25;30;Diana;Missouri;Falkland Islands;0.45;13.50;United States;South America;Male;Female 76 | Ben;27.09.2013 18:59:24;27.09.2013 19:57:24;58;Adelina;South Dakota;Mauritania;0.98;56.84;United States;Africa;Male;Female 77 | Leni;27.09.2013 19:01:19;27.09.2013 19:07:19;6;Medina;Missouri;Hong Kong;0.09;0.54;United States;Asia;Male;Female 78 | Leni;27.09.2013 19:01:31;27.09.2013 19:34:31;33;Medina;Missouri;Hong Kong;0.09;2.97;United States;Asia;Male;Female 79 | Max;27.09.2013 19:14:35;27.09.2013 19:52:35;38;Diana;Ohio;Falkland Islands;0.45;17.10;United States;South America;Male;Female 80 | Ida;27.09.2013 19:51:48;27.09.2013 20:36:48;45;Soraya;Maine;Algeria;0.48;21.60;United States;Africa;Female;Female 81 | Ida;27.09.2013 19:51:48;27.09.2013 21:04:48;73;Soraya;Maine;Algeria;0.48;35.04;United States;Africa;Female;Female 82 | Ida;27.09.2013 20:18:09;27.09.2013 20:21:09;3;Elaine;Maine;Laos;2.39;7.17;United States;Asia;Female;Female 83 | Ida;27.09.2013 20:18:09;27.09.2013 21:30:09;72;Elaine;Maine;Laos;2.39;172.08;United States;Asia;Female;Female 84 | Emma;27.09.2013 20:37:51;27.09.2013 21:20:51;43;Diana;New Jersey;Falkland Islands;0.45;19.35;United States;South America;Female;Female 85 | Emma;27.09.2013 21:20:14;27.09.2013 23:15:14;115;Diana;New Jersey;Falkland Islands;0.45;51.75;United States;South America;Female;Female 86 | Leni;27.09.2013 21:22:15;27.09.2013 21:29:15;7;Medina;Missouri;Hong Kong;0.09;0.63;United States;Asia;Male;Female 87 | Leni;27.09.2013 21:22:29;27.09.2013 21:27:29;5;Medina;Missouri;Hong Kong;0.09;0.45;United States;Asia;Male;Female 88 | Clara;27.09.2013 21:48:50;27.09.2013 21:57:50;9;Jordan;Maryland;Bosnia;0.34;3.06;United States;Europe;Female;Male 89 | Jonathan;27.09.2013 21:48:50;27.09.2013 22:23:50;35;Diana;West Virginia;Falkland Islands;0.45;15.75;United States;South America;Male;Female 90 | Leni;27.09.2013 23:47:49;28.09.2013 00:16:49;29;Anthony;Missouri;Portugal;0.09;2.61;United States;Europe;Male;Male 91 | -------------------------------------------------------------------------------- /resources/sample-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caller": "Ben", 4 | "start": "27.09.2013 02:00:19", 5 | "end": "27.09.2013 02:44:19", 6 | "duration": 44, 7 | "recipient": "Kimi", 8 | "origin": "South Dakota", 9 | "destination": "United Kingdom", 10 | "ratepermin": 0.07, 11 | "costs": 3.08, 12 | "origincontinent": "United States", 13 | "destinationcontinent": "Europe", 14 | "callergender": "Male", 15 | "recipientgender": "Female" 16 | }, 17 | { 18 | "caller": "Ben", 19 | "start": "27.09.2013 02:02:36", 20 | "end": "27.09.2013 02:07:36", 21 | "duration": 5, 22 | "recipient": "Diana", 23 | "origin": "South Dakota", 24 | "destination": "Falkland Islands", 25 | "ratepermin": 0.45, 26 | "costs": 2.25, 27 | "origincontinent": "United States", 28 | "destinationcontinent": "South America", 29 | "callergender": "Male", 30 | "recipientgender": "Female" 31 | }, 32 | { 33 | "caller": "Ben", 34 | "start": "27.09.2013 02:07:40", 35 | "end": "27.09.2013 02:30:40", 36 | "duration": 23, 37 | "recipient": "Diana", 38 | "origin": "South Dakota", 39 | "destination": "Falkland Islands", 40 | "ratepermin": 0.45, 41 | "costs": 10.35, 42 | "origincontinent": "United States", 43 | "destinationcontinent": "South America", 44 | "callergender": "Male", 45 | "recipientgender": "Female" 46 | }, 47 | { 48 | "caller": "Leni", 49 | "start": "27.09.2013 02:11:39", 50 | "end": "27.09.2013 02:46:39", 51 | "duration": 35, 52 | "recipient": "Clemens", 53 | "origin": "Missouri", 54 | "destination": "Namibia", 55 | "ratepermin": 0.79, 56 | "costs": 27.65, 57 | "origincontinent": "United States", 58 | "destinationcontinent": "Africa", 59 | "callergender": "Male", 60 | "recipientgender": "Male" 61 | }, 62 | { 63 | "caller": "Max", 64 | "start": "27.09.2013 02:13:26", 65 | "end": "27.09.2013 02:41:26", 66 | "duration": 28, 67 | "recipient": "Jasmin", 68 | "origin": "Ohio", 69 | "destination": "Mauritius", 70 | "ratepermin": 1.22, 71 | "costs": 34.16, 72 | "origincontinent": "United States", 73 | "destinationcontinent": "Africa", 74 | "callergender": "Male", 75 | "recipientgender": "Female" 76 | }, 77 | { 78 | "caller": "Max", 79 | "start": "27.09.2013 02:14:06", 80 | "end": "27.09.2013 02:22:06", 81 | "duration": 8, 82 | "recipient": "Felina", 83 | "origin": "Ohio", 84 | "destination": "Suriname", 85 | "ratepermin": 1.22, 86 | "costs": 9.76, 87 | "origincontinent": "United States", 88 | "destinationcontinent": "South America", 89 | "callergender": "Male", 90 | "recipientgender": "Female" 91 | }, 92 | { 93 | "caller": "Max", 94 | "start": "27.09.2013 02:14:26", 95 | "end": "27.09.2013 02:48:26", 96 | "duration": 34, 97 | "recipient": "Melinda", 98 | "origin": "Ohio", 99 | "destination": "Taiwan", 100 | "ratepermin": 0.07, 101 | "costs": 2.38, 102 | "origincontinent": "United States", 103 | "destinationcontinent": "Asia", 104 | "callergender": "Male", 105 | "recipientgender": "Female" 106 | }, 107 | { 108 | "caller": "Ida", 109 | "start": "27.09.2013 02:14:47", 110 | "end": "27.09.2013 02:20:47", 111 | "duration": 6, 112 | "recipient": "Sidney", 113 | "origin": "Maine", 114 | "destination": "Mayotte Island", 115 | "ratepermin": 1, 116 | "costs": 6, 117 | "origincontinent": "United States", 118 | "destinationcontinent": "Africa", 119 | "callergender": "Female", 120 | "recipientgender": "Male" 121 | }, 122 | { 123 | "caller": "Ida", 124 | "start": "27.09.2013 02:14:47", 125 | "end": "27.09.2013 02:27:47", 126 | "duration": 13, 127 | "recipient": "Sidney", 128 | "origin": "Maine", 129 | "destination": "Mayotte Island", 130 | "ratepermin": 1, 131 | "costs": 13, 132 | "origincontinent": "United States", 133 | "destinationcontinent": "Africa", 134 | "callergender": "Female", 135 | "recipientgender": "Male" 136 | }, 137 | { 138 | "caller": "Ida", 139 | "start": "27.09.2013 02:14:58", 140 | "end": "27.09.2013 02:56:58", 141 | "duration": 42, 142 | "recipient": "Sidney", 143 | "origin": "Maine", 144 | "destination": "Mayotte Island", 145 | "ratepermin": 1, 146 | "costs": 42, 147 | "origincontinent": "United States", 148 | "destinationcontinent": "Africa", 149 | "callergender": "Female", 150 | "recipientgender": "Male" 151 | }, 152 | { 153 | "caller": "Ida", 154 | "start": "27.09.2013 02:14:58", 155 | "end": "27.09.2013 03:21:58", 156 | "duration": 67, 157 | "recipient": "Sidney", 158 | "origin": "Maine", 159 | "destination": "Mayotte Island", 160 | "ratepermin": 1, 161 | "costs": 67, 162 | "origincontinent": "United States", 163 | "destinationcontinent": "Africa", 164 | "callergender": "Female", 165 | "recipientgender": "Male" 166 | }, 167 | { 168 | "caller": "Ida", 169 | "start": "27.09.2013 02:15:44", 170 | "end": "27.09.2013 02:43:44", 171 | "duration": 28, 172 | "recipient": "Sidney", 173 | "origin": "Maine", 174 | "destination": "Mayotte Island", 175 | "ratepermin": 1, 176 | "costs": 28, 177 | "origincontinent": "United States", 178 | "destinationcontinent": "Africa", 179 | "callergender": "Female", 180 | "recipientgender": "Male" 181 | }, 182 | { 183 | "caller": "Ida", 184 | "start": "27.09.2013 02:15:44", 185 | "end": "27.09.2013 02:24:44", 186 | "duration": 9, 187 | "recipient": "Sidney", 188 | "origin": "Maine", 189 | "destination": "Mayotte Island", 190 | "ratepermin": 1, 191 | "costs": 9, 192 | "origincontinent": "United States", 193 | "destinationcontinent": "Africa", 194 | "callergender": "Female", 195 | "recipientgender": "Male" 196 | }, 197 | { 198 | "caller": "Clara", 199 | "start": "27.09.2013 02:16:07", 200 | "end": "27.09.2013 02:22:07", 201 | "duration": 6, 202 | "recipient": "Malia", 203 | "origin": "Maryland", 204 | "destination": "Monaco", 205 | "ratepermin": 0.14, 206 | "costs": 0.84, 207 | "origincontinent": "United States", 208 | "destinationcontinent": "Europe", 209 | "callergender": "Female", 210 | "recipientgender": "Female" 211 | }, 212 | { 213 | "caller": "Jonathan", 214 | "start": "27.09.2013 02:16:07", 215 | "end": "27.09.2013 02:22:07", 216 | "duration": 6, 217 | "recipient": "Malia", 218 | "origin": "West Virginia", 219 | "destination": "Monaco", 220 | "ratepermin": 0.14, 221 | "costs": 0.84, 222 | "origincontinent": "United States", 223 | "destinationcontinent": "Europe", 224 | "callergender": "Male", 225 | "recipientgender": "Female" 226 | }, 227 | { 228 | "caller": "Leni", 229 | "start": "27.09.2013 02:19:29", 230 | "end": "27.09.2013 04:08:29", 231 | "duration": 109, 232 | "recipient": "Matthias", 233 | "origin": "Missouri", 234 | "destination": "Mali Republic", 235 | "ratepermin": 1.03, 236 | "costs": 112.27, 237 | "origincontinent": "United States", 238 | "destinationcontinent": "Africa", 239 | "callergender": "Male", 240 | "recipientgender": "Male" 241 | }, 242 | { 243 | "caller": "Paul", 244 | "start": "27.09.2013 02:21:34", 245 | "end": "27.09.2013 02:49:34", 246 | "duration": 28, 247 | "recipient": "Jasmin", 248 | "origin": "Montana", 249 | "destination": "Mauritius", 250 | "ratepermin": 1.22, 251 | "costs": 34.16, 252 | "origincontinent": "United States", 253 | "destinationcontinent": "Africa", 254 | "callergender": "Male", 255 | "recipientgender": "Female" 256 | }, 257 | { 258 | "caller": "Leni", 259 | "start": "27.09.2013 02:23:15", 260 | "end": "27.09.2013 02:47:15", 261 | "duration": 24, 262 | "recipient": "Henriette", 263 | "origin": "Missouri", 264 | "destination": "Zimbabwe", 265 | "ratepermin": 0.48, 266 | "costs": 11.52, 267 | "origincontinent": "United States", 268 | "destinationcontinent": "Africa", 269 | "callergender": "Male", 270 | "recipientgender": "Female" 271 | }, 272 | { 273 | "caller": "Ben", 274 | "start": "27.09.2013 02:23:37", 275 | "end": "27.09.2013 02:37:37", 276 | "duration": 14, 277 | "recipient": "Jasmin", 278 | "origin": "South Dakota", 279 | "destination": "Mauritius", 280 | "ratepermin": 1.22, 281 | "costs": 17.08, 282 | "origincontinent": "United States", 283 | "destinationcontinent": "Africa", 284 | "callergender": "Male", 285 | "recipientgender": "Female" 286 | }, 287 | { 288 | "caller": "Ben", 289 | "start": "27.09.2013 02:24:15", 290 | "end": "27.09.2013 02:43:15", 291 | "duration": 19, 292 | "recipient": "Hugo", 293 | "origin": "South Dakota", 294 | "destination": "Zimbabwe", 295 | "ratepermin": 0.48, 296 | "costs": 9.12, 297 | "origincontinent": "United States", 298 | "destinationcontinent": "Africa", 299 | "callergender": "Male", 300 | "recipientgender": "Male" 301 | }, 302 | { 303 | "caller": "Ben", 304 | "start": "27.09.2013 02:25:12", 305 | "end": "27.09.2013 03:12:12", 306 | "duration": 47, 307 | "recipient": "Diana", 308 | "origin": "South Dakota", 309 | "destination": "Falkland Islands", 310 | "ratepermin": 0.45, 311 | "costs": 21.15, 312 | "origincontinent": "United States", 313 | "destinationcontinent": "South America", 314 | "callergender": "Male", 315 | "recipientgender": "Female" 316 | }, 317 | { 318 | "caller": "Ben", 319 | "start": "27.09.2013 02:26:58", 320 | "end": "27.09.2013 04:15:58", 321 | "duration": 109, 322 | "recipient": "Diana", 323 | "origin": "South Dakota", 324 | "destination": "Falkland Islands", 325 | "ratepermin": 0.45, 326 | "costs": 49.05, 327 | "origincontinent": "United States", 328 | "destinationcontinent": "South America", 329 | "callergender": "Male", 330 | "recipientgender": "Female" 331 | }, 332 | { 333 | "caller": "Max", 334 | "start": "27.09.2013 02:31:48", 335 | "end": "27.09.2013 03:04:48", 336 | "duration": 33, 337 | "recipient": "Diana", 338 | "origin": "Ohio", 339 | "destination": "Falkland Islands", 340 | "ratepermin": 0.45, 341 | "costs": 14.85, 342 | "origincontinent": "United States", 343 | "destinationcontinent": "South America", 344 | "callergender": "Male", 345 | "recipientgender": "Female" 346 | }, 347 | { 348 | "caller": "Clara", 349 | "start": "27.09.2013 02:36:52", 350 | "end": "27.09.2013 03:30:52", 351 | "duration": 54, 352 | "recipient": "Jasmin", 353 | "origin": "Maryland", 354 | "destination": "Mauritius", 355 | "ratepermin": 1.22, 356 | "costs": 65.88, 357 | "origincontinent": "United States", 358 | "destinationcontinent": "Africa", 359 | "callergender": "Female", 360 | "recipientgender": "Female" 361 | }, 362 | { 363 | "caller": "Jonathan", 364 | "start": "27.09.2013 02:36:52", 365 | "end": "27.09.2013 03:30:52", 366 | "duration": 54, 367 | "recipient": "Jasmin", 368 | "origin": "West Virginia", 369 | "destination": "Mauritius", 370 | "ratepermin": 1.22, 371 | "costs": 65.88, 372 | "origincontinent": "United States", 373 | "destinationcontinent": "Africa", 374 | "callergender": "Male", 375 | "recipientgender": "Female" 376 | }, 377 | { 378 | "caller": "Emma", 379 | "start": "27.09.2013 02:37:56", 380 | "end": "27.09.2013 02:38:56", 381 | "duration": 1, 382 | "recipient": "Jasmin", 383 | "origin": "New Jersey", 384 | "destination": "Mauritius", 385 | "ratepermin": 1.22, 386 | "costs": 1.22, 387 | "origincontinent": "United States", 388 | "destinationcontinent": "Africa", 389 | "callergender": "Female", 390 | "recipientgender": "Female" 391 | }, 392 | { 393 | "caller": "Clara", 394 | "start": "27.09.2013 02:38:38", 395 | "end": "27.09.2013 03:32:38", 396 | "duration": 54, 397 | "recipient": "Jordan", 398 | "origin": "Maryland", 399 | "destination": "Bosnia", 400 | "ratepermin": 0.34, 401 | "costs": 18.36, 402 | "origincontinent": "United States", 403 | "destinationcontinent": "Europe", 404 | "callergender": "Female", 405 | "recipientgender": "Male" 406 | }, 407 | { 408 | "caller": "Jonathan", 409 | "start": "27.09.2013 02:38:38", 410 | "end": "27.09.2013 04:31:38", 411 | "duration": 113, 412 | "recipient": "Diana", 413 | "origin": "West Virginia", 414 | "destination": "Falkland Islands", 415 | "ratepermin": 0.45, 416 | "costs": 50.85, 417 | "origincontinent": "United States", 418 | "destinationcontinent": "South America", 419 | "callergender": "Male", 420 | "recipientgender": "Female" 421 | }, 422 | { 423 | "caller": "Paul", 424 | "start": "27.09.2013 02:38:50", 425 | "end": "27.09.2013 02:53:50", 426 | "duration": 15, 427 | "recipient": "Frauke", 428 | "origin": "Montana", 429 | "destination": "French Antilles", 430 | "ratepermin": 0.62, 431 | "costs": 9.3, 432 | "origincontinent": "United States", 433 | "destinationcontinent": "South America", 434 | "callergender": "Male", 435 | "recipientgender": "Female" 436 | }, 437 | { 438 | "caller": "Emma", 439 | "start": "27.09.2013 02:38:50", 440 | "end": "27.09.2013 02:50:50", 441 | "duration": 12, 442 | "recipient": "Jasmin", 443 | "origin": "New Jersey", 444 | "destination": "Mauritius", 445 | "ratepermin": 1.22, 446 | "costs": 14.64, 447 | "origincontinent": "United States", 448 | "destinationcontinent": "Africa", 449 | "callergender": "Female", 450 | "recipientgender": "Female" 451 | }, 452 | { 453 | "caller": "Emma", 454 | "start": "27.09.2013 02:39:26", 455 | "end": "27.09.2013 03:05:26", 456 | "duration": 26, 457 | "recipient": "Jasmin", 458 | "origin": "New Jersey", 459 | "destination": "Mauritius", 460 | "ratepermin": 1.22, 461 | "costs": 31.72, 462 | "origincontinent": "United States", 463 | "destinationcontinent": "Africa", 464 | "callergender": "Female", 465 | "recipientgender": "Female" 466 | }, 467 | { 468 | "caller": "Ida", 469 | "start": "27.09.2013 02:39:49", 470 | "end": "27.09.2013 03:39:49", 471 | "duration": 60, 472 | "recipient": "Jayden", 473 | "origin": "Maine", 474 | "destination": "Kiribati", 475 | "ratepermin": 1.16, 476 | "costs": 69.6, 477 | "origincontinent": "United States", 478 | "destinationcontinent": "Oceania", 479 | "callergender": "Female", 480 | "recipientgender": "Male" 481 | }, 482 | { 483 | "caller": "Paul", 484 | "start": "27.09.2013 02:40:06", 485 | "end": "27.09.2013 03:44:06", 486 | "duration": 64, 487 | "recipient": "Frauke", 488 | "origin": "Montana", 489 | "destination": "French Antilles", 490 | "ratepermin": 0.62, 491 | "costs": 39.68, 492 | "origincontinent": "United States", 493 | "destinationcontinent": "South America", 494 | "callergender": "Male", 495 | "recipientgender": "Female" 496 | }, 497 | { 498 | "caller": "Clara", 499 | "start": "27.09.2013 02:42:13", 500 | "end": "27.09.2013 02:48:13", 501 | "duration": 6, 502 | "recipient": "Jordan", 503 | "origin": "Maryland", 504 | "destination": "Bosnia", 505 | "ratepermin": 0.34, 506 | "costs": 2.04, 507 | "origincontinent": "United States", 508 | "destinationcontinent": "Europe", 509 | "callergender": "Female", 510 | "recipientgender": "Male" 511 | }, 512 | { 513 | "caller": "Jonathan", 514 | "start": "27.09.2013 02:42:13", 515 | "end": "27.09.2013 04:14:13", 516 | "duration": 92, 517 | "recipient": "Diana", 518 | "origin": "West Virginia", 519 | "destination": "Falkland Islands", 520 | "ratepermin": 0.45, 521 | "costs": 41.4, 522 | "origincontinent": "United States", 523 | "destinationcontinent": "South America", 524 | "callergender": "Male", 525 | "recipientgender": "Female" 526 | }, 527 | { 528 | "caller": "Ida", 529 | "start": "27.09.2013 02:47:35", 530 | "end": "27.09.2013 03:54:35", 531 | "duration": 67, 532 | "recipient": "Ylvi", 533 | "origin": "Maine", 534 | "destination": "Anguilla", 535 | "ratepermin": 0.57, 536 | "costs": 38.19, 537 | "origincontinent": "United States", 538 | "destinationcontinent": "South America", 539 | "callergender": "Female", 540 | "recipientgender": "Female" 541 | }, 542 | { 543 | "caller": "Ida", 544 | "start": "27.09.2013 02:47:35", 545 | "end": "27.09.2013 04:23:35", 546 | "duration": 96, 547 | "recipient": "Ylvi", 548 | "origin": "Maine", 549 | "destination": "Anguilla", 550 | "ratepermin": 0.57, 551 | "costs": 54.72, 552 | "origincontinent": "United States", 553 | "destinationcontinent": "South America", 554 | "callergender": "Female", 555 | "recipientgender": "Female" 556 | }, 557 | { 558 | "caller": "Leni", 559 | "start": "27.09.2013 02:49:37", 560 | "end": "27.09.2013 04:19:37", 561 | "duration": 90, 562 | "recipient": "Henriette", 563 | "origin": "Missouri", 564 | "destination": "Zimbabwe", 565 | "ratepermin": 0.48, 566 | "costs": 43.2, 567 | "origincontinent": "United States", 568 | "destinationcontinent": "Africa", 569 | "callergender": "Male", 570 | "recipientgender": "Female" 571 | }, 572 | { 573 | "caller": "Leni", 574 | "start": "27.09.2013 02:51:42", 575 | "end": "27.09.2013 04:14:42", 576 | "duration": 83, 577 | "recipient": "Anni", 578 | "origin": "Missouri", 579 | "destination": "Cayman Islands", 580 | "ratepermin": 0.24, 581 | "costs": 19.92, 582 | "origincontinent": "United States", 583 | "destinationcontinent": "Oceania", 584 | "callergender": "Male", 585 | "recipientgender": "Female" 586 | }, 587 | { 588 | "caller": "Emma", 589 | "start": "27.09.2013 02:53:54", 590 | "end": "27.09.2013 04:14:54", 591 | "duration": 81, 592 | "recipient": "Diana", 593 | "origin": "New Jersey", 594 | "destination": "Falkland Islands", 595 | "ratepermin": 0.45, 596 | "costs": 36.45, 597 | "origincontinent": "United States", 598 | "destinationcontinent": "South America", 599 | "callergender": "Female", 600 | "recipientgender": "Female" 601 | }, 602 | { 603 | "caller": "Clara", 604 | "start": "27.09.2013 02:59:43", 605 | "end": "27.09.2013 04:02:43", 606 | "duration": 63, 607 | "recipient": "Jordan", 608 | "origin": "Maryland", 609 | "destination": "Bosnia", 610 | "ratepermin": 0.34, 611 | "costs": 21.42, 612 | "origincontinent": "United States", 613 | "destinationcontinent": "Europe", 614 | "callergender": "Female", 615 | "recipientgender": "Male" 616 | }, 617 | { 618 | "caller": "Jonathan", 619 | "start": "27.09.2013 02:59:43", 620 | "end": "27.09.2013 04:02:43", 621 | "duration": 63, 622 | "recipient": "Diana", 623 | "origin": "West Virginia", 624 | "destination": "Falkland Islands", 625 | "ratepermin": 0.45, 626 | "costs": 28.35, 627 | "origincontinent": "United States", 628 | "destinationcontinent": "South America", 629 | "callergender": "Male", 630 | "recipientgender": "Female" 631 | }, 632 | { 633 | "caller": "Paul", 634 | "start": "27.09.2013 03:07:57", 635 | "end": "27.09.2013 03:27:57", 636 | "duration": 20, 637 | "recipient": "Patrick", 638 | "origin": "Montana", 639 | "destination": "Colombia", 640 | "ratepermin": 0.19, 641 | "costs": 3.8, 642 | "origincontinent": "United States", 643 | "destinationcontinent": "South America", 644 | "callergender": "Male", 645 | "recipientgender": "Male" 646 | }, 647 | { 648 | "caller": "Paul", 649 | "start": "27.09.2013 03:08:45", 650 | "end": "27.09.2013 03:45:45", 651 | "duration": 37, 652 | "recipient": "Frauke", 653 | "origin": "Montana", 654 | "destination": "French Antilles", 655 | "ratepermin": 0.62, 656 | "costs": 22.94, 657 | "origincontinent": "United States", 658 | "destinationcontinent": "South America", 659 | "callergender": "Male", 660 | "recipientgender": "Female" 661 | }, 662 | { 663 | "caller": "Clara", 664 | "start": "27.09.2013 03:11:50", 665 | "end": "27.09.2013 04:50:50", 666 | "duration": 99, 667 | "recipient": "Jordan", 668 | "origin": "Maryland", 669 | "destination": "Bosnia", 670 | "ratepermin": 0.34, 671 | "costs": 33.66, 672 | "origincontinent": "United States", 673 | "destinationcontinent": "Europe", 674 | "callergender": "Female", 675 | "recipientgender": "Male" 676 | }, 677 | { 678 | "caller": "Jonathan", 679 | "start": "27.09.2013 03:11:50", 680 | "end": "27.09.2013 04:50:50", 681 | "duration": 99, 682 | "recipient": "Diana", 683 | "origin": "West Virginia", 684 | "destination": "Falkland Islands", 685 | "ratepermin": 0.45, 686 | "costs": 44.55, 687 | "origincontinent": "United States", 688 | "destinationcontinent": "South America", 689 | "callergender": "Male", 690 | "recipientgender": "Female" 691 | }, 692 | { 693 | "caller": "Paul", 694 | "start": "27.09.2013 03:16:09", 695 | "end": "27.09.2013 03:45:09", 696 | "duration": 29, 697 | "recipient": "Elif", 698 | "origin": "Montana", 699 | "destination": "Burundi", 700 | "ratepermin": 1.39, 701 | "costs": 40.31, 702 | "origincontinent": "United States", 703 | "destinationcontinent": "Africa", 704 | "callergender": "Male", 705 | "recipientgender": "Female" 706 | }, 707 | { 708 | "caller": "Ben", 709 | "start": "27.09.2013 03:16:17", 710 | "end": "27.09.2013 04:09:17", 711 | "duration": 53, 712 | "recipient": "Talea", 713 | "origin": "South Dakota", 714 | "destination": "Denmark", 715 | "ratepermin": 0.08, 716 | "costs": 4.24, 717 | "origincontinent": "United States", 718 | "destinationcontinent": "Europe", 719 | "callergender": "Male", 720 | "recipientgender": "Female" 721 | }, 722 | { 723 | "caller": "Paul", 724 | "start": "27.09.2013 03:47:32", 725 | "end": "27.09.2013 04:11:32", 726 | "duration": 24, 727 | "recipient": "Stina", 728 | "origin": "Montana", 729 | "destination": "Mexico", 730 | "ratepermin": 0.07, 731 | "costs": 1.68, 732 | "origincontinent": "United States", 733 | "destinationcontinent": "North America", 734 | "callergender": "Male", 735 | "recipientgender": "Female" 736 | }, 737 | { 738 | "caller": "Ben", 739 | "start": "27.09.2013 05:08:06", 740 | "end": "27.09.2013 06:28:06", 741 | "duration": 80, 742 | "recipient": "Hugo", 743 | "origin": "South Dakota", 744 | "destination": "Zimbabwe", 745 | "ratepermin": 0.48, 746 | "costs": 38.4, 747 | "origincontinent": "United States", 748 | "destinationcontinent": "Africa", 749 | "callergender": "Male", 750 | "recipientgender": "Male" 751 | }, 752 | { 753 | "caller": "Ben", 754 | "start": "27.09.2013 05:24:38", 755 | "end": "27.09.2013 05:35:38", 756 | "duration": 11, 757 | "recipient": "Diana", 758 | "origin": "South Dakota", 759 | "destination": "Falkland Islands", 760 | "ratepermin": 0.45, 761 | "costs": 4.95, 762 | "origincontinent": "United States", 763 | "destinationcontinent": "South America", 764 | "callergender": "Male", 765 | "recipientgender": "Female" 766 | }, 767 | { 768 | "caller": "Ben", 769 | "start": "27.09.2013 11:41:36", 770 | "end": "27.09.2013 12:17:36", 771 | "duration": 36, 772 | "recipient": "Diana", 773 | "origin": "South Dakota", 774 | "destination": "Falkland Islands", 775 | "ratepermin": 0.45, 776 | "costs": 16.2, 777 | "origincontinent": "United States", 778 | "destinationcontinent": "South America", 779 | "callergender": "Male", 780 | "recipientgender": "Female" 781 | }, 782 | { 783 | "caller": "Ben", 784 | "start": "27.09.2013 11:44:26", 785 | "end": "27.09.2013 12:17:26", 786 | "duration": 33, 787 | "recipient": "Diana", 788 | "origin": "South Dakota", 789 | "destination": "Falkland Islands", 790 | "ratepermin": 0.45, 791 | "costs": 14.85, 792 | "origincontinent": "United States", 793 | "destinationcontinent": "South America", 794 | "callergender": "Male", 795 | "recipientgender": "Female" 796 | }, 797 | { 798 | "caller": "Ben", 799 | "start": "27.09.2013 11:46:02", 800 | "end": "27.09.2013 12:00:02", 801 | "duration": 14, 802 | "recipient": "Diana", 803 | "origin": "South Dakota", 804 | "destination": "Falkland Islands", 805 | "ratepermin": 0.45, 806 | "costs": 6.3, 807 | "origincontinent": "United States", 808 | "destinationcontinent": "South America", 809 | "callergender": "Male", 810 | "recipientgender": "Female" 811 | }, 812 | { 813 | "caller": "Leni", 814 | "start": "27.09.2013 13:35:27", 815 | "end": "27.09.2013 13:58:27", 816 | "duration": 23, 817 | "recipient": "Medina", 818 | "origin": "Missouri", 819 | "destination": "Hong Kong", 820 | "ratepermin": 0.09, 821 | "costs": 2.07, 822 | "origincontinent": "United States", 823 | "destinationcontinent": "Asia", 824 | "callergender": "Male", 825 | "recipientgender": "Female" 826 | }, 827 | { 828 | "caller": "Ida", 829 | "start": "27.09.2013 13:37:29", 830 | "end": "27.09.2013 14:09:29", 831 | "duration": 32, 832 | "recipient": "Valentin", 833 | "origin": "Maine", 834 | "destination": "Pakistan", 835 | "ratepermin": 0.36, 836 | "costs": 11.52, 837 | "origincontinent": "United States", 838 | "destinationcontinent": "Asia", 839 | "callergender": "Female", 840 | "recipientgender": "Male" 841 | }, 842 | { 843 | "caller": "Ida", 844 | "start": "27.09.2013 13:37:29", 845 | "end": "27.09.2013 14:53:29", 846 | "duration": 76, 847 | "recipient": "Valentin", 848 | "origin": "Maine", 849 | "destination": "Pakistan", 850 | "ratepermin": 0.36, 851 | "costs": 27.36, 852 | "origincontinent": "United States", 853 | "destinationcontinent": "Asia", 854 | "callergender": "Female", 855 | "recipientgender": "Male" 856 | }, 857 | { 858 | "caller": "Leni", 859 | "start": "27.09.2013 13:44:47", 860 | "end": "27.09.2013 13:59:47", 861 | "duration": 15, 862 | "recipient": "Medina", 863 | "origin": "Missouri", 864 | "destination": "Hong Kong", 865 | "ratepermin": 0.09, 866 | "costs": 1.35, 867 | "origincontinent": "United States", 868 | "destinationcontinent": "Asia", 869 | "callergender": "Male", 870 | "recipientgender": "Female" 871 | }, 872 | { 873 | "caller": "Leni", 874 | "start": "27.09.2013 13:58:06", 875 | "end": "27.09.2013 14:50:06", 876 | "duration": 52, 877 | "recipient": "Christopher", 878 | "origin": "Missouri", 879 | "destination": "Germany", 880 | "ratepermin": 0.07, 881 | "costs": 3.64, 882 | "origincontinent": "United States", 883 | "destinationcontinent": "Europe", 884 | "callergender": "Male", 885 | "recipientgender": "Male" 886 | }, 887 | { 888 | "caller": "Clara", 889 | "start": "27.09.2013 14:06:40", 890 | "end": "27.09.2013 14:53:40", 891 | "duration": 47, 892 | "recipient": "Jordan", 893 | "origin": "Maryland", 894 | "destination": "Bosnia", 895 | "ratepermin": 0.34, 896 | "costs": 15.98, 897 | "origincontinent": "United States", 898 | "destinationcontinent": "Europe", 899 | "callergender": "Female", 900 | "recipientgender": "Male" 901 | }, 902 | { 903 | "caller": "Jonathan", 904 | "start": "27.09.2013 14:06:40", 905 | "end": "27.09.2013 14:53:40", 906 | "duration": 47, 907 | "recipient": "Diana", 908 | "origin": "West Virginia", 909 | "destination": "Falkland Islands", 910 | "ratepermin": 0.45, 911 | "costs": 21.15, 912 | "origincontinent": "United States", 913 | "destinationcontinent": "South America", 914 | "callergender": "Male", 915 | "recipientgender": "Female" 916 | }, 917 | { 918 | "caller": "Leni", 919 | "start": "27.09.2013 14:08:34", 920 | "end": "27.09.2013 14:55:34", 921 | "duration": 47, 922 | "recipient": "Christopher", 923 | "origin": "Missouri", 924 | "destination": "Germany", 925 | "ratepermin": 0.07, 926 | "costs": 3.29, 927 | "origincontinent": "United States", 928 | "destinationcontinent": "Europe", 929 | "callergender": "Male", 930 | "recipientgender": "Male" 931 | }, 932 | { 933 | "caller": "Leni", 934 | "start": "27.09.2013 14:08:42", 935 | "end": "27.09.2013 14:33:42", 936 | "duration": 25, 937 | "recipient": "Medina", 938 | "origin": "Missouri", 939 | "destination": "Hong Kong", 940 | "ratepermin": 0.09, 941 | "costs": 2.25, 942 | "origincontinent": "United States", 943 | "destinationcontinent": "Asia", 944 | "callergender": "Male", 945 | "recipientgender": "Female" 946 | }, 947 | { 948 | "caller": "Leni", 949 | "start": "27.09.2013 14:13:24", 950 | "end": "27.09.2013 14:44:24", 951 | "duration": 31, 952 | "recipient": "Medina", 953 | "origin": "Missouri", 954 | "destination": "Hong Kong", 955 | "ratepermin": 0.09, 956 | "costs": 2.79, 957 | "origincontinent": "United States", 958 | "destinationcontinent": "Asia", 959 | "callergender": "Male", 960 | "recipientgender": "Female" 961 | }, 962 | { 963 | "caller": "Leni", 964 | "start": "27.09.2013 14:15:51", 965 | "end": "27.09.2013 14:25:51", 966 | "duration": 10, 967 | "recipient": "Diana", 968 | "origin": "Missouri", 969 | "destination": "Falkland Islands", 970 | "ratepermin": 0.45, 971 | "costs": 4.5, 972 | "origincontinent": "United States", 973 | "destinationcontinent": "South America", 974 | "callergender": "Male", 975 | "recipientgender": "Female" 976 | }, 977 | { 978 | "caller": "Clara", 979 | "start": "27.09.2013 15:16:24", 980 | "end": "27.09.2013 17:00:24", 981 | "duration": 104, 982 | "recipient": "Jordan", 983 | "origin": "Maryland", 984 | "destination": "Bosnia", 985 | "ratepermin": 0.34, 986 | "costs": 35.36, 987 | "origincontinent": "United States", 988 | "destinationcontinent": "Europe", 989 | "callergender": "Female", 990 | "recipientgender": "Male" 991 | }, 992 | { 993 | "caller": "Jonathan", 994 | "start": "27.09.2013 15:16:24", 995 | "end": "27.09.2013 15:36:24", 996 | "duration": 20, 997 | "recipient": "Diana", 998 | "origin": "West Virginia", 999 | "destination": "Falkland Islands", 1000 | "ratepermin": 0.45, 1001 | "costs": 9, 1002 | "origincontinent": "United States", 1003 | "destinationcontinent": "South America", 1004 | "callergender": "Male", 1005 | "recipientgender": "Female" 1006 | }, 1007 | { 1008 | "caller": "Ben", 1009 | "start": "27.09.2013 16:45:27", 1010 | "end": "27.09.2013 17:16:27", 1011 | "duration": 31, 1012 | "recipient": "Diana", 1013 | "origin": "South Dakota", 1014 | "destination": "Falkland Islands", 1015 | "ratepermin": 0.45, 1016 | "costs": 13.95, 1017 | "origincontinent": "United States", 1018 | "destinationcontinent": "South America", 1019 | "callergender": "Male", 1020 | "recipientgender": "Female" 1021 | }, 1022 | { 1023 | "caller": "Leni", 1024 | "start": "27.09.2013 16:56:35", 1025 | "end": "27.09.2013 17:04:35", 1026 | "duration": 8, 1027 | "recipient": "Medina", 1028 | "origin": "Missouri", 1029 | "destination": "Hong Kong", 1030 | "ratepermin": 0.09, 1031 | "costs": 0.72, 1032 | "origincontinent": "United States", 1033 | "destinationcontinent": "Asia", 1034 | "callergender": "Male", 1035 | "recipientgender": "Female" 1036 | }, 1037 | { 1038 | "caller": "Leni", 1039 | "start": "27.09.2013 16:57:51", 1040 | "end": "27.09.2013 17:04:51", 1041 | "duration": 7, 1042 | "recipient": "Medina", 1043 | "origin": "Missouri", 1044 | "destination": "Hong Kong", 1045 | "ratepermin": 0.09, 1046 | "costs": 0.63, 1047 | "origincontinent": "United States", 1048 | "destinationcontinent": "Asia", 1049 | "callergender": "Male", 1050 | "recipientgender": "Female" 1051 | }, 1052 | { 1053 | "caller": "Leni", 1054 | "start": "27.09.2013 17:17:28", 1055 | "end": "27.09.2013 18:54:28", 1056 | "duration": 97, 1057 | "recipient": "Medina", 1058 | "origin": "Missouri", 1059 | "destination": "Hong Kong", 1060 | "ratepermin": 0.09, 1061 | "costs": 8.73, 1062 | "origincontinent": "United States", 1063 | "destinationcontinent": "Asia", 1064 | "callergender": "Male", 1065 | "recipientgender": "Female" 1066 | }, 1067 | { 1068 | "caller": "Clara", 1069 | "start": "27.09.2013 17:27:58", 1070 | "end": "27.09.2013 18:07:58", 1071 | "duration": 40, 1072 | "recipient": "Jordan", 1073 | "origin": "Maryland", 1074 | "destination": "Bosnia", 1075 | "ratepermin": 0.34, 1076 | "costs": 13.6, 1077 | "origincontinent": "United States", 1078 | "destinationcontinent": "Europe", 1079 | "callergender": "Female", 1080 | "recipientgender": "Male" 1081 | }, 1082 | { 1083 | "caller": "Jonathan", 1084 | "start": "27.09.2013 17:27:58", 1085 | "end": "27.09.2013 19:18:58", 1086 | "duration": 111, 1087 | "recipient": "Diana", 1088 | "origin": "West Virginia", 1089 | "destination": "Falkland Islands", 1090 | "ratepermin": 0.45, 1091 | "costs": 49.95, 1092 | "origincontinent": "United States", 1093 | "destinationcontinent": "South America", 1094 | "callergender": "Male", 1095 | "recipientgender": "Female" 1096 | }, 1097 | { 1098 | "caller": "Leni", 1099 | "start": "27.09.2013 17:48:25", 1100 | "end": "27.09.2013 18:18:25", 1101 | "duration": 30, 1102 | "recipient": "Diana", 1103 | "origin": "Missouri", 1104 | "destination": "Falkland Islands", 1105 | "ratepermin": 0.45, 1106 | "costs": 13.5, 1107 | "origincontinent": "United States", 1108 | "destinationcontinent": "South America", 1109 | "callergender": "Male", 1110 | "recipientgender": "Female" 1111 | }, 1112 | { 1113 | "caller": "Ben", 1114 | "start": "27.09.2013 18:59:24", 1115 | "end": "27.09.2013 19:57:24", 1116 | "duration": 58, 1117 | "recipient": "Adelina", 1118 | "origin": "South Dakota", 1119 | "destination": "Mauritania", 1120 | "ratepermin": 0.98, 1121 | "costs": 56.84, 1122 | "origincontinent": "United States", 1123 | "destinationcontinent": "Africa", 1124 | "callergender": "Male", 1125 | "recipientgender": "Female" 1126 | }, 1127 | { 1128 | "caller": "Leni", 1129 | "start": "27.09.2013 19:01:19", 1130 | "end": "27.09.2013 19:07:19", 1131 | "duration": 6, 1132 | "recipient": "Medina", 1133 | "origin": "Missouri", 1134 | "destination": "Hong Kong", 1135 | "ratepermin": 0.09, 1136 | "costs": 0.54, 1137 | "origincontinent": "United States", 1138 | "destinationcontinent": "Asia", 1139 | "callergender": "Male", 1140 | "recipientgender": "Female" 1141 | }, 1142 | { 1143 | "caller": "Leni", 1144 | "start": "27.09.2013 19:01:31", 1145 | "end": "27.09.2013 19:34:31", 1146 | "duration": 33, 1147 | "recipient": "Medina", 1148 | "origin": "Missouri", 1149 | "destination": "Hong Kong", 1150 | "ratepermin": 0.09, 1151 | "costs": 2.97, 1152 | "origincontinent": "United States", 1153 | "destinationcontinent": "Asia", 1154 | "callergender": "Male", 1155 | "recipientgender": "Female" 1156 | }, 1157 | { 1158 | "caller": "Max", 1159 | "start": "27.09.2013 19:14:35", 1160 | "end": "27.09.2013 19:52:35", 1161 | "duration": 38, 1162 | "recipient": "Diana", 1163 | "origin": "Ohio", 1164 | "destination": "Falkland Islands", 1165 | "ratepermin": 0.45, 1166 | "costs": 17.1, 1167 | "origincontinent": "United States", 1168 | "destinationcontinent": "South America", 1169 | "callergender": "Male", 1170 | "recipientgender": "Female" 1171 | }, 1172 | { 1173 | "caller": "Ida", 1174 | "start": "27.09.2013 19:51:48", 1175 | "end": "27.09.2013 20:36:48", 1176 | "duration": 45, 1177 | "recipient": "Soraya", 1178 | "origin": "Maine", 1179 | "destination": "Algeria", 1180 | "ratepermin": 0.48, 1181 | "costs": 21.6, 1182 | "origincontinent": "United States", 1183 | "destinationcontinent": "Africa", 1184 | "callergender": "Female", 1185 | "recipientgender": "Female" 1186 | }, 1187 | { 1188 | "caller": "Ida", 1189 | "start": "27.09.2013 19:51:48", 1190 | "end": "27.09.2013 21:04:48", 1191 | "duration": 73, 1192 | "recipient": "Soraya", 1193 | "origin": "Maine", 1194 | "destination": "Algeria", 1195 | "ratepermin": 0.48, 1196 | "costs": 35.04, 1197 | "origincontinent": "United States", 1198 | "destinationcontinent": "Africa", 1199 | "callergender": "Female", 1200 | "recipientgender": "Female" 1201 | }, 1202 | { 1203 | "caller": "Ida", 1204 | "start": "27.09.2013 20:18:09", 1205 | "end": "27.09.2013 20:21:09", 1206 | "duration": 3, 1207 | "recipient": "Elaine", 1208 | "origin": "Maine", 1209 | "destination": "Laos", 1210 | "ratepermin": 2.39, 1211 | "costs": 7.17, 1212 | "origincontinent": "United States", 1213 | "destinationcontinent": "Asia", 1214 | "callergender": "Female", 1215 | "recipientgender": "Female" 1216 | }, 1217 | { 1218 | "caller": "Ida", 1219 | "start": "27.09.2013 20:18:09", 1220 | "end": "27.09.2013 21:30:09", 1221 | "duration": 72, 1222 | "recipient": "Elaine", 1223 | "origin": "Maine", 1224 | "destination": "Laos", 1225 | "ratepermin": 2.39, 1226 | "costs": 172.08, 1227 | "origincontinent": "United States", 1228 | "destinationcontinent": "Asia", 1229 | "callergender": "Female", 1230 | "recipientgender": "Female" 1231 | }, 1232 | { 1233 | "caller": "Emma", 1234 | "start": "27.09.2013 20:37:51", 1235 | "end": "27.09.2013 21:20:51", 1236 | "duration": 43, 1237 | "recipient": "Diana", 1238 | "origin": "New Jersey", 1239 | "destination": "Falkland Islands", 1240 | "ratepermin": 0.45, 1241 | "costs": 19.35, 1242 | "origincontinent": "United States", 1243 | "destinationcontinent": "South America", 1244 | "callergender": "Female", 1245 | "recipientgender": "Female" 1246 | }, 1247 | { 1248 | "caller": "Emma", 1249 | "start": "27.09.2013 21:20:14", 1250 | "end": "27.09.2013 23:15:14", 1251 | "duration": 115, 1252 | "recipient": "Diana", 1253 | "origin": "New Jersey", 1254 | "destination": "Falkland Islands", 1255 | "ratepermin": 0.45, 1256 | "costs": 51.75, 1257 | "origincontinent": "United States", 1258 | "destinationcontinent": "South America", 1259 | "callergender": "Female", 1260 | "recipientgender": "Female" 1261 | }, 1262 | { 1263 | "caller": "Leni", 1264 | "start": "27.09.2013 21:22:15", 1265 | "end": "27.09.2013 21:29:15", 1266 | "duration": 7, 1267 | "recipient": "Medina", 1268 | "origin": "Missouri", 1269 | "destination": "Hong Kong", 1270 | "ratepermin": 0.09, 1271 | "costs": 0.63, 1272 | "origincontinent": "United States", 1273 | "destinationcontinent": "Asia", 1274 | "callergender": "Male", 1275 | "recipientgender": "Female" 1276 | }, 1277 | { 1278 | "caller": "Leni", 1279 | "start": "27.09.2013 21:22:29", 1280 | "end": "27.09.2013 21:27:29", 1281 | "duration": 5, 1282 | "recipient": "Medina", 1283 | "origin": "Missouri", 1284 | "destination": "Hong Kong", 1285 | "ratepermin": 0.09, 1286 | "costs": 0.45, 1287 | "origincontinent": "United States", 1288 | "destinationcontinent": "Asia", 1289 | "callergender": "Male", 1290 | "recipientgender": "Female" 1291 | }, 1292 | { 1293 | "caller": "Clara", 1294 | "start": "27.09.2013 21:48:50", 1295 | "end": "27.09.2013 21:57:50", 1296 | "duration": 9, 1297 | "recipient": "Jordan", 1298 | "origin": "Maryland", 1299 | "destination": "Bosnia", 1300 | "ratepermin": 0.34, 1301 | "costs": 3.06, 1302 | "origincontinent": "United States", 1303 | "destinationcontinent": "Europe", 1304 | "callergender": "Female", 1305 | "recipientgender": "Male" 1306 | }, 1307 | { 1308 | "caller": "Jonathan", 1309 | "start": "27.09.2013 21:48:50", 1310 | "end": "27.09.2013 22:23:50", 1311 | "duration": 35, 1312 | "recipient": "Diana", 1313 | "origin": "West Virginia", 1314 | "destination": "Falkland Islands", 1315 | "ratepermin": 0.45, 1316 | "costs": 15.75, 1317 | "origincontinent": "United States", 1318 | "destinationcontinent": "South America", 1319 | "callergender": "Male", 1320 | "recipientgender": "Female" 1321 | }, 1322 | { 1323 | "caller": "Leni", 1324 | "start": "27.09.2013 23:47:49", 1325 | "end": "28.09.2013 00:16:49", 1326 | "duration": 29, 1327 | "recipient": "Anthony", 1328 | "origin": "Missouri", 1329 | "destination": "Portugal", 1330 | "ratepermin": 0.09, 1331 | "costs": 2.61, 1332 | "origincontinent": "United States", 1333 | "destinationcontinent": "Europe", 1334 | "callergender": "Male", 1335 | "recipientgender": "Male" 1336 | } 1337 | ] -------------------------------------------------------------------------------- /resources/simple-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmeisen/js-gantt/6aff3eb02aae43ded30b11ee8e000e383dbd419d/resources/simple-example.png -------------------------------------------------------------------------------- /server/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | unnecessary 7 | unnecessary 8 | 1.0.0 9 | Sample Server 10 | POM for the Sample Server Providing some Data 11 | zip 12 | 13 | 14 | 15 | net.meisen.general 16 | net-meisen-general-server-http-listener 17 | TRUNK-SNAPSHOT 18 | 19 | 20 | -------------------------------------------------------------------------------- /server/resources/data.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Following status can be used (full list see org.apache.http.HttpStatus): 3 | * - org.apache.http.HttpStatus.SC_UNAUTHORIZED 4 | * - org.apache.http.HttpStatus.SC_OK 5 | * - org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED 6 | */ 7 | var status = Packages.org.apache.http.HttpStatus.SC_OK; 8 | 9 | var pad = function(n){ 10 | return n < 10 ? '0' + n : n 11 | }; 12 | 13 | var readFile = function(file, encoding, separator, limit) { 14 | var records = []; 15 | var names = []; 16 | 17 | var reader = null; 18 | try { 19 | reader = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)) 20 | var line = null; 21 | var first = true; 22 | do { 23 | do { 24 | line = reader.readLine(); 25 | } while (line != null && line.trim().isEmpty()); 26 | 27 | // stop if we got enough or if no more lines are available 28 | if (line == null || (!first && limit <= records.length)) { 29 | break; 30 | } 31 | 32 | // parse the line 33 | var data = line.split(separator); 34 | var record = []; 35 | for (var i = 0; i < data.length; i++) { 36 | var value; 37 | 38 | var date = Packages.net.meisen.general.genmisc.types.Dates.isDate(data[i], Packages.net.meisen.general.genmisc.types.Dates.GENERAL_TIMEZONE); 39 | if (date != null) { 40 | value = date; 41 | } else { 42 | value = '' + data[i]; 43 | 44 | if(value.match(/^\d+$/)){ 45 | value = parseInt(value); 46 | } else if (value.match(/^\d+\.\d+$/)) { 47 | value = parseFloat(value); 48 | } 49 | } 50 | record.push(value); 51 | } 52 | 53 | if (first) { 54 | names = record; 55 | first = false; 56 | } else { 57 | records.push(record); 58 | } 59 | } while (line != null); 60 | } catch (e) { 61 | names = []; 62 | records = []; 63 | } finally { 64 | if (reader != null) { 65 | reader.close(); 66 | } 67 | } 68 | 69 | return { names: names, data: records }; 70 | }; 71 | 72 | var transformToJson = function(object) { 73 | var json; 74 | 75 | if (object == null) { 76 | json = 'null'; 77 | } else if (typeof object === 'boolean') { 78 | json = '' + object; 79 | } else if (typeof object === 'number') { 80 | json = '' + object; 81 | } else if (typeof object === 'string') { 82 | json = '"' + object + '"'; 83 | } else if (typeof object === 'object' && (object instanceof Date)) { 84 | json = '"' + object.getUTCFullYear() + '-' + 85 | pad(object.getUTCMonth() + 1) + '-' + 86 | pad(object.getUTCDate())+'T' + 87 | pad(object.getUTCHours()) + ':' + 88 | pad(object.getUTCMinutes()) + ':' + 89 | pad(object.getUTCSeconds()) + 'Z' + '"'; 90 | } else if (object instanceof java.util.Date) { 91 | json = transformToJson(new Date(object.getTime())); 92 | } else if (typeof object === 'object' && (object instanceof Array)) { 93 | var separator = ''; 94 | 95 | json = '['; 96 | for(var key in object) { 97 | json += separator + transformToJson(object[key]); 98 | separator = ','; 99 | } 100 | json += ']'; 101 | } else if (typeof object === 'object') { 102 | var separator = ''; 103 | 104 | json = '{'; 105 | for(var key in object) { 106 | json += separator + '"' + key + '":'; 107 | json += transformToJson(object[key]); 108 | separator = ','; 109 | } 110 | json += '}'; 111 | } else { 112 | json = object; 113 | } 114 | 115 | return json; 116 | }; 117 | 118 | var parameters = Packages.net.meisen.general.server.http.listener.util.RequestHandlingUtilities.parseParameter(request); 119 | 120 | // determine the data to be send 121 | var type = parameters.get('type'); 122 | 123 | var result; 124 | if ('file'.equals(type)) { 125 | var file = parameters.get('file'); 126 | var encoding = parameters.get('encoding'); 127 | var separator = parameters.get('separator'); 128 | var limit = parameters.get('limit'); 129 | encoding = encoding == null ? 'UTF8' : encoding; 130 | separator = separator == null ? ',' : separator; 131 | limit = limit == null ? 100 : limit; 132 | 133 | result = readFile(file, encoding, separator, limit); 134 | } else if ('fail'.equals(type)) { 135 | status = Packages.org.apache.http.HttpStatus.SC_UNAUTHORIZED; 136 | } else { 137 | var names = ['Start', 'End', 'Integer', 'String']; 138 | var data = []; 139 | 140 | data.push([ new Date(Date.UTC(1981, 0, 20, 8, 0, 0)), new Date(Date.UTC(1981,0, 20, 8, 7, 0)), 500, 'Test' ]); 141 | 142 | result = { names: names, data: data }; 143 | } 144 | 145 | // create the answer 146 | var entity = new Packages.org.apache.http.entity.StringEntity(transformToJson(result), Packages.org.apache.http.entity.ContentType.create("text/html", "UTF-8")); 147 | entity.setContentEncoding('UTF-8'); 148 | response.setEntity(entity); 149 | response.setHeader("Access-Control-Allow-Origin", "*"); 150 | response.setHeader("Access-Control-Allow-Methods", "*"); 151 | response.setHeader("Access-Control-Allow-Headers", "Content-Type"); 152 | 153 | // set the status 154 | response.setStatusCode(status); -------------------------------------------------------------------------------- /server/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=TRACE, stdout 3 | # Redirect log messages to console 4 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 5 | log4j.appender.stdout.Target=System.out 6 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 7 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n -------------------------------------------------------------------------------- /server/resources/sample.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmeisen/js-gantt/6aff3eb02aae43ded30b11ee8e000e383dbd419d/server/resources/sample.csv -------------------------------------------------------------------------------- /server/resources/serverSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | net.meisen.general.server.http.listener.servlets.ScriptedServlet 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/ExampleApp.js: -------------------------------------------------------------------------------- 1 | // define the baseUrl 2 | requirejs.config({ 3 | 4 | // define the baseUrl defined by the processenabler 5 | baseUrl: 'scripts' 6 | }); 7 | 8 | // now start the entry-point 9 | require(['jquery', 'net/meisen/general/date/DateLibrary', 'net/meisen/ui/gantt/GanttChart'], function ($, datelib) { 10 | var chartFixedData1 = $("#chartFixedData1").ganttChart({ 11 | data: { 12 | names: ['start', 'end', 'value', 'number'], 13 | records: [ 14 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 2, 0, 0), '2 Hours', 120], 15 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 4, 0, 0), '4 Hours', 240], 16 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 6, 0, 0), '6 Hours', 360], 17 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 8, 0, 0), '8 Hours', 480], 18 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 10, 0, 0), '10 Hours', 600], 19 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 12, 0, 0), '12 Hours', 720], 20 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 23, 59, 0), 'All Day', 1439] 21 | ], 22 | mapper: { 23 | startname: 'start', 24 | endname: 'end', 25 | tooltip: ['number', 'start'] 26 | }, 27 | timeaxis: { 28 | end: datelib.modifyUTC(datelib.createUTC(null, null, null, 23, 59, 0), 1, 'd'), 29 | granularity: 'mi' 30 | } 31 | }, 32 | illustrator: { 33 | config: { 34 | axis: { 35 | tickInterval: 120 36 | }, 37 | view: { 38 | showGrid: false, 39 | showPositionMarker: false, 40 | theme: { 41 | intervalColor: '#f7a35c', 42 | intervalHeight: 40, 43 | intervalBorderSize: 1 44 | } 45 | } 46 | } 47 | } 48 | }); 49 | 50 | var rndSize = 900; 51 | chartFixedData1.ganttChart().resize(rndSize, 300); 52 | setInterval(function () { 53 | rndSize = Math.max(700, Math.random() * 1200); 54 | }, 5000); 55 | $(window).resize(function () { 56 | chartFixedData1.ganttChart().resize(rndSize, 300); 57 | }); 58 | 59 | var chartFixedData2 = $("#chartFixedData2").ganttChart({ 60 | data: { 61 | names: ['start', 'end', 'value', 'number'], 62 | records: [ 63 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 2, 0, 0), '2 Hours', 120], 64 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 4, 0, 0), '4 Hours', 240], 65 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 6, 0, 0), '6 Hours', 360], 66 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 8, 0, 0), '8 Hours', 480], 67 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 10, 0, 0), '10 Hours', 600], 68 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 12, 0, 0), '12 Hours', 720], 69 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 23, 59, 0), 'All Day', 1439] 70 | ], 71 | mapper: { 72 | startname: 'start', 73 | endname: 'end', 74 | tooltip: ['number', 'start', 'end'] 75 | }, 76 | timeaxis: { 77 | end: datelib.modifyUTC(datelib.createUTC(null, null, null, 23, 59, 0), 1, 'd'), 78 | granularity: 'mi' 79 | } 80 | }, 81 | illustrator: { 82 | config: { 83 | axis: { 84 | tickInterval: 120 85 | }, 86 | view: { 87 | showGrid: false, 88 | showPositionMarker: false, 89 | tooltip: function (interval) { 90 | return 'Some marks: ?ßgGÁ\nSome marks: ?ßgGÁ\n{1|number|#.00}\n{2|date|HH:mm:ss} - {3|date|HH:mm:ss}\n' + interval.get('_raw')[2]; 91 | }, 92 | theme: { 93 | intervalColor: '#f7a35c', 94 | intervalHeight: 40, 95 | intervalBorderSize: 1 96 | } 97 | } 98 | } 99 | } 100 | }); 101 | chartFixedData2.resize(1200, 300); 102 | 103 | var chartFixedData3 = $("#chartFixedData3").ganttChart({ 104 | data: { 105 | names: ['start', 'end', 'value', 'number'], 106 | records: [ 107 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 2, 0, 0), '2 Hours', 120], 108 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 4, 0, 0), '4 Hours', 240], 109 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 6, 0, 0), '6 Hours', 360], 110 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 8, 0, 0), '8 Hours', 480], 111 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 10, 0, 0), '10 Hours', 600], 112 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 12, 0, 0), '12 Hours', 720], 113 | [datelib.createUTC(null, null, null, 0, 0, 0), datelib.createUTC(null, null, null, 23, 59, 0), 'All Day', 1439] 114 | ], 115 | mapper: { 116 | startname: 'start', 117 | endname: 'end', 118 | tooltip: ['number', 'start', 'end'] 119 | }, 120 | timeaxis: { 121 | end: datelib.modifyUTC(datelib.createUTC(null, null, null, 23, 59, 0), 1, 'd'), 122 | granularity: 'mi' 123 | } 124 | }, 125 | illustrator: { 126 | config: { 127 | axis: { 128 | tickInterval: 120 129 | }, 130 | view: { 131 | showGrid: false, 132 | showPositionMarker: false, 133 | tooltip: '{1}\nStart: {2|date|HH:mm:ss}, End: {3|date|HH:mm:ss}', 134 | theme: { 135 | intervalColor: '#f7a35c', 136 | intervalHeight: 40, 137 | intervalBorderSize: 1, 138 | tooltipSize: 25 139 | } 140 | } 141 | } 142 | } 143 | }); 144 | chartFixedData3.resize(1200, 300); 145 | 146 | var chartLoadedData = $("#chartLoadedData").ganttChart({ 147 | data: { 148 | url: 'http://localhost:19999/data?type=file&file=resources/sample.csv&separator=;&limit=10000', 149 | postProcessor: function (data) { 150 | for (var i = 0; i < data.data.length; i++) { 151 | var record = data.data[i]; 152 | record[1] = datelib.parseISO8601(record[1]); 153 | record[2] = datelib.parseISO8601(record[2]); 154 | } 155 | 156 | return {names: data.names, records: data.data}; 157 | }, 158 | mapper: { 159 | startname: 'start', 160 | endname: 'end', 161 | group: ['callergender', 'recipientgender'], 162 | label: ['callergender', 'start', 'caller'], 163 | tooltip: ['caller', 'recipient', 'start', 'end'] 164 | }, 165 | timeaxis: { 166 | start: datelib.createUTC(2013, 9, 1), 167 | end: datelib.createUTC(2014, 12, 31, 23, 59, 0), 168 | granularity: 'mi' 169 | } 170 | }, 171 | illustrator: { 172 | config: { 173 | axis: { 174 | viewSize: 1440, 175 | tickInterval: 360 176 | }, 177 | view: { 178 | showBorder: false, 179 | tooltip: '{1} called {2}\non the {3|date|dd.MM.yyyy} ({3|date|HH:mm} - {4|date|HH:mm})', 180 | coloring: { 181 | groupMapping: { 182 | '["Female","Female"]': '#7cb5ec', 183 | '["Male","Female"]': '#434348', 184 | '["Female","Male"]': '#f7a35c', 185 | '["Male","Male"]': '#90ed7d' 186 | } 187 | }, 188 | theme: { 189 | backgroundColor: '#EEEEEE', 190 | intervalColor: '#444444', 191 | intervalHeight: 10, 192 | intervalBorderSize: 0 193 | } 194 | } 195 | } 196 | } 197 | }); 198 | chartLoadedData.resize(800, 300); 199 | 200 | var chartFailData = $("#chartFailData").ganttChart({ 201 | data: { 202 | url: 'http://localhost:19999/data?type=fail', 203 | } 204 | }); 205 | chartFailData.resize(800, 300); 206 | 207 | var chartEmptyData1 = $("#chartEmptyData1").ganttChart({ 208 | data: { 209 | url: 'http://localhost:19999/data?type=file&file=resources/sample.csv&separator=;&limit=0', 210 | postProcessor: function (data) { 211 | return {names: data.names, records: data.data}; 212 | } 213 | } 214 | }); 215 | chartEmptyData1.resize(800, 300); 216 | 217 | var chartEmptyData2 = $("#chartEmptyData2").ganttChart({ 218 | data: { 219 | names: ['start', 'end'], 220 | mapper: { 221 | startname: 'start', 222 | endname: 'end' 223 | } 224 | } 225 | }); 226 | chartEmptyData2.ganttChart().resize(800, 300); 227 | }); -------------------------------------------------------------------------------- /src/Optimizer.js: -------------------------------------------------------------------------------- 1 | // define the baseUrl 2 | requirejs.config({ 3 | baseUrl: 'scripts', 4 | 5 | // see http://requirejs.org/docs/jquery.html 6 | map: { 7 | '*': { 'jquery': 'jquery-private' }, 8 | 'jquery-private': { 'jquery': 'jquery' } 9 | } 10 | }); 11 | 12 | // make sure the different instances are loaded 13 | require(['net/meisen/ui/gantt/GanttChart', 'jquery-private' 14 | ], function(GanttChart) {}); 15 | 16 | // actually retrieve the loaded instances 17 | var instance = { 18 | GanttChart: require('net/meisen/ui/gantt/GanttChart') 19 | }; 20 | 21 | // we are using the system within a browser 22 | if (typeof window !== 'undefined') { 23 | for (var property in instance) { 24 | 25 | if (instance.hasOwnProperty(property)) { 26 | window[property] = instance[property]; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/jquery-private.js: -------------------------------------------------------------------------------- 1 | define(['jquery'], function ($) { 2 | return $.noConflict(true); 3 | }); -------------------------------------------------------------------------------- /src/net/meisen/ui/gantt/GanttChart.js: -------------------------------------------------------------------------------- 1 | define(['jquery', 2 | 'net/meisen/general/date/DateLibrary', 3 | 'net/meisen/ui/gantt/svg/SvgIllustrator', 4 | 'net/meisen/ui/svglibrary/SvgLibrary', 5 | 'net/meisen/ui/svglibrary/required-svg/LoadingCircles'], function ($, 6 | datelib, 7 | SvgIllustrator, 8 | svglib, 9 | loadingImage) { 10 | 11 | /* 12 | * Hidden utilities, only used within the GanttChart. 13 | */ 14 | var utilities = { 15 | generateMap: function (mapper, names) { 16 | var group = this.validateArray(mapper.group); 17 | var label = this.validateArray(mapper.label); 18 | var tooltip = this.validateArray(mapper.tooltip); 19 | 20 | var mappedGroup = this.createArray(group.length, -1); 21 | var mappedLabel = this.createArray(label.length, -1); 22 | var mappedTooltip = this.createArray(tooltip.length, -1); 23 | 24 | // create the initial map 25 | var map = { 26 | start: -1, 27 | end: -1, 28 | group: mappedGroup, 29 | label: mappedLabel, 30 | tooltip: mappedTooltip, 31 | 32 | val: function (name, record) { 33 | if (record == null || !$.isArray(record) || record.length == 0) { 34 | return; 35 | } 36 | 37 | for (var i = 0; i < names.length; i++) { 38 | if (names[i] === name) { 39 | return record[i]; 40 | } 41 | } 42 | }, 43 | 44 | get: function (type, record) { 45 | 46 | // get the array 47 | var arr = null; 48 | if (type == 'group') { 49 | arr = this.group; 50 | } else if (type == 'label') { 51 | arr = this.label; 52 | } else if (type == 'tooltip') { 53 | arr = this.tooltip; 54 | } else { 55 | arr = null; 56 | } 57 | 58 | // make sure we have something 59 | if (arr == null || arr.length == 0) { 60 | return []; 61 | } else { 62 | var len = arr.length; 63 | var vals = []; 64 | for (var i = 0; i < len; i++) { 65 | var val = record[arr[i]]; 66 | vals.push(val); 67 | } 68 | return vals; 69 | } 70 | } 71 | }; 72 | 73 | // get the arrays to look through 74 | var arrays = [ 75 | [group, mappedGroup], 76 | [label, mappedLabel], 77 | [tooltip, mappedTooltip] 78 | ]; 79 | 80 | // look-up the names and the defined maps 81 | $.each(names, function (idx, val) { 82 | if (val == mapper.startname) { 83 | map.start = idx; 84 | } else if (val == mapper.endname) { 85 | map.end = idx; 86 | } 87 | 88 | $.each(arrays, function (nr, pair) { 89 | var arrayIdx = $.inArray(val, pair[0]); 90 | if (arrayIdx != -1) { 91 | pair[1][arrayIdx] = idx; 92 | } 93 | }); 94 | }); 95 | 96 | // validate the result, no -1 present anymore 97 | var validateValue = function (value) { 98 | if (!$.isNumeric(value) || parseInt(value) !== value || value < 0) { 99 | throw Error('Mapping failed (reason: value="' + value + '", map="' + JSON.stringify(map) + '", names="' + JSON.stringify(names) + '")'); 100 | } 101 | }; 102 | $.each(map, function (key, value) { 103 | 104 | if ($.isFunction(value)) { 105 | // nothing to do 106 | } else if ($.isArray(value)) { 107 | $.each(value, function (idx, val) { 108 | validateValue(val); 109 | }); 110 | } else { 111 | validateValue(value); 112 | } 113 | }); 114 | 115 | return map; 116 | }, 117 | 118 | createArray: function (length, value) { 119 | var res = []; 120 | 121 | for (var i = 0; i < length; i++) { 122 | res[i] = value; 123 | } 124 | 125 | return res; 126 | }, 127 | 128 | validateArray: function (array) { 129 | var res; 130 | 131 | if (!$.isArray(array)) { 132 | res = []; 133 | res.push(array); 134 | } else { 135 | res = array; 136 | } 137 | 138 | return res; 139 | }, 140 | 141 | initTimeaxis: function (timeaxis, map, records) { 142 | var start = timeaxis.start; 143 | var end = timeaxis.end; 144 | 145 | var needStart = start == null || typeof(start) == 'undefined'; 146 | var needEnd = end == null || typeof(end) == 'undefined'; 147 | 148 | if (needStart || needEnd) { 149 | 150 | // TODO! What if we have numbers 151 | // get the needed values 152 | if (records == null || typeof(records) == 'undefined' || !$.isArray(records) || records.length == 0 || 153 | map == null || typeof(map) == 'undefined' || map.start == -1 || map.end == -1) { 154 | start = needStart ? datelib.createUTC() : start; 155 | end = needEnd ? datelib.createUTC(null, null, null, 23, 59, 0) : end; 156 | } else { 157 | var max = -1; 158 | var min = -1; 159 | 160 | $.each(records, function (idx, val) { 161 | var s = val[map.start].getTime(); 162 | var e = val[map.end].getTime(); 163 | 164 | if (needStart && $.isNumeric(s)) { 165 | min = min == -1 || min > s ? s : min; 166 | } 167 | 168 | if (needEnd && $.isNumeric(e)) { 169 | max = max == -1 || max < e ? e : max; 170 | } 171 | }); 172 | 173 | // get the start if needed 174 | if (needStart) { 175 | if (min == -1 && max == -1) { 176 | start = datelib.createUTC(); 177 | } else if (min == -1) { 178 | start = new Date(max); 179 | start = datelib.createUTC(start.getUTCFullYear(), start.getUTCMonth() + 1, start.getUTCDate()); 180 | } else { 181 | start = new Date(min); 182 | start = datelib.createUTC(start.getUTCFullYear(), start.getUTCMonth() + 1, start.getUTCDate()); 183 | } 184 | } 185 | 186 | // get the end if needed 187 | if (needEnd) { 188 | if (min == -1 && max == -1) { 189 | end = datelib.createUTC(null, null, null, 23, 59, 0); 190 | } else if (max == -1) { 191 | end = new Date(min); 192 | end = datelib.createUTC(end.getUTCFullYear(), end.getUTCMonth() + 1, end.getUTCDate(), 23, 59, 0); 193 | } else { 194 | end = new Date(max); 195 | end = datelib.createUTC(end.getUTCFullYear(), end.getUTCMonth() + 1, end.getUTCDate(), 23, 59, 0); 196 | } 197 | } 198 | } 199 | 200 | // set the new values 201 | return $.extend(true, timeaxis, {start: start, end: end}); 202 | } else { 203 | return timeaxis; 204 | } 205 | } 206 | }; 207 | 208 | /* 209 | * Default constructor... 210 | */ 211 | GanttChart = function () { 212 | }; 213 | 214 | /* 215 | * Extended prototype 216 | */ 217 | GanttChart.prototype = { 218 | defaultCfg: { 219 | 220 | theme: { 221 | loadingBackgroundColor: '#CCCCCC', 222 | loadingBackgroundPosition: 'center center', 223 | loadingBackgroundRepeat: 'no-repeat', 224 | 225 | errorBackgroundColor: '#A30B1D' 226 | }, 227 | 228 | illustrator: { 229 | factory: function () { 230 | return new SvgIllustrator() 231 | }, 232 | config: {} 233 | }, 234 | 235 | position: 'center', 236 | 237 | throwException: false, 238 | 239 | data: { 240 | url: null, 241 | loader: null, 242 | postProcessor: function (data) { 243 | if (!$.isArray(data.names) || !$.isArray(data.records)) { 244 | return null; 245 | } else { 246 | return data; 247 | } 248 | }, 249 | mapper: { 250 | startname: 'start', 251 | endname: 'end', 252 | group: [], 253 | label: [], 254 | tooltip: [] 255 | }, 256 | names: [], 257 | records: [], 258 | timeaxis: { 259 | start: null, 260 | end: null, 261 | granularity: 'days' 262 | } 263 | } 264 | }, 265 | 266 | init: function (selector, cfg) { 267 | this.opts = $.extend(true, {}, this.defaultCfg, cfg); 268 | 269 | selector = selector instanceof $ ? selector : $(selector); 270 | selector.children('.ganttchart').remove(); 271 | 272 | this.container = $('
'); 273 | this.container.addClass('ganttchart'); 274 | this.container.css('overflow', 'hidden'); 275 | this.container.css('position', 'relative'); 276 | 277 | // set the positioning 278 | if (this.opts.position == 'center' || this.opts.position == 'left') { 279 | this.container.css('margin-right', 'auto'); 280 | } 281 | if (this.opts.position == 'center' || this.opts.position == 'right') { 282 | this.container.css('margin-left', 'auto'); 283 | } 284 | this.container.appendTo(selector); 285 | 286 | this.view = $('
'); 287 | this.view.addClass('ganttview'); 288 | this.view.css('position', 'absolute'); 289 | this.view.css('overflow', 'hidden'); 290 | this.view.css('zIndex', 0); 291 | this.view.appendTo(this.container); 292 | 293 | this.indicator = $('
'); 294 | this.indicator.addClass('ganttloading'); 295 | this.indicator.css('backgroundRepeat', this.opts.theme.loadingBackgroundRepeat); 296 | this.indicator.css('backgroundPosition', this.opts.theme.loadingBackgroundPosition); 297 | this.indicator.css('backgroundColor', this.opts.theme.loadingBackgroundColor); 298 | this.indicator.css('position', 'absolute'); 299 | this.indicator.css('zIndex', 1000); 300 | svglib.setBackgroundImage(this.indicator, loadingImage); 301 | this.indicator.appendTo(this.container); 302 | 303 | this.error = $('
'); 304 | this.error.addClass('gantterror'); 305 | this.error.css('backgroundColor', this.opts.theme.errorBackgroundColor); 306 | this.error.css('position', 'absolute'); 307 | this.error.css('zIndex', '500'); 308 | this.error.appendTo(this.container); 309 | 310 | var _ref = this; 311 | $(this.view).on('load', function () { 312 | _ref.mask(); 313 | }).on('renderStart', function () { 314 | _ref.mask(); 315 | }).on('renderEnd', function () { 316 | _ref.unmask(); 317 | }).on('error', function (event, data) { 318 | _ref.showError(data); 319 | }).on('changeTimeaxis', function (event, data) { 320 | _ref.changeTimeaxis(data.start, data.end, data.granularity, data.force); 321 | }); 322 | 323 | // initialize the illustrator 324 | this.illustrator = this.opts.illustrator.factory(); 325 | this.illustrator.init(this.view, this.opts.illustrator.config); 326 | this.illustrator.on('finishedLayouting', function () { 327 | _ref.view.trigger('renderEnd'); 328 | }); 329 | 330 | this.load(); 331 | }, 332 | 333 | mask: function () { 334 | if (this.masking != 'loading') { 335 | this.masking = 'loading'; 336 | 337 | this.error.hide(); 338 | this.indicator.show(); 339 | } 340 | }, 341 | 342 | unmask: function () { 343 | if (this.masking != null) { 344 | this.indicator.hide(); 345 | this.error.hide(); 346 | 347 | this.masking = null; 348 | } 349 | }, 350 | 351 | resize: function (width, height) { 352 | this.container.css('width', width); 353 | this.container.css('height', height); 354 | 355 | var innerWidth = this.container.width(); 356 | var innerHeight = this.container.height(); 357 | this.indicator.css('width', innerWidth); 358 | this.indicator.css('height', innerHeight); 359 | this.error.css('width', innerWidth); 360 | this.error.css('height', innerHeight); 361 | 362 | // fire the resize event 363 | this.view.trigger('sizechanged', {width: innerWidth, height: innerHeight}); 364 | }, 365 | 366 | changeTimeaxis: function (start, end, granularity, force) { 367 | if (force || (typeof(granularity) != 'undefined' && this.opts.timeaxis.granularity != granularity) 368 | || (typeof(start) != 'undefined' && this.opts.timeaxis.start != start) 369 | || (typeof(end) != 'undefined' && this.opts.timeaxis.end != end)) { 370 | 371 | this.opts.timeaxis.start = typeof(start) != 'undefined' ? start : this.opts.timeaxis.start; 372 | this.opts.timeaxis.end = typeof(end) != 'undefined' ? end : this.opts.timeaxis.end; 373 | this.opts.timeaxis.granularity = typeof(granularity) != 'undefined' ? granularity : this.opts.timeaxis.granularity; 374 | 375 | this.render(); 376 | } 377 | }, 378 | 379 | load: function () { 380 | var _ref = this; 381 | var postProcessor = function (data) { 382 | var postProcessedData = $.isFunction(_ref.opts.data.postProcessor) ? _ref.opts.data.postProcessor(data) : data; 383 | if (postProcessedData == null) { 384 | _ref.view.trigger('error', {error: null, message: 'Postprocessing of data failed', nr: '1001'}); 385 | } else { 386 | _ref.render(postProcessedData); 387 | } 388 | }; 389 | 390 | this.view.trigger('load'); 391 | 392 | if ($.isFunction(this.opts.data.loader)) { 393 | this.opts.data.loader(function (data) { 394 | postProcessor(data); 395 | }, function (e) { 396 | _ref.view.trigger('error', {error: e, message: 'Unable to load data', nr: '1000'}); 397 | }); 398 | } else if (typeof(this.opts.data.url) == 'undefined' || this.opts.data.url == null) { 399 | this.render(); 400 | } else { 401 | $.getJSON(this.opts.data.url).done(function (data) { 402 | postProcessor(data); 403 | }).fail(function (error) { 404 | _ref.view.trigger('error', {error: error, message: 'Unable to load data', nr: '1000'}); 405 | }); 406 | } 407 | }, 408 | 409 | render: function (loaded) { 410 | 411 | // rendering will be started 412 | this.view.trigger('renderStart'); 413 | 414 | // combine the loaded data and the data 415 | var data = $.extend(true, {}, this.opts.data, loaded); 416 | 417 | // make sure we have a valid time-axis 418 | var map; 419 | try { 420 | map = utilities.generateMap(data.mapper, data.names); 421 | data.timeaxis = utilities.initTimeaxis(data.timeaxis, map, data.records); 422 | } catch (error) { 423 | this.view.trigger('error', {error: error, message: 'Failed to initialize rendering', nr: '1002'}); 424 | return; 425 | } 426 | 427 | // use a time-out to make sure that the mask is shown 428 | var _ref = this; 429 | window.setTimeout(function () { 430 | 431 | try { 432 | _ref.illustrator.draw(data.timeaxis, data.records, map); 433 | } catch (error) { 434 | _ref.view.trigger('error', {error: error, message: 'Failed to draw', nr: '1003'}); 435 | } 436 | }, 50); 437 | }, 438 | 439 | showError: function (data) { 440 | if (this.opts.throwException === true) { 441 | throw data.error; 442 | } else if (console && $.isFunction(console.error)) { 443 | console.error(data); 444 | } 445 | 446 | if (this.masking != 'error') { 447 | this.masking = 'error'; 448 | 449 | this.indicator.hide(); 450 | this.error.show(); 451 | } 452 | }, 453 | 454 | createSampleEnd: function (n) { 455 | n = typeof n !== n instanceof Date ? n : new Date(); 456 | return new Date(Date.UTC(n.getUTCFullYear(), n.getUTCMonth(), n.getUTCDate(), 23, 59, 0)); 457 | }, 458 | 459 | createSampleData: function (n, amount) { 460 | 461 | // just some stuff to create some sample time-intervals 462 | n = typeof n !== n instanceof Date ? n : new Date(); 463 | var createDate = function (h, m, s) { 464 | return new Date(Date.UTC(n.getUTCFullYear(), n.getUTCMonth(), n.getUTCDate(), h, m, s)); 465 | }; 466 | var createRecord = function (h1, m1, s1, h2, m2, s2, label) { 467 | return [ 468 | createDate(h1, m1, s1), createDate(h2, m2, s2), label 469 | ]; 470 | }; 471 | var rnd = function (min, max) { 472 | return Math.floor(min + Math.random() * (max - min)); 473 | }; 474 | 475 | var result = []; 476 | amount = typeof amount === 'number' ? amount : 5; 477 | for (var i = 0; i < amount; i++) { 478 | var sH = rnd(1, 20); 479 | var sM = rnd(1, 59); 480 | var sS = rnd(1, 59); 481 | var eH = rnd(sH + 1, 23); 482 | var eM = rnd(1, 59); 483 | var eS = rnd(1, 59); 484 | 485 | var start = sH * 60 * 60 + sM * 60 + sS; 486 | var end = eH * 60 * 60 + eM * 60 + eS; 487 | 488 | var duration = end - start; 489 | 490 | result.push(createRecord(sH, sM, sS, eH, eM, eS, duration + ' seconds')); 491 | } 492 | 493 | return result; 494 | } 495 | }; 496 | 497 | /* 498 | * Add the plug-in functionality for jQuery 499 | */ 500 | GanttChart.bind = function ($) { 501 | 502 | $.fn.ganttChart = function () { 503 | var charts = []; 504 | 505 | // get the arguments 506 | var args = Array.prototype.slice.call(arguments); 507 | 508 | // get the arguments 509 | var config = args.length == 1 && typeof(args[0]) == 'object' ? args[0] : null; 510 | 511 | // create a chart for each element, if we have a configuration defined 512 | this.each(function () { 513 | var el = $(this); 514 | var chart = el.data('ganttchart'); 515 | 516 | if (config != null && (typeof(chart) == 'undefined' || chart == null)) { 517 | chart = new GanttChart(); 518 | chart.init(el, config); 519 | el.data('ganttchart', chart); 520 | charts.push(chart); 521 | } else if (config == null && typeof(chart) != 'undefined' && chart != null) { 522 | charts.push(chart); 523 | } else if (config == null && typeof(chart) != 'undefined' && chart != null) { 524 | chart.init(el, config); 525 | charts.push(chart); 526 | } 527 | }); 528 | 529 | // make the resize function available 530 | this.resize = function (width, height) { 531 | $.each(charts, function () { 532 | //noinspection JSPotentiallyInvalidUsageOfThis 533 | this.resize(width, height); 534 | }); 535 | }; 536 | 537 | // make the changeGranularity function available 538 | this.changeTimeaxis = function (start, end, granularity, force) { 539 | $.each(charts, function () { 540 | //noinspection JSPotentiallyInvalidUsageOfThis 541 | this.changeTimeaxis(start, end, granularity, force); 542 | }); 543 | }; 544 | 545 | return this; 546 | }; 547 | }; 548 | 549 | // bind internally to the 550 | GanttChart.bind($); 551 | 552 | // some static stuff 553 | GanttChart.DateUtil = datelib; 554 | 555 | return GanttChart; 556 | }); -------------------------------------------------------------------------------- /src/net/meisen/ui/gantt/svg/Scrollbar.js: -------------------------------------------------------------------------------- 1 | define(['jquery', 'net/meisen/general/Utility'], function ($, Utility) { 2 | 3 | var utilities = { 4 | createArrow: function (size, direction, click, theme) { 5 | var arrow = $(document.createElementNS('http://www.w3.org/2000/svg', 'g')); 6 | arrow.attr('class', 'gantt-scrollbar-arrow'); 7 | 8 | el = $(document.createElementNS('http://www.w3.org/2000/svg', 'rect')); 9 | el.attr({'width': size, 'height': size, 'x': 0, y: 0, 'rx': 0, 'ry': 0}); 10 | el.css({'fill': theme.buttonColor, 'stroke': theme.buttonColorBorder, 'stroke-width': 1}); 11 | el.appendTo(arrow); 12 | 13 | el = $(document.createElementNS('http://www.w3.org/2000/svg', 'path')); 14 | if (direction == 'left') { 15 | el.attr({'d': 'M 8 4 L 8 10 5 7'}); 16 | } else if (direction == 'right') { 17 | el.attr({'d': 'M 6 4 L 6 10 9 7'}); 18 | } else if (direction == 'top') { 19 | el.attr({'d': 'M 4 8 L 10 8 7 5'}); 20 | } else if (direction == 'bottom') { 21 | el.attr({'d': 'M 4 6 L 10 6 7 9'}); 22 | } 23 | el.css({'fill': theme.arrowColor}); 24 | el.appendTo(arrow); 25 | 26 | arrow.click(function () { 27 | if ($.isFunction(click)) { 28 | click({direction: direction}); 29 | } 30 | }); 31 | 32 | return arrow 33 | }, 34 | 35 | createScroll: function (scrollbar) { 36 | var _ref = this; 37 | 38 | return function (event) { 39 | 40 | var status = { 41 | anchor: scrollbar.type == 'horizontal' ? event.pageX : event.pageY, 42 | scrollbar: scrollbar 43 | }; 44 | 45 | var moveHandler = function (event) { 46 | var isHorizontal = scrollbar.type == 'horizontal'; 47 | var pos = isHorizontal ? event.pageX : event.pageY; 48 | var diff = pos - status.anchor; 49 | 50 | if (diff == 0) { 51 | return; 52 | } else { 53 | var direction = diff < 0 ? (isHorizontal ? 'left' : 'top') : (isHorizontal ? 'right' : 'bottom'); 54 | diff = status.scrollbar.pixelToCoord(Math.abs(diff)); 55 | status.scrollbar.move(direction, diff); 56 | } 57 | status.anchor = pos; 58 | }; 59 | var disableHandler = function (event) { 60 | $(window).unbind('mousemove', moveHandler); 61 | $(window).unbind('mouseup', disableHandler); 62 | }; 63 | 64 | $(window).bind('mousemove', moveHandler); 65 | $(window).bind('mouseup', disableHandler); 66 | }; 67 | } 68 | }; 69 | 70 | /* 71 | * Default constructor... 72 | */ 73 | Scrollbar = function (type) { 74 | this.type = typeof(type) == 'undefined' || type == null ? 'horizontal' : type; 75 | }; 76 | 77 | /* 78 | * Extended prototype 79 | */ 80 | Scrollbar.prototype = { 81 | bar: null, 82 | scrollarea: null, 83 | marker: null, 84 | leftArrow: null, 85 | rightArrow: null, 86 | 87 | size: {height: 0, width: 0}, 88 | 89 | extent: 0, 90 | view: { 91 | position: 0, 92 | size: 0, 93 | total: 0 94 | }, 95 | 96 | defaultCfg: { 97 | theme: { 98 | arrowSize: 14, 99 | scrollareaColor: '#EEEEEE', 100 | markerColor: '#BFC8D1', 101 | buttonColorBorder: '#666666', 102 | arrowColor: '#666666', 103 | buttonColor: '#EBE7E8' 104 | }, 105 | hideOnNoScroll: true, 106 | propagateScrollOnNoMove: false, 107 | step: null 108 | }, 109 | 110 | init: function (canvas, cfg) { 111 | var _ref = this; 112 | 113 | this.opts = $.extend(true, {}, this.defaultCfg, cfg); 114 | 115 | // create a group for the scrollbar 116 | this.bar = $(document.createElementNS('http://www.w3.org/2000/svg', 'g')); 117 | this.bar.attr('class', 'gantt-scrollbar-container'); 118 | 119 | // create the scrollbar 120 | var extentAttribute = this.type == 'horizontal' ? 'height' : 'width'; 121 | this.scrollarea = $(document.createElementNS('http://www.w3.org/2000/svg', 'rect')); 122 | this.scrollarea.attr({y: 0, 'rx': 0, 'ry': 0}); 123 | this.scrollarea.attr(extentAttribute, this.getFixedExtent()); 124 | this.scrollarea.css({ 125 | 'fill': this.opts.theme.scrollareaColor, 126 | 'stroke': this.opts.theme.scrollareaColor, 127 | 'stroke-width': 1 128 | }); 129 | this.scrollarea.appendTo(this.bar); 130 | this.scrollarea.click(function (event) { 131 | var offset = _ref.bar.offset(); 132 | 133 | var pos = _ref.type == 'horizontal' ? event.pageX - offset.left : event.pageY - offset.top; 134 | var coord = _ref.pixelToCoord(pos - (_ref.getFixedExtent() + 1)); 135 | 136 | var direction; 137 | if (this.type == 'horizontal') { 138 | direction = coord < _ref.view.position ? 'left' : 'right'; 139 | } else { 140 | direction = coord < _ref.view.position ? 'top' : 'bottom'; 141 | } 142 | 143 | _ref.move(direction); 144 | }); 145 | 146 | // create the scroll-marker 147 | this.marker = $(document.createElementNS('http://www.w3.org/2000/svg', 'rect')); 148 | this.marker.attr({'y': 0, 'rx': 0, 'ry': 0}); 149 | this.marker.attr(extentAttribute, this.getFixedExtent()); 150 | this.marker.css({ 151 | 'fill': this.opts.theme.markerColor, 152 | 'stroke': this.opts.theme.markerColor, 153 | 'stroke-width': 1, 154 | 'cursor': 'default' 155 | }); 156 | this.marker.appendTo(this.bar); 157 | this.marker.mousedown(utilities.createScroll(this)); 158 | 159 | // create the left arrow 160 | this.leftArrow = utilities.createArrow(this.getFixedExtent(), this.type == 'horizontal' ? 'left' : 'top', function () { 161 | _ref.move('left', _ref.opts.step); 162 | }, this.opts.theme); 163 | this.leftArrow.appendTo(this.bar); 164 | 165 | // create the right arrow 166 | this.rightArrow = utilities.createArrow(this.getFixedExtent(), this.type == 'horizontal' ? 'right' : 'bottom', function () { 167 | _ref.move('right', _ref.opts.step); 168 | }, this.opts.theme); 169 | this.rightArrow.appendTo(this.bar); 170 | 171 | // append the scrollbar 172 | this.bar.appendTo(canvas); 173 | }, 174 | 175 | setPosition: function (x, y) { 176 | 177 | /* 178 | * Nicer sharper look, see: 179 | * http://stackoverflow.com/questions/18019453/svg-rectangle-blurred-in-all-browsers 180 | */ 181 | x = Math.floor(x) + 0.5; 182 | y = Math.floor(y) + 0.5; 183 | 184 | this.bar.attr({'transform': 'translate(' + x + ', ' + y + ')'}); 185 | }, 186 | 187 | move: function (direction, steps) { 188 | steps = typeof(steps) == 'undefined' ? this.view.size - 1 : steps; 189 | steps = steps == null ? Math.max(1.0, this.view.size / 10) : steps; 190 | 191 | var newPosition = this.view.position + ((direction == 'left' || direction == 'top' ? -1 : 1) * steps); 192 | newPosition = Math.max(0, newPosition); 193 | newPosition = Math.min(newPosition, this.view.total - this.view.size); 194 | 195 | this.setView(newPosition, null, null, false); 196 | }, 197 | 198 | setView: function (position, size, total, force) { 199 | position = typeof(position) == 'undefined' || position == null ? this.view.position : position; 200 | size = typeof(size) == 'undefined' || size == null ? this.view.size : size; 201 | total = typeof(total) == 'undefined' || total == null ? this.view.total : total; 202 | force = typeof(force) == 'undefined' || force == null ? false : force; 203 | 204 | total = Math.max(0, total); 205 | position = Math.max(0, position); 206 | size = Math.max(0, size); 207 | 208 | // validate some values 209 | if (size > total) { 210 | size = total; 211 | } 212 | if (position + size > total) { 213 | position = total - size; 214 | } 215 | 216 | // check if we have a change or if it was forced to update 217 | var changed = this.view.position != position || this.view.size != size; 218 | if (!force && !changed && this.view.total == total) { 219 | return; 220 | } else { 221 | this.view = {position: position, size: size, total: total}; 222 | } 223 | 224 | if (this.isVisible()) { 225 | this.bar.css('visibility', 'visible'); 226 | } else { 227 | this.bar.css('visibility', 'hidden'); 228 | } 229 | 230 | var offset = this.getFixedExtent() + 1; 231 | 232 | var scrollareaExtent = this.getScrollareaExtent(); 233 | scrollareaExtent = isNaN(scrollareaExtent) ? 0 : scrollareaExtent; 234 | 235 | var markerExtent = this.coordToPixel(size); 236 | var markerPos = offset + this.coordToPixel(position); 237 | if (this.type == 'horizontal') { 238 | this.marker.attr({'width': markerExtent, 'x': markerPos}); 239 | } else { 240 | this.marker.attr({'height': markerExtent, 'y': markerPos}); 241 | } 242 | 243 | // trigger the event if there was a change 244 | if (changed) { 245 | this.bar.trigger('viewchange', {position: position, size: size, total: total}); 246 | 247 | // trigger a size change if needed 248 | var _ref = this; 249 | setTimeout(function () { 250 | var bbox = _ref.bar.get(0).getBBox(); 251 | var size = {'height': bbox.height, 'width': bbox.width}; 252 | if (_ref.size.height != size.height || _ref.size.width != size.width) { 253 | _ref.size = size; 254 | _ref.bar.trigger('sizechanged', _ref); 255 | } 256 | }, 0); 257 | } 258 | }, 259 | 260 | isVisible: function () { 261 | return !this.opts.hideOnNoScroll || this.isScrollable(); 262 | }, 263 | 264 | isScrollable: function () { 265 | return this.view.size != this.view.total; 266 | }, 267 | 268 | pixelToCoord: function (pixel) { 269 | return pixel * (this.view.total / this.getScrollareaExtent()); 270 | }, 271 | 272 | coordToPixel: function (coord) { 273 | return this.view.total == 0 ? 0 : (coord / this.view.total) * this.getScrollareaExtent(); 274 | }, 275 | 276 | setExtent: function (extent, force) { 277 | extent = typeof(extent) == 'undefined' || extent == null ? this.extent : extent; 278 | force = typeof(force) == 'undefined' || force == null ? false : force; 279 | 280 | // check if we have a change or if it was forced to update 281 | if (!force && this.extent == extent) { 282 | return; 283 | } else { 284 | this.extent = extent; 285 | } 286 | 287 | // calculate the new values 288 | var offset = this.getFixedExtent() + 1; 289 | var scrollareaExtent = this.getScrollareaExtent(); 290 | 291 | if (this.type == 'horizontal') { 292 | this.scrollarea.attr({'width': scrollareaExtent, 'x': offset}); 293 | this.rightArrow.attr({'transform': 'translate(' + (offset + scrollareaExtent + 1) + ', 0)'}); 294 | } else { 295 | this.scrollarea.attr({'height': scrollareaExtent, 'y': offset}); 296 | this.rightArrow.attr({'transform': 'translate(0, ' + (offset + scrollareaExtent + 1) + ')'}); 297 | } 298 | 299 | // force a redraw of the marker 300 | this.setView(null, null, null, true); 301 | }, 302 | 303 | getFixedExtent: function () { 304 | return this.opts.theme.arrowSize; 305 | }, 306 | 307 | getView: function () { 308 | return this.view; 309 | }, 310 | 311 | getExtent: function () { 312 | return this.extent; 313 | }, 314 | 315 | getScrollareaExtent: function () { 316 | var offset = this.getFixedExtent() + 1; 317 | return Math.max(0, this.extent - 2 * offset); 318 | }, 319 | 320 | bindToWheel: function (selector) { 321 | var el = selector instanceof $ ? selector : $(el); 322 | 323 | var eventName = Utility.getSupportedEvent(['mousewheel', 'wheel']); 324 | if (eventName == null) { 325 | return; 326 | } 327 | 328 | var _ref = this; 329 | el.on(eventName, function (e) { 330 | var oEvent = e.originalEvent; 331 | 332 | /* 333 | * Chrome and Internet Explorer support wheelDelta, whereby FireFox only 334 | * returns the deltaY (which is pretty small and 40 seems to be a good value) 335 | * to multiply with. 336 | */ 337 | var delta = (-1 * oEvent.wheelDelta) || 40 * oEvent.deltaY; 338 | 339 | var direction = delta > 0 ? 'bottom' : 'top'; 340 | var oldPos = _ref.view.position; 341 | _ref.move(direction, Math.abs(delta / 120)); 342 | 343 | // if there was no scroll propagate it 344 | return _ref.opts.propagateScrollOnNoMove && oldPos == _ref.view.position; 345 | }); 346 | }, 347 | 348 | on: function (event, handler) { 349 | this.bar.on(event, handler); 350 | }, 351 | 352 | off: function (event, handler) { 353 | this.bar.off(event, handler); 354 | }, 355 | 356 | getSize: function () { 357 | return this.size; 358 | } 359 | }; 360 | 361 | return Scrollbar; 362 | }); -------------------------------------------------------------------------------- /src/net/meisen/ui/gantt/svg/SvgIllustrator.js: -------------------------------------------------------------------------------- 1 | define(['jquery', 'net/meisen/general/date/DateLibrary' 2 | , 'net/meisen/general/interval/IntervalCollection' 3 | , 'net/meisen/general/interval/Interval' 4 | , 'net/meisen/ui/gantt/svg/Scrollbar' 5 | , 'net/meisen/ui/gantt/svg/TimeAxis' 6 | , 'net/meisen/ui/gantt/svg/IntervalView'], 7 | function ($, datelib 8 | , IntervalCollection 9 | , Interval 10 | , Scrollbar 11 | , TimeAxis 12 | , IntervalView) { 13 | 14 | /* 15 | * Default constructor... 16 | */ 17 | SvgIllustrator = function () { 18 | this.layoutStatus = {}; 19 | this.resetStatus(); 20 | }; 21 | 22 | /* 23 | * Extended prototype 24 | */ 25 | SvgIllustrator.prototype = { 26 | defaultCfg: { 27 | theme: { 28 | fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', 29 | fontSize: '12px' 30 | }, 31 | general: { 32 | margin: 2 33 | }, 34 | /* 35 | * The view is passed to the view as configuration. Therefore 36 | * all settings of the view can be applied here. 37 | */ 38 | view: {}, 39 | /* 40 | * The axis is passed to the time-axis as configuration. Therefore 41 | * all settings of the axis can be applied here. 42 | */ 43 | axis: { 44 | /* 45 | * The viewSize determines how many entries are on 46 | * one view. If null the viewSize varies depending on 47 | * the defined granularity. 48 | */ 49 | viewSize: null, 50 | /* 51 | * The left and right padding of the axis. 52 | */ 53 | padding: 100 54 | }, 55 | scrollbars: { 56 | vertical: { 57 | hideOnNoScroll: false 58 | }, 59 | horizontal: { 60 | } 61 | } 62 | }, 63 | 64 | init: function (panel, cfg) { 65 | this.opts = $.extend(true, {}, this.defaultCfg, cfg); 66 | 67 | this.panel = panel; 68 | this.panel.empty(); 69 | 70 | this.intervalCollection = new IntervalCollection(); 71 | 72 | this.canvas = $(document.createElementNS('http://www.w3.org/2000/svg', 'svg')); 73 | this.canvas.attr('version', '1.1'); 74 | this.canvas.css('fontFamily', this.opts.theme.fontFamily); 75 | this.canvas.css('fontSize', this.opts.theme.fontSize); 76 | this.canvas.css('cursor', 'default'); 77 | this.canvas.appendTo(this.panel); 78 | 79 | // observe the resize event 80 | var _ref = this; 81 | this.panel.on('sizechanged', function (event, data) { 82 | 83 | // make sure that the event was triggered for this 84 | if (event.target == this) { 85 | _ref.resize(data.width, data.height); 86 | } 87 | }); 88 | this.canvas.on('layoutable', function () { 89 | _ref.layout(); 90 | 91 | // trigger the final layout 92 | _ref.canvas.trigger('finishedLayouting'); 93 | }); 94 | 95 | // create a scrollbar for the time-axis 96 | this.scrollbar = new Scrollbar('horizontal'); 97 | this.scrollbar.init(this.canvas, this.opts.scrollbars.horizontal); 98 | this.scrollbar.on('viewchange', function (event, data) { 99 | _ref.timeaxis.setView(data.position, data.size, data.total); 100 | }); 101 | this.scrollbar.on('sizechanged', function (event, data) { 102 | _ref.setLayoutStatus('scrollbar', true); 103 | }); 104 | 105 | // create the axis 106 | this.timeaxis = new TimeAxis(); 107 | this.timeaxis.init(this.canvas, this.opts.axis); 108 | this.timeaxis.on('viewchange', function (event, data) { 109 | _ref.intervalview.setView(data.rawstart, data.rawend, null, null, _ref.timeaxis); 110 | }); 111 | this.timeaxis.on('sizechanged', function (event, data) { 112 | _ref.setLayoutStatus('timeaxis', true); 113 | }); 114 | 115 | // create a scrollbar for the view's swim-lanes 116 | this.scrollbar2 = new Scrollbar('vertical'); 117 | this.scrollbar2.init(this.canvas, this.opts.scrollbars.vertical); 118 | this.scrollbar2.on('viewchange', function (event, data) { 119 | _ref.intervalview.setView(null, null, data.position, data.position + data.size, _ref.timeaxis); 120 | }); 121 | this.scrollbar2.bindToWheel(this.panel); 122 | 123 | // create the view 124 | this.intervalview = new IntervalView(); 125 | this.intervalview.setResolver(this.timeaxis); 126 | this.intervalview.init(this.canvas, this.opts.view); 127 | this.intervalview.on('viewchange', function (event, data) { 128 | _ref.scrollbar2.setView(data.top, data.swimlanesView, data.swimlanesTotal); 129 | }); 130 | 131 | // initialize the scrollbar2 132 | this.scrollbar2.setView(0, 1, 1); 133 | }, 134 | 135 | layout: function () { 136 | var canvasSize = this.getSize(); 137 | 138 | var totalHeight = canvasSize.height - 2 * this.opts.general.margin; 139 | var totalWidth = canvasSize.width - 2 * this.opts.general.margin; 140 | 141 | var totalPosX = this.opts.general.margin; 142 | var totalPosY = this.opts.general.margin; 143 | 144 | var timeaxisSize = this.timeaxis.getSize(); 145 | var scrollbarSize = this.scrollbar.isVisible() ? this.scrollbar.getSize() : {height: 0, width: 0}; 146 | 147 | var scrollbarLeft = totalPosX; 148 | var scrollbarTop = totalPosY + totalHeight - scrollbarSize.height; 149 | this.scrollbar.setPosition(scrollbarLeft, scrollbarTop); 150 | 151 | var timeaxisLeft = scrollbarLeft + this.opts.axis.padding * 0.5; 152 | var timeaxisTop = scrollbarTop - timeaxisSize.height - 5; // add 5 pixel margin 153 | 154 | this.timeaxis.setPosition(timeaxisLeft, timeaxisTop); 155 | 156 | // set the new size and position of the view 157 | var intervalviewLeft = timeaxisLeft; 158 | var intervalviewTop = totalPosY; 159 | var intervalviewWidth = Math.max(0, totalWidth - this.opts.axis.padding); 160 | var intervalviewHeight = Math.max(0, timeaxisTop - intervalviewTop); 161 | this.intervalview.setSize(intervalviewWidth, intervalviewHeight); 162 | this.intervalview.setPosition(intervalviewLeft, intervalviewTop); 163 | 164 | // set the new size and position of the scrollbar2 165 | this.scrollbar2.setPosition(intervalviewLeft + intervalviewWidth, intervalviewTop); 166 | this.scrollbar2.setExtent(intervalviewHeight); 167 | }, 168 | 169 | resetStatus: function () { 170 | this.layoutStatus.intervalview = true; 171 | this.layoutStatus.timeaxis = false; 172 | this.layoutStatus.scrollbar = false; 173 | this.layoutStatus.scrollbar2 = true; 174 | }, 175 | 176 | setLayoutStatus: function (entity, value) { 177 | this.layoutStatus[entity] = value; 178 | 179 | var status = true; 180 | for (var property in this.layoutStatus) { 181 | if (this.layoutStatus.hasOwnProperty(property)) { 182 | if (this.layoutStatus[property] === false) { 183 | status = false; 184 | break; 185 | } 186 | } 187 | } 188 | 189 | if (status) { 190 | this.canvas.trigger('layoutable'); 191 | this.resetStatus(); 192 | } 193 | }, 194 | 195 | resize: function (width, height) { 196 | this.panel.css('width', width); 197 | this.panel.css('height', height); 198 | 199 | this.canvas.width(width); 200 | this.canvas.height(height); 201 | this.canvas.attr('width', width); 202 | this.canvas.attr('height', height); 203 | 204 | width = width - 2 * this.opts.general.margin; 205 | height = height - 2 * this.opts.general.margin; 206 | 207 | this.scrollbar.setExtent(width); 208 | this.timeaxis.setWidth(width - this.opts.axis.padding); 209 | 210 | this.layout(); 211 | }, 212 | 213 | draw: function (timeaxisDef, records, map) { 214 | 215 | // get the records into a usable data-structure 216 | this.map = map; 217 | this.intervalCollection.clear(); 218 | var intervals = []; 219 | for (var i = 0; i < records.length; i++) { 220 | var record = records[i]; 221 | 222 | var start = record[map.start]; 223 | var end = record[map.end]; 224 | 225 | start = start === null || typeof start === 'undefined' ? timeaxisDef.start : start; 226 | end = end === null || typeof end === 'undefined' ? timeaxisDef.end : end; 227 | 228 | var interval = new Interval(start, end); 229 | interval.set(IntervalView.gRawAttr, record); 230 | intervals.push(interval); 231 | } 232 | this.intervalCollection.insertAll(intervals); 233 | 234 | // set the data of the intervalView 235 | this.intervalview.setData(this.intervalCollection, this.map); 236 | 237 | // set the axis 238 | var level = datelib.normalizeLevel(timeaxisDef.granularity); 239 | this.timeaxis.setAxis(timeaxisDef.start, timeaxisDef.end, level); 240 | 241 | // set the view of the scrollbar, everything else will be triggered 242 | this.scrollbar.setView(0, this.getViewSize(level), this.timeaxis.getAmountOfEntries()); 243 | }, 244 | 245 | getViewSize: function (level) { 246 | 247 | // the size is defined or calculated based on the level used 248 | var viewSize; 249 | if (typeof(this.opts.axis.viewSize) == 'undefined' || this.opts.axis.viewSize == null) { 250 | switch (level) { 251 | case 'y': 252 | viewSize = 10; 253 | break; 254 | case 'm': 255 | viewSize = 12; 256 | break; 257 | case 'd': 258 | viewSize = 7; 259 | break; 260 | case 'h': 261 | viewSize = 24; 262 | break; 263 | case 'mi': 264 | viewSize = 1440; 265 | break; 266 | case 's': 267 | viewSize = 60 * 1440; 268 | break; 269 | } 270 | } else { 271 | viewSize = this.opts.axis.viewSize; 272 | } 273 | 274 | return viewSize; 275 | }, 276 | 277 | getSize: function () { 278 | return {width: this.canvas.width(), height: this.canvas.height()}; 279 | }, 280 | 281 | on: function (event, handler) { 282 | this.canvas.on(event, handler); 283 | }, 284 | 285 | off: function (event, handler) { 286 | this.canvas.off(event, handler); 287 | } 288 | }; 289 | 290 | return SvgIllustrator; 291 | }); -------------------------------------------------------------------------------- /src/net/meisen/ui/gantt/svg/TimeAxis.js: -------------------------------------------------------------------------------- 1 | define(['jquery', 'net/meisen/general/date/DateLibrary'], function ($, datelib) { 2 | 3 | var utilities = { 4 | drawTicks: function (ticks, gap, numberOfGaps, theme) { 5 | 6 | // remove all ticks 7 | ticks.empty(); 8 | 9 | // create the ticks 10 | for (var i = 0; i <= numberOfGaps + 1; i++) { 11 | var x = i * gap; 12 | 13 | var g = $(document.createElementNS('http://www.w3.org/2000/svg', 'g')); 14 | g.attr('class', 'gantt-timeaxis-text'); 15 | g.css({ 16 | '-webkit-touch-callout': 'none', 17 | '-webkit-user-select': 'none', 18 | '-khtml-user-select': 'none', 19 | '-moz-user-select': 'none', 20 | '-ms-user-select': 'none', 21 | 'user-select': 'none' 22 | }); 23 | 24 | var tick = $(document.createElementNS('http://www.w3.org/2000/svg', 'line')); 25 | tick.attr({'x1': x, 'y1': 0, 'x2': x, 'y2': 10}); 26 | tick.css({'stroke': theme.tickColor, 'stroke-width': theme.tickWidth}); 27 | tick.appendTo(g); 28 | 29 | var label = $(document.createElementNS('http://www.w3.org/2000/svg', 'text')); 30 | label.attr({'x': x, 'y': theme.labelSize + 10}); 31 | label.css({ 32 | 'color': theme.labelColor, 33 | 'cursor': 'default', 34 | 'fontSize': theme.labelSize + 'px', 35 | 'fill': theme.labelColor 36 | }); 37 | 38 | var text = $(document.createElementNS('http://www.w3.org/2000/svg', 'tspan')); 39 | text.attr({'x': x, 'text-anchor': 'middle'}); 40 | text.appendTo(label); 41 | label.appendTo(g); 42 | 43 | g.appendTo(ticks); 44 | } 45 | } 46 | }; 47 | 48 | /* 49 | * Default constructor... 50 | */ 51 | TimeAxis = function () { 52 | this.width = 0; 53 | this.gap = 0; 54 | this.relativeMove = 0; 55 | this.size = { 56 | height: 0, 57 | width: 0 58 | }; 59 | this.settings = { 60 | type: null, 61 | rawstart: null, 62 | rawend: null, 63 | last: null, 64 | level: null 65 | }; 66 | this.view = { 67 | position: 0, 68 | size: 0, 69 | total: 0 70 | }; 71 | }; 72 | 73 | /* 74 | * Extended prototype 75 | */ 76 | TimeAxis.prototype = { 77 | defaultCfg: { 78 | tickInterval: null, 79 | formatter: function (value, type, level) { 80 | if (type == 'number') { 81 | return value; 82 | } else if (type == 'date') { 83 | 84 | var format; 85 | switch (level) { 86 | case 'y': 87 | format = 'yyyy'; 88 | break; 89 | case 'm': 90 | format = 'MM.yyyy'; 91 | break; 92 | case 'd': 93 | format = 'dd.MM.yyyy'; 94 | break; 95 | case 'h': 96 | format = 'dd.MM.yyyy HH'; 97 | break; 98 | case 'mi': 99 | format = 'dd.MM.yyyy HH:mm'; 100 | break; 101 | case 's': 102 | format = 'dd.MM.yyyy HH:mm:ss'; 103 | break; 104 | } 105 | 106 | return datelib.formatUTC(value, format); 107 | } else { 108 | return value; 109 | } 110 | }, 111 | theme: { 112 | tickColor: '#C0D0E0', 113 | tickWidth: 1, 114 | labelColor: '#606060', 115 | labelSize: 11 116 | } 117 | }, 118 | 119 | init: function (canvas, cfg) { 120 | this.opts = $.extend(true, {}, this.defaultCfg, cfg); 121 | 122 | this.axis = $(document.createElementNS('http://www.w3.org/2000/svg', 'g')); 123 | this.axis.attr('class', 'gantt-timeaxis-container'); 124 | 125 | // create a separating line 126 | this.sepLine = $(document.createElementNS('http://www.w3.org/2000/svg', 'line')); 127 | this.sepLine.attr({'x1': 0, 'y1': 0, 'y2': 0}); 128 | this.sepLine.css({'stroke': '#C0D0E0', 'stroke-width': 1}); 129 | this.sepLine.appendTo(this.axis); 130 | 131 | // create the group of the ticks 132 | this.ticks = $(document.createElementNS('http://www.w3.org/2000/svg', 'g')); 133 | this.ticks.attr('class', 'gantt-timeaxis-ticks'); 134 | this.ticks.appendTo(this.axis); 135 | 136 | var _ref = this; 137 | this.ticks.on('labelchange', function () { 138 | setTimeout(function () { 139 | _ref.recalibrateLabels(); 140 | }, 0); 141 | }); 142 | 143 | this.axis.appendTo(canvas); 144 | }, 145 | 146 | setWidth: function (width, force) { 147 | width = typeof(width) == 'undefined' || width == null ? this.width : width; 148 | force = typeof(force) == 'undefined' || force == null ? false : force; 149 | 150 | // check if we have a change or if it was forced to update 151 | if (!force && this.width == width) { 152 | return; 153 | } else { 154 | this.width = width; 155 | } 156 | 157 | this.sepLine.attr({'x2': width}); 158 | 159 | // force a redraw of the marker 160 | this.setAxis(null, null, null, true); 161 | }, 162 | 163 | setView: function (position, size, total, force) { 164 | position = typeof(position) == 'undefined' || position == null ? this.view.position : position; 165 | size = typeof(size) == 'undefined' || size == null ? this.view.size : size; 166 | total = typeof(total) == 'undefined' || total == null ? this.view.total : total; 167 | force = typeof(force) == 'undefined' || force == null ? false : force; 168 | 169 | total = Math.max(0, total); 170 | position = Math.max(0, position); 171 | size = Math.max(0, size); 172 | 173 | // validate some values 174 | if (size > total) { 175 | size = total; 176 | } 177 | if (position + size > total) { 178 | position = total - size; 179 | } 180 | 181 | // check if we have a change or if it was forced to update 182 | var redrawTicks = force || this.view.size != size || this.view.total != total; 183 | var changed = redrawTicks || this.view.position != position; 184 | if (!force && !changed) { 185 | return; 186 | } else { 187 | this.view = {position: position, size: size, total: total}; 188 | } 189 | 190 | // determine the tickInterval and the number of gaps 191 | var numberOfGaps; 192 | var tickInterval; 193 | if (typeof(this.opts.tickInterval) == 'undefined' || this.opts.tickInterval == null) { 194 | tickInterval = 1; 195 | while ((numberOfGaps = Math.max(1, Math.ceil((size) / tickInterval))) > 8) { 196 | tickInterval++; 197 | } 198 | } else { 199 | tickInterval = this.opts.tickInterval; 200 | numberOfGaps = Math.max(1, Math.ceil((size) / tickInterval)); 201 | } 202 | 203 | // determine the ratio of one pos value to the pixels 204 | var totalWidth = this.getTotalWidth(); 205 | var ratio = total == 0 ? 0 : totalWidth / (total - 1); 206 | 207 | // use the ratio to calculate the size of the gap and the relativeMove 208 | this.gap = tickInterval * ratio; 209 | this.relativeMove = this.gap == 0 ? 0 : -1 * ((position * ratio) % this.gap); 210 | 211 | // move the axis based on the current position 212 | this.ticks.attr({'transform': 'translate(' + this.relativeMove + ', 0)'}); 213 | 214 | // redraw the ticks if needed 215 | if (redrawTicks) { 216 | utilities.drawTicks(this.ticks, this.gap, numberOfGaps, this.opts.theme); 217 | } 218 | 219 | // add the number of the labels 220 | var start = Math.round(position / tickInterval) * tickInterval; 221 | var i = 0; 222 | var _ref = this; 223 | this.ticks.find('g').each(function (idx, el) { 224 | var tickGroup = $(el); 225 | 226 | // check if the first one is currently used 227 | if (idx == 0 && _ref.relativeMove <= -0.5 * _ref.gap) { 228 | tickGroup.removeAttr('data-index'); 229 | } else { 230 | var pos = start + i * tickInterval; 231 | if (pos < 0 || pos > _ref.settings.last) { 232 | tickGroup.removeAttr('data-index'); 233 | } else { 234 | tickGroup.attr('data-index', pos); 235 | } 236 | i++; 237 | } 238 | }); 239 | 240 | // trigger the event if there was a change 241 | if (changed) { 242 | var data = _ref.getViewPositions(); 243 | data.rawstart = this.getRawValue(data.start); 244 | data.rawend = this.getRawValue(data.end); 245 | 246 | data.axis = this; 247 | 248 | // get the rawValues 249 | this.axis.trigger('viewchange', data); 250 | } 251 | 252 | // make sure the labels are fixed 253 | this.ticks.trigger('labelchange'); 254 | }, 255 | 256 | getTotalWidth: function () { 257 | return this.view.size == 0 ? this.width : this.width * (this.view.total - 1) / (this.view.size - 1); 258 | }, 259 | 260 | recalibrateLabels: function () { 261 | var _ref = this; 262 | 263 | this.ticks.children('g').each(function (idx, el) { 264 | var tickGroup = $(el); 265 | var text = tickGroup.children('text'); 266 | 267 | // get the number 268 | var number = tickGroup.attr('data-index'); 269 | number = typeof(number) == 'undefined' || number == null ? -1 : parseInt(number); 270 | 271 | var textNumber = tickGroup.attr('data-format'); 272 | textNumber = typeof(textNumber) == 'undefined' || textNumber == null ? -1 : parseInt(textNumber); 273 | 274 | // do the formatting if needed 275 | if (number != -1 && number != textNumber) { 276 | _ref.formatLabel(number, text); 277 | tickGroup.attr('data-format', number); 278 | } 279 | 280 | // determine if the value is out of scope 281 | var viewPos = _ref.getViewPositions(); 282 | if (number == -1 || viewPos.start > number || viewPos.end < number) { 283 | tickGroup.css('visibility', 'hidden'); 284 | } else { 285 | tickGroup.css('visibility', 'visible'); 286 | } 287 | }); 288 | 289 | var bbox = this.axis.get(0).getBBox(); 290 | var size = {'height': bbox.height, 'width': bbox.width}; 291 | if (this.size.height != size.height || this.size.width != size.width) { 292 | this.size = size; 293 | this.axis.trigger('sizechanged', this); 294 | } 295 | }, 296 | 297 | getViewPositions: function () { 298 | var sPos = this.view.position; 299 | var ePos = Math.max(0, sPos + this.view.size - 1); 300 | 301 | return {start: sPos, end: ePos}; 302 | }, 303 | 304 | formatLabel: function (number, label) { 305 | var formattedText = this.opts.formatter(this.getRawValue(number), this.settings.type, this.settings.level); 306 | var tspans = label.children('tspan'); 307 | tspans.text(formattedText); 308 | 309 | // determine if the element has to be shown 310 | bbox = label.get(0).getBBox(); 311 | 312 | // let's try to split the text if no space is available 313 | if (bbox.width > this.gap) { 314 | 315 | // split the text in the middle 316 | var middle = Math.floor(formattedText.length * 0.5); 317 | var pos = -1; 318 | for (var i = 0; i < middle; i++) { 319 | if (formattedText[middle - i] == ' ') { 320 | pos = middle - i; 321 | break; 322 | } else if (formattedText[middle + i] == ' ') { 323 | pos = middle + i; 324 | break; 325 | } 326 | } 327 | 328 | if (pos != -1) { 329 | var tspanMain = tspans.eq(0); 330 | var tspanSub = tspans.eq(1); 331 | tspanSub = tspanSub.length == 0 ? $(document.createElementNS('http://www.w3.org/2000/svg', 'tspan')).appendTo(label) : tspanSub; 332 | 333 | // set the new text 334 | tspanMain.text(formattedText.substring(0, pos)); 335 | tspanSub.text(formattedText.substring(pos + 1)); 336 | tspanSub.attr({ 337 | 'text-anchor': 'middle', 338 | 'x': tspanMain.attr('x'), 339 | 'dy': 1.2 * this.opts.theme.labelSize 340 | }); 341 | } 342 | } else if (tspans.length > 1) { 343 | tspans.slice(1).remove(); 344 | } 345 | }, 346 | 347 | getLastViewPosition: function () { 348 | return this.settings.last; 349 | }, 350 | 351 | getAmountOfEntries: function () { 352 | return this.settings.last + 1; 353 | }, 354 | 355 | setPosition: function (x, y) { 356 | 357 | /* 358 | * Nicer sharper look, see: 359 | * http://stackoverflow.com/questions/18019453/svg-rectangle-blurred-in-all-browsers 360 | */ 361 | x = Math.floor(x) + 0.5; 362 | y = Math.floor(y) + 0.5; 363 | this.axis.attr({'transform': 'translate(' + x + ', ' + y + ')'}); 364 | }, 365 | 366 | getRawValue: function (pos) { 367 | pos = typeof(pos) == 'undefined' || pos == null ? 0 : pos; 368 | 369 | if (this.settings.type == 'number') { 370 | return pos + this.settings.rawstart; 371 | } else if (this.settings.type == 'date') { 372 | return datelib.modifyUTC(this.settings.rawstart, pos, this.settings.level, true); 373 | } else { 374 | return pos; 375 | } 376 | }, 377 | 378 | getPos: function (rawValue) { 379 | var pos; 380 | if (this.settings.type == 'number') { 381 | pos = rawValue - this.settings.rawstart; 382 | } else if (this.settings.type == 'date') { 383 | pos = datelib.distanceUTC(this.settings.rawstart, rawValue, this.settings.level, true); 384 | } else { 385 | return null; 386 | } 387 | 388 | return pos; 389 | }, 390 | 391 | getPixelPos: function (rawValue) { 392 | var pos = this.getPos(rawValue); 393 | return this.getPixelPosOfPos(pos); 394 | }, 395 | 396 | getPixelPosOfPos: function (pos) { 397 | var totalWidth = this.getTotalWidth(); 398 | var ratio = this.view.total == 0 ? 0 : totalWidth / (this.view.total - 1); 399 | 400 | return pos * ratio + this.relativeMove; 401 | }, 402 | 403 | getRelativePixelPosOfPos: function (pos) { 404 | var pxAxisStartPos = this.getPixelPosOfPos(this.view.position); 405 | var pxPos = this.getPixelPosOfPos(pos); 406 | 407 | return pxPos - pxAxisStartPos; 408 | }, 409 | 410 | getRelativePixelPos: function (rawValue) { 411 | var pos = this.getPos(rawValue); 412 | return this.getRelativePixelPosOfPos(pos); 413 | }, 414 | 415 | setAxis: function (start, end, level, force) { 416 | var recalc = false; 417 | 418 | // check the start 419 | var rawstart; 420 | if (typeof(start) == 'undefined' || start == null) { 421 | rawstart = this.settings.rawstart; 422 | } else { 423 | rawstart = start; 424 | recalc = true; 425 | } 426 | 427 | // check the end 428 | var rawend; 429 | if (typeof(end) == 'undefined' || end == null) { 430 | rawend = this.settings.rawend; 431 | } else { 432 | rawend = end; 433 | recalc = true; 434 | } 435 | 436 | // check level and force 437 | level = typeof(level) == 'undefined' || level == null ? this.settings.level : datelib.normalizeLevel(level); 438 | force = typeof(force) == 'undefined' || force == null ? false : force; 439 | 440 | // determine the type 441 | var type; 442 | if (rawstart instanceof Date && rawend instanceof Date) { 443 | type = 'date'; 444 | } else if ($.isNumeric(rawstart) && $.isNumeric(rawend)) { 445 | type = 'number'; 446 | } else { 447 | type = null; 448 | } 449 | 450 | // finally get the last 451 | var last; 452 | if (recalc) { 453 | if (type == 'date') { 454 | last = datelib.distanceUTC(rawstart, rawend, level); 455 | } else if (type == 'number') { 456 | last = rawend - rawstart; 457 | } else { 458 | last = null; 459 | } 460 | } else { 461 | last = this.settings.last; 462 | } 463 | 464 | // check if we have a change or if it was forced to update 465 | var changed = recalc || this.settings.type != type || this.settings.last != last || this.settings.level != level; 466 | if (!force && !changed) { 467 | return; 468 | } else { 469 | this.settings = {type: type, rawstart: rawstart, rawend: rawend, last: last, level: level}; 470 | } 471 | 472 | // trigger the event if there was a change 473 | this.setView(null, null, null, true); 474 | }, 475 | 476 | on: function (event, handler) { 477 | this.axis.on(event, handler); 478 | }, 479 | 480 | off: function (event, handler) { 481 | this.axis.off(event, handler); 482 | }, 483 | 484 | getSize: function () { 485 | return this.size; 486 | } 487 | }; 488 | 489 | return TimeAxis; 490 | }); --------------------------------------------------------------------------------