/** * @class Ext.chart.series.sprite.Bar * @extends Ext.chart.series.sprite.StackedCartesian * * Draws a sprite used in the bar series. */Ext.define('Ext.chart.series.sprite.Bar', { alias: 'sprite.barSeries', extend: 'Ext.chart.series.sprite.StackedCartesian', inheritableStatics: { def: { processors: { /** * @cfg {Number} [minBarWidth=2] The minimum bar width. */ minBarWidth: 'number', /** * @cfg {Number} [maxBarWidth=100] The maximum bar width. */ maxBarWidth: 'number', /** * @cfg {Number} [minGapWidth=5] The minimum gap between bars. */ minGapWidth: 'number', /** * @cfg {Number} [radius=0] The degree of rounding for rounded bars. */ radius: 'number', /** * @cfg {Number} [inGroupGapWidth=3] The gap between grouped bars. */ inGroupGapWidth: 'number' }, defaults: { minBarWidth: 2, maxBarWidth: 100, minGapWidth: 5, inGroupGapWidth: 3, radius: 0 } } }, drawLabel: function (text, dataX, dataStartY, dataY, labelId) { var me = this, attr = me.attr, label = me.getMarker('labels'), labelTpl = label.getTemplate(), labelCfg = me.labelCfg || (me.labelCfg = {}), surfaceMatrix = me.surfaceMatrix, labelOverflowPadding = attr.labelOverflowPadding, labelDisplay = labelTpl.attr.display, labelOrientation = labelTpl.attr.orientation, isVerticalText = (labelOrientation === 'horizontal' && attr.flipXY) || (labelOrientation === 'vertical' && !attr.flipXY) || !labelOrientation, calloutLine = labelTpl.getCalloutLine(), labelY, halfText, labelBBox, calloutLineLength, changes, hasPendingChanges, params; // The coordinates below (data point converted to surface coordinates) // are just for the renderer to give it a notion of where the label will be positioned. // The actual position of the label will be different // (unless the renderer returns x/y coordinates in the changes object) // and depend on several things including the size of the text, // which has to be measured after the renderer call, // since text can be modified by the renderer. labelCfg.x = surfaceMatrix.x(dataX, dataY); labelCfg.y = surfaceMatrix.y(dataX, dataY); if (calloutLine) { calloutLineLength = calloutLine.length; } else { calloutLineLength = 0; } // Set defaults if (!attr.flipXY) { labelCfg.rotationRads = -Math.PI * 0.5; } else { labelCfg.rotationRads = 0; } labelCfg.calloutVertical = !attr.flipXY; // Check if we have a specific orientation specified, if so, set // the appropriate values. switch (labelOrientation) { case 'horizontal': labelCfg.rotationRads = 0; labelCfg.calloutVertical = false; break; case 'vertical': labelCfg.rotationRads = -Math.PI * 0.5; labelCfg.calloutVertical = true; break; } labelCfg.text = text; if (labelTpl.attr.renderer) { // The label instance won't exist on first render before the renderer is called, // it's only created later by `me.putMarker` after the renderer call. To make // sure the renderer always can access the label instance, we make this check here. if (!label.get(labelId)) { label.putMarkerFor('labels', {}, labelId); } params = [text, label, labelCfg, {store: me.getStore()}, labelId]; changes = Ext.callback(labelTpl.attr.renderer, null, params, 0, me.getSeries()); if (typeof changes === 'string') { labelCfg.text = changes; } else if (typeof changes === 'object') { if ('text' in changes) { labelCfg.text = changes.text; } hasPendingChanges = true; } } labelBBox = me.getMarkerBBox('labels', labelId, true); if (!labelBBox) { me.putMarker('labels', labelCfg, labelId); labelBBox = me.getMarkerBBox('labels', labelId, true); } if (calloutLineLength > 0) { halfText = calloutLineLength; } else if (calloutLineLength === 0) { halfText = (isVerticalText ? labelBBox.width : labelBBox.height) / 2; } else { halfText = (isVerticalText ? labelBBox.width : labelBBox.height) / 2 + labelOverflowPadding; } if (dataStartY > dataY) { halfText = -halfText; } if (isVerticalText) { labelY = (labelDisplay === 'insideStart') ? dataStartY + halfText : dataY - halfText; } else { labelY = (labelDisplay === 'insideStart') ? dataStartY + labelOverflowPadding * 2 : dataY - labelOverflowPadding * 2; } labelCfg.x = surfaceMatrix.x(dataX, labelY); labelCfg.y = surfaceMatrix.y(dataX, labelY); labelY = (labelDisplay === 'insideStart') ? dataStartY : dataY; labelCfg.calloutStartX = surfaceMatrix.x(dataX, labelY); labelCfg.calloutStartY = surfaceMatrix.y(dataX, labelY); labelY = (labelDisplay === 'insideStart') ? dataStartY - halfText : dataY + halfText; labelCfg.calloutPlaceX = surfaceMatrix.x(dataX, labelY); labelCfg.calloutPlaceY = surfaceMatrix.y(dataX, labelY); labelCfg.calloutColor = (calloutLine && calloutLine.color) || me.attr.fillStyle; if (calloutLine) { if (calloutLine.width) { labelCfg.calloutWidth = calloutLine.width; } } else { labelCfg.calloutColor = 'none'; } if (dataStartY > dataY) { halfText = -halfText; } if (Math.abs(dataY - dataStartY) <= halfText * 2 || labelDisplay === 'outside') { labelCfg.callout = 1; } else { labelCfg.callout = 0; } if (hasPendingChanges) { Ext.apply(labelCfg, changes); } me.putMarker('labels', labelCfg, labelId); }, drawBar: function (ctx, surface, rect, left, top, right, bottom, index) { var me = this, itemCfg = {}, renderer = me.attr.renderer, changes; itemCfg.x = left; itemCfg.y = top; itemCfg.width = right - left; itemCfg.height = bottom - top; itemCfg.radius = me.attr.radius; if (renderer) { changes = Ext.callback(renderer, null, [me, itemCfg, {store: me.getStore()}, index], 0, me.getSeries()); Ext.apply(itemCfg, changes); } me.putMarker('items', itemCfg, index, !renderer); }, renderClipped: function (surface, ctx, dataClipRect) { if (this.cleanRedraw) { return; } var me = this, attr = me.attr, dataX = attr.dataX, dataY = attr.dataY, dataText = attr.labels, dataStartY = attr.dataStartY, groupCount = attr.groupCount, groupOffset = attr.groupOffset - (groupCount - 1) * 0.5, inGroupGapWidth = attr.inGroupGapWidth, lineWidth = ctx.lineWidth, matrix = attr.matrix, xx = matrix.elements[0], yy = matrix.elements[3], dx = matrix.elements[4], dy = surface.roundPixel(matrix.elements[5]) - 1, maxBarWidth = Math.abs(xx) - attr.minGapWidth, minBarWidth = ( Math.min(maxBarWidth, attr.maxBarWidth) - inGroupGapWidth * (groupCount - 1) ) / groupCount, barWidth = surface.roundPixel( Math.max(attr.minBarWidth, minBarWidth) ), surfaceMatrix = me.surfaceMatrix, left, right, bottom, top, i, center, halfLineWidth = 0.5 * attr.lineWidth, // Finding min/max so that bars render properly in both LTR and RTL modes. min = Math.min(dataClipRect[0], dataClipRect[2]), max = Math.max(dataClipRect[0], dataClipRect[2]), start = Math.max(0, Math.floor(min)), end = Math.min(dataX.length - 1, Math.ceil(max)), isDrawLabels = dataText && me.getMarker('labels'), yLow, yHi; // The scaling (xx) and translation (dx) here will already be such that the midpoints // of the first and last bars are not at the surface edges (which would mean that // bars are half-clipped), but padded, so that those bars are fully visible (assuming no pan/zoom). // TODO: Oddly enough, this is controlled by the 'labelInSpan' config of the xAxis, which is set // TODO: to 'true' by the Bar series in its 'updateXAxis' method. The 'labelInSpan' config // TODO: doesn't actually do what it says it does: 'Draws the labels in the middle of the spans.' // TODO: and its use to control the way bars render is puzzling. // TODO: The way this works is the 'axis.getRange' method expands the axis range by the value // TODO: of 'increment' - another axis config, which defaults to 0.5. // TODO: So, for example, if the visible range was [0, 11] (for, say, twelve months of the year), // TODO: it will become [-0.5, 11.5], making space for bars at the edges of a chart. for (i = start; i <= end; i++) { yLow = dataStartY ? dataStartY[i] : 0; yHi = dataY[i]; center = dataX[i] * xx + dx + groupOffset * (barWidth + inGroupGapWidth); left = surface.roundPixel(center - barWidth / 2) + halfLineWidth; top = surface.roundPixel(yHi * yy + dy + lineWidth); right = surface.roundPixel(center + barWidth / 2) - halfLineWidth; bottom = surface.roundPixel(yLow * yy + dy + lineWidth); me.drawBar(ctx, surface, dataClipRect, left, top - halfLineWidth, right, bottom - halfLineWidth, i); // We want 0 values to be passed to the renderer if (isDrawLabels && dataText[i] != null) { me.drawLabel(dataText[i], center, bottom, top, i); } me.putMarker('markers', { translationX: surfaceMatrix.x(center, top), translationY: surfaceMatrix.y(center, top) }, i, true); } }, getIndexNearPoint: function (x, y) { var sprite = this, attr = sprite.attr, dataX = attr.dataX, surface = sprite.getSurface(), surfaceRect = surface.getRect() || [0,0,0,0], surfaceHeight = surfaceRect[3], hitX, hitY, i, bbox, index = -1; // The "items" sprites that draw the bars work in a reverse vertical coordinate system // starting with 0 at the bottom and increasing the Y coordinate toward the top. // See also Ext.chart.series.Bar.getItemForPoint(x,y) regarding the chart's innerPadding. if (attr.flipXY) { hitX = surfaceHeight - y; if (surface.getInherited().rtl) { hitY = surfaceRect[2] - x; } else { hitY = x; } } else { hitX = x; hitY = surfaceHeight - y; } for (i = 0; i < dataX.length; i++) { bbox = sprite.getMarkerBBox('items', i); if (Ext.draw.Draw.isPointInBBox(hitX, hitY, bbox)) { index = i; break; } } return index; } });