UNPKG

15.9 kBJavaScriptView Raw
1import util from 'vis-util';
2import Component from'./Component';
3import TimeStep from '../TimeStep';
4import DateUtil from '../DateUtil';
5import moment from '../../module/moment';
6
7import './css/timeaxis.css';
8
9/**
10 * A horizontal time axis
11 * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
12 * @param {Object} [options] See TimeAxis.setOptions for the available
13 * options.
14 * @constructor TimeAxis
15 * @extends Component
16 */
17function TimeAxis (body, options) {
18 this.dom = {
19 foreground: null,
20 lines: [],
21 majorTexts: [],
22 minorTexts: [],
23 redundant: {
24 lines: [],
25 majorTexts: [],
26 minorTexts: []
27 }
28 };
29 this.props = {
30 range: {
31 start: 0,
32 end: 0,
33 minimumStep: 0
34 },
35 lineTop: 0
36 };
37
38 this.defaultOptions = {
39 orientation: {
40 axis: 'bottom'
41 }, // axis orientation: 'top' or 'bottom'
42 showMinorLabels: true,
43 showMajorLabels: true,
44 maxMinorChars: 7,
45 format: TimeStep.FORMAT,
46 moment: moment,
47 timeAxis: null
48 };
49 this.options = util.extend({}, this.defaultOptions);
50
51 this.body = body;
52
53 // create the HTML DOM
54 this._create();
55
56 this.setOptions(options);
57}
58
59TimeAxis.prototype = new Component();
60
61/**
62 * Set options for the TimeAxis.
63 * Parameters will be merged in current options.
64 * @param {Object} options Available options:
65 * {string} [orientation.axis]
66 * {boolean} [showMinorLabels]
67 * {boolean} [showMajorLabels]
68 */
69TimeAxis.prototype.setOptions = function(options) {
70 if (options) {
71 // copy all options that we know
72 util.selectiveExtend([
73 'showMinorLabels',
74 'showMajorLabels',
75 'maxMinorChars',
76 'hiddenDates',
77 'timeAxis',
78 'moment',
79 'rtl'
80 ], this.options, options);
81
82 // deep copy the format options
83 util.selectiveDeepExtend(['format'], this.options, options);
84
85 if ('orientation' in options) {
86 if (typeof options.orientation === 'string') {
87 this.options.orientation.axis = options.orientation;
88 }
89 else if (typeof options.orientation === 'object' && 'axis' in options.orientation) {
90 this.options.orientation.axis = options.orientation.axis;
91 }
92 }
93
94 // apply locale to moment.js
95 // TODO: not so nice, this is applied globally to moment.js
96 if ('locale' in options) {
97 if (typeof moment.locale === 'function') {
98 // moment.js 2.8.1+
99 moment.locale(options.locale);
100 }
101 else {
102 moment.lang(options.locale);
103 }
104 }
105 }
106};
107
108/**
109 * Create the HTML DOM for the TimeAxis
110 */
111TimeAxis.prototype._create = function() {
112 this.dom.foreground = document.createElement('div');
113 this.dom.background = document.createElement('div');
114
115 this.dom.foreground.className = 'vis-time-axis vis-foreground';
116 this.dom.background.className = 'vis-time-axis vis-background';
117};
118
119/**
120 * Destroy the TimeAxis
121 */
122TimeAxis.prototype.destroy = function() {
123 // remove from DOM
124 if (this.dom.foreground.parentNode) {
125 this.dom.foreground.parentNode.removeChild(this.dom.foreground);
126 }
127 if (this.dom.background.parentNode) {
128 this.dom.background.parentNode.removeChild(this.dom.background);
129 }
130
131 this.body = null;
132};
133
134/**
135 * Repaint the component
136 * @return {boolean} Returns true if the component is resized
137 */
138TimeAxis.prototype.redraw = function () {
139 var props = this.props;
140 var foreground = this.dom.foreground;
141 var background = this.dom.background;
142
143 // determine the correct parent DOM element (depending on option orientation)
144 var parent = (this.options.orientation.axis == 'top') ? this.body.dom.top : this.body.dom.bottom;
145 var parentChanged = (foreground.parentNode !== parent);
146
147 // calculate character width and height
148 this._calculateCharSize();
149
150 // TODO: recalculate sizes only needed when parent is resized or options is changed
151 var showMinorLabels = this.options.showMinorLabels && this.options.orientation.axis !== 'none';
152 var showMajorLabels = this.options.showMajorLabels && this.options.orientation.axis !== 'none';
153
154 // determine the width and height of the elemens for the axis
155 props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
156 props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
157 props.height = props.minorLabelHeight + props.majorLabelHeight;
158 props.width = foreground.offsetWidth;
159
160 props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight -
161 (this.options.orientation.axis == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height);
162 props.minorLineWidth = 1; // TODO: really calculate width
163 props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight;
164 props.majorLineWidth = 1; // TODO: really calculate width
165
166 // take foreground and background offline while updating (is almost twice as fast)
167 var foregroundNextSibling = foreground.nextSibling;
168 var backgroundNextSibling = background.nextSibling;
169 foreground.parentNode && foreground.parentNode.removeChild(foreground);
170 background.parentNode && background.parentNode.removeChild(background);
171
172 foreground.style.height = this.props.height + 'px';
173
174 this._repaintLabels();
175
176 // put DOM online again (at the same place)
177 if (foregroundNextSibling) {
178 parent.insertBefore(foreground, foregroundNextSibling);
179 }
180 else {
181 parent.appendChild(foreground)
182 }
183 if (backgroundNextSibling) {
184 this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling);
185 }
186 else {
187 this.body.dom.backgroundVertical.appendChild(background)
188 }
189 return this._isResized() || parentChanged;
190};
191
192/**
193 * Repaint major and minor text labels and vertical grid lines
194 * @private
195 */
196TimeAxis.prototype._repaintLabels = function () {
197 var orientation = this.options.orientation.axis;
198
199 // calculate range and step (step such that we have space for 7 characters per label)
200 var start = util.convert(this.body.range.start, 'Number');
201 var end = util.convert(this.body.range.end, 'Number');
202 var timeLabelsize = this.body.util.toTime((this.props.minorCharWidth || 10) * this.options.maxMinorChars).valueOf();
203 var minimumStep = timeLabelsize - DateUtil.getHiddenDurationBefore(this.options.moment, this.body.hiddenDates, this.body.range, timeLabelsize);
204 minimumStep -= this.body.util.toTime(0).valueOf();
205
206 var step = new TimeStep(new Date(start), new Date(end), minimumStep, this.body.hiddenDates, this.options);
207 step.setMoment(this.options.moment);
208 if (this.options.format) {
209 step.setFormat(this.options.format);
210 }
211 if (this.options.timeAxis) {
212 step.setScale(this.options.timeAxis);
213 }
214 this.step = step;
215
216 // Move all DOM elements to a "redundant" list, where they
217 // can be picked for re-use, and clear the lists with lines and texts.
218 // At the end of the function _repaintLabels, left over elements will be cleaned up
219 var dom = this.dom;
220 dom.redundant.lines = dom.lines;
221 dom.redundant.majorTexts = dom.majorTexts;
222 dom.redundant.minorTexts = dom.minorTexts;
223 dom.lines = [];
224 dom.majorTexts = [];
225 dom.minorTexts = [];
226
227 var current;
228 var next;
229 var x;
230 var xNext;
231 var isMajor;
232 var showMinorGrid;
233 var width = 0, prevWidth;
234 var line;
235 var xFirstMajorLabel = undefined;
236 var count = 0;
237 const MAX = 1000;
238 var className;
239
240 step.start();
241 next = step.getCurrent();
242 xNext = this.body.util.toScreen(next);
243 while (step.hasNext() && count < MAX) {
244 count++;
245
246 isMajor = step.isMajor();
247 className = step.getClassName();
248
249 current = next;
250 x = xNext;
251
252 step.next();
253 next = step.getCurrent();
254 xNext = this.body.util.toScreen(next);
255
256 prevWidth = width;
257 width = xNext - x;
258 switch (step.scale) {
259 case 'week': showMinorGrid = true; break;
260 default: showMinorGrid = (width >= prevWidth * 0.4); break; // prevent displaying of the 31th of the month on a scale of 5 days
261 }
262
263 if (this.options.showMinorLabels && showMinorGrid) {
264 var label = this._repaintMinorText(x, step.getLabelMinor(current), orientation, className);
265 label.style.width = width + 'px'; // set width to prevent overflow
266 }
267
268 if (isMajor && this.options.showMajorLabels) {
269 if (x > 0) {
270 if (xFirstMajorLabel == undefined) {
271 xFirstMajorLabel = x;
272 }
273 label = this._repaintMajorText(x, step.getLabelMajor(current), orientation, className);
274 }
275 line = this._repaintMajorLine(x, width, orientation, className);
276 }
277 else { // minor line
278 if (showMinorGrid) {
279 line = this._repaintMinorLine(x, width, orientation, className);
280 }
281 else {
282 if (line) {
283 // adjust the width of the previous grid
284 line.style.width = (parseInt (line.style.width) + width) + 'px';
285 }
286 }
287 }
288 }
289
290 if (count === MAX && !warnedForOverflow) {
291 console.warn(`Something is wrong with the Timeline scale. Limited drawing of grid lines to ${MAX} lines.`);
292 warnedForOverflow = true;
293 }
294
295 // create a major label on the left when needed
296 if (this.options.showMajorLabels) {
297 var leftTime = this.body.util.toTime(0),
298 leftText = step.getLabelMajor(leftTime),
299 widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
300
301 if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
302 this._repaintMajorText(0, leftText, orientation, className);
303 }
304 }
305
306 // Cleanup leftover DOM elements from the redundant list
307 util.forEach(this.dom.redundant, function (arr) {
308 while (arr.length) {
309 var elem = arr.pop();
310 if (elem && elem.parentNode) {
311 elem.parentNode.removeChild(elem);
312 }
313 }
314 });
315};
316
317/**
318 * Create a minor label for the axis at position x
319 * @param {number} x
320 * @param {string} text
321 * @param {string} orientation "top" or "bottom" (default)
322 * @param {string} className
323 * @return {Element} Returns the HTML element of the created label
324 * @private
325 */
326TimeAxis.prototype._repaintMinorText = function (x, text, orientation, className) {
327 // reuse redundant label
328 var label = this.dom.redundant.minorTexts.shift();
329
330 if (!label) {
331 // create new label
332 var content = document.createTextNode('');
333 label = document.createElement('div');
334 label.appendChild(content);
335 this.dom.foreground.appendChild(label);
336 }
337 this.dom.minorTexts.push(label);
338 label.innerHTML = text;
339
340 label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0';
341
342 if (this.options.rtl) {
343 label.style.left = "";
344 label.style.right = x + 'px';
345 } else {
346 label.style.left = x + 'px';
347 }
348 label.className = 'vis-text vis-minor ' + className;
349 //label.title = title; // TODO: this is a heavy operation
350
351 return label;
352};
353
354/**
355 * Create a Major label for the axis at position x
356 * @param {number} x
357 * @param {string} text
358 * @param {string} orientation "top" or "bottom" (default)
359 * @param {string} className
360 * @return {Element} Returns the HTML element of the created label
361 * @private
362 */
363TimeAxis.prototype._repaintMajorText = function (x, text, orientation, className) {
364 // reuse redundant label
365 var label = this.dom.redundant.majorTexts.shift();
366
367 if (!label) {
368 // create label
369 var content = document.createElement('div');
370 label = document.createElement('div');
371 label.appendChild(content);
372 this.dom.foreground.appendChild(label);
373 }
374
375 label.childNodes[0].innerHTML = text;
376 label.className = 'vis-text vis-major ' + className;
377 //label.title = title; // TODO: this is a heavy operation
378
379 label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px');
380 if (this.options.rtl) {
381 label.style.left = "";
382 label.style.right = x + 'px';
383 } else {
384 label.style.left = x + 'px';
385 }
386
387 this.dom.majorTexts.push(label);
388 return label;
389};
390
391/**
392 * Create a minor line for the axis at position x
393 * @param {number} x
394 * @param {number} width
395 * @param {string} orientation "top" or "bottom" (default)
396 * @param {string} className
397 * @return {Element} Returns the created line
398 * @private
399 */
400TimeAxis.prototype._repaintMinorLine = function (x, width, orientation, className) {
401 // reuse redundant line
402 var line = this.dom.redundant.lines.shift();
403 if (!line) {
404 // create vertical line
405 line = document.createElement('div');
406 this.dom.background.appendChild(line);
407 }
408 this.dom.lines.push(line);
409
410 var props = this.props;
411 if (orientation == 'top') {
412 line.style.top = props.majorLabelHeight + 'px';
413 }
414 else {
415 line.style.top = this.body.domProps.top.height + 'px';
416 }
417 line.style.height = props.minorLineHeight + 'px';
418 if (this.options.rtl) {
419 line.style.left = "";
420 line.style.right = (x - props.minorLineWidth / 2) + 'px';
421 line.className = 'vis-grid vis-vertical-rtl vis-minor ' + className;
422 } else {
423 line.style.left = (x - props.minorLineWidth / 2) + 'px';
424 line.className = 'vis-grid vis-vertical vis-minor ' + className;
425 }
426 line.style.width = width + 'px';
427
428
429
430 return line;
431};
432
433/**
434 * Create a Major line for the axis at position x
435 * @param {number} x
436 * @param {number} width
437 * @param {string} orientation "top" or "bottom" (default)
438 * @param {string} className
439 * @return {Element} Returns the created line
440 * @private
441 */
442TimeAxis.prototype._repaintMajorLine = function (x, width, orientation, className) {
443 // reuse redundant line
444 var line = this.dom.redundant.lines.shift();
445 if (!line) {
446 // create vertical line
447 line = document.createElement('div');
448 this.dom.background.appendChild(line);
449 }
450 this.dom.lines.push(line);
451
452 var props = this.props;
453 if (orientation == 'top') {
454 line.style.top = '0';
455 }
456 else {
457 line.style.top = this.body.domProps.top.height + 'px';
458 }
459
460 if (this.options.rtl) {
461 line.style.left = "";
462 line.style.right = (x - props.majorLineWidth / 2) + 'px';
463 line.className = 'vis-grid vis-vertical-rtl vis-major ' + className;
464 } else {
465 line.style.left = (x - props.majorLineWidth / 2) + 'px';
466 line.className = 'vis-grid vis-vertical vis-major ' + className;
467 }
468
469 line.style.height = props.majorLineHeight + 'px';
470 line.style.width = width + 'px';
471
472 return line;
473};
474
475/**
476 * Determine the size of text on the axis (both major and minor axis).
477 * The size is calculated only once and then cached in this.props.
478 * @private
479 */
480TimeAxis.prototype._calculateCharSize = function () {
481 // Note: We calculate char size with every redraw. Size may change, for
482 // example when any of the timelines parents had display:none for example.
483
484 // determine the char width and height on the minor axis
485 if (!this.dom.measureCharMinor) {
486 this.dom.measureCharMinor = document.createElement('DIV');
487 this.dom.measureCharMinor.className = 'vis-text vis-minor vis-measure';
488 this.dom.measureCharMinor.style.position = 'absolute';
489
490 this.dom.measureCharMinor.appendChild(document.createTextNode('0'));
491 this.dom.foreground.appendChild(this.dom.measureCharMinor);
492 }
493 this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight;
494 this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth;
495
496 // determine the char width and height on the major axis
497 if (!this.dom.measureCharMajor) {
498 this.dom.measureCharMajor = document.createElement('DIV');
499 this.dom.measureCharMajor.className = 'vis-text vis-major vis-measure';
500 this.dom.measureCharMajor.style.position = 'absolute';
501
502 this.dom.measureCharMajor.appendChild(document.createTextNode('0'));
503 this.dom.foreground.appendChild(this.dom.measureCharMajor);
504 }
505 this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight;
506 this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth;
507};
508
509
510var warnedForOverflow = false;
511
512export default TimeAxis;