diff --git a/samples/bubble.html b/samples/bubble.html
new file mode 100644
index 000000000..a52cd9c0c
--- /dev/null
+++ b/samples/bubble.html
@@ -0,0 +1,206 @@
+
+
+
+
+ Bar Chart
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/charts/Chart.Bubble.js b/src/charts/Chart.Bubble.js
new file mode 100644
index 000000000..d4715c026
--- /dev/null
+++ b/src/charts/Chart.Bubble.js
@@ -0,0 +1,39 @@
+(function() {
+ "use strict";
+
+ var root = this;
+ var Chart = root.Chart;
+ var helpers = Chart.helpers;
+
+ var defaultConfig = {
+ hover: {
+ mode: 'single',
+ },
+
+ scales: {
+ xAxes: [{
+ type: "linear", // bubble should probably use a linear scale by default
+ position: "bottom",
+ id: "x-axis-0", // need an ID so datasets can reference the scale
+ }],
+ yAxes: [{
+ type: "linear",
+ position: "left",
+ id: "y-axis-0",
+ }],
+ },
+
+ tooltips: {
+ template: "(<%= value.x %>, <%= value.y %>)",
+ multiTemplate: "<%if (datasetLabel){%><%=datasetLabel%>: <%}%>(<%= value.x %>, <%= value.y %>)",
+ },
+
+ };
+
+ Chart.Bubble = function(context, config) {
+ config.options = helpers.configMerge(defaultConfig, config.options);
+ config.type = 'bubble';
+ return new Chart(context, config);
+ };
+
+}).call(this);
diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js
new file mode 100644
index 000000000..f6c522838
--- /dev/null
+++ b/src/controllers/controller.bubble.js
@@ -0,0 +1,218 @@
+(function() {
+
+ "use strict";
+
+ var root = this,
+ Chart = root.Chart,
+ helpers = Chart.helpers;
+
+ Chart.defaults.bubble = {
+ hover: {
+ mode: "single"
+ },
+
+ scales: {
+ xAxes: [{
+ type: "linear", // bubble should probably use a linear scale by default
+ position: "bottom",
+ id: "x-axis-0", // need an ID so datasets can reference the scale
+ }],
+ yAxes: [{
+ type: "linear",
+ position: "left",
+ id: "y-axis-0",
+ }],
+ },
+
+ tooltips: {
+ template: "(<%= value.x %>, <%= value.y %>, <%= value.r %>)",
+ multiTemplate: "<%if (datasetLabel){%><%=datasetLabel%>: <%}%>(<%= value.x %>, <%= value.y %>, <%= value.r %>)",
+ },
+ };
+
+
+ Chart.controllers.bubble = function(chart, datasetIndex) {
+ this.initialize.call(this, chart, datasetIndex);
+ };
+
+ helpers.extend(Chart.controllers.bubble.prototype, {
+
+ initialize: function(chart, datasetIndex) {
+ this.chart = chart;
+ this.index = datasetIndex;
+ this.linkScales();
+ this.addElements();
+ },
+ updateIndex: function(datasetIndex) {
+ this.index = datasetIndex;
+ },
+
+ linkScales: function() {
+ if (!this.getDataset().xAxisID) {
+ this.getDataset().xAxisID = this.chart.options.scales.xAxes[0].id;
+ }
+
+ if (!this.getDataset().yAxisID) {
+ this.getDataset().yAxisID = this.chart.options.scales.yAxes[0].id;
+ }
+ },
+
+ getDataset: function() {
+ return this.chart.data.datasets[this.index];
+ },
+
+ getScaleForId: function(scaleID) {
+ return this.chart.scales[scaleID];
+ },
+
+ addElements: function() {
+
+ this.getDataset().metaData = this.getDataset().metaData || [];
+
+ helpers.each(this.getDataset().data, function(value, index) {
+ this.getDataset().metaData[index] = this.getDataset().metaData[index] || new Chart.elements.Point({
+ _chart: this.chart.chart,
+ _datasetIndex: this.index,
+ _index: index,
+ });
+ }, this);
+ },
+ addElementAndReset: function(index) {
+ this.getDataset().metaData = this.getDataset().metaData || [];
+ var point = new Chart.elements.Point({
+ _chart: this.chart.chart,
+ _datasetIndex: this.index,
+ _index: index,
+ });
+
+ // Reset the point
+ this.updateElement(point, index, true);
+
+ // Add to the points array
+ this.getDataset().metaData.splice(index, 0, point);
+
+ },
+ removeElement: function(index) {
+ this.getDataset().metaData.splice(index, 1);
+ },
+
+ reset: function() {
+ this.update(true);
+ },
+
+ buildOrUpdateElements: function buildOrUpdateElements() {
+ // Handle the number of data points changing
+ var numData = this.getDataset().data.length;
+ var numPoints = this.getDataset().metaData.length;
+
+ // Make sure that we handle number of datapoints changing
+ if (numData < numPoints) {
+ // Remove excess bars for data points that have been removed
+ this.getDataset().metaData.splice(numData, numPoints - numData);
+ } else if (numData > numPoints) {
+ // Add new elements
+ for (var index = numPoints; index < numData; ++index) {
+ this.addElementAndReset(index);
+ }
+ }
+ },
+
+ update: function update(reset) {
+ var points = this.getDataset().metaData;
+
+ var yScale = this.getScaleForId(this.getDataset().yAxisID);
+ var xScale = this.getScaleForId(this.getDataset().xAxisID);
+ var scaleBase;
+
+ if (yScale.min < 0 && yScale.max < 0) {
+ scaleBase = yScale.getPixelForValue(yScale.max);
+ } else if (yScale.min > 0 && yScale.max > 0) {
+ scaleBase = yScale.getPixelForValue(yScale.min);
+ } else {
+ scaleBase = yScale.getPixelForValue(0);
+ }
+
+ // Update Points
+ helpers.each(points, function(point, index) {
+ this.updateElement(point, index, reset);
+ }, this);
+
+ },
+
+ updateElement: function(point, index, reset) {
+ var yScale = this.getScaleForId(this.getDataset().yAxisID);
+ var xScale = this.getScaleForId(this.getDataset().xAxisID);
+ var scaleBase;
+
+ if (yScale.min < 0 && yScale.max < 0) {
+ scaleBase = yScale.getPixelForValue(yScale.max);
+ } else if (yScale.min > 0 && yScale.max > 0) {
+ scaleBase = yScale.getPixelForValue(yScale.min);
+ } else {
+ scaleBase = yScale.getPixelForValue(0);
+ }
+
+ helpers.extend(point, {
+ // Utility
+ _chart: this.chart.chart,
+ _xScale: xScale,
+ _yScale: yScale,
+ _datasetIndex: this.index,
+ _index: index,
+
+ // Desired view properties
+ _model: {
+ x: reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(this.getDataset().data[index], index, this.index, this.chart.isCombo),
+ y: reset ? scaleBase : yScale.getPixelForValue(this.getDataset().data[index], index, this.index),
+ // Appearance
+ radius: reset ? 0 : point.custom && point.custom.radius ? point.custom.radius : this.getRadius(this.getDataset().data[index]),
+ backgroundColor: point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.point.backgroundColor),
+ borderColor: point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.point.borderColor),
+ borderWidth: point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.point.borderWidth),
+ skip: point.custom && point.custom.skip ? point.custom.skip : this.getDataset().data[index] === null,
+
+ // Tooltip
+ hitRadius: point.custom && point.custom.hitRadius ? point.custom.hitRadius : helpers.getValueAtIndexOrDefault(this.getDataset().hitRadius, index, this.chart.options.elements.point.hitRadius),
+ },
+ });
+
+ point.pivot();
+ },
+
+ getRadius: function(value) {
+ return value.r || this.chart.options.elements.point.radius;
+ },
+
+ draw: function(ease) {
+ var easingDecimal = ease || 1;
+
+ // Transition and Draw the Points
+ helpers.each(this.getDataset().metaData, function(point, index) {
+ point.transition(easingDecimal);
+ point.draw();
+ }, this);
+
+ },
+
+ setHoverStyle: function(point) {
+ // Point
+ var dataset = this.chart.data.datasets[point._datasetIndex];
+ var index = point._index;
+
+ point._model.radius = point.custom && point.custom.hoverRadius ? point.custom.hoverRadius : (helpers.getValueAtIndexOrDefault(dataset.hoverRadius, index, this.chart.options.elements.point.hoverRadius)) + this.getRadius(this.getDataset().data[point._index]);
+ point._model.backgroundColor = point.custom && point.custom.hoverBackgroundColor ? point.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(point._model.backgroundColor).saturate(0.5).darken(0.1).rgbString());
+ point._model.borderColor = point.custom && point.custom.hoverBorderColor ? point.custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.color(point._model.borderColor).saturate(0.5).darken(0.1).rgbString());
+ point._model.borderWidth = point.custom && point.custom.hoverBorderWidth ? point.custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, point._model.borderWidth);
+ },
+
+ removeHoverStyle: function(point) {
+ var dataset = this.chart.data.datasets[point._datasetIndex];
+ var index = point._index;
+
+ point._model.radius = point.custom && point.custom.radius ? point.custom.radius : this.getRadius(this.getDataset().data[point._index]);
+ point._model.backgroundColor = point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.point.backgroundColor);
+ point._model.borderColor = point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.point.borderColor);
+ point._model.borderWidth = point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.point.borderWidth);
+ }
+ });
+}).call(this);