/** * A sprite that represents an individual box with whiskers. * This sprite is meant to be managed by the {@link Ext.chart.series.sprite.BoxPlot} * {@link Ext.chart.MarkerHolder MarkerHolder}, but can also be used independently: * * @example * new Ext.draw.Container({ * width: 100, * height: 100, * renderTo: Ext.getBody(), * sprites: [{ * type: 'boxplot', * translationX: 50, * translationY: 50 * }] * }); * * IMPORTANT: the attributes that represent y-coordinates are in screen coordinates, * just like with any other sprite. For this particular sprite this means that, if 'low' * and 'high' attributes are 10 and 90, then the minimium whisker is rendered at the top * of a draw container {@link Ext.draw.Surface surface} at y = 10, and the maximum whisker * is rendered at the bottom at y = 90. But because the series surface is flipped vertically * in cartesian charts, this means that there minimum is rendered at the bottom and maximum * at the top, just as one would expect. */Ext.define('Ext.chart.sprite.BoxPlot', { extend: 'Ext.draw.sprite.Sprite', alias: 'sprite.boxplot', type: 'boxplot', inheritableStatics: { def: { processors: { /** * @cfg {Number} [x=0] The coordinate of the horizontal center of a boxplot. */ x: 'number', /** * @cfg {Number} [low=-20] The y-coordinate of the whisker that represents * the minimum. */ low: 'number', /** * @cfg {Number} [q1=-10] The y-coordinate of the box edge that represents * the 1-st quartile. */ q1: 'number', /** * @cfg {Number} [median=0] The y-coordinate of the line that represents the median. */ median: 'number', /** * @cfg {Number} [q3=10] The y-coordinate of the box edge that represents * the 3-rd quartile. */ q3: 'number', /** * @cfg {Number} [high=20] The y-coordinate of the whisker that represents * the maximum. */ high: 'number', /** * @cfg {Number} [boxWidth=12] The width of the box in pixels. */ boxWidth: 'number', /** * @cfg {Number} [whiskerWidth=0.5] The length of the lines at the ends * of the whiskers, as a ratio of `boxWidth`. */ whiskerWidth: 'number', /** * @cfg {Boolean} [crisp=true] Whether to snap the rendered lines to the pixel grid * of not. Generally, it's best to have this set to `true` (which is the default) * for pixel perfect results (especially on non-HiDPI displays), but for boxplots * with small `boxWidth` visible artifacts caused by pixel grid snapping may become * noticeable, and setting this to `false` can be a remedy at the expense * of clarity. */ crisp: 'bool' }, triggers: { x: 'bbox', low: 'bbox', high: 'bbox', boxWidth: 'bbox', whiskerWidth: 'bbox', crisp: 'bbox' }, defaults: { x: 0, low: -20, q1: -10, median: 0, q3: 10, high: 20, boxWidth: 12, whiskerWidth: 0.5, crisp: true, fillStyle: '#ccc', strokeStyle: '#000' } } }, updatePlainBBox: function(plain) { var me = this, attr = me.attr, halfLineWidth = attr.lineWidth / 2, x = attr.x - attr.boxWidth / 2 - halfLineWidth, y = attr.high - halfLineWidth, width = attr.boxWidth + attr.lineWidth, height = attr.low - attr.high + attr.lineWidth; plain.x = x; plain.y = y; plain.width = width; plain.height = height; }, render: function(surface, ctx) { var me = this, attr = me.attr; attr.matrix.toContext(ctx); // enable sprite transformations if (attr.crisp) { me.crispRender(surface, ctx); } else { me.softRender(surface, ctx); } //<debug> // eslint-disable-next-line vars-on-top, one-var var debug = attr.debug || this.statics().debug || Ext.draw.sprite.Sprite.debug; if (debug) { // This assumes no part of the sprite is rendered after this call. // If it is, we need to re-apply transformations. // But the bounding box should always be rendered as is, untransformed. this.attr.inverseMatrix.toContext(ctx); debug.bbox && this.renderBBox(surface, ctx); } //</debug> }, /** * @private * Renders a single box with whiskers. * Changes to this method have to be reflected in the {@link #crispRender} as well. * @param surface * @param ctx */ softRender: function(surface, ctx) { var me = this, attr = me.attr, x = attr.x, low = attr.low, q1 = attr.q1, median = attr.median, q3 = attr.q3, high = attr.high, halfBoxWidth = attr.boxWidth / 2, halfWhiskerWidth = attr.boxWidth * attr.whiskerWidth / 2, dash = ctx.getLineDash(); ctx.setLineDash([]); // Only stem can be dashed. // Box. ctx.beginPath(); ctx.moveTo(x - halfBoxWidth, q3); ctx.lineTo(x + halfBoxWidth, q3); ctx.lineTo(x + halfBoxWidth, q1); ctx.lineTo(x - halfBoxWidth, q1); ctx.closePath(); ctx.fillStroke(attr, true); // Stem. ctx.setLineDash(dash); ctx.beginPath(); ctx.moveTo(x, q3); ctx.lineTo(x, high); ctx.moveTo(x, q1); ctx.lineTo(x, low); ctx.stroke(); ctx.setLineDash([]); // Whiskers. ctx.beginPath(); ctx.moveTo(x - halfWhiskerWidth, low); ctx.lineTo(x + halfWhiskerWidth, low); ctx.moveTo(x - halfBoxWidth, median); ctx.lineTo(x + halfBoxWidth, median); ctx.moveTo(x - halfWhiskerWidth, high); ctx.lineTo(x + halfWhiskerWidth, high); ctx.stroke(); }, alignLine: function(x, lineWidth) { lineWidth = lineWidth || this.attr.lineWidth; x = Math.round(x); if (lineWidth % 2 === 1) { x -= 0.5; } return x; }, /** * @private * Renders a pixel-perfect single box with whiskers by aligning to the pixel grid. * Changes to this method have to be reflected in the {@link #softRender} as well. * * Note: crisp image is only guaranteed when `attr.lineWidth` is a whole number. * @param surface * @param ctx */ crispRender: function(surface, ctx) { var me = this, attr = me.attr, x = attr.x, low = me.alignLine(attr.low), q1 = me.alignLine(attr.q1), median = me.alignLine(attr.median), q3 = me.alignLine(attr.q3), high = me.alignLine(attr.high), halfBoxWidth = attr.boxWidth / 2, halfWhiskerWidth = attr.boxWidth * attr.whiskerWidth / 2, stemX = me.alignLine(x), boxLeft = me.alignLine(x - halfBoxWidth), boxRight = me.alignLine(x + halfBoxWidth), whiskerLeft = stemX + Math.round(-halfWhiskerWidth), whiskerRight = stemX + Math.round(halfWhiskerWidth), dash = ctx.getLineDash(); ctx.setLineDash([]); // Only stem can be dashed. // Box. ctx.beginPath(); ctx.moveTo(boxLeft, q3); ctx.lineTo(boxRight, q3); ctx.lineTo(boxRight, q1); ctx.lineTo(boxLeft, q1); ctx.closePath(); ctx.fillStroke(attr, true); // Stem. ctx.setLineDash(dash); ctx.beginPath(); ctx.moveTo(stemX, q3); ctx.lineTo(stemX, high); ctx.moveTo(stemX, q1); ctx.lineTo(stemX, low); ctx.stroke(); ctx.setLineDash([]); // Whiskers. ctx.beginPath(); ctx.moveTo(whiskerLeft, low); ctx.lineTo(whiskerRight, low); ctx.moveTo(boxLeft, median); ctx.lineTo(boxRight, median); ctx.moveTo(whiskerLeft, high); ctx.lineTo(whiskerRight, high); ctx.stroke(); }});