| @ -0,0 +1,209 @@ | |||
| /** | |||
| * @constructor DataStep | |||
| * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an | |||
| * end data point. The class itself determines the best scale (step size) based on the | |||
| * provided start Date, end Date, and minimumStep. | |||
| * | |||
| * If minimumStep is provided, the step size is chosen as close as possible | |||
| * to the minimumStep but larger than minimumStep. If minimumStep is not | |||
| * provided, the scale is set to 1 DAY. | |||
| * The minimumStep should correspond with the onscreen size of about 6 characters | |||
| * | |||
| * Alternatively, you can set a scale by hand. | |||
| * After creation, you can initialize the class by executing first(). Then you | |||
| * can iterate from the start date to the end date via next(). You can check if | |||
| * the end date is reached with the function hasNext(). After each step, you can | |||
| * retrieve the current date via getCurrent(). | |||
| * The DataStep has scales ranging from milliseconds, seconds, minutes, hours, | |||
| * days, to years. | |||
| * | |||
| * Version: 1.2 | |||
| * | |||
| * @param {Date} [start] The start date, for example new Date(2010, 9, 21) | |||
| * or new Date(2010, 9, 21, 23, 45, 00) | |||
| * @param {Date} [end] The end date | |||
| * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds | |||
| */ | |||
| function DataStep(start, end, minimumStep, containerHeight) { | |||
| // variables | |||
| this.current = 0; | |||
| this.containerHeight = containerHeight; | |||
| this.autoScale = true; | |||
| this.stepIndex = 0; | |||
| this.step = 1; | |||
| this.scale = 1; | |||
| this.marginStart; | |||
| this.marginEnd; | |||
| this.majorSteps = [1, 2, 5, 10]; | |||
| this.minorSteps = [0.25, 0.5, 1, 2]; | |||
| this.setRange(start,end,minimumStep, containerHeight); | |||
| } | |||
| /** | |||
| * Set a new range | |||
| * If minimumStep is provided, the step size is chosen as close as possible | |||
| * to the minimumStep but larger than minimumStep. If minimumStep is not | |||
| * provided, the scale is set to 1 DAY. | |||
| * The minimumStep should correspond with the onscreen size of about 6 characters | |||
| * @param {Number} [start] The start date and time. | |||
| * @param {Number} [end] The end date and time. | |||
| * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds | |||
| */ | |||
| DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight) { | |||
| this._start = start; | |||
| this._end = end; | |||
| this.setFirst(); | |||
| if (this.autoScale) { | |||
| this.setMinimumStep(minimumStep, containerHeight); | |||
| } | |||
| }; | |||
| /** | |||
| * Set the range iterator to the start date. | |||
| */ | |||
| DataStep.prototype.first = function() { | |||
| this.setFirst(); | |||
| }; | |||
| /** | |||
| * Round the current date to the first minor date value | |||
| * This must be executed once when the current date is set to start Date | |||
| */ | |||
| DataStep.prototype.setFirst = function() { | |||
| var niceStart = this._start - (this.scale * this.minorSteps[this.stepIndex]); | |||
| var niceEnd = this._end + (this.scale * this.minorSteps[this.stepIndex]); | |||
| this.marginEnd = this.roundToMinor(niceEnd); | |||
| this.marginStart = this.roundToMinor(niceStart); | |||
| this.marginRange = this.marginEnd - this.marginStart; | |||
| this.current = this.marginEnd; | |||
| }; | |||
| DataStep.prototype.roundToMinor = function(value) { | |||
| var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex])); | |||
| if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) { | |||
| return rounded + (this.scale * this.minorSteps[this.stepIndex]); | |||
| } | |||
| else { | |||
| return rounded; | |||
| } | |||
| } | |||
| /** | |||
| * Check if the there is a next step | |||
| * @return {boolean} true if the current date has not passed the end date | |||
| */ | |||
| DataStep.prototype.hasNext = function () { | |||
| return (this.current >= this.marginStart); | |||
| }; | |||
| /** | |||
| * Do the next step | |||
| */ | |||
| DataStep.prototype.next = function() { | |||
| var prev = this.current; | |||
| this.current -= this.step; | |||
| // safety mechanism: if current time is still unchanged, move to the end | |||
| if (this.current == prev) { | |||
| this.current = this._end; | |||
| } | |||
| }; | |||
| /** | |||
| * Get the current datetime | |||
| * @return {Date} current The current date | |||
| */ | |||
| DataStep.prototype.getCurrent = function() { | |||
| return this.current; | |||
| }; | |||
| /** | |||
| * Automatically determine the scale that bests fits the provided minimum step | |||
| * @param {Number} [minimumStep] The minimum step size in milliseconds | |||
| */ | |||
| DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) { | |||
| // round to floor | |||
| var size = this._end - this._start; | |||
| var safeSize = size * 1.1; | |||
| var minimumStepValue = minimumStep * (safeSize / containerHeight); | |||
| var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10); | |||
| var minorStepIdx = -1; | |||
| var magnitudefactor = Math.pow(10,orderOfMagnitude); | |||
| var solutionFound = false; | |||
| for (var i = 0; i <= orderOfMagnitude; i++) { | |||
| magnitudefactor = Math.pow(10,i); | |||
| for (var j = 0; j < this.minorSteps.length; j++) { | |||
| var stepSize = magnitudefactor * this.minorSteps[j]; | |||
| if (stepSize >= minimumStepValue) { | |||
| solutionFound = true; | |||
| minorStepIdx = j; | |||
| break; | |||
| } | |||
| } | |||
| if (solutionFound == true) { | |||
| break; | |||
| } | |||
| } | |||
| this.stepIndex = minorStepIdx; | |||
| this.scale = magnitudefactor; | |||
| this.step = magnitudefactor * this.minorSteps[minorStepIdx]; | |||
| }; | |||
| /** | |||
| * Snap a date to a rounded value. | |||
| * The snap intervals are dependent on the current scale and step. | |||
| * @param {Date} date the date to be snapped. | |||
| * @return {Date} snappedDate | |||
| */ | |||
| DataStep.prototype.snap = function(date) { | |||
| }; | |||
| /** | |||
| * Check if the current value is a major value (for example when the step | |||
| * is DAY, a major value is each first day of the MONTH) | |||
| * @return {boolean} true if current date is major, else false. | |||
| */ | |||
| DataStep.prototype.isMajor = function() { | |||
| return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0); | |||
| }; | |||
| /** | |||
| * Returns formatted text for the minor axislabel, depending on the current | |||
| * date and the scale. For example when scale is MINUTE, the current time is | |||
| * formatted as "hh:mm". | |||
| * @param {Date} [date] custom date. if not provided, current date is taken | |||
| */ | |||
| DataStep.prototype.getLabelMinor = function() { | |||
| return this.current; | |||
| }; | |||
| /** | |||
| * Returns formatted text for the major axis label, depending on the current | |||
| * date and the scale. For example when scale is MINUTE, the major scale is | |||
| * hours, and the hour will be formatted as "hh". | |||
| * @param {Date} [date] custom date. if not provided, current date is taken | |||
| */ | |||
| DataStep.prototype.getLabelMajor = function() { | |||
| return this.current; | |||
| }; | |||
| @ -0,0 +1,405 @@ | |||
| /** | |||
| * A horizontal time axis | |||
| * @param {Object} [options] See DataAxis.setOptions for the available | |||
| * options. | |||
| * @constructor DataAxis | |||
| * @extends Component | |||
| */ | |||
| function DataAxis (options) { | |||
| this.id = util.randomUUID(); | |||
| this.dom = { | |||
| majorLines: [], | |||
| majorTexts: [], | |||
| minorLines: [], | |||
| minorTexts: [], | |||
| redundant: { | |||
| majorLines: [], | |||
| majorTexts: [], | |||
| minorLines: [], | |||
| minorTexts: [] | |||
| } | |||
| }; | |||
| this.props = { | |||
| range: { | |||
| start: 0, | |||
| end: 0, | |||
| minimumStep: 0 | |||
| }, | |||
| lineTop: 0 | |||
| }; | |||
| this.options = options || {}; | |||
| this.defaultOptions = { | |||
| orientation: 'left', // supported: 'left' | |||
| showMinorLabels: true, | |||
| showMajorLabels: true | |||
| }; | |||
| this.range = null; | |||
| this.conversionFactor = 1; | |||
| // create the HTML DOM | |||
| this._create(); | |||
| } | |||
| DataAxis.prototype = new Component(); | |||
| // TODO: comment options | |||
| DataAxis.prototype.setOptions = Component.prototype.setOptions; | |||
| /** | |||
| * Create the HTML DOM for the DataAxis | |||
| */ | |||
| DataAxis.prototype._create = function _create() { | |||
| this.frame = document.createElement('div'); | |||
| }; | |||
| /** | |||
| * Set a range (start and end) | |||
| * @param {Range | Object} range A Range or an object containing start and end. | |||
| */ | |||
| DataAxis.prototype.setRange = function (range) { | |||
| if (!(range instanceof Range) && (!range || range.start === undefined || range.end === undefined)) { | |||
| throw new TypeError('Range must be an instance of Range, ' + | |||
| 'or an object containing start and end.'); | |||
| } | |||
| this.range = range; | |||
| }; | |||
| /** | |||
| * Get the outer frame of the time axis | |||
| * @return {HTMLElement} frame | |||
| */ | |||
| DataAxis.prototype.getFrame = function getFrame() { | |||
| return this.frame; | |||
| }; | |||
| /** | |||
| * Repaint the component | |||
| * @return {boolean} Returns true if the component is resized | |||
| */ | |||
| DataAxis.prototype.repaint = function () { | |||
| var asSize = util.option.asSize; | |||
| var options = this.options; | |||
| var props = this.props; | |||
| var frame = this.frame; | |||
| // update classname | |||
| frame.className = 'dataaxis'; // TODO: add className from options if defined | |||
| // calculate character width and height | |||
| this._calculateCharSize(); | |||
| // TODO: recalculate sizes only needed when parent is resized or options is changed | |||
| var orientation = this.getOption('orientation'); | |||
| var showMinorLabels = this.getOption('showMinorLabels'); | |||
| var showMajorLabels = this.getOption('showMajorLabels'); | |||
| // determine the width and height of the elemens for the axis | |||
| props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; | |||
| props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; | |||
| this.height = this.options.height; | |||
| this.width = frame.offsetWidth; // TODO: only update the width when the frame is resized? | |||
| props.minorLineWidth = this.options.svg.offsetWidth; | |||
| props.minorLineHeight = 1; // TODO: really calculate width | |||
| props.majorLineWidth = this.options.svg.offsetWidth; | |||
| props.majorLineHeight = 1; // TODO: really calculate width | |||
| // take frame offline while updating (is almost twice as fast) | |||
| // TODO: top/bottom positioning should be determined by options set in the Timeline, not here | |||
| if (orientation == 'left') { | |||
| frame.style.top = '0'; | |||
| frame.style.left = '0'; | |||
| frame.style.bottom = ''; | |||
| frame.style.width = this.width + 'px'; | |||
| frame.style.height = this.height + "px"; | |||
| } | |||
| else { // right | |||
| frame.style.top = ''; | |||
| frame.style.bottom = '0'; | |||
| frame.style.left = '0'; | |||
| frame.style.width = this.width + 'px'; | |||
| frame.style.height = this.height + "px"; | |||
| } | |||
| this._repaintLabels(); | |||
| }; | |||
| /** | |||
| * Repaint major and minor text labels and vertical grid lines | |||
| * @private | |||
| */ | |||
| DataAxis.prototype._repaintLabels = function () { | |||
| var orientation = this.getOption('orientation'); | |||
| // calculate range and step (step such that we have space for 7 characters per label) | |||
| var start = this.range.start; | |||
| var end = this.range.end; | |||
| var minimumStep = (this.props.minorCharHeight || 10); //in pixels | |||
| var step = new DataStep(start, end, minimumStep, this.options.svg.offsetHeight); | |||
| this.step = step; | |||
| // Move all DOM elements to a "redundant" list, where they | |||
| // can be picked for re-use, and clear the lists with lines and texts. | |||
| // At the end of the function _repaintLabels, left over elements will be cleaned up | |||
| var dom = this.dom; | |||
| dom.redundant.majorLines = dom.majorLines; | |||
| dom.redundant.majorTexts = dom.majorTexts; | |||
| dom.redundant.minorLines = dom.minorLines; | |||
| dom.redundant.minorTexts = dom.minorTexts; | |||
| dom.majorLines = []; | |||
| dom.majorTexts = []; | |||
| dom.minorLines = []; | |||
| dom.minorTexts = []; | |||
| step.first(); | |||
| var stepPixels = this.options.svg.offsetHeight / ((step.marginRange / step.step) + 1); | |||
| var xFirstMajorLabel = undefined; | |||
| this.valueAtZero = step.marginEnd; | |||
| var marginStartPos = 0; | |||
| var max = 0; | |||
| while (step.hasNext() && max < 1000) { | |||
| var y = max * stepPixels; | |||
| y = y.toPrecision(5) | |||
| var isMajor = step.isMajor(); | |||
| if (this.getOption('showMinorLabels') && isMajor == false) { | |||
| this._repaintMinorText(y, step.getLabelMinor(), orientation); | |||
| } | |||
| if (isMajor && this.getOption('showMajorLabels')) { | |||
| if (y > 0) { | |||
| if (xFirstMajorLabel == undefined) { | |||
| xFirstMajorLabel = y; | |||
| } | |||
| this._repaintMajorText(y, step.getLabelMajor(), orientation); | |||
| } | |||
| this._repaintMajorLine(y, orientation); | |||
| } | |||
| else { | |||
| this._repaintMinorLine(y, orientation); | |||
| } | |||
| step.next(); | |||
| marginStartPos = y; | |||
| max++; | |||
| } | |||
| this.conversionFactor = marginStartPos/step.marginRange; | |||
| console.log(marginStartPos, step.marginRange, this.conversionFactor); | |||
| // create a major label on the left when needed | |||
| if (this.getOption('showMajorLabels')) { | |||
| var leftPoint = this._start; | |||
| var leftText = step.getLabelMajor(leftPoint); | |||
| var widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation | |||
| if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { | |||
| this._repaintMajorText(0, leftText, orientation); | |||
| } | |||
| } | |||
| // Cleanup leftover DOM elements from the redundant list | |||
| util.forEach(this.dom.redundant, function (arr) { | |||
| while (arr.length) { | |||
| var elem = arr.pop(); | |||
| if (elem && elem.parentNode) { | |||
| elem.parentNode.removeChild(elem); | |||
| } | |||
| } | |||
| }); | |||
| }; | |||
| DataAxis.prototype.convertValues = function(data) { | |||
| for (var i = 0; i < data.length; i++) { | |||
| data[i].y = this._getPos(data[i].y); | |||
| } | |||
| return data; | |||
| } | |||
| DataAxis.prototype._getPos = function(value) { | |||
| var invertedValue = this.valueAtZero - value; | |||
| var convertedValue = invertedValue * this.conversionFactor; | |||
| return convertedValue | |||
| } | |||
| /** | |||
| * Create a minor label for the axis at position x | |||
| * @param {Number} x | |||
| * @param {String} text | |||
| * @param {String} orientation "top" or "bottom" (default) | |||
| * @private | |||
| */ | |||
| DataAxis.prototype._repaintMinorText = function (x, text, orientation) { | |||
| // reuse redundant label | |||
| var label = this.dom.redundant.minorTexts.shift(); | |||
| if (!label) { | |||
| // create new label | |||
| var content = document.createTextNode(''); | |||
| label = document.createElement('div'); | |||
| label.appendChild(content); | |||
| label.className = 'yAxis minor'; | |||
| this.frame.appendChild(label); | |||
| } | |||
| this.dom.minorTexts.push(label); | |||
| label.childNodes[0].nodeValue = text; | |||
| if (orientation == 'left') { | |||
| label.style.left = '-2px'; | |||
| label.style.textAlign = "right"; | |||
| } | |||
| else { | |||
| label.style.left = '2px'; | |||
| label.style.textAlign = "left"; | |||
| } | |||
| label.style.top = x + 'px'; | |||
| //label.title = title; // TODO: this is a heavy operation | |||
| }; | |||
| /** | |||
| * Create a Major label for the axis at position x | |||
| * @param {Number} x | |||
| * @param {String} text | |||
| * @param {String} orientation "top" or "bottom" (default) | |||
| * @private | |||
| */ | |||
| DataAxis.prototype._repaintMajorText = function (x, text, orientation) { | |||
| // reuse redundant label | |||
| var label = this.dom.redundant.majorTexts.shift(); | |||
| if (!label) { | |||
| // create label | |||
| var content = document.createTextNode(text); | |||
| label = document.createElement('div'); | |||
| label.className = 'yAxis major'; | |||
| label.appendChild(content); | |||
| this.frame.appendChild(label); | |||
| } | |||
| this.dom.majorTexts.push(label); | |||
| label.childNodes[0].nodeValue = text; | |||
| //label.title = title; // TODO: this is a heavy operation | |||
| if (orientation == 'left') { | |||
| label.style.left = '-2px'; | |||
| label.style.textAlign = "right"; | |||
| } | |||
| else { | |||
| label.style.left = '2'; | |||
| label.style.textAlign = "left"; | |||
| } | |||
| label.style.top = x + 'px'; | |||
| }; | |||
| /** | |||
| * Create a minor line for the axis at position y | |||
| * @param {Number} y | |||
| * @param {String} orientation "top" or "bottom" (default) | |||
| * @private | |||
| */ | |||
| DataAxis.prototype._repaintMinorLine = function (y, orientation) { | |||
| // reuse redundant line | |||
| var line = this.dom.redundant.minorLines.shift(); | |||
| if (!line) { | |||
| // create vertical line | |||
| line = document.createElement('div'); | |||
| line.className = 'grid horizontal minor'; | |||
| this.frame.appendChild(line); | |||
| } | |||
| this.dom.minorLines.push(line); | |||
| var props = this.props; | |||
| if (orientation == 'left') { | |||
| line.style.left = (this.width - 15) + 'px'; | |||
| } | |||
| else { | |||
| line.style.left = -1*(this.width - 15) + 'px'; | |||
| } | |||
| line.style.width = props.minorLineWidth + 'px'; | |||
| line.style.top = y + 'px'; | |||
| }; | |||
| /** | |||
| * Create a Major line for the axis at position x | |||
| * @param {Number} x | |||
| * @param {String} orientation "top" or "bottom" (default) | |||
| * @private | |||
| */ | |||
| DataAxis.prototype._repaintMajorLine = function (y, orientation) { | |||
| // reuse redundant line | |||
| var line = this.dom.redundant.majorLines.shift(); | |||
| if (!line) { | |||
| // create vertical line | |||
| line = document.createElement('div'); | |||
| line.className = 'grid horizontal major'; | |||
| this.frame.appendChild(line); | |||
| } | |||
| this.dom.majorLines.push(line); | |||
| var props = this.props; | |||
| if (orientation == 'left') { | |||
| line.style.left = (this.width - 25) + 'px'; | |||
| } | |||
| else { | |||
| line.style.left = -1*(this.width - 25) + 'px'; | |||
| } | |||
| line.style.top = y + 'px'; | |||
| line.style.width = props.majorLineWidth + 'px'; | |||
| }; | |||
| /** | |||
| * Determine the size of text on the axis (both major and minor axis). | |||
| * The size is calculated only once and then cached in this.props. | |||
| * @private | |||
| */ | |||
| DataAxis.prototype._calculateCharSize = function () { | |||
| // determine the char width and height on the minor axis | |||
| if (!('minorCharHeight' in this.props)) { | |||
| var textMinor = document.createTextNode('0'); | |||
| var measureCharMinor = document.createElement('DIV'); | |||
| measureCharMinor.className = 'text minor measure'; | |||
| measureCharMinor.appendChild(textMinor); | |||
| this.frame.appendChild(measureCharMinor); | |||
| this.props.minorCharHeight = measureCharMinor.clientHeight; | |||
| this.props.minorCharWidth = measureCharMinor.clientWidth; | |||
| this.frame.removeChild(measureCharMinor); | |||
| } | |||
| if (!('majorCharHeight' in this.props)) { | |||
| var textMajor = document.createTextNode('0'); | |||
| var measureCharMajor = document.createElement('DIV'); | |||
| measureCharMajor.className = 'text major measure'; | |||
| measureCharMajor.appendChild(textMajor); | |||
| this.frame.appendChild(measureCharMajor); | |||
| this.props.majorCharHeight = measureCharMajor.clientHeight; | |||
| this.props.majorCharWidth = measureCharMajor.clientWidth; | |||
| this.frame.removeChild(measureCharMajor); | |||
| } | |||
| }; | |||
| /** | |||
| * Snap a date to a rounded value. | |||
| * The snap intervals are dependent on the current scale and step. | |||
| * @param {Date} date the date to be snapped. | |||
| * @return {Date} snappedDate | |||
| */ | |||
| DataAxis.prototype.snap = function snap (date) { | |||
| return this.step.snap(date); | |||
| }; | |||
| @ -0,0 +1,500 @@ | |||
| /** | |||
| * Created by Alex on 5/6/14. | |||
| */ | |||
| var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items | |||
| /** | |||
| * An ItemSet holds a set of items and ranges which can be displayed in a | |||
| * range. The width is determined by the parent of the ItemSet, and the height | |||
| * is determined by the size of the items. | |||
| * @param {Panel} backgroundPanel Panel which can be used to display the | |||
| * vertical lines of box items. | |||
| * @param {Panel} axisPanel Panel on the axis where the dots of box-items | |||
| * can be displayed. | |||
| * @param {Panel} sidePanel Left side panel holding labels | |||
| * @param {Object} [options] See ItemSet.setOptions for the available options. | |||
| * @constructor ItemSet | |||
| * @extends Panel | |||
| */ | |||
| function Linegraph(backgroundPanel, axisPanel, sidePanel, options, timeline, sidePanelParent) { | |||
| this.id = util.randomUUID(); | |||
| this.timeline = timeline; | |||
| // one options object is shared by this itemset and all its items | |||
| this.options = options || {}; | |||
| this.backgroundPanel = backgroundPanel; | |||
| this.axisPanel = axisPanel; | |||
| this.sidePanel = sidePanel; | |||
| this.sidePanelParent = sidePanelParent; | |||
| this.itemOptions = Object.create(this.options); | |||
| this.dom = {}; | |||
| this.hammer = null; | |||
| this.itemsData = null; // DataSet | |||
| this.groupsData = null; // DataSet | |||
| this.range = null; // Range or Object {start: number, end: number} | |||
| // listeners for the DataSet of the items | |||
| // this.itemListeners = { | |||
| // 'add': function(event, params, senderId) { | |||
| // if (senderId != me.id) me._onAdd(params.items); | |||
| // }, | |||
| // 'update': function(event, params, senderId) { | |||
| // if (senderId != me.id) me._onUpdate(params.items); | |||
| // }, | |||
| // 'remove': function(event, params, senderId) { | |||
| // if (senderId != me.id) me._onRemove(params.items); | |||
| // } | |||
| // }; | |||
| // | |||
| // // listeners for the DataSet of the groups | |||
| // this.groupListeners = { | |||
| // 'add': function(event, params, senderId) { | |||
| // if (senderId != me.id) me._onAddGroups(params.items); | |||
| // }, | |||
| // 'update': function(event, params, senderId) { | |||
| // if (senderId != me.id) me._onUpdateGroups(params.items); | |||
| // }, | |||
| // 'remove': function(event, params, senderId) { | |||
| // if (senderId != me.id) me._onRemoveGroups(params.items); | |||
| // } | |||
| // }; | |||
| this.items = {}; // object with an Item for every data item | |||
| this.groups = {}; // Group object for every group | |||
| this.groupIds = []; | |||
| this.selection = []; // list with the ids of all selected nodes | |||
| this.stackDirty = true; // if true, all items will be restacked on next repaint | |||
| this.touchParams = {}; // stores properties while dragging | |||
| // create the HTML DOM | |||
| this.lastStart = 0; | |||
| this._create(); | |||
| var me = this; | |||
| this.timeline.on("rangechange", function() { | |||
| if (me.lastStart != 0) { | |||
| var offset = me.range.start - me.lastStart; | |||
| var range = me.range.end - me.range.start; | |||
| if (me.width != 0) { | |||
| var rangePerPixelInv = me.width/range; | |||
| var xOffset = offset * rangePerPixelInv; | |||
| me.svg.style.left = util.option.asSize(-me.width - xOffset); | |||
| } | |||
| } | |||
| }) | |||
| this.timeline.on("rangechanged", function() { | |||
| me.lastStart = me.range.start; | |||
| me.svg.style.left = util.option.asSize(-me.width); | |||
| me.setData.apply(me); | |||
| }); | |||
| // this.data = new DataView(this.items) | |||
| } | |||
| Linegraph.prototype = new Panel(); | |||
| /** | |||
| * Create the HTML DOM for the ItemSet | |||
| */ | |||
| Linegraph.prototype._create = function(){ | |||
| var frame = document.createElement('div'); | |||
| frame['timeline-linegraph'] = this; | |||
| this.frame = frame; | |||
| this.frame.className = 'itemset'; | |||
| // create background panel | |||
| var background = document.createElement('div'); | |||
| background.className = 'background'; | |||
| this.backgroundPanel.frame.appendChild(background); | |||
| this.dom.background = background; | |||
| // create foreground panel | |||
| var foreground = document.createElement('div'); | |||
| foreground.className = 'foreground'; | |||
| frame.appendChild(foreground); | |||
| this.dom.foreground = foreground; | |||
| // // create axis panel | |||
| // var axis = document.createElement('div'); | |||
| // axis.className = 'axis'; | |||
| // this.dom.axis = axis; | |||
| // this.axisPanel.frame.appendChild(axis); | |||
| // | |||
| // // create labelset | |||
| // var labelSet = document.createElement('div'); | |||
| // labelSet.className = 'labelset'; | |||
| // this.dom.labelSet = labelSet; | |||
| // this.sidePanel.frame.appendChild(labelSet); | |||
| this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg"); | |||
| this.svg.style.position = "relative" | |||
| this.svg.style.height = "300px"; | |||
| this.svg.style.display = "block"; | |||
| this.path = document.createElementNS('http://www.w3.org/2000/svg',"path"); | |||
| this.path.setAttributeNS(null, "fill","none"); | |||
| this.path.setAttributeNS(null, "stroke","blue"); | |||
| this.path.setAttributeNS(null, "stroke-width","1"); | |||
| this.path2 = document.createElementNS('http://www.w3.org/2000/svg',"path"); | |||
| this.path2.setAttributeNS(null, "fill","none"); | |||
| this.path2.setAttributeNS(null, "stroke","red"); | |||
| this.path2.setAttributeNS(null, "stroke-width","1"); | |||
| this.path3 = document.createElementNS('http://www.w3.org/2000/svg',"path"); | |||
| this.path3.setAttributeNS(null, "fill","none"); | |||
| this.path3.setAttributeNS(null, "stroke","green"); | |||
| this.path3.setAttributeNS(null, "stroke-width","1"); | |||
| this.dom.foreground.appendChild(this.svg); | |||
| this.svg.appendChild(this.path3); | |||
| this.svg.appendChild(this.path2); | |||
| this.svg.appendChild(this.path); | |||
| // this.yAxisDiv = document.createElement('div'); | |||
| // this.yAxisDiv.style.backgroundColor = 'rgb(220,220,220)'; | |||
| // this.yAxisDiv.style.width = '100px'; | |||
| // this.yAxisDiv.style.height = this.svg.style.height; | |||
| this._createAxis(); | |||
| // this.dom.yAxisDiv = this.yAxisDiv; | |||
| // this.sidePanel.frame.appendChild(this.yAxisDiv); | |||
| this.sidePanel.showPanel.apply(this.sidePanel); | |||
| this.sidePanelParent.showPanel(); | |||
| }; | |||
| Linegraph.prototype._createAxis = function() { | |||
| // panel with time axis | |||
| var dataAxisOptions = { | |||
| range: this.range, | |||
| left: null, | |||
| top: null, | |||
| width: null, | |||
| height: 300, | |||
| svg: this.svg | |||
| }; | |||
| this.yAxis = new DataAxis(dataAxisOptions); | |||
| this.sidePanel.frame.appendChild(this.yAxis.getFrame()); | |||
| } | |||
| Linegraph.prototype.setData = function() { | |||
| if (this.width != 0) { | |||
| var dataview = new DataView(this.timeline.itemsData, | |||
| {filter: function (item) {return (item.value);}}) | |||
| var datapoints = dataview.get(); | |||
| if (datapoints != null) { | |||
| if (datapoints.length > 0) { | |||
| var dataset = this._extractData(datapoints); | |||
| var data = dataset.data; | |||
| console.log("height",data,datapoints, dataset); | |||
| this.yAxis.setRange({start:dataset.range.low,end:dataset.range.high}); | |||
| this.yAxis.repaint(); | |||
| data = this.yAxis.convertValues(data); | |||
| var d, d2, d3; | |||
| d = this._catmullRom(data,0.5); | |||
| d3 = this._catmullRom(data,0); | |||
| d2 = this._catmullRom(data,1); | |||
| // var data2 = []; | |||
| // this.startTime = this.range.start; | |||
| // var min = Date.now() - 3600000 * 24 * 30; | |||
| // var max = Date.now() + 3600000 * 24 * 10; | |||
| // var count = 60; | |||
| // var step = (max-min) / count; | |||
| // | |||
| // var range = this.range.end - this.range.start; | |||
| // var rangePerPixel = range/this.width; | |||
| // var rangePerPixelInv = this.width/range; | |||
| // var xOffset = -this.range.start + this.width*rangePerPixel; | |||
| // | |||
| // for (var i = 0; i < count; i++) { | |||
| // data2.push({x:(min + i*step + xOffset) * rangePerPixelInv, y: 250*(i%2) + 25}) | |||
| // } | |||
| // | |||
| // var d2 = this._catmullRom(data2); | |||
| this.path.setAttributeNS(null, "d",d); | |||
| this.path2.setAttributeNS(null, "d",d2); | |||
| this.path3.setAttributeNS(null, "d",d3); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Set options for the Linegraph. Existing options will be extended/overwritten. | |||
| * @param {Object} [options] The following options are available: | |||
| * {String | function} [className] | |||
| * class name for the itemset | |||
| * {String} [type] | |||
| * Default type for the items. Choose from 'box' | |||
| * (default), 'point', or 'range'. The default | |||
| * Style can be overwritten by individual items. | |||
| * {String} align | |||
| * Alignment for the items, only applicable for | |||
| * ItemBox. Choose 'center' (default), 'left', or | |||
| * 'right'. | |||
| * {String} orientation | |||
| * Orientation of the item set. Choose 'top' or | |||
| * 'bottom' (default). | |||
| * {Number} margin.axis | |||
| * Margin between the axis and the items in pixels. | |||
| * Default is 20. | |||
| * {Number} margin.item | |||
| * Margin between items in pixels. Default is 10. | |||
| * {Number} padding | |||
| * Padding of the contents of an item in pixels. | |||
| * Must correspond with the items css. Default is 5. | |||
| * {Function} snap | |||
| * Function to let items snap to nice dates when | |||
| * dragging items. | |||
| */ | |||
| Linegraph.prototype.setOptions = function(options) { | |||
| Component.prototype.setOptions.call(this, options); | |||
| }; | |||
| Linegraph.prototype._extractData = function(dataset) { | |||
| var extractedData = []; | |||
| var low = dataset[0].value; | |||
| var high = dataset[0].value; | |||
| var range = this.range.end - this.range.start; | |||
| var rangePerPixel = range/this.width; | |||
| var rangePerPixelInv = this.width/range; | |||
| var xOffset = -this.range.start + this.width*rangePerPixel; | |||
| for (var i = 0; i < dataset.length; i++) { | |||
| var val = new Date(dataset[i].start).getTime(); | |||
| val += xOffset; | |||
| val *= rangePerPixelInv; | |||
| extractedData.push({x:val, y:dataset[i].value}); | |||
| if (low > dataset[i].value) { | |||
| low = dataset[i].value; | |||
| } | |||
| if (high < dataset[i].value) { | |||
| high = dataset[i].value; | |||
| } | |||
| } | |||
| //extractedData.sort(function (a,b) {return a.x - b.x;}) | |||
| return {range:{low:low,high:high},data:extractedData}; | |||
| } | |||
| Linegraph.prototype._catmullRomUniform = function(data) { | |||
| // catmull rom | |||
| var p0, p1, p2, p3, bp1, bp2 | |||
| var d = "M" + Math.round(data[0].x) + "," + Math.round(data[0].y) + " "; | |||
| var normalization = 1/6; | |||
| var length = data.length; | |||
| for (var i = 0; i < length - 1; i++) { | |||
| p0 = (i == 0) ? data[0] : data[i-1]; | |||
| p1 = data[i]; | |||
| p2 = data[i+1]; | |||
| p3 = (i + 2 < length) ? data[i+2] : p2; | |||
| // Catmull-Rom to Cubic Bezier conversion matrix | |||
| // 0 1 0 0 | |||
| // -1/6 1 1/6 0 | |||
| // 0 1/6 1 -1/6 | |||
| // 0 0 1 0 | |||
| // bp0 = { x: p1.x, y: p1.y }; | |||
| bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)}; | |||
| bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)}; | |||
| // bp0 = { x: p2.x, y: p2.y }; | |||
| d += "C" + | |||
| Math.round(bp1.x) + "," + | |||
| Math.round(bp1.y) + " " + | |||
| Math.round(bp2.x) + "," + | |||
| Math.round(bp2.y) + " " + | |||
| Math.round(p2.x) + "," + | |||
| Math.round(p2.y) + " "; | |||
| } | |||
| return d; | |||
| }; | |||
| Linegraph.prototype._catmullRom = function(data, alpha) { | |||
| if (alpha == 0 || alpha === undefined) { | |||
| return this._catmullRomUniform(data); | |||
| } | |||
| else { | |||
| var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M; | |||
| var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA; | |||
| var d = "M" + Math.round(data[0].x) + "," + Math.round(data[0].y) + " "; | |||
| var length = data.length; | |||
| for (var i = 0; i < length - 1; i++) { | |||
| p0 = (i == 0) ? data[0] : data[i-1]; | |||
| p1 = data[i]; | |||
| p2 = data[i+1]; | |||
| p3 = (i + 2 < length) ? data[i+2] : p2; | |||
| d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2)); | |||
| d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2)); | |||
| d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2)); | |||
| // Catmull-Rom to Cubic Bezier conversion matrix | |||
| // | |||
| // A = 2d1^2a + 3d1^a * d2^a + d3^2a | |||
| // B = 2d3^2a + 3d3^a * d2^a + d2^2a | |||
| // | |||
| // [ 0 1 0 0 ] | |||
| // [ -d2^2a/N A/N d1^2a/N 0 ] | |||
| // [ 0 d3^2a/M B/M -d2^2a/M ] | |||
| // [ 0 0 1 0 ] | |||
| // [ 0 1 0 0 ] | |||
| // [ -d2pow2a/N A/N d1pow2a/N 0 ] | |||
| // [ 0 d3pow2a/M B/M -d2pow2a/M ] | |||
| // [ 0 0 1 0 ] | |||
| d3powA = Math.pow(d3, alpha); | |||
| d3pow2A = Math.pow(d3,2*alpha); | |||
| d2powA = Math.pow(d2, alpha); | |||
| d2pow2A = Math.pow(d2,2*alpha); | |||
| d1powA = Math.pow(d1, alpha); | |||
| d1pow2A = Math.pow(d1,2*alpha); | |||
| A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A; | |||
| B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A; | |||
| N = 3*d1powA * (d1powA + d2powA); | |||
| if (N > 0) {N = 1 / N;} | |||
| M = 3*d3powA * (d3powA + d2powA); | |||
| if (M > 0) {M = 1 / M;} | |||
| bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N), | |||
| y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)}; | |||
| bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M), | |||
| y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)}; | |||
| if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;} | |||
| if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;} | |||
| d += "C" + | |||
| Math.round(bp1.x) + "," + | |||
| Math.round(bp1.y) + " " + | |||
| Math.round(bp2.x) + "," + | |||
| Math.round(bp2.y) + " " + | |||
| Math.round(p2.x) + "," + | |||
| Math.round(p2.y) + " "; | |||
| } | |||
| return d; | |||
| } | |||
| }; | |||
| Linegraph.prototype._linear = function(data) { | |||
| // linear | |||
| var d = ""; | |||
| for (var i = 0; i < data.length; i++) { | |||
| if (i == 0) { | |||
| d += "M" + data[i].x + "," + data[i].y; | |||
| } | |||
| else { | |||
| d += " " + data[i].x + "," + data[i].y; | |||
| } | |||
| } | |||
| return d; | |||
| } | |||
| /** | |||
| * Set range (start and end). | |||
| * @param {Range | Object} range A Range or an object containing start and end. | |||
| */ | |||
| Linegraph.prototype.setRange = function(range) { | |||
| if (!(range instanceof Range) && (!range || !range.start || !range.end)) { | |||
| throw new TypeError('Range must be an instance of Range, ' + | |||
| 'or an object containing start and end.'); | |||
| } | |||
| this.range = range; | |||
| }; | |||
| Linegraph.prototype.repaint = function() { | |||
| var margin = this.options.margin, | |||
| range = this.range, | |||
| asSize = util.option.asSize, | |||
| asString = util.option.asString, | |||
| options = this.options, | |||
| orientation = this.getOption('orientation'), | |||
| resized = false, | |||
| frame = this.frame; | |||
| // TODO: document this feature to specify one margin for both item and axis distance | |||
| if (typeof margin === 'number') { | |||
| margin = { | |||
| item: margin, | |||
| axis: margin | |||
| }; | |||
| } | |||
| // update className | |||
| this.frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : ''); | |||
| // check whether zoomed (in that case we need to re-stack everything) | |||
| // TODO: would be nicer to get this as a trigger from Range | |||
| var visibleInterval = this.range.end - this.range.start; | |||
| var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth); | |||
| if (zoomed) this.stackDirty = true; | |||
| this.lastVisibleInterval = visibleInterval; | |||
| this.lastWidth = this.width; | |||
| // reposition frame | |||
| this.frame.style.left = asSize(options.left, ''); | |||
| this.frame.style.right = asSize(options.right, ''); | |||
| this.frame.style.top = asSize((orientation == 'top') ? '0' : ''); | |||
| this.frame.style.bottom = asSize((orientation == 'top') ? '' : '0'); | |||
| this.frame.style.width = asSize(options.width, '100%'); | |||
| // frame.style.height = asSize(height); | |||
| //frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height | |||
| // calculate actual size and position | |||
| this.top = this.frame.offsetTop; | |||
| this.left = this.frame.offsetLeft; | |||
| this.width = this.frame.offsetWidth; | |||
| // this.height = height; | |||
| // check if this component is resized | |||
| resized = this._isResized() || resized; | |||
| if (resized) { | |||
| this.svg.style.width = asSize(3*this.width); | |||
| this.svg.style.left = asSize(-this.width); | |||
| } | |||
| if (zoomed) { | |||
| this.setData(); | |||
| } | |||
| } | |||
| @ -0,0 +1,34 @@ | |||
| .vis.timeline .dataaxis .grid.horizontal { | |||
| position: absolute; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 0; | |||
| border-bottom: 1px solid; | |||
| } | |||
| .vis.timeline .dataaxis .grid.minor { | |||
| border-color: #e5e5e5; | |||
| } | |||
| .vis.timeline .dataaxis .grid.major { | |||
| border-color: #bfbfbf; | |||
| } | |||
| .vis.timeline .dataaxis .yAxis.major { | |||
| font-size:12px; | |||
| width: 100%; | |||
| position: absolute; | |||
| color: #4d4d4d; | |||
| white-space: nowrap; | |||
| } | |||
| .vis.timeline .dataaxis .yAxis.minor{ | |||
| font-size:9px; | |||
| position: absolute; | |||
| width: 100%; | |||
| color: #4d4d4d; | |||
| white-space: nowrap; | |||
| } | |||