mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-05 16:04:03 +01:00
Merge branch 'multiline_labels' of https://github.com/Tarqwyn/Chart.js into Tarqwyn-multiline_labels
Conflicts: src/core/core.scale.js
This commit is contained in:
@@ -74,7 +74,7 @@ The grid line configuration is nested under the scale configuration in the `tick
|
||||
Name | Type | Default | Description
|
||||
--- | --- | --- | ---
|
||||
autoSkip | Boolean | true | If true, automatically calculates how many labels that can be shown and hides labels accordingly. Turn it off to show all labels no matter what
|
||||
callback | Function | `function(value) { return '' + value; } ` | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](#scales-creating-custom-tick-formats) section below.
|
||||
callback | Function | `function(value) { return helpers.isArray(value) ? value : '' + value; }` | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](#scales-creating-custom-tick-formats) section below.
|
||||
display | Boolean | true | If true, show the ticks.
|
||||
fontColor | Color | "#666" | Font color for the tick labels.
|
||||
fontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family for the tick labels, follows CSS font-family options.
|
||||
@@ -359,8 +359,8 @@ The default configuration for a scale can be easily changed using the scale serv
|
||||
For example, to set the minimum value of 0 for all linear scales, you would do the following. Any linear scales created after this time would now have a minimum of 0.
|
||||
```
|
||||
Chart.scaleService.updateScaleDefaults('linear', {
|
||||
ticks: {
|
||||
min: 0
|
||||
}
|
||||
ticks: {
|
||||
min: 0
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -96,7 +96,7 @@ The label key on each dataset is optional, and can be used when generating a sca
|
||||
|
||||
### Data Points
|
||||
|
||||
The data passed to the chart can be passed in two formats. The most common method is to pass the data array as an array of numbers. In this case, the `data.labels` array must be specified and must contain a label for each point.
|
||||
The data passed to the chart can be passed in two formats. The most common method is to pass the data array as an array of numbers. In this case, the `data.labels` array must be specified and must contain a label for each point or, in the case of labels to be displayed over multiple lines an array of labels (one for each line) i.e `[["June","2015"], "July"]`.
|
||||
|
||||
The alternate is used for sparse datasets. Data is specified using an object containing `x` and `y` properties. This is used for scatter charts as documented below.
|
||||
|
||||
@@ -177,4 +177,4 @@ var stackedLine = new Chart(ctx, {
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
```
|
||||
218
samples/line-multiline-labels.html
Normal file
218
samples/line-multiline-labels.html
Normal file
@@ -0,0 +1,218 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Line Chart</title>
|
||||
<script src="../dist/Chart.bundle.js"></script>
|
||||
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
|
||||
<style>
|
||||
canvas{
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="width:90%;">
|
||||
<canvas id="canvas"></canvas>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<button id="randomizeData">Randomize Data</button>
|
||||
<button id="changeDataObject">Change Data Object</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 MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
||||
|
||||
var randomScalingFactor = function() {
|
||||
return Math.round(Math.random() * 100);
|
||||
//return 0;
|
||||
};
|
||||
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: [["June","2015"], "July", "August", "September", "October", "November", "December", ["January","2016"],"February", "March", "April", "May"],
|
||||
datasets: [{
|
||||
label: "My First dataset",
|
||||
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
|
||||
fill: false,
|
||||
borderDash: [5, 5],
|
||||
}, {
|
||||
hidden: true,
|
||||
label: 'hidden dataset',
|
||||
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
|
||||
}, {
|
||||
label: "My Second dataset",
|
||||
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
title:{
|
||||
display:true,
|
||||
text:'Chart.js Line Chart'
|
||||
},
|
||||
tooltips: {
|
||||
mode: 'label',
|
||||
callbacks: {
|
||||
// beforeTitle: function() {
|
||||
// return '...beforeTitle';
|
||||
// },
|
||||
// afterTitle: function() {
|
||||
// return '...afterTitle';
|
||||
// },
|
||||
// beforeBody: function() {
|
||||
// return '...beforeBody';
|
||||
// },
|
||||
// afterBody: function() {
|
||||
// return '...afterBody';
|
||||
// },
|
||||
// beforeFooter: function() {
|
||||
// return '...beforeFooter';
|
||||
// },
|
||||
// footer: function() {
|
||||
// return 'Footer';
|
||||
// },
|
||||
// afterFooter: function() {
|
||||
// return '...afterFooter';
|
||||
// },
|
||||
}
|
||||
},
|
||||
hover: {
|
||||
mode: 'dataset'
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
show: true,
|
||||
labelString: 'Month'
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
show: true,
|
||||
labelString: 'Value'
|
||||
},
|
||||
ticks: {
|
||||
suggestedMin: -10,
|
||||
suggestedMax: 250,
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.each(config.data.datasets, function(i, dataset) {
|
||||
dataset.borderColor = randomColor(0.4);
|
||||
dataset.backgroundColor = randomColor(0.5);
|
||||
dataset.pointBorderColor = randomColor(0.7);
|
||||
dataset.pointBackgroundColor = randomColor(0.5);
|
||||
dataset.pointBorderWidth = 1;
|
||||
});
|
||||
|
||||
window.onload = function() {
|
||||
var ctx = document.getElementById("canvas").getContext("2d");
|
||||
window.myLine = new Chart(ctx, config);
|
||||
};
|
||||
|
||||
$('#randomizeData').click(function() {
|
||||
$.each(config.data.datasets, function(i, dataset) {
|
||||
dataset.data = dataset.data.map(function() {
|
||||
return randomScalingFactor();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
window.myLine.update();
|
||||
});
|
||||
|
||||
$('#changeDataObject').click(function() {
|
||||
config.data = {
|
||||
labels: ["July", "August", "September", "October", "November", "December"],
|
||||
datasets: [{
|
||||
label: "My First dataset",
|
||||
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
|
||||
fill: false,
|
||||
}, {
|
||||
label: "My Second dataset",
|
||||
fill: false,
|
||||
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
|
||||
}]
|
||||
};
|
||||
|
||||
$.each(config.data.datasets, function(i, dataset) {
|
||||
dataset.borderColor = randomColor(0.4);
|
||||
dataset.backgroundColor = randomColor(0.5);
|
||||
dataset.pointBorderColor = randomColor(0.7);
|
||||
dataset.pointBackgroundColor = randomColor(0.5);
|
||||
dataset.pointBorderWidth = 1;
|
||||
});
|
||||
|
||||
// Update the chart
|
||||
window.myLine.update();
|
||||
});
|
||||
|
||||
$('#addDataset').click(function() {
|
||||
var newDataset = {
|
||||
label: 'Dataset ' + config.data.datasets.length,
|
||||
borderColor: randomColor(0.4),
|
||||
backgroundColor: randomColor(0.5),
|
||||
pointBorderColor: randomColor(0.7),
|
||||
pointBackgroundColor: randomColor(0.5),
|
||||
pointBorderWidth: 1,
|
||||
data: [],
|
||||
};
|
||||
|
||||
for (var index = 0; index < config.data.labels.length; ++index) {
|
||||
newDataset.data.push(randomScalingFactor());
|
||||
}
|
||||
|
||||
config.data.datasets.push(newDataset);
|
||||
window.myLine.update();
|
||||
});
|
||||
|
||||
$('#addData').click(function() {
|
||||
if (config.data.datasets.length > 0) {
|
||||
var month = MONTHS[config.data.labels.length % MONTHS.length];
|
||||
config.data.labels.push(month);
|
||||
|
||||
$.each(config.data.datasets, function(i, dataset) {
|
||||
dataset.data.push(randomScalingFactor());
|
||||
});
|
||||
|
||||
window.myLine.update();
|
||||
}
|
||||
});
|
||||
|
||||
$('#removeDataset').click(function() {
|
||||
config.data.datasets.splice(0, 1);
|
||||
window.myLine.update();
|
||||
});
|
||||
|
||||
$('#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();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -817,7 +817,7 @@ module.exports = function(Chart) {
|
||||
helpers.fontString = function(pixelSize, fontStyle, fontFamily) {
|
||||
return fontStyle + " " + pixelSize + "px " + fontFamily;
|
||||
};
|
||||
helpers.longestText = function(ctx, font, arrayOfStrings, cache) {
|
||||
helpers.longestText = function(ctx, font, arrayOfThings, cache) {
|
||||
cache = cache || {};
|
||||
var data = cache.data = cache.data || {};
|
||||
var gc = cache.garbageCollect = cache.garbageCollect || [];
|
||||
@@ -830,31 +830,53 @@ module.exports = function(Chart) {
|
||||
|
||||
ctx.font = font;
|
||||
var longest = 0;
|
||||
helpers.each(arrayOfStrings, function(string) {
|
||||
// Undefined strings should not be measured
|
||||
if (string !== undefined && string !== null) {
|
||||
var textWidth = data[string];
|
||||
if (!textWidth) {
|
||||
textWidth = data[string] = ctx.measureText(string).width;
|
||||
gc.push(string);
|
||||
}
|
||||
|
||||
if (textWidth > longest) {
|
||||
longest = textWidth;
|
||||
}
|
||||
helpers.each(arrayOfThings, function(thing) {
|
||||
// Undefined strings and arrays should not be measured
|
||||
if (thing !== undefined && thing !== null && helpers.isArray(thing) !== true) {
|
||||
longest = helpers.measureText(ctx, data, gc, longest, thing);
|
||||
} else if (helpers.isArray(thing)) {
|
||||
// if it is an array lets measure each element
|
||||
// to do maybe simplify this function a bit so we can do this more recursively?
|
||||
helpers.each(thing, function(nestedThing) {
|
||||
// Undefined strings and arrays should not be measured
|
||||
if (nestedThing !== undefined && nestedThing !== null && !helpers.isArray(nestedThing)) {
|
||||
longest = helpers.measureText(ctx, data, gc, longest, nestedThing);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var gcLen = gc.length / 2;
|
||||
if (gcLen > arrayOfStrings.length) {
|
||||
if (gcLen > arrayOfThings.length) {
|
||||
for (var i = 0; i < gcLen; i++) {
|
||||
delete data[gc[i]];
|
||||
}
|
||||
gc.splice(0, gcLen);
|
||||
}
|
||||
|
||||
return longest;
|
||||
};
|
||||
helpers.measureText = function (ctx, data, gc, longest, string) {
|
||||
var textWidth = data[string];
|
||||
if (!textWidth) {
|
||||
textWidth = data[string] = ctx.measureText(string).width;
|
||||
gc.push(string);
|
||||
}
|
||||
if (textWidth > longest) {
|
||||
longest = textWidth;
|
||||
}
|
||||
return longest;
|
||||
};
|
||||
helpers.numberOfLabelLines = function(arrayOfThings) {
|
||||
var numberOfLines = 1;
|
||||
helpers.each(arrayOfThings, function(thing) {
|
||||
if (helpers.isArray(thing)) {
|
||||
if (thing.length > numberOfLines) {
|
||||
numberOfLines = thing.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
return numberOfLines;
|
||||
};
|
||||
helpers.drawRoundedRectangle = function(ctx, x, y, width, height, radius) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, y);
|
||||
|
||||
@@ -43,8 +43,9 @@ module.exports = function(Chart) {
|
||||
autoSkip: true,
|
||||
autoSkipPadding: 0,
|
||||
labelOffset: 0,
|
||||
// We pass through arrays to be rendered as multiline labels, we convert Others to strings here.
|
||||
callback: function(value) {
|
||||
return '' + value;
|
||||
return helpers.isArray(value) ? value : '' + value;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -320,13 +321,15 @@ module.exports = function(Chart) {
|
||||
}
|
||||
|
||||
var largestTextWidth = helpers.longestText(me.ctx, tickLabelFont, me.ticks, me.longestTextCache);
|
||||
var tallestLabelHeightInLines = helpers.numberOfLabelLines(me.ticks);
|
||||
var lineSpace = tickFontSize * 0.5;
|
||||
|
||||
if (isHorizontal) {
|
||||
// A horizontal axis is more constrained by the height.
|
||||
me.longestLabelWidth = largestTextWidth;
|
||||
|
||||
// TODO - improve this calculation
|
||||
var labelHeight = (Math.sin(helpers.toRadians(me.labelRotation)) * me.longestLabelWidth) + 1.5 * tickFontSize;
|
||||
var labelHeight = (Math.sin(helpers.toRadians(me.labelRotation)) * me.longestLabelWidth) + (tickFontSize * tallestLabelHeightInLines) + (lineSpace * tallestLabelHeightInLines);
|
||||
|
||||
minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight);
|
||||
me.ctx.font = tickLabelFont;
|
||||
@@ -602,7 +605,18 @@ module.exports = function(Chart) {
|
||||
context.font = tickLabelFont;
|
||||
context.textAlign = (isRotated) ? "right" : "center";
|
||||
context.textBaseline = (isRotated) ? "middle" : options.position === "top" ? "bottom" : "top";
|
||||
context.fillText(label, 0, 0);
|
||||
|
||||
if (helpers.isArray(label)) {
|
||||
for (var i = 0, y = 0; i < label.length; ++i) {
|
||||
// We just make sure the multiline element is a string here..
|
||||
context.fillText('' + label[i], 0, y);
|
||||
// apply same lineSpacing as calculated @ L#320
|
||||
y += (tickFontSize * 1.5);
|
||||
}
|
||||
} else {
|
||||
context.fillText(label, 0, 0);
|
||||
}
|
||||
|
||||
context.restore();
|
||||
}
|
||||
}, me);
|
||||
|
||||
@@ -454,6 +454,68 @@ describe('Core helper tests', function() {
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should return the width of the longest text in an Array and 2D Array', function() {
|
||||
var context = window.createMockContext();
|
||||
var font = "normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif";
|
||||
var arrayOfThings_1D = ['FooBar','Bar'];
|
||||
var arrayOfThings_2D = [['FooBar_1','Bar_2'],'Foo_1'];
|
||||
|
||||
|
||||
// Regardless 'FooBar' is the longest label it should return (charcters * 10)
|
||||
expect(helpers.longestText(context, font, arrayOfThings_1D, {})).toEqual(60);
|
||||
expect(helpers.longestText(context, font, arrayOfThings_2D, {})).toEqual(80);
|
||||
// We check to make sure we made the right calls to the canvas.
|
||||
expect(context.getCalls()).toEqual([{
|
||||
name: 'measureText',
|
||||
args: ['FooBar']
|
||||
}, {
|
||||
name: 'measureText',
|
||||
args: ['Bar']
|
||||
}, {
|
||||
name: 'measureText',
|
||||
args: ['FooBar_1']
|
||||
}, {
|
||||
name: 'measureText',
|
||||
args: ['Bar_2']
|
||||
}, {
|
||||
name: 'measureText',
|
||||
args: ['Foo_1']
|
||||
}]);
|
||||
});
|
||||
|
||||
it('compare text with current longest and update', function() {
|
||||
var context = window.createMockContext();
|
||||
var data = {};
|
||||
var gc = [];
|
||||
var longest = 70;
|
||||
|
||||
expect(helpers.measureText(context, data, gc, longest, 'foobar')).toEqual(70);
|
||||
expect(helpers.measureText(context, data, gc, longest, 'foobar_')).toEqual(70);
|
||||
expect(helpers.measureText(context, data, gc, longest, 'foobar_1')).toEqual(80);
|
||||
// We check to make sure we made the right calls to the canvas.
|
||||
expect(context.getCalls()).toEqual([{
|
||||
name: 'measureText',
|
||||
args: ['foobar']
|
||||
}, {
|
||||
name: 'measureText',
|
||||
args: ['foobar_']
|
||||
}, {
|
||||
name: 'measureText',
|
||||
args: ['foobar_1']
|
||||
}]);
|
||||
});
|
||||
|
||||
it('count look at all the labels and return maximum number of lines', function() {
|
||||
var context = window.createMockContext();
|
||||
var arrayOfThings_1 = ['Foo','Bar'];
|
||||
var arrayOfThings_2 = [['Foo','Bar'],'Foo'];
|
||||
var arrayOfThings_3 = [['Foo','Bar','Boo'],['Foo','Bar'],'Foo'];
|
||||
|
||||
expect(helpers.numberOfLabelLines(arrayOfThings_1)).toEqual(1);
|
||||
expect(helpers.numberOfLabelLines(arrayOfThings_2)).toEqual(2);
|
||||
expect(helpers.numberOfLabelLines(arrayOfThings_3)).toEqual(3);
|
||||
});
|
||||
|
||||
it('should draw a rounded rectangle', function() {
|
||||
var context = window.createMockContext();
|
||||
helpers.drawRoundedRectangle(context, 10, 20, 30, 40, 5);
|
||||
|
||||
Reference in New Issue
Block a user