]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
50% Canvas Legend Support
authorTanner Linsley <tannerlinsley@gmail.com>
Mon, 26 Oct 2015 19:57:59 +0000 (13:57 -0600)
committerTanner Linsley <tannerlinsley@gmail.com>
Mon, 26 Oct 2015 19:57:59 +0000 (13:57 -0600)
samples/line-legend.html [new file with mode: 0644]
src/core/core.controller.js
src/core/core.layoutService.js [new file with mode: 0644]
src/core/core.legend.js [new file with mode: 0644]
src/core/core.scaleService.js

diff --git a/samples/line-legend.html b/samples/line-legend.html
new file mode 100644 (file)
index 0000000..6373cb5
--- /dev/null
@@ -0,0 +1,196 @@
+<!doctype html>
+<html>
+
+<head>
+    <title>Line Chart</title>
+    <script src="../Chart.js"></script>
+    <script src="../node_modules/jquery/dist/jquery.min.js"></script>
+    <style>
+        canvas {
+            -webkit-box-shadow: 0 0 20px 0 rgba(0, 0, 0, .5);
+        }
+    </style>
+</head>
+
+<body>
+    <div style="width:100%;">
+        <canvas id="canvas" style="width:100%;height:100%"></canvas>
+    </div>
+    <br>
+    <br>
+    <button id="randomizeData">Randomize Data</button>
+    <button id="addDataset">Add Dataset</button>
+    <button id="removeDataset">Remove Dataset</button>
+    <button id="addData">Add Data</button>
+    <button id="removeData">Remove Data</button>
+    <script>
+        var randomScalingFactor = function() {
+            return Math.round(Math.random() * 100 * (Math.random() > 0.5 ? -1 : 1));
+        };
+        var randomColorFactor = function() {
+            return Math.round(Math.random() * 255);
+        };
+        var randomColor = function(opacity) {
+            return 'rgba(' + randomColorFactor() + ',' + randomColorFactor() + ',' + randomColorFactor() + ',' + (opacity || '.3') + ')';
+        };
+
+        var config = {
+            type: 'line',
+            data: {
+                labels: ["January", "February", "March", "April", "May", "June", "July"],
+                datasets: [{
+                    label: "My First dataset",
+                    data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
+                    fill: false,
+                }, {
+                    label: "My Second dataset",
+                    data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
+                    fill: false,
+                }, {
+                    label: "My Third dataset",
+                    data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
+                    fill: false,
+                }, {
+                    label: "My Fourth dataset",
+                    data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
+                    fill: false,
+                }, {
+                    label: "My First dataset",
+                    data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
+                    fill: false,
+                }, {
+                    label: "My Second dataset",
+                    data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
+                    fill: false,
+                }, {
+                    label: "My Third dataset",
+                    data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
+                    fill: false,
+                }, {
+                    label: "My Fourth dataset",
+                    data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
+                    fill: false,
+                }]
+            },
+            options: {
+                responsive: true,
+                legend: {
+                    position: 'top',
+                    title: {
+                        display: true,
+                        text: 'Our 4 Favorite Datasets'
+                    }
+                },
+                hover: {
+                    mode: 'label'
+                },
+                scales: {
+                    xAxes: [{
+                        display: true,
+                        scaleLabel: {
+                            show: true,
+                            labelString: 'Month'
+                        }
+                    }],
+                    yAxes: [{
+                        display: true,
+                        scaleLabel: {
+                            show: true,
+                            labelString: 'Value'
+                        }
+                    }]
+                }
+            }
+        };
+
+        $.each(config.data.datasets, function(i, dataset) {
+            var background = randomColor(0.5);
+            dataset.borderColor = background;
+            dataset.backgroundColor = background;
+            dataset.pointBorderColor = background;
+            dataset.pointBackgroundColor = background;
+            dataset.pointBorderWidth = 1;
+        });
+
+        console.log(config.data);
+
+        window.onload = function() {
+            var ctx = document.getElementById("canvas").getContext("2d");
+            window.myLine = new Chart(ctx, config);
+
+            updateLegend();
+        };
+
+        function updateLegend() {
+            $legendContainer = $('#legendContainer');
+            $legendContainer.empty();
+            $legendContainer.append(window.myLine.generateLegend());
+        }
+
+        $('#randomizeData').click(function() {
+            $.each(config.data.datasets, function(i, dataset) {
+                dataset.data = dataset.data.map(function() {
+                    return randomScalingFactor();
+                });
+
+            });
+
+            window.myLine.update();
+            updateLegend();
+        });
+
+        $('#addDataset').click(function() {
+            var background = randomColor(0.5);
+            var newDataset = {
+                label: 'Dataset ' + config.data.datasets.length,
+                borderColor: background,
+                backgroundColor: background,
+                pointBorderColor: background,
+                pointBackgroundColor: background,
+                pointBorderWidth: 1,
+                fill: false,
+                data: [],
+            };
+
+            for (var index = 0; index < config.data.labels.length; ++index) {
+                newDataset.data.push(randomScalingFactor());
+            }
+
+            config.data.datasets.push(newDataset);
+            window.myLine.update();
+            updateLegend();
+        });
+
+        $('#addData').click(function() {
+            if (config.data.datasets.length > 0) {
+                config.data.labels.push('dataset #' + config.data.labels.length);
+
+                $.each(config.data.datasets, function(i, dataset) {
+                    dataset.data.push(randomScalingFactor());
+                });
+
+                window.myLine.update();
+                updateLegend();
+            }
+        });
+
+        $('#removeDataset').click(function() {
+            config.data.datasets.splice(0, 1);
+            window.myLine.update();
+            updateLegend();
+        });
+
+        $('#removeData').click(function() {
+            config.data.labels.splice(-1, 1); // remove the label first
+
+            config.data.datasets.forEach(function(dataset, datasetIndex) {
+                dataset.data.pop();
+            });
+
+            window.myLine.update();
+            updateLegend();
+        });
+    </script>
+</body>
+
+</html>
index 915ae85121d7cddfd6cecd454f8457aa0a040827..18593955aa0a84d143d0e1ff98c3e8e51ae9b776 100644 (file)
@@ -54,6 +54,8 @@
                        this.ensureScalesHaveIDs();
                        this.buildOrUpdateControllers();
                        this.buildScales();
