]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Time Scale Rewrite 1459/head
authorTanner Linsley <tannerlinsley@gmail.com>
Tue, 15 Sep 2015 17:40:01 +0000 (11:40 -0600)
committerTanner Linsley <tannerlinsley@gmail.com>
Tue, 15 Sep 2015 17:40:01 +0000 (11:40 -0600)
samples/line-time-scale.html
src/scales/scale.time.js

index 266981ffe0106bcc27a0bbb9d726852a26a3d6c5..01df3a4e2d7361fa1cfd3098bf42d951f5c8a2be 100644 (file)
         var randomColor = function(opacity) {
             return 'rgba(' + randomColorFactor() + ',' + randomColorFactor() + ',' + randomColorFactor() + ',' + (opacity || '.3') + ')';
         };
+        var newDate = function(days) {
+            var date = new Date();
+            return date.setDate(date.getDate() + days);
+        };
+        var newTimestamp = function(days) {
+            return Date.now() - days * 100000;
+        };
 
         var config = {
             type: 'line',
             data: {
-                // labels: ["01/01/2015 20:00", "01/02/2015 21:00", "01/03/2015 22:00", "01/06/2015 23:00", "01/15/2015 03:00", "01/17/2015 10:00", "01/30/2015 11:00"], // Hours
-                labels: ["01/01/2015", "01/02/2015", "01/03/2015", "01/06/2015", "01/15/2015", "01/17/2015", "01/30/2015"], // Days
+                //labels: [newTimestamp(0), newTimestamp(1), newTimestamp(2), newTimestamp(3), newTimestamp(4), newTimestamp(5), newTimestamp(6)], // unix timestamps
+                // labels: [newDate(0), newDate(1), newDate(2), newDate(3), newDate(4), newDate(5), newDate(6)], // Date Objects
+                labels: ["01/01/2015 20:00", "01/02/2015 21:00", "01/03/2015 22:00", "01/05/2015 23:00", "01/07/2015 03:00", "01/08/2015 10:00", "02/1/2015"], // Hours
+                // labels: ["01/01/2015", "01/02/2015", "01/03/2015", "01/06/2015", "01/15/2015", "01/17/2015", "01/30/2015"], // Days
                 // labels: ["12/25/2014", "01/08/2015", "01/15/2015", "01/22/2015", "01/29/2015", "02/05/2015", "02/12/2015"], // Weeks
                 datasets: [{
                     label: "My First dataset",
                     xAxes: [{
                         type: "time",
                         display: true,
-                        time: {
+                        tick: {
                             format: 'MM/DD/YYYY HH:mm',
+                            // round: 'day'
                         }
                     }, ],
                     yAxes: [{
                         display: true
                     }]
-                }
+                },
+                elements: {
+                    line: {
+                        tension: 0
+                    }
+                },
             }
         };
 
index cbf6473ca6ee3def225d64c7bcf5e651d95cc122..18df1a6ac8f65ff5cdd932f678829e75c6a8c0b8 100644 (file)
@@ -5,6 +5,58 @@
                Chart = root.Chart,
                helpers = Chart.helpers;
 
+       var time = {
+               units: [
+                       'millisecond',
+                       'second',
+                       'minute',
+                       'hour',
+                       'day',
+                       'week',
+                       'month',
+                       'quarter',
+                       'year',
+               ],
+               unit: {
+                       'millisecond': {
+                               display: 'SSS [ms]', // 002 ms
+                               maxStep: 1000,
+                       },
+                       'second': {
+                               display: 'h:mm:ss a', // 11:20:01 AM
+                               maxStep: 60,
+                       },
+                       'minute': {
+                               display: 'h:mm:ss a', // 11:20:01 AM
+                               maxStep: 60,
+                       },
+                       'hour': {
+                               display: 'MMM D, hA', // Sept 4, 5PM
+                               maxStep: 24,
+                       },
+                       'day': {
+                               display: 'll', // Sep 4 2015
+                               maxStep: 7,
+                       },
+                       'week': {
+                               display: 'll', // Week 46, or maybe "[W]WW - YYYY" ?
+                               maxStep: 4.3333,
+                       },
+                       'month': {
+                               display: 'MMM YYYY', // Sept 2015
+                               maxStep: 12,
+                       },
+                       'quarter': {
+                               display: '[Q]Q - YYYY', // Q3
+                               maxStep: 4,
+                       },
+                       'year': {
+                               display: 'YYYY', // 2015
+                               maxStep: false,
+                       },
+               }
+       };
+
        var defaultConfig = {
                display: true,
                position: "bottom",
                        drawTicks: true, // draw ticks extending towards the label
                },
 
-               time: {
-                       format: false, // http://momentjs.com/docs/#/parsing/string-format/
-                       unit: false, // week, month, year, etc.
-                       aggregation: 'average',
-                       display: false, //http://momentjs.com/docs/#/parsing/string-format/
-                       unitFormats: {
-                               'millisecond': 'h:mm:ss SSS', // 11:20:01 002
-                               'second': 'h:mm:ss', // 11:20:01
-                               'minute': 'h:mm:ss a', // 11:20:01 AM
-                               'hour': 'MMM D, hA', // Sept 4, 5PM
-                               'day': 'll', // Sep 4 2015 8:30 PM
-                               'week': '[W]WW - YYYY', // Week 46
-                               'month': 'MMM YYYY', // Sept 2015
-                               'quarter': '[Q]Q - YYYY', // Q3
-                               'year': 'YYYY' // 2015
-                       }
+               tick: {
+                       format: false, // false == date objects or use pattern string from http://momentjs.com/docs/#/parsing/string-format/
+                       unit: false, // false == automatic or override with week, month, year, etc.
+                       round: false, // none, or override with week, month, year, etc.
+                       displayFormat: false, // defaults to unit's corresponding unitFormat below or override using pattern string from http://momentjs.com/docs/#/displaying/format/
                },
 
                // scale numbers
@@ -49,7 +90,8 @@
                        fontSize: 12,
                        fontStyle: "normal",
                        fontColor: "#666",
-                       fontFamily: "Helvetica Neue"
+                       fontFamily: "Helvetica Neue",
+                       maxRotation: 45,
                }
        };
 
                        return this.options.position == "top" || this.options.position == "bottom";
                },
                parseTime: function(label) {
-                       if (typeof this.options.time.format !== 'string' && this.options.time.format.call) {
-                               return this.options.time.format(label);
-                       } else {
-                               return moment(label, this.options.time.format);
+                       // Date objects
+                       if (typeof label.getMonth === 'function' || typeof label == 'number') {
+                               return moment(label);
+                       }
+                       // Moment support
+                       if (label.isValid && label.isValid()) {
+                               return label;
                        }
+                       // Custom parsing (return an instance of moment)
+                       if (typeof this.options.tick.format !== 'string' && this.options.tick.format.call) {
+                               return this.options.tick.format(label);
+                       }
+                       // Moment format parsing
+                       return moment(label, this.options.tick.format);
                },
-               buildLabels: function(index) {
-                       // Actual labels on the grid
-                       this.labels = [];
-                       // A map of original labelIndex to time labelIndex
-                       this.timeLabelIndexMap = {};
-                       // The time formatted versions of the labels for use by tooltips
-                       this.data.timeLabels = [];
+               generateTicks: function(index) {
 
-                       var definedMoments = [];
+                       this.ticks = [];
+                       this.labelMoments = [];
 
-                       // Format each point into a moment
+                       // Parse each label into a moment
                        this.data.labels.forEach(function(label, index) {
-                               definedMoments.push(this.parseTime(label));
+                               var labelMoment = this.parseTime(label);
+                               if (this.options.tick.round) {
+                                       labelMoment.startOf(this.options.tick.round);
+                               }
+                               this.labelMoments.push(labelMoment);
                        }, this);
 
+                       // Find the first and last moments, and range
+                       this.firstTick = moment.min.call(this, this.labelMoments).clone();
+                       this.lastTick = moment.max.call(this, this.labelMoments).clone();
 
-                       // Find or set the unit of time
-                       if (!this.options.time.unit) {
-
+                       // Set unit override if applicable
+                       if (this.options.tick.unit) {
+                               this.tickUnit = this.options.tick.unit || 'day';
+                               this.displayFormat = time.unit.day.display;
+                               this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, this.tickUnit, true));
+                       } else {
                                // Determine the smallest needed unit of the time
-                               helpers.each([
-                                       'millisecond',
-                                       'second',
-                                       'minute',
-                                       'hour',
-                                       'day',
-                                       'week',
-                                       'month',
-                                       'quarter',
-                                       'year',
-                               ], function(format) {
-                                       if (this.timeUnit) {
+                               var innerWidth = this.width - (this.paddingLeft + this.paddingRight);
+                               var labelCapacity = innerWidth / this.options.labels.fontSize + 4;
+                               var buffer = this.options.tick.round ? 0 : 2;
+
+                               this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, true) + buffer);
+                               var done;
+
+                               helpers.each(time.units, function(format) {
+                                       if (this.tickRange <= labelCapacity) {
                                                return;
                                        }
-                                       var start;
-                                       helpers.each(definedMoments, function(mom) {
-                                               if (!start) {
-                                                       start = mom[format]();
-                                               }
+                                       this.tickUnit = format;
+                                       this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, this.tickUnit) + buffer);
+                                       this.displayFormat = time.unit[format].display;
 
-                                               if (mom[format]() !== start) {
-                                                       this.timeUnit = format;
-                                                       if (!this.displayFormat) {
-                                                               this.displayFormat = this.options.time.unitFormats[format];
-                                                       }
-                                               }
-                                       }, this);
                                }, this);
-                       } else {
-                               this.timeUnit = this.options.time.unit;
                        }
 
-                       if (!this.timeUnit) {
-                               this.timeUnit = 'day';
-                               this.displayFormat = this.options.time.unitFormats.day;
-                       }
+                       this.firstTick.startOf(this.tickUnit);
+                       this.lastTick.endOf(this.tickUnit);
 
 
-                       if (this.options.time.display) {
-                               this.displayFormat = this.options.time.display;
+                       // Tick displayFormat override
+                       if (this.options.tick.displayFormat) {
+                               this.displayFormat = this.options.tick.displayFormat;
                        }
 
-
-                       // Find the first and last moments
-                       this.firstMoment = moment.min.call(this, definedMoments);
-                       this.lastMoment = moment.max.call(this, definedMoments);
-
-                       // Find the length of the timeframe in the desired unit
-                       var momentRangeLength = this.lastMoment.diff(this.firstMoment, this.timeUnit);
-
-                       helpers.each(definedMoments, function(definedMoment, index) {
-                               this.timeLabelIndexMap[index] = momentRangeLength - this.lastMoment.diff(definedMoment, this.timeUnit);
-                               this.data.timeLabels.push(
-                                       definedMoment
-                                       .format(this.options.time.display ? this.options.time.display : this.displayFormat)
-                               );
-                       }, this);
-
                        // For every unit in between the first and last moment, create a moment and add it to the labels tick
                        var i = 0;
                        if (this.options.labels.userCallback) {
-                               for (; i <= momentRangeLength; i++) {
-                                       this.labels.push(
-                                               this.options.labels.userCallback(this.firstMoment
-                                                       .add((!i ? 0 : 1), this.timeUnit)
-                                                       .format(this.options.time.display ? this.options.time.display : this.displayFormat)
+                               for (; i <= this.tickRange; i++) {
+                                       this.ticks.push(
+                                               this.options.labels.userCallback(this.firstTick.clone()
+                                                       .add(i, this.tickUnit)
+                                                       .format(this.options.tick.displayFormat ? this.options.tick.displayFormat : time.unit[this.tickUnit].display)
                                                )
                                        );
                                }
                        } else {
-                               for (; i <= momentRangeLength; i++) {
-                                       this.labels.push(this.firstMoment
-                                               .add((!i ? 0 : 1), this.timeUnit)
-                                               .format(this.options.time.display ? this.options.time.display : this.displayFormat)
+                               for (; i <= this.tickRange; i++) {
+                                       this.ticks.push(this.firstTick.clone()
+                                               .add(i, this.tickUnit)
+                                               .format(this.options.tick.displayFormat ? this.options.tick.displayFormat : time.unit[this.tickUnit].display)
                                        );
                                }
                        }
-
-
                },
-               getPixelForValue: function(value, index, datasetIndex, includeOffset) {
+               getPixelForValue: function(value, decimal, datasetIndex, includeOffset) {
                        // This must be called after fit has been run so that 
                        //      this.left, this.top, this.right, and this.bottom have been defined
                        if (this.isHorizontal()) {
                                var innerWidth = this.width - (this.paddingLeft + this.paddingRight);
-                               var valueWidth = innerWidth / Math.max((this.labels.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1);
-                               var valueOffset = (valueWidth * index) + this.paddingLeft;
+                               var valueWidth = innerWidth / Math.max((this.ticks.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1);
+                               var valueOffset = (innerWidth * decimal) + this.paddingLeft;
 
                                if (this.options.gridLines.offsetGridLines && includeOffset) {
                                        valueOffset += (valueWidth / 2);
 
                                return this.left + Math.round(valueOffset);
                        } else {
-                               return this.top + (index * (this.height / this.labels.length));
+                               return this.top + (decimal * (this.height / this.ticks.length));
                        }
                },
                getPointPixelForValue: function(value, index, datasetIndex) {
-                       // This function references the timeLaabelIndexMap to know which index in the timeLabels corresponds to the index of original labels
-                       return this.getPixelForValue(value, this.timeLabelIndexMap[index], datasetIndex, true);
+
+                       var offset = this.labelMoments[index].diff(this.firstTick, this.tickUnit, true);
+                       return this.getPixelForValue(value, offset / this.tickRange, datasetIndex);
                },
 
                // Functions needed for bar charts
                calculateBaseWidth: function() {
-                       return (this.getPixelForValue(null, 1, 0, true) - this.getPixelForValue(null, 0, 0, true)) - (2 * this.options.categorySpacing);
+                       return (this.getPixelForValue(null, this.ticks.length / 100, 0, true) - this.getPixelForValue(null, 0, 0, true)) - (2 * this.options.categorySpacing);
                },
                calculateBarWidth: function(barDatasetCount) {
                        //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset
                        return (baseWidth / barDatasetCount);
                },
                calculateBarX: function(barDatasetCount, datasetIndex, elementIndex) {
+
                        var xWidth = this.calculateBaseWidth(),
                                xAbsolute = this.getPixelForValue(null, elementIndex, datasetIndex, true) - (xWidth / 2),
                                barWidth = this.calculateBarWidth(barDatasetCount);
                        return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * this.options.spacing) + barWidth / 2;
                },
 
-               calculateLabelRotation: function(maxHeight, margins) {
+               calculateTickRotation: function(maxHeight, margins) {
                        //Get the width of each grid by calculating the difference
                        //between x offsets between 0 and 1.
                        var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily);
                        this.ctx.font = labelFont;
 
-                       var firstWidth = this.ctx.measureText(this.labels[0]).width;
-                       var lastWidth = this.ctx.measureText(this.labels[this.labels.length - 1]).width;
+                       var firstWidth = this.ctx.measureText(this.ticks[0]).width;
+                       var lastWidth = this.ctx.measureText(this.ticks[this.ticks.length - 1]).width;
                        var firstRotated;
                        var lastRotated;
 
                        this.labelRotation = 0;
 
                        if (this.options.display) {
-                               var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels);
+                               var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.ticks);
                                var cosRotation;
                                var sinRotation;
                                var firstRotatedWidth;
                                //Allow 3 pixels x2 padding either side for label readability
                                // only the index matters for a dataset scale, but we want a consistent interface between scales
 
-                               var datasetWidth = Math.floor(this.getPixelForValue(0, 1) - this.getPixelForValue(0, 0)) - 6;
+                               var datasetWidth = Math.floor(this.getPixelForValue(null, 1 / this.ticks.length) - this.getPixelForValue(null, 0)) - 6;
 
                                //Max label rotation can be set or default to 90 - also act as a loop counter
                                while (this.labelWidth > datasetWidth && this.labelRotation <= this.options.labels.maxRotation) {
                                        this.labelRotation++;
                                        this.labelWidth = cosRotation * originalLabelWidth;
 
+
                                }
                        } else {
                                this.labelWidth = 0;
                                this.height = maxHeight;
                        }
 
-                       this.buildLabels();
-                       this.calculateLabelRotation(maxHeight, margins);
+                       this.generateTicks();
+                       this.calculateTickRotation(maxHeight, margins);
 
                        var minSize = {
                                width: 0,
                        };
 
                        var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily);
-                       var longestLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels);
+                       var longestLabelWidth = helpers.longestText(this.ctx, labelFont, this.ticks);
 
                        // Width
                        if (this.isHorizontal()) {
                                        var isRotated = this.labelRotation !== 0;
                                        var skipRatio = false;
 
-                                       if ((this.options.labels.fontSize + 4) * this.labels.length > (this.width - (this.paddingLeft + this.paddingRight))) {
-                                               skipRatio = 1 + Math.floor(((this.options.labels.fontSize + 4) * this.labels.length) / (this.width - (this.paddingLeft + this.paddingRight)));
+                                       if ((this.options.labels.fontSize + 4) * this.ticks.length > (this.width - (this.paddingLeft + this.paddingRight))) {
+                                               skipRatio = 1 + Math.floor(((this.options.labels.fontSize + 4) * this.ticks.length) / (this.width - (this.paddingLeft + this.paddingRight)));
                                        }
 
-                                       helpers.each(this.labels, function(label, index) {
-                                               // Blank labels
-                                               if ((skipRatio > 1 && index % skipRatio > 0) || (label === undefined || label === null)) {
+                                       helpers.each(this.ticks, function(tick, index) {
+                                               // Blank ticks
+                                               if ((skipRatio > 1 && index % skipRatio > 0) || (tick === undefined || tick === null)) {
                                                        return;
                                                }
-                                               var xLineValue = this.getPixelForValue(label, index, null, false); // xvalues for grid lines
-                                               var xLabelValue = this.getPixelForValue(label, index, null, true); // x values for labels (need to consider offsetLabel option)
+                                               var xLineValue = this.getPixelForValue(null, (1 / (this.ticks.length - 1)) * index, null, false); // xvalues for grid lines
+                                               var xLabelValue = this.getPixelForValue(null, (1 / (this.ticks.length - 1)) * index, null, true); // x values for ticks (need to consider offsetLabel option)
 
                                                if (this.options.gridLines.show) {
                                                        if (index === 0) {
 
                                                        xLineValue += helpers.aliasPixel(this.ctx.lineWidth);
 
-                                                       // Draw the label area
+                                                       // Draw the tick area
                                                        this.ctx.beginPath();
 
                                                        if (this.options.gridLines.drawTicks) {
                                                        this.ctx.font = this.font;
                                                        this.ctx.textAlign = (isRotated) ? "right" : "center";
                                                        this.ctx.textBaseline = (isRotated) ? "middle" : "top";
-                                                       this.ctx.fillText(label, 0, 0);
+                                                       this.ctx.fillText(tick, 0, 0);
                                                        this.ctx.restore();
                                                }
                                        }, this);