/** * @class Ext.chart.series.Line * @extends Ext.chart.series.Cartesian * * Creates a Line Chart. A Line Chart is a useful visualization technique to display quantitative information for different * categories or other real values (as opposed to the bar chart), that can show some progression (or regression) in the dataset. * As with all other series, the Line Series must be appended in the *series* Chart array configuration. See the Chart * documentation for more information. A typical configuration object for the line series could be: * * @example * var store = Ext.create('Ext.data.JsonStore', { * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'], * data: [ * { 'name': 'metric one', 'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8, 'data5': 13 }, * { 'name': 'metric two', 'data1': 7, 'data2': 8, 'data3': 16, 'data4': 10, 'data5': 3 }, * { 'name': 'metric three', 'data1': 5, 'data2': 2, 'data3': 14, 'data4': 12, 'data5': 7 }, * { 'name': 'metric four', 'data1': 2, 'data2': 14, 'data3': 6, 'data4': 1, 'data5': 23 }, * { 'name': 'metric five', 'data1': 4, 'data2': 4, 'data3': 36, 'data4': 13, 'data5': 33 } * ] * }); * * Ext.create('Ext.chart.Chart', { * renderTo: Ext.getBody(), * width: 500, * height: 300, * animate: true, * store: store, * axes: [ * { * type: 'Numeric', * position: 'left', * fields: ['data1', 'data2'], * label: { * renderer: Ext.util.Format.numberRenderer('0,0') * }, * title: 'Sample Values', * grid: true, * minimum: 0 * }, * { * type: 'Category', * position: 'bottom', * fields: ['name'], * title: 'Sample Metrics' * } * ], * series: [ * { * type: 'line', * highlight: { * size: 7, * radius: 7 * }, * axis: 'left', * xField: 'name', * yField: 'data1', * markerConfig: { * type: 'cross', * size: 4, * radius: 4, * 'stroke-width': 0 * } * }, * { * type: 'line', * highlight: { * size: 7, * radius: 7 * }, * axis: 'left', * fill: true, * xField: 'name', * yField: 'data2', * markerConfig: { * type: 'circle', * size: 4, * radius: 4, * 'stroke-width': 0 * } * } * ] * }); * * In this configuration we're adding two series (or lines), one bound to the `data1` * property of the store and the other to `data3`. The type for both configurations is * `line`. The `xField` for both series is the same, the name propert of the store. * Both line series share the same axis, the left axis. You can set particular marker * configuration by adding properties onto the markerConfig object. Both series have * an object as highlight so that markers animate smoothly to the properties in highlight * when hovered. The second series has `fill=true` which means that the line will also * have an area below it of the same color. * * **Note:** In the series definition remember to explicitly set the axis to bind the * values of the line series to. This can be done by using the `axis` configuration property. */ Ext.define('Ext.chart.series.Line', { /* Begin Definitions */ extend: 'Ext.chart.series.Cartesian', alternateClassName: ['Ext.chart.LineSeries', 'Ext.chart.LineChart'], requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.draw.Draw', 'Ext.fx.Anim'], /* End Definitions */ type: 'line', alias: 'series.line', /** * @cfg {String} axis * The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'. * You must explicitly set this value to bind the values of the line series to the ones in the axis, otherwise a * relative scale will be used. */ /** * @cfg {Number} selectionTolerance * The offset distance from the cursor position to the line series to trigger events (then used for highlighting series, etc). */ selectionTolerance: 20, /** * @cfg {Boolean} showMarkers * Whether markers should be displayed at the data points along the line. If true, * then the {@link #markerConfig} config item will determine the markers' styling. */ showMarkers: true, /** * @cfg {Object} markerConfig * The display style for the markers. Only used if {@link #showMarkers} is true. * The markerConfig is a configuration object containing the same set of properties defined in * the Sprite class. For example, if we were to set red circles as markers to the line series we could * pass the object: *
markerConfig: {
type: 'circle',
radius: 4,
'fill': '#f00'
}
*/
markerConfig: {},
/**
* @cfg {Object} style
* An object containing style properties for the visualization lines and fill.
* These styles will override the theme styles. The following are valid style properties:
*
* - `stroke` - an rgb or hex color string for the background color of the line
* - `stroke-width` - the width of the stroke (integer)
* - `fill` - the background fill color string (hex or rgb), only works if {@link #fill} is `true`
* - `opacity` - the opacity of the line and the fill color (decimal)
*
* Example usage:
*
* style: {
* stroke: '#00ff00',
* 'stroke-width': 10,
* fill: '#80A080',
* opacity: 0.2
* }
*/
style: {},
/**
* @cfg {Boolean/Number} smooth
* If set to `true` or a non-zero number, the line will be smoothed/rounded around its points; otherwise
* straight line segments will be drawn.
*
* A numeric value is interpreted as a divisor of the horizontal distance between consecutive points in
* the line; larger numbers result in sharper curves while smaller numbers result in smoother curves.
*
* If set to `true` then a default numeric value of 3 will be used. Defaults to `false`.
*/
smooth: false,
/**
* @private Default numeric smoothing value to be used when {@link #smooth} = true.
*/
defaultSmoothness: 3,
/**
* @cfg {Boolean} fill
* If true, the area below the line will be filled in using the {@link #style eefill} and
* {@link #style opacity} config properties. Defaults to false.
*/
fill: false,
constructor: function(config) {
this.callParent(arguments);
var me = this,
surface = me.chart.surface,
shadow = me.chart.shadow,
i, l;
config.highlightCfg = Ext.Object.merge({ 'stroke-width': 3 }, config.highlightCfg);
Ext.apply(me, config, {
shadowAttributes: [{
"stroke-width": 6,
"stroke-opacity": 0.05,
stroke: 'rgb(0, 0, 0)',
translate: {
x: 1,
y: 1
}
}, {
"stroke-width": 4,
"stroke-opacity": 0.1,
stroke: 'rgb(0, 0, 0)',
translate: {
x: 1,
y: 1
}
}, {
"stroke-width": 2,
"stroke-opacity": 0.15,
stroke: 'rgb(0, 0, 0)',
translate: {
x: 1,
y: 1
}
}]
});
me.group = surface.getGroup(me.seriesId);
if (me.showMarkers) {
me.markerGroup = surface.getGroup(me.seriesId + '-markers');
}
if (shadow) {
for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
}
}
},
// @private makes an average of points when there are more data points than pixels to be rendered.
shrink: function(xValues, yValues, size) {
// Start at the 2nd point...
var len = xValues.length,
ratio = Math.floor(len / size),
i = 1,
xSum = 0,
ySum = 0,
xRes = [+xValues[0]],
yRes = [+yValues[0]];
for (; i < len; ++i) {
xSum += +xValues[i] || 0;
ySum += +yValues[i] || 0;
if (i % ratio == 0) {
xRes.push(xSum/ratio);
yRes.push(ySum/ratio);
xSum = 0;
ySum = 0;
}
}
return {
x: xRes,
y: yRes
};
},
/**
* Draws the series for the current chart.
*/
drawSeries: function() {
var me = this,
chart = me.chart,
chartAxes = chart.axes,
store = chart.getChartStore(),
data = store.data.items,
record,
storeCount = store.getCount(),
surface = me.chart.surface,
bbox = {},
group = me.group,
showMarkers = me.showMarkers,
markerGroup = me.markerGroup,
enableShadows = chart.shadow,
shadowGroups = me.shadowGroups,
shadowAttributes = me.shadowAttributes,
smooth = me.smooth,
lnsh = shadowGroups.length,
dummyPath = ["M"],
path = ["M"],
renderPath = ["M"],
smoothPath = ["M"],
markerIndex = chart.markerIndex,
axes = [].concat(me.axis),
shadowBarAttr,
xValues = [],
xValueMap = {},
yValues = [],
yValueMap = {},
onbreak = false,
storeIndices = [],
markerStyle = me.markerStyle,
seriesStyle = me.seriesStyle,
colorArrayStyle = me.colorArrayStyle,
colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
isNumber = Ext.isNumber,
seriesIdx = me.seriesIdx,
boundAxes = me.getAxesForXAndYFields(),
boundXAxis = boundAxes.xAxis,
boundYAxis = boundAxes.yAxis,
shadows, shadow, shindex, fromPath, fill, fillPath, rendererAttributes,
x, y, prevX, prevY, firstX, firstY, markerCount, i, j, ln, axis, ends, marker, markerAux, item, xValue,
yValue, coords, xScale, yScale, minX, maxX, minY, maxY, line, animation, endMarkerStyle,
endLineStyle, type, count, opacity, lineOpacity, fillOpacity, fillDefaultValue;
if (me.fireEvent('beforedraw', me) === false) {
return;
}
//if store is empty or the series is excluded in the legend then there's nothing to draw.
if (!storeCount || me.seriesIsHidden) {
me.hide();
me.items = [];
if (me.line) {
me.line.hide(true);
if (me.line.shadows) {
shadows = me.line.shadows;
for (j = 0, lnsh = shadows.length; j < lnsh; j++) {
shadow = shadows[j];
shadow.hide(true);
}
}
if (me.fillPath) {
me.fillPath.hide(true);
}
}
me.line = null;
me.fillPath = null;
return;
}
//prepare style objects for line and markers
endMarkerStyle = Ext.apply(markerStyle || {}, me.markerConfig, {
fill: me.seriesStyle.fill || colorArrayStyle[seriesIdx % colorArrayStyle.length]
});
type = endMarkerStyle.type;
delete endMarkerStyle.type;
endLineStyle = seriesStyle;
//if no stroke with is specified force it to 0.5 because this is
//about making *lines*
if (!endLineStyle['stroke-width']) {
endLineStyle['stroke-width'] = 0.5;
}
//set opacity values
opacity = 'opacity' in endLineStyle ? endLineStyle.opacity : 1;
fillDefaultValue = 'opacity' in endLineStyle ? endLineStyle.opacity : 0.3;
lineOpacity = 'lineOpacity' in endLineStyle ? endLineStyle.lineOpacity : opacity;
fillOpacity = 'fillOpacity' in endLineStyle ? endLineStyle.fillOpacity : fillDefaultValue;
//If we're using a time axis and we need to translate the points,
//then reuse the first markers as the last markers.
if (markerIndex && markerGroup && markerGroup.getCount()) {
for (i = 0; i < markerIndex; i++) {
marker = markerGroup.getAt(i);
markerGroup.remove(marker);
markerGroup.add(marker);
markerAux = markerGroup.getAt(markerGroup.getCount() - 2);
marker.setAttributes({
x: 0,
y: 0,
translate: {
x: markerAux.attr.translation.x,
y: markerAux.attr.translation.y
}
}, true);
}
}
me.unHighlightItem();
me.cleanHighlights();
me.setBBox();
bbox = me.bbox;
me.clipRect = [bbox.x, bbox.y, bbox.width, bbox.height];
if (axis = chartAxes.get(boundXAxis)) {
ends = axis.applyData();
minX = ends.from;
maxX = ends.to;
}
if (axis = chartAxes.get(boundYAxis)) {
ends = axis.applyData();
minY = ends.from;
maxY = ends.to;
}
// If a field was specified without a corresponding axis, create one to get bounds
if (me.xField && !Ext.isNumber(minX)) {
axis = me.getMinMaxXValues();
minX = axis[0];
maxX = axis[1];
}
if (me.yField && !Ext.isNumber(minY)) {
axis = me.getMinMaxYValues();
minY = axis[0];
maxY = axis[1];
}
if (isNaN(minX)) {
minX = 0;
xScale = bbox.width / ((storeCount - 1) || 1);
}
else {
xScale = bbox.width / ((maxX - minX) || (storeCount -1) || 1);
}
if (isNaN(minY)) {
minY = 0;
yScale = bbox.height / ((storeCount - 1) || 1);
}
else {
yScale = bbox.height / ((maxY - minY) || (storeCount - 1) || 1);
}
// Extract all x and y values from the store
for (i = 0, ln = data.length; i < ln; i++) {
record = data[i];
xValue = record.get(me.xField);
// Ensure a value
if (typeof xValue == 'string' || typeof xValue == 'object' && !Ext.isDate(xValue)
//set as uniform distribution if the axis is a category axis.
|| boundXAxis && chartAxes.get(boundXAxis) && chartAxes.get(boundXAxis).type == 'Category') {
if (xValue in xValueMap) {
xValue = xValueMap[xValue];
} else {
xValue = xValueMap[xValue] = i;
}
}
// Filter out values that don't fit within the pan/zoom buffer area
yValue = record.get(me.yField);
//skip undefined values
if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) {
//