+                       this.buildLegends();
+                       this.updateLayout();
                        this.resetElements();
                        this.initToolTip();
                        this.update();
                                this.scales.radialScale = scale;
                        }
 
-                       Chart.scaleService.update(this, this.chart.width, this.chart.height);
+                       Chart.scaleService.addScalesToLayout(this);
+               },
+
+               buildLegends: function() {
+                       if (!this.options.legend) {
+                               return;
+                       }
+
+                       this.legend = new Chart.Legend({
+                               ctx: this.chart.ctx,
+                               options: this.options.legend,
+                               chart: this,
+                       });
+
+                       Chart.layoutService.addBox(this, this.legend);
+               },
+
+               updateLayout: function() {
+                       Chart.layoutService.update(this, this.chart.width, this.chart.height);
                },
 
                buildOrUpdateControllers: function buildOrUpdateControllers(resetNewControllers) {
                },
 
                update: function update(animationDuration, lazy) {
-                       Chart.scaleService.update(this, this.chart.width, this.chart.height);
+                       Chart.layoutService.update(this, this.chart.width, this.chart.height);
 
                        // Make sure dataset controllers are updated and new controllers are reset
                        this.buildOrUpdateControllers(true);
                        this.clear();
 
                        // Draw all the scales
-                       helpers.each(this.scales, function(scale) {
-                               scale.draw(this.chartArea);
+                       helpers.each(this.boxes, function(box) {
+                               box.draw(this.chartArea);
                        }, this);
                        if (this.scale) {
                                this.scale.draw();
diff --git a/src/core/core.layoutService.js b/src/core/core.layoutService.js
new file mode 100644 (file)
index 0000000..66399e3
--- /dev/null
@@ -0,0 +1,347 @@
+(function() {
+       "use strict";
+
+       var root = this,
+               Chart = root.Chart,
+               helpers = Chart.helpers;
+
+       // The layout service is ver self explanatory.  It's responsible for the layout within a chart.  
+       // Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need
+       // It is this service's responsibility of carrying out that layout.
+       Chart.layoutService = {
+               defaults: {},
+
+               // Register a box to a chartInstance. A box is simply a reference to an object that requires layout. eg. Scales, Legend, Plugins.
+               addBox: function(chartInstance, box) {
+                       if (!chartInstance.boxes) {
+                               chartInstance.boxes = [];
+                       }
+                       chartInstance.boxes.push(box);
+               },
+
+               removeBox: function(chartInstance, box) {
+                       if (!chartInstance.boxes) {
+                               return;
+                       }
+                       chartInstance.boxes.splice(chartInstance.boxes.indexOf(box), 1);
+               },
+
+               // The most important function
+               update: function(chartInstance, width, height) {
+
+                       if (!chartInstance) {
+                               return;
+                       }
+
+                       var xPadding = width > 30 ? 5 : 2;
+                       var yPadding = height > 30 ? 5 : 2;
+
+                       var leftBoxes = helpers.where(chartInstance.boxes, function(box) {
+                               return box.options.position == "left";
+                       });
+                       var rightBoxes = helpers.where(chartInstance.boxes, function(box) {
+                               return box.options.position == "right";
+                       });
+                       var topBoxes = helpers.where(chartInstance.boxes, function(box) {
+                               return box.options.position == "top";
+                       });
+                       var bottomBoxes = helpers.where(chartInstance.boxes, function(box) {
+                               return box.options.position == "bottom";
+                       });
+
+                       // Boxes that overlay the chartarea such as the radialLinear scale
+                       var chartAreaBoxes = helpers.where(chartInstance.boxes, function(box) {
+                               return box.options.position == "chartArea";
+                       });
+
+                       // Essentially we now have any number of boxes on each of the 4 sides.
+                       // Our canvas looks like the following.
+                       // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and 
+                       // B1 is the bottom axis
+                       // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays
+                       // These locations are single-box locations only, when trying to register a chartArea location that is already taken,
+                       // an error will be thrown.
+                       //
+                       // |----------------------------------------------------|
+                       // |                  T1 (Full Width)                   |
+                       // |----------------------------------------------------|
+                       // |    |    |                 T2                  |    |
+                       // |    |----|-------------------------------------|----|
+                       // |    |    | C1 |                           | C2 |    |
+                       // |    |    |----|                           |----|    |
+                       // |    |    |                                     |    |
+                       // | L1 | L2 |           ChartArea (C0)            | R1 |
+                       // |    |    |                                     |    |
+                       // |    |    |----|                           |----|    |
+                       // |    |    | C3 |                           | C4 |    |
+                       // |    |----|-------------------------------------|----|
+                       // |    |    |                 B1                  |    |
+                       // |----------------------------------------------------|
+                       // |                  B2 (Full Width)                   |
+                       // |----------------------------------------------------|
+                       //
+                       // What we do to find the best sizing, we do the following
+                       // 1. Determine the minimum size of the chart area. 
+                       // 2. Split the remaining width equally between each vertical axis
+                       // 3. Split the remaining height equally between each horizontal axis
+                       // 4. Give each layout the maximum size it can be. The layout will return it's minimum size
+                       // 5. Adjust the sizes of each axis based on it's minimum reported size. 
+                       // 6. Refit each axis
+                       // 7. Position each axis in the final location
+                       // 8. Tell the chart the final location of the chart area
+                       // 9. Tell any axes that overlay the chart area the positions of the chart area
+
+                       // Step 1
+                       var chartWidth = width / 2; // min 50%
+                       var chartHeight = height / 2; // min 50%
+
+                       chartWidth -= (2 * xPadding);
+                       chartHeight -= (2 * yPadding);
+
+                       // Step 2
+                       var verticalBoxWidth = (width - chartWidth) / (leftBoxes.length + rightBoxes.length);
+
+                       // Step 3
+                       var horizontalBoxHeight = (height - chartHeight) / (topBoxes.length + bottomBoxes.length);
+
+                       // Step 4;
+                       var minBoxSizes = [];
+
+                       // vertical boxes
+                       helpers.each(leftBoxes, verticalBoxMinSizeFunction);
+                       helpers.each(rightBoxes, verticalBoxMinSizeFunction);
+
+                       function verticalBoxMinSizeFunction(box) {
+                               var minSize = box.update(verticalBoxWidth, chartHeight);
+                               minBoxSizes.push({
+                                       horizontal: false,
+                                       minSize: minSize,
+                                       box: box,
+                               });
+                       }
+
+                       // horizontal boxes
+                       helpers.each(topBoxes, horizontalBoxMinSizeFunction);
+                       helpers.each(bottomBoxes, horizontalBoxMinSizeFunction);
+
+                       function horizontalBoxMinSizeFunction(box) {
+                               var minSize = box.update(chartWidth, horizontalBoxHeight);
+                               minBoxSizes.push({
+                                       horizontal: true,
+                                       minSize: minSize,
+                                       box: box,
+                               });
+                       }
+
+                       // Step 5
+                       var maxChartHeight = height - (2 * yPadding);
+                       var maxChartWidth = width - (2 * xPadding);
+
+                       helpers.each(minBoxSizes, function(minimumBoxSize) {
+                               if (minimumBoxSize.horizontal) {
+                                       maxChartHeight -= minimumBoxSize.minSize.height;
+                               } else {
+                                       maxChartWidth -= minimumBoxSize.minSize.width;
+                               }
+                       });
+
+                       // At this point, maxChartHeight and maxChartWidth are the size the chart area could
+                       // be if the axes are drawn at their minimum sizes.
+
+                       // Step 6
+                       var totalLeftWidth = xPadding;
+                       var totalRightWidth = xPadding;
+                       var totalTopHeight = yPadding;
+                       var totalBottomHeight = yPadding;
+
+                       helpers.each(leftBoxes, verticalBoxFitFunction);
+                       helpers.each(rightBoxes, verticalBoxFitFunction);
+
+                       function verticalBoxFitFunction(box) {
+                               var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBoxSize) {
+                                       return minBoxSize.box === box;
+                               });
+
+                               if (minBoxSize) {
+                                       box.update(minBoxSize.minSize.width, maxChartHeight);
+                               }
+                       }
+
+                       // Figure out how much margin is on the left and right of the horizontal axes
+                       helpers.each(leftBoxes, function(box) {
+                               totalLeftWidth += box.width;
+                       });
+
+                       helpers.each(rightBoxes, function(box) {
+                               totalRightWidth += box.width;
+                       });
+
+                       helpers.each(topBoxes, horizontalBoxFitFunction);
+                       helpers.each(bottomBoxes, horizontalBoxFitFunction);
+
+                       function horizontalBoxFitFunction(box) {
+                               var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBoxSize) {
+                                       return minBoxSize.box === box;
+                               });
+
+                               var scaleMargin = {
+                                       left: totalLeftWidth,
+                                       right: totalRightWidth,
+                                       top: 0,
+                                       bottom: 0,
+                               };
+
+                               if (minBoxSize) {
+                                       box.update(maxChartWidth, minBoxSize.minSize.height, scaleMargin);
+                               }
+                       }
+
+                       helpers.each(topBoxes, function(box) {
+                               totalTopHeight += box.height;
+                       });
+
+                       helpers.each(bottomBoxes, function(box) {
+                               totalBottomHeight += box.height;
+                       });
+
+                       // Let the left layout know the final margin
+                       helpers.each(leftBoxes, function(box) {
+                               var wrapper = helpers.findNextWhere(minBoxSizes, function(wrapper) {
+                                       return wrapper.box === box;
+                               });
+
+                               var scaleMargin = {
+                                       left: 0,
+                                       right: 0,
+                                       top: totalTopHeight,
+                                       bottom: totalBottomHeight
+                               };
+
+                               if (wrapper) {
+                                       box.update(wrapper.minSize.width, maxChartHeight, scaleMargin);
+                               }
+                       });
+
+                       helpers.each(rightBoxes, function(box) {
+                               var wrapper = helpers.findNextWhere(minBoxSizes, function(wrapper) {
+                                       return wrapper.box === box;
+                               });
+
+                               var scaleMargin = {
+                                       left: 0,
+                                       right: 0,
+                                       top: totalTopHeight,
+                                       bottom: totalBottomHeight
+                               };
+
+                               if (wrapper) {
+                                       box.update(wrapper.minSize.width, maxChartHeight, scaleMargin);
+                               }
+                       });
+
+                       // Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance)
+                       totalLeftWidth = xPadding;
+                       totalRightWidth = xPadding;
+                       totalTopHeight = yPadding;
+                       totalBottomHeight = yPadding;
+
+                       helpers.each(leftBoxes, function(box) {
+                               totalLeftWidth += box.width;
+                       });
+
+                       helpers.each(rightBoxes, function(box) {
+                               totalRightWidth += box.width;
+                       });
+
+                       helpers.each(topBoxes, function(box) {
+                               totalTopHeight += box.height;
+                       });
+                       helpers.each(bottomBoxes, function(box) {
+                               totalBottomHeight += box.height;
+                       });
+
+                       // Figure out if our chart area changed. This would occur if the dataset layout label rotation
+                       // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do
+                       // without calling `fit` again
+                       var newMaxChartHeight = height - totalTopHeight - totalBottomHeight;
+                       var newMaxChartWidth = width - totalLeftWidth - totalRightWidth;
+
+                       if (newMaxChartWidth !== maxChartWidth || newMaxChartHeight !== maxChartHeight) {
+                               helpers.each(leftBoxes, function(box) {
+                                       box.height = newMaxChartHeight;
+                               });
+
+                               helpers.each(rightBoxes, function(box) {
+                                       box.height = newMaxChartHeight;
+                               });
+
+                               helpers.each(topBoxes, function(box) {
+                                       box.width = newMaxChartWidth;
+                               });
+
+                               helpers.each(bottomBoxes, function(box) {
+                                       box.width = newMaxChartWidth;
+                               });
+
+                               maxChartHeight = newMaxChartHeight;
+                               maxChartWidth = newMaxChartWidth;
+                       }
+
+                       // Step 7 - Position the boxes
+                       var left = xPadding;
+                       var top = yPadding;
+                       var right = 0;
+                       var bottom = 0;
+
+                       helpers.each(leftBoxes, verticalBoxPlacer);
+                       helpers.each(topBoxes, horizontalBoxPlacer);
+
+                       // Account for chart width and height
+                       left += maxChartWidth;
+                       top += maxChartHeight;
+
+                       helpers.each(rightBoxes, verticalBoxPlacer);
+                       helpers.each(bottomBoxes, horizontalBoxPlacer);
+
+                       function verticalBoxPlacer(box) {
+                               box.left = left;
+                               box.right = left + box.width;
+                               box.top = totalTopHeight;
+                               box.bottom = totalTopHeight + maxChartHeight;
+
+                               // Move to next point
+                               left = box.right;
+                       }
+
+                       function horizontalBoxPlacer(box) {
+                               box.left = totalLeftWidth;
+                               box.right = totalLeftWidth + maxChartWidth;
+                               box.top = top;
+                               box.bottom = top + box.height;
+
+                               // Move to next point 
+                               top = box.bottom;
+                       }
+
+                       // Step 8
+                       chartInstance.chartArea = {
+                               left: totalLeftWidth,
+                               top: totalTopHeight,
+                               right: totalLeftWidth + maxChartWidth,
+                               bottom: totalTopHeight + maxChartHeight,
+                       };
+
+                       // Step 9
+                       helpers.each(chartAreaBoxes, function(box) {
+                               box.left = chartInstance.chartArea.left;
+                               box.top = chartInstance.chartArea.top;
+                               box.right = chartInstance.chartArea.right;
+                               box.bottom = chartInstance.chartArea.bottom;
+
+                               box.update(maxChartWidth, maxChartHeight);
+                       });
+               }
+       };
+
+
+}).call(this);
diff --git a/src/core/core.legend.js b/src/core/core.legend.js
new file mode 100644 (file)
index 0000000..28e3194
--- /dev/null
@@ -0,0 +1,271 @@
+(function() {
+       "use strict";
+
+       var root = this,
+               Chart = root.Chart,
+               helpers = Chart.helpers;
+
+       Chart.defaults.legend = {
+
+               display: true,
+               position: 'top',
+               onClick: false, // a callback will override the default behavior of toggling the datasets
+
+               title: {
+                       position: 'top',
+                       fontColor: '#666',
+                       fontFamily: 'Helvetica Neue',
+                       fontSize: 12,
+                       fontStyle: 'bold',
+                       padding: 10,
+
+                       // actual title
+                       text: '',
+
+                       // display property
+                       display: false,
+               },
+
+               labels: {
+                       fontSize: 12,
+                       fontStyle: "normal",
+                       fontColor: "#666",
+                       fontFamily: "Helvetica Neue",
+                       padding: 10,
+                       reverse: false,
+                       display: true,
+                       callback: function(value) {
+                               return '' + value;
+                       },
+               },
+       };
+
+       Chart.Legend = Chart.Element.extend({
+
+               initialize: function(config) {
+                       helpers.extend(this, config);
+                       this.options = helpers.configMerge(Chart.defaults.legend, config.options);
+               },
+
+               // These methods are ordered by lifecyle. Utilities then follow.
+               // Any function defined here is inherited by all legend types.
+               // Any function can be extended by the legend type
+
+               beforeUpdate: helpers.noop,
+               update: function(maxWidth, maxHeight, margins) {
+
+                       // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;)
+                       this.beforeUpdate();
+
+                       // Absorb the master measurements
+                       this.maxWidth = maxWidth;
+                       this.maxHeight = maxHeight;
+                       this.margins = margins;
+
+                       // Dimensions
+                       this.beforeSetDimensions();
+                       this.setDimensions();
+                       this.afterSetDimensions();
+                       // Labels
+                       this.beforeBuildLabels();
+                       this.buildLabels();
+                       this.afterBuildLabels();
+
+                       // Fit
+                       this.beforeFit();
+                       this.fit();
+                       this.afterFit();
+                       //
+                       this.afterUpdate();
+
+                       return this.minSize;
+
+               },
+               afterUpdate: helpers.noop,
+
+               //
+
+               beforeSetDimensions: helpers.noop,
+               setDimensions: function() {
+                       // Set the unconstrained dimension before label rotation
+                       if (this.isHorizontal()) {
+                               // Reset position before calculating rotation
+                               this.width = this.maxWidth;
+                               this.left = 0;
+                               this.right = this.width;
+                       } else {
+                               this.height = this.maxHeight;
+
+                               // Reset position before calculating rotation
+                               this.top = 0;
+                               this.bottom = this.height;
+                       }
+
+                       // Reset padding
+                       this.paddingLeft = 0;
+                       this.paddingTop = 0;
+                       this.paddingRight = 0;
+                       this.paddingBottom = 0;
+               },
+               afterSetDimensions: helpers.noop,
+
+               //
+
+               beforeBuildLabels: helpers.noop,
+               buildLabels: function() {
+                       // Convert ticks to strings
+                       this.labels = this.chart.data.datasets.map(function(dataset) {
+                               return this.options.labels.callback.call(this, dataset.label);
+                       }, this);
+               },
+               afterBuildLabels: helpers.noop,
+
+               //
+
+               beforeFit: helpers.noop,
+               fit: function() {
+
+                       var ctx = this.ctx;
+                       var titleFont = helpers.fontString(this.options.title.fontSize, this.options.title.fontStyle, this.options.title.fontFamily);
+                       var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily);
+
+                       this.minSize = {
+                               width: 0,
+                               height: 0,
+                       };
+
+                       // Legends can use all available space by default as no alignment to the chartArea is necessary 
+
+                       // Width
+                       if (this.isHorizontal()) {
+                               this.minSize.width = this.maxWidth; // fill all the width
+                       } else {
+                               this.minSize.width = this.options.display ? 10 : 0;
+                       }
+
+                       // height
+                       if (this.isHorizontal()) {
+                               this.minSize.height = this.options.display ? 10 : 0;
+                       } else {
+                               this.minSize.height = this.maxHeight; // fill all the height
+                       }
+
+                       // Increase sizes here
+                       if (this.isHorizontal()) {
+
+                               // Title
+                               if (this.options.title.display) {
+                                       this.minSize.height += this.options.title.fontSize + (this.options.title.padding * 2);
+                               }
+
+                               // Labels
+
+                               var totalWidth = 0;
+                               var totalHeight = this.labels.length ? this.options.labels.fontSize + (this.options.labels.padding) : 0;
+
+                               ctx.font = labelFont;
+
+                               helpers.each(this.labels, function(label, i) {
+                                       var width = ctx.measureText(label).width;
+                                       if (totalWidth + width > this.width) {
+                                               totalHeight += this.options.labels.fontSize + (this.options.labels.padding);
+                                               totalWidth = 0;
+                                       }
+                                       totalWidth += width + this.options.labels.padding;
+                               }, this);
+
+                               this.minSize.height += totalHeight;
+
+                       } else {
+                               // TODO vertical
+                       }
+
+                       this.width = this.minSize.width;
+                       this.height = this.minSize.height;
+
+               },
+               afterFit: helpers.noop,
+
+               // Shared Methods
+               isHorizontal: function() {
+                       return this.options.position == "top" || this.options.position == "bottom";
+               },
+
+               // Actualy draw the legend on the canvas
+               draw: function() {
+                       if (this.options.display) {
+
+                               var ctx = this.ctx;
+                               var cursor = {
+                                       x: this.left,
+                                       y: this.top,
+                               };
+
+                               // Horizontal
+                               if (this.isHorizontal()) {
+
+                                       // Title Spacing if on top
+                                       if (this.options.title.display && this.options.title.position == 'top') {
+                                               cursor.y += this.options.title.fontSize + (this.options.title.padding * 2);
+                                       }
+
+                                       // Labels
+                                       ctx.textAlign = "left";
+                                       ctx.textBaseline = 'top';
+                                       ctx.fillStyle = this.options.labels.fontColor; // render in correct colour
+                                       ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily);
+
+                                       helpers.each(this.labels, function(label, i) {
+                                               var width = ctx.measureText(label).width;
+                                               if (cursor.x + width > this.width) {
+                                                       cursor.y += this.options.labels.fontSize + (this.options.labels.padding);
+                                                       cursor.x = this.left;
+                                               }
+                                               ctx.fillText(label, cursor.x, cursor.y);
+                                               cursor.x += width + (this.options.labels.padding);
+                                       }, this);
+
+
+                                       // Title
+                                       if (this.options.title.display) {
+
+                                               ctx.textAlign = "center";
+                                               ctx.textBaseline = 'middle';
+                                               ctx.fillStyle = this.options.title.fontColor; // render in correct colour
+                                               ctx.font = helpers.fontString(this.options.title.fontSize, this.options.title.fontStyle, this.options.title.fontFamily);
+
+                                               var titleX = this.left + ((this.right - this.left) / 2); // midpoint of the width
+                                               var titleY = this.options.position == 'bottom' ? this.bottom - (this.options.title.fontSize / 2) - this.options.title.padding : this.top + (this.options.title.fontSize / 2) + this.options.title.padding;
+
+                                               ctx.fillText(this.options.title.text, titleX, titleY);
+                                       }
+
+
+                               } else {
+
+                                       // Title
+                                       if (this.options.title.display) {
+
+                                               // Draw the legend label
+                                               titleX = this.options.position == 'left' ? this.left + (this.options.title.fontSize / 2) : this.right - (this.options.title.fontSize / 2);
+                                               titleY = this.top + ((this.bottom - this.top) / 2);
+                                               var rotation = this.options.position == 'left' ? -0.5 * Math.PI : 0.5 * Math.PI;
+
+                                               ctx.save();
+                                               ctx.translate(titleX, titleY);
+                                               ctx.rotate(rotation);
+                                               ctx.textAlign = "center";
+                                               ctx.fillStyle = this.options.title.fontColor; // render in correct colour
+                                               ctx.font = helpers.fontString(this.options.title.fontSize, this.options.title.fontStyle, this.options.title.fontFamily);
+                                               ctx.textBaseline = 'middle';
+                                               ctx.fillText(this.options.title.text, 0, 0);
+                                               ctx.restore();
+
+                                       }
+
+                               }
+                       }
+               }
+       });
+
+}).call(this);
index a1c1f1c207758c898d7bf5cb4213b22865c6c3db..2100eae7d2ab375a2c26d7a4f213efcfcee5a78d 100644 (file)
@@ -5,9 +5,6 @@
                Chart = root.Chart,
                helpers = Chart.helpers;
 
