From 8bff97ad55e4b5a754e9e900a094e498a8d0603c Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 12 Jun 2014 16:07:49 +0200 Subject: [PATCH] added wip versions of linegraph --- src/timeline/DataStep.js | 209 ++++++++++ src/timeline/component/DataAxis.js | 405 +++++++++++++++++++ src/timeline/component/Linegraph.js | 500 ++++++++++++++++++++++++ src/timeline/component/css/dataaxis.css | 34 ++ 4 files changed, 1148 insertions(+) create mode 100644 src/timeline/DataStep.js create mode 100644 src/timeline/component/DataAxis.js create mode 100644 src/timeline/component/Linegraph.js create mode 100644 src/timeline/component/css/dataaxis.css diff --git a/src/timeline/DataStep.js b/src/timeline/DataStep.js new file mode 100644 index 00000000..f45d080c --- /dev/null +++ b/src/timeline/DataStep.js @@ -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; +}; diff --git a/src/timeline/component/DataAxis.js b/src/timeline/component/DataAxis.js new file mode 100644 index 00000000..d1851409 --- /dev/null +++ b/src/timeline/component/DataAxis.js @@ -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); +}; diff --git a/src/timeline/component/Linegraph.js b/src/timeline/component/Linegraph.js new file mode 100644 index 00000000..c98815cc --- /dev/null +++ b/src/timeline/component/Linegraph.js @@ -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(); + } + + + + +} diff --git a/src/timeline/component/css/dataaxis.css b/src/timeline/component/css/dataaxis.css new file mode 100644 index 00000000..7c7665ba --- /dev/null +++ b/src/timeline/component/css/dataaxis.css @@ -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; +} \ No newline at end of file