Browse Source

added wip versions of linegraph

css_transitions
Alex de Mulder 10 years ago
parent
commit
8bff97ad55
4 changed files with 1148 additions and 0 deletions
  1. +209
    -0
      src/timeline/DataStep.js
  2. +405
    -0
      src/timeline/component/DataAxis.js
  3. +500
    -0
      src/timeline/component/Linegraph.js
  4. +34
    -0
      src/timeline/component/css/dataaxis.css

+ 209
- 0
src/timeline/DataStep.js View File

@ -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;
};

+ 405
- 0
src/timeline/component/DataAxis.js View File

@ -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);
};

+ 500
- 0
src/timeline/component/Linegraph.js View File

@ -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();
}
}

+ 34
- 0
src/timeline/component/css/dataaxis.css View File

@ -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;
}

Loading…
Cancel
Save