-       // The scale service is used to resize charts along with all of their axes. We make this as
-       // a service where scales are registered with their respective charts so that changing the
-       // scales does not require 
        Chart.scaleService = {
                // Scale registration object. Extensions can register new scale types (such as log or DB scales) and then
                // use the new chart options to grab the correct scale
                        // Return the scale defaults merged with the global settings so that we always use the latest ones
                        return this.defaults.hasOwnProperty(type) ? helpers.scaleMerge(Chart.defaults.scale, this.defaults[type]) : {};
                },
-               // The interesting function
-               update: function(chartInstance, width, height) {
-                       var xPadding = width > 30 ? 5 : 2;
-                       var yPadding = height > 30 ? 5 : 2;
-
-                       if (chartInstance) {
-                               var leftScales = helpers.where(chartInstance.scales, function(scaleInstance) {
-                                       return scaleInstance.options.position == "left";
-                               });
-                               var rightScales = helpers.where(chartInstance.scales, function(scaleInstance) {
-                                       return scaleInstance.options.position == "right";
-                               });
-                               var topScales = helpers.where(chartInstance.scales, function(scaleInstance) {
-                                       return scaleInstance.options.position == "top";
-                               });
-                               var bottomScales = helpers.where(chartInstance.scales, function(scaleInstance) {
-                                       return scaleInstance.options.position == "bottom";
-                               });
-
-                               // Scales that overlay the chartarea such as the radialLinear scale
-                               var chartAreaScales = helpers.where(chartInstance.scales, function(scaleInstance) {
-                                       return scaleInstance.options.position == "chartArea";
-                               });
-
-                               // Essentially we now have any number of scales on each of the 4 sides.
-                               // Our canvas looks like the following.
-                               // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and 
-                               // B1 is the bottom axis
-                               // |------------------------------------------------------|
-                               // |          |             T1                      |     |
-                               // |----|-----|-------------------------------------|-----|
-                               // |    |     |                                     |     |
-                               // | L1 |  L2 |         Chart area                  |  R1 |
-                               // |    |     |                                     |     |
-                               // |    |     |                                     |     |
-                               // |----|-----|-------------------------------------|-----|
-                               // |          |             B1                      |     |
-                               // |          |                                     |     |
-                               // |------------------------------------------------------|
-
-                               // What we do to find the best sizing, we do the following
-                               // 1. Determine the minimum size of the chart area. 
-                               // 2. Split the remaining width equally between each vertical axis
-                               // 3. Split the remaining height equally between each horizontal axis
-                               // 4. Give each scale the maximum size it can be. The scale will return it's minimum size
-                               // 5. Adjust the sizes of each axis based on it's minimum reported size. 
-                               // 6. Refit each axis
-                               // 7. Position each axis in the final location
-                               // 8. Tell the chart the final location of the chart area
-                               // 9. Tell any axes that overlay the chart area the positions of the chart area
-
-                               // Step 1
-                               var chartWidth = width / 2; // min 50%
-                               var chartHeight = height / 2; // min 50%
-
-                               chartWidth -= (2 * xPadding);
-                               chartHeight -= (2 * yPadding);
-
-                               // Step 2
-                               var verticalScaleWidth = (width - chartWidth) / (leftScales.length + rightScales.length);
-
-                               // Step 3
-                               var horizontalScaleHeight = (height - chartHeight) / (topScales.length + bottomScales.length);
-
-                               // Step 4;
-                               var minimumScaleSizes = [];
-
-                               var verticalScaleMinSizeFunction = function(scaleInstance) {
-                                       var minSize = scaleInstance.update(verticalScaleWidth, chartHeight);
-                                       minimumScaleSizes.push({
-                                               horizontal: false,
-                                               minSize: minSize,
-                                               scale: scaleInstance,
-                                       });
-                               };
-
-                               var horizontalScaleMinSizeFunction = function(scaleInstance) {
-                                       var minSize = scaleInstance.update(chartWidth, horizontalScaleHeight);
-                                       minimumScaleSizes.push({
-                                               horizontal: true,
-                                               minSize: minSize,
-                                               scale: scaleInstance,
-                                       });
-                               };
-
-                               // vertical scales
-                               helpers.each(leftScales, verticalScaleMinSizeFunction);
-                               helpers.each(rightScales, verticalScaleMinSizeFunction);
-
-                               // horizontal scales
-                               helpers.each(topScales, horizontalScaleMinSizeFunction);
-                               helpers.each(bottomScales, horizontalScaleMinSizeFunction);
-
-                               // Step 5
-                               var maxChartHeight = height - (2 * yPadding);
-                               var maxChartWidth = width - (2 * xPadding);
-
-                               helpers.each(minimumScaleSizes, function(wrapper) {
-                                       if (wrapper.horizontal) {
-                                               maxChartHeight -= wrapper.minSize.height;
-                                       } else {
-                                               maxChartWidth -= wrapper.minSize.width;
-                                       }
-                               });
-
-                               // At this point, maxChartHeight and maxChartWidth are the size the chart area could
-                               // be if the axes are drawn at their minimum sizes.
-
-                               // Step 6
-                               var verticalScaleFitFunction = function(scaleInstance) {
-                                       var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) {
-                                               return wrapper.scale === scaleInstance;
-                                       });
-
-                                       if (wrapper) {
-                                               scaleInstance.update(wrapper.minSize.width, maxChartHeight);
-                                       }
-                               };
-
-                               var horizontalScaleFitFunction = function(scaleInstance) {
-                                       var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) {
-                                               return wrapper.scale === scaleInstance;
-                                       });
-
-                                       var scaleMargin = {
-                                               left: totalLeftWidth,
-                                               right: totalRightWidth,
-                                               top: 0,
-                                               bottom: 0,
-                                       };
-
-                                       if (wrapper) {
-                                               scaleInstance.update(maxChartWidth, wrapper.minSize.height, scaleMargin);
-                                       }
-                               };
-
-                               var totalLeftWidth = xPadding;
-                               var totalRightWidth = xPadding;
-                               var totalTopHeight = yPadding;
-                               var totalBottomHeight = yPadding;
-
-                               helpers.each(leftScales, verticalScaleFitFunction);
-                               helpers.each(rightScales, verticalScaleFitFunction);
-
-                               // Figure out how much margin is on the left and right of the horizontal axes
-                               helpers.each(leftScales, function(scaleInstance) {
-                                       totalLeftWidth += scaleInstance.width;
-                               });
-
-                               helpers.each(rightScales, function(scaleInstance) {
-                                       totalRightWidth += scaleInstance.width;
-                               });
-
-                               helpers.each(topScales, horizontalScaleFitFunction);
-                               helpers.each(bottomScales, horizontalScaleFitFunction);
-
-                               helpers.each(topScales, function(scaleInstance) {
-                                       totalTopHeight += scaleInstance.height;
-                               });
-                               helpers.each(bottomScales, function(scaleInstance) {
-                                       totalBottomHeight += scaleInstance.height;
-                               });
-
-                               // Let the left scale know the final margin
-                               helpers.each(leftScales, function(scaleInstance) {
-                                       var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) {
-                                               return wrapper.scale === scaleInstance;
-                                       });
-
-                                       var scaleMargin = {
-                                               left: 0,
-                                               right: 0,
-                                               top: totalTopHeight,
-                                               bottom: totalBottomHeight
-                                       };
-
-                                       if (wrapper) {
-                                               scaleInstance.update(wrapper.minSize.width, maxChartHeight, scaleMargin);
-                                       }
-                               });
-
-                               helpers.each(rightScales, function(scaleInstance) {
-                                       var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) {
-                                               return wrapper.scale === scaleInstance;
-                                       });
-
-                                       var scaleMargin = {
-                                               left: 0,
-                                               right: 0,
-                                               top: totalTopHeight,
-                                               bottom: totalBottomHeight
-                                       };
-
-                                       if (wrapper) {
-                                               scaleInstance.update(wrapper.minSize.width, maxChartHeight, scaleMargin);
-                                       }
-                               });
-
-                               // Recalculate because the size of each scale might have changed slightly due to the margins (label rotation for instance)
-                               totalLeftWidth = xPadding;
-                               totalRightWidth = xPadding;
-                               totalTopHeight = yPadding;
-                               totalBottomHeight = yPadding;
-
-                               helpers.each(leftScales, function(scaleInstance) {
-                                       totalLeftWidth += scaleInstance.width;
-                               });
-
-                               helpers.each(rightScales, function(scaleInstance) {
-                                       totalRightWidth += scaleInstance.width;
-                               });
-
-                               helpers.each(topScales, function(scaleInstance) {
-                                       totalTopHeight += scaleInstance.height;
-                               });
-                               helpers.each(bottomScales, function(scaleInstance) {
-                                       totalBottomHeight += scaleInstance.height;
-                               });
-
-                               // Figure out if our chart area changed. This would occur if the dataset scale label rotation
-                               // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do
-                               // without calling `fit` again
-                               var newMaxChartHeight = height - totalTopHeight - totalBottomHeight;
-                               var newMaxChartWidth = width - totalLeftWidth - totalRightWidth;
-
-                               if (newMaxChartWidth !== maxChartWidth || newMaxChartHeight !== maxChartHeight) {
-                                       helpers.each(leftScales, function(scale) {
-                                               scale.height = newMaxChartHeight;
-                                       });
-
-                                       helpers.each(rightScales, function(scale) {
-                                               scale.height = newMaxChartHeight;
-                                       });
-
-                                       helpers.each(topScales, function(scale) {
-                                               scale.width = newMaxChartWidth;
-                                       });
-
-                                       helpers.each(bottomScales, function(scale) {
-                                               scale.width = newMaxChartWidth;
-                                       });
-
-                                       maxChartHeight = newMaxChartHeight;
-                                       maxChartWidth = newMaxChartWidth;
-                               }
-
-                               // Step 7 
-                               // Position the scales
-                               var left = xPadding;
-                               var top = yPadding;
-                               var right = 0;
-                               var bottom = 0;
-
-                               var verticalScalePlacer = function(scaleInstance) {
-                                       scaleInstance.left = left;
-                                       scaleInstance.right = left + scaleInstance.width;
-                                       scaleInstance.top = totalTopHeight;
-                                       scaleInstance.bottom = totalTopHeight + maxChartHeight;
-
-                                       // Move to next point
-                                       left = scaleInstance.right;
-                               };
-
-                               var horizontalScalePlacer = function(scaleInstance) {
-                                       scaleInstance.left = totalLeftWidth;
-                                       scaleInstance.right = totalLeftWidth + maxChartWidth;
-                                       scaleInstance.top = top;
-                                       scaleInstance.bottom = top + scaleInstance.height;
-
-                                       // Move to next point 
-                                       top = scaleInstance.bottom;
-                               };
-
-                               helpers.each(leftScales, verticalScalePlacer);
-                               helpers.each(topScales, horizontalScalePlacer);
-
-                               // Account for chart width and height
-                               left += maxChartWidth;
-                               top += maxChartHeight;
-
-                               helpers.each(rightScales, verticalScalePlacer);
-                               helpers.each(bottomScales, horizontalScalePlacer);
-
-                               // Step 8
-                               chartInstance.chartArea = {
-                                       left: totalLeftWidth,
-                                       top: totalTopHeight,
-                                       right: totalLeftWidth + maxChartWidth,
-                                       bottom: totalTopHeight + maxChartHeight,
-                               };
-
-                               // Step 9
-                               helpers.each(chartAreaScales, function(scaleInstance) {
-                                       scaleInstance.left = chartInstance.chartArea.left;
-                                       scaleInstance.top = chartInstance.chartArea.top;
-                                       scaleInstance.right = chartInstance.chartArea.right;
-                                       scaleInstance.bottom = chartInstance.chartArea.bottom;
-                                       
-                                       scaleInstance.update(maxChartWidth, maxChartHeight);
-                               });
-                       }
-               }
+               addScalesToLayout: function(chartInstance) {
+                       // Adds each scale to the chart.boxes array to be sized accordingly
+                       helpers.each(chartInstance.scales, function(scale) {
+                               Chart.layoutService.addBox(chartInstance, scale);
+                       });
+               },
        };