/** * @class Ext.chart.series.sprite.Pie3DPart * @extends Ext.draw.sprite.Path * * Pie3D series sprite. */Ext.define('Ext.chart.series.sprite.Pie3DPart', { extend: 'Ext.draw.sprite.Path', mixins: { markerHolder: 'Ext.chart.MarkerHolder' }, alias: 'sprite.pie3dPart', inheritableStatics: { def: { processors: { /** * @cfg {Number} [centerX=0] * The central point of the series on the x-axis. */ centerX: 'number', /** * @cfg {Number} [centerY=0] * The central point of the series on the x-axis. */ centerY: 'number', /** * @cfg {Number} [startAngle=0] * The starting angle of the polar series. */ startAngle: 'number', /** * @cfg {Number} [endAngle=Math.PI] * The ending angle of the polar series. */ endAngle: 'number', /** * @cfg {Number} [startRho=0] * The starting radius of the polar series. */ startRho: 'number', /** * @cfg {Number} [endRho=150] * The ending radius of the polar series. */ endRho: 'number', /** * @cfg {Number} [margin=0] * Margin from the center of the pie. Used for donut. */ margin: 'number', /** * @cfg {Number} [thickness=0] * The thickness of the 3D pie part. */ thickness: 'number', /** * @cfg {Number} [bevelWidth=5] * The size of the 3D pie bevel. */ bevelWidth: 'number', /** * @cfg {Number} [distortion=0] * The distortion of the 3D pie part. */ distortion: 'number', /** * @cfg {Object} [baseColor='white'] * The color of the 3D pie part before adding the 3D effect. */ baseColor: 'color', /** * @cfg {Number} [colorSpread=0.7] * An attribute used to control how flat the gradient of the sprite looks. * A value of 0 essentially means no gradient (flat color). */ colorSpread: 'number', /** * @cfg {Number} [baseRotation=0] * The starting rotation of the polar series. */ baseRotation: 'number', /** * @cfg {String} [part='top'] * The part of the 3D Pie represented by the sprite. */ part: 'enums(top,bottom,start,end,innerFront,innerBack,outerFront,outerBack)', /** * @cfg {String} [label=''] * The label associated with the 'top' part of the sprite. */ label: 'string' }, aliases: { rho: 'endRho' }, triggers: { centerX: 'path,bbox', centerY: 'path,bbox', startAngle: 'path,partZIndex', endAngle: 'path,partZIndex', startRho: 'path', endRho: 'path,bbox', margin: 'path,bbox', thickness: 'path', distortion: 'path', baseRotation: 'path,partZIndex', baseColor: 'partZIndex,partColor', colorSpread: 'partColor', part: 'path,partZIndex', globalAlpha: 'canvas,alpha', fillOpacity: 'canvas,alpha' }, defaults: { centerX: 0, centerY: 0, startAngle: Math.PI * 2, endAngle: Math.PI * 2, startRho: 0, endRho: 150, margin: 0, thickness: 35, distortion: 0.5, baseRotation: 0, baseColor: 'white', colorSpread: 0.5, miterLimit: 1, bevelWidth: 5, strokeOpacity: 0, part: 'top', label: '' }, updaters: { alpha: 'alphaUpdater', partColor: 'partColorUpdater', partZIndex: 'partZIndexUpdater' } } }, config: { renderer: null, rendererData: null, rendererIndex: 0, series: null }, bevelParams: [], constructor: function(config) { this.callParent([config]); this.bevelGradient = new Ext.draw.gradient.Linear({ stops: [{ offset: 0, color: 'rgba(255,255,255,0)' }, { offset: 0.7, color: 'rgba(255,255,255,0.6)' }, { offset: 1, color: 'rgba(255,255,255,0)' }] }); }, updateRenderer: function() { this.setDirty(true); }, updateRendererData: function() { this.setDirty(true); }, updateRendererIndex: function() { this.setDirty(true); }, alphaUpdater: function(attr) { var me = this, opacity = attr.globalAlpha, fillOpacity = attr.fillOpacity, oldOpacity = me.oldOpacity, oldFillOpacity = me.oldFillOpacity; // Update the path when the sprite becomes translucent or completely opaque. if ((opacity !== oldOpacity && (opacity === 1 || oldOpacity === 1)) || (fillOpacity !== oldFillOpacity && (fillOpacity === 1 || oldFillOpacity === 1))) { me.scheduleUpdater(attr, 'path', ['globalAlpha']); me.oldOpacity = opacity; me.oldFillOpacity = fillOpacity; } }, partColorUpdater: function(attr) { var color = Ext.util.Color.fly(attr.baseColor), colorString = color.toString(), colorSpread = attr.colorSpread, fillStyle; switch (attr.part) { case 'top': fillStyle = new Ext.draw.gradient.Radial({ start: { x: 0, y: 0, r: 0 }, end: { x: 0, y: 0, r: 1 }, stops: [{ offset: 0, color: color.createLighter(0.1 * colorSpread) }, { offset: 1, color: color.createDarker(0.1 * colorSpread) }] }); break; case 'bottom': fillStyle = new Ext.draw.gradient.Radial({ start: { x: 0, y: 0, r: 0 }, end: { x: 0, y: 0, r: 1 }, stops: [{ offset: 0, color: color.createDarker(0.2 * colorSpread) }, { offset: 1, color: color.toString() }] }); break; case 'outerFront': case 'outerBack': fillStyle = new Ext.draw.gradient.Linear({ stops: [{ offset: 0, color: color.createDarker(0.15 * colorSpread).toString() }, { offset: 0.3, color: colorString }, { offset: 0.8, color: color.createLighter(0.2 * colorSpread).toString() }, { offset: 1, color: color.createDarker(0.25 * colorSpread).toString() }] }); break; case 'start': fillStyle = new Ext.draw.gradient.Linear({ stops: [{ offset: 0, color: color.createDarker(0.1 * colorSpread).toString() }, { offset: 1, color: color.createLighter(0.2 * colorSpread).toString() }] }); break; case 'end': fillStyle = new Ext.draw.gradient.Linear({ stops: [{ offset: 0, color: color.createDarker(0.1 * colorSpread).toString() }, { offset: 1, color: color.createLighter(0.2 * colorSpread).toString() }] }); break; case 'innerFront': case 'innerBack': fillStyle = new Ext.draw.gradient.Linear({ stops: [{ offset: 0, color: color.createDarker(0.1 * colorSpread).toString() }, { offset: 0.2, color: color.createLighter(0.2 * colorSpread).toString() }, { offset: 0.7, color: colorString }, { offset: 1, color: color.createDarker(0.1 * colorSpread).toString() }] }); break; } attr.fillStyle = fillStyle; attr.canvasAttributes.fillStyle = fillStyle; }, partZIndexUpdater: function(attr) { var normalize = Ext.draw.sprite.AttributeParser.angle, rotation = attr.baseRotation, startAngle = attr.startAngle, endAngle = attr.endAngle, depth; switch (attr.part) { case 'top': attr.zIndex = 6; break; case 'outerFront': startAngle = normalize(startAngle + rotation); endAngle = normalize(endAngle + rotation); if (startAngle >= 0 && endAngle < 0) { depth = Math.sin(startAngle); } else if (startAngle <= 0 && endAngle > 0) { depth = Math.sin(endAngle); } else if (startAngle >= 0 && endAngle > 0) { if (startAngle > endAngle) { depth = 0; } else { depth = Math.max(Math.sin(startAngle), Math.sin(endAngle)); } } else { depth = 1; } attr.zIndex = 4 + depth; break; case 'outerBack': attr.zIndex = 1; break; case 'start': attr.zIndex = 4 + Math.sin(normalize(startAngle + rotation)); break; case 'end': attr.zIndex = 4 + Math.sin(normalize(endAngle + rotation)); break; case 'innerFront': attr.zIndex = 2; break; case 'innerBack': attr.zIndex = 4 + Math.sin(normalize((startAngle + endAngle) / 2 + rotation)); break; case 'bottom': attr.zIndex = 0; break; } attr.dirtyZIndex = true; }, updatePlainBBox: function(plain) { var attr = this.attr, part = attr.part, baseRotation = attr.baseRotation, centerX = attr.centerX, centerY = attr.centerY, rho, angle, x, y, sin, cos; if (part === 'start') { angle = attr.startAngle + baseRotation; } else if (part === 'end') { angle = attr.endAngle + baseRotation; } if (Ext.isNumber(angle)) { sin = Math.sin(angle); cos = Math.cos(angle); x = Math.min( centerX + cos * attr.startRho, centerX + cos * attr.endRho ); y = centerY + sin * attr.startRho * attr.distortion; plain.x = x; plain.y = y; plain.width = cos * (attr.endRho - attr.startRho); plain.height = attr.thickness + sin * (attr.endRho - attr.startRho) * 2; return; } if (part === 'innerFront' || part === 'innerBack') { rho = attr.startRho; } else { rho = attr.endRho; } plain.width = rho * 2; plain.height = rho * attr.distortion * 2 + attr.thickness; plain.x = attr.centerX - rho; plain.y = attr.centerY - rho * attr.distortion; }, updateTransformedBBox: function(transform) { if (this.attr.part === 'start' || this.attr.part === 'end') { return this.callParent(arguments); } return this.updatePlainBBox(transform); }, updatePath: function(path) { if (!this.attr.globalAlpha) { return; } if (this.attr.endAngle < this.attr.startAngle) { return; } this[this.attr.part + 'Renderer'](path); }, render: function(surface, ctx, rect) { var me = this, renderer = me.getRenderer(), attr = me.attr, part = attr.part, itemCfg, changes; if (!attr.globalAlpha || Ext.Number.isEqual(attr.startAngle, attr.endAngle, 1e-8)) { return; } if (renderer) { itemCfg = { type: 'pie3dPart', part: attr.part, margin: attr.margin, distortion: attr.distortion, centerX: attr.centerX, centerY: attr.centerY, baseRotation: attr.baseRotation, startAngle: attr.startAngle, endAngle: attr.endAngle, startRho: attr.startRho, endRho: attr.endRho }; changes = Ext.callback(renderer, null, [me, itemCfg, me.getRendererData(), me.getRendererIndex()], 0, me.getSeries()); if (changes) { if (changes.part) { // Can't let users change the nature of the sprite. changes.part = part; } me.setAttributes(changes); me.useAttributes(ctx, rect); } } me.callParent([surface, ctx]); me.bevelRenderer(surface, ctx); // Only the top part will have the label attribute (set by the series). if (attr.label && me.getMarker('labels')) { me.placeLabel(); } }, placeLabel: function() { var me = this, attr = me.attr, attributeId = attr.attributeId, margin = attr.margin, distortion = attr.distortion, centerX = attr.centerX, centerY = attr.centerY, baseRotation = attr.baseRotation, startAngle = attr.startAngle + baseRotation, endAngle = attr.endAngle + baseRotation, midAngle = (startAngle + endAngle) / 2, startRho = attr.startRho + margin, endRho = attr.endRho + margin, midRho = (startRho + endRho) / 2, sin = Math.sin(midAngle), cos = Math.cos(midAngle), surfaceMatrix = me.surfaceMatrix, label = me.getMarker('labels'), labelTpl = label.getTemplate(), calloutLine = labelTpl.getCalloutLine(), calloutLineLength = calloutLine && calloutLine.length || 40, labelCfg = {}, rendererParams, rendererChanges, x, y; surfaceMatrix.appendMatrix(attr.matrix); labelCfg.text = attr.label; x = centerX + cos * midRho; y = centerY + sin * midRho * distortion; labelCfg.x = surfaceMatrix.x(x, y); labelCfg.y = surfaceMatrix.y(x, y); x = centerX + cos * endRho; y = centerY + sin * endRho * distortion; labelCfg.calloutStartX = surfaceMatrix.x(x, y); labelCfg.calloutStartY = surfaceMatrix.y(x, y); x = centerX + cos * (endRho + calloutLineLength); y = centerY + sin * (endRho + calloutLineLength) * distortion; labelCfg.calloutPlaceX = surfaceMatrix.x(x, y); labelCfg.calloutPlaceY = surfaceMatrix.y(x, y); labelCfg.calloutWidth = 2; if (labelTpl.attr.renderer) { rendererParams = [me.attr.label, label, labelCfg, me.getRendererData(), me.getRendererIndex()]; rendererChanges = Ext.callback(labelTpl.attr.renderer, null, rendererParams, 0, me.getSeries()); if (typeof rendererChanges === 'string') { labelCfg.text = rendererChanges; } else { Ext.apply(labelCfg, rendererChanges); } } me.putMarker('labels', labelCfg, attributeId); me.putMarker('labels', { callout: 1 }, attributeId); }, bevelRenderer: function(surface, ctx) { var me = this, attr = me.attr, bevelWidth = attr.bevelWidth, params = me.bevelParams, i; for (i = 0; i < params.length; i++) { ctx.beginPath(); ctx.ellipse.apply(ctx, params[i]); ctx.save(); ctx.lineWidth = bevelWidth; ctx.strokeOpacity = bevelWidth ? 1 : 0; ctx.strokeGradient = me.bevelGradient; ctx.stroke(attr); ctx.restore(); } }, lidRenderer: function(path, thickness) { var attr = this.attr, margin = attr.margin, distortion = attr.distortion, centerX = attr.centerX, centerY = attr.centerY, baseRotation = attr.baseRotation, startAngle = attr.startAngle + baseRotation, endAngle = attr.endAngle + baseRotation, midAngle = (startAngle + endAngle) / 2, startRho = attr.startRho, endRho = attr.endRho, sinEnd = Math.sin(endAngle), cosEnd = Math.cos(endAngle); centerX += Math.cos(midAngle) * margin; centerY += Math.sin(midAngle) * margin * distortion; path.ellipse( centerX, centerY + thickness, startRho, startRho * distortion, 0, startAngle, endAngle, false ); path.lineTo( centerX + cosEnd * endRho, centerY + thickness + sinEnd * endRho * distortion ); path.ellipse( centerX, centerY + thickness, endRho, endRho * distortion, 0, endAngle, startAngle, true ); path.closePath(); }, topRenderer: function(path) { this.lidRenderer(path, 0); }, bottomRenderer: function(path) { var attr = this.attr, none = Ext.util.Color.RGBA_NONE; if (attr.globalAlpha < 1 || attr.fillOpacity < 1 || attr.shadowColor !== none) { this.lidRenderer(path, attr.thickness); } }, sideRenderer: function(path, position) { var attr = this.attr, margin = attr.margin, centerX = attr.centerX, centerY = attr.centerY, distortion = attr.distortion, baseRotation = attr.baseRotation, startAngle = attr.startAngle + baseRotation, endAngle = attr.endAngle + baseRotation, // eslint-disable-next-line max-len isFullPie = (!attr.startAngle && Ext.Number.isEqual(Math.PI * 2, attr.endAngle, 0.0000001)), thickness = attr.thickness, startRho = attr.startRho, endRho = attr.endRho, angle = (position === 'start' && startAngle) || (position === 'end' && endAngle), sin = Math.sin(angle), cos = Math.cos(angle), isTranslucent = attr.globalAlpha < 1, isVisible = position === 'start' && cos < 0 || position === 'end' && cos > 0 || isTranslucent, midAngle; if (isVisible && !isFullPie) { midAngle = (startAngle + endAngle) / 2; centerX += Math.cos(midAngle) * margin; centerY += Math.sin(midAngle) * margin * distortion; path.moveTo( centerX + cos * startRho, centerY + sin * startRho * distortion ); path.lineTo( centerX + cos * endRho, centerY + sin * endRho * distortion ); path.lineTo( centerX + cos * endRho, centerY + sin * endRho * distortion + thickness ); path.lineTo( centerX + cos * startRho, centerY + sin * startRho * distortion + thickness ); path.closePath(); } }, startRenderer: function(path) { this.sideRenderer(path, 'start'); }, endRenderer: function(path) { this.sideRenderer(path, 'end'); }, rimRenderer: function(path, radius, isDonut, isFront) { var me = this, attr = me.attr, margin = attr.margin, centerX = attr.centerX, centerY = attr.centerY, distortion = attr.distortion, baseRotation = attr.baseRotation, normalize = Ext.draw.sprite.AttributeParser.angle, startAngle = attr.startAngle + baseRotation, endAngle = attr.endAngle + baseRotation, // It's critical to use non-normalized start and end angles // for middle angle calculation. Consider a situation where the // start angle is +170 degrees and the end engle is -170 degrees // after normalization (the middle angle is 0 then, but it should be 180 degrees). midAngle = normalize((startAngle + endAngle) / 2), thickness = attr.thickness, isTranslucent = attr.globalAlpha < 1, isAllFront, isAllBack, params; me.bevelParams = []; startAngle = normalize(startAngle); endAngle = normalize(endAngle); centerX += Math.cos(midAngle) * margin; centerY += Math.sin(midAngle) * margin * distortion; isAllFront = startAngle >= 0 && endAngle >= 0; isAllBack = startAngle <= 0 && endAngle <= 0; function renderLeftFrontChunk() { path.ellipse( centerX, centerY + thickness, radius, radius * distortion, 0, Math.PI, startAngle, true ); path.lineTo( centerX + Math.cos(startAngle) * radius, centerY + Math.sin(startAngle) * radius * distortion ); params = [ centerX, centerY, radius, radius * distortion, 0, startAngle, Math.PI, false ]; if (!isDonut) { me.bevelParams.push(params); } path.ellipse.apply(path, params); path.closePath(); } function renderRightFrontChunk() { path.ellipse( centerX, centerY + thickness, radius, radius * distortion, 0, 0, endAngle, false ); path.lineTo( centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius * distortion ); params = [ centerX, centerY, radius, radius * distortion, 0, endAngle, 0, true ]; if (!isDonut) { me.bevelParams.push(params); } path.ellipse.apply(path, params); path.closePath(); } function renderLeftBackChunk() { path.ellipse( centerX, centerY + thickness, radius, radius * distortion, 0, Math.PI, endAngle, false ); path.lineTo( centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius * distortion ); params = [ centerX, centerY, radius, radius * distortion, 0, endAngle, Math.PI, true ]; if (isDonut) { me.bevelParams.push(params); } path.ellipse.apply(path, params); path.closePath(); } function renderRightBackChunk() { path.ellipse( centerX, centerY + thickness, radius, radius * distortion, 0, startAngle, 0, false ); path.lineTo( centerX + radius, centerY ); params = [ centerX, centerY, radius, radius * distortion, 0, 0, startAngle, true ]; if (isDonut) { me.bevelParams.push(params); } path.ellipse.apply(path, params); path.closePath(); } if (isFront) { if (!isDonut || isTranslucent) { if (startAngle >= 0 && endAngle < 0) { renderLeftFrontChunk(); } else if (startAngle <= 0 && endAngle > 0) { renderRightFrontChunk(); } else if (startAngle <= 0 && endAngle < 0) { if (startAngle > endAngle) { path.ellipse( centerX, centerY + thickness, radius, radius * distortion, 0, 0, Math.PI, false ); path.lineTo( centerX - radius, centerY ); params = [ centerX, centerY, radius, radius * distortion, 0, Math.PI, 0, true ]; if (!isDonut) { me.bevelParams.push(params); } path.ellipse.apply(path, params); path.closePath(); } } else { // startAngle >= 0 && endAngle > 0 // obtuse horseshoe-like slice with the gap facing forward if (startAngle > endAngle) { renderLeftFrontChunk(); renderRightFrontChunk(); } else { // acute slice facing forward params = [ centerX, centerY, radius, radius * distortion, 0, startAngle, endAngle, false ]; if (isAllFront && !isDonut || isAllBack && isDonut) { me.bevelParams.push(params); } path.ellipse.apply(path, params); path.lineTo( centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius * distortion + thickness ); path.ellipse( centerX, centerY + thickness, radius, radius * distortion, 0, endAngle, startAngle, true ); path.closePath(); } } } } else { if (isDonut || isTranslucent) { if (startAngle >= 0 && endAngle < 0) { renderLeftBackChunk(); } else if (startAngle <= 0 && endAngle > 0) { renderRightBackChunk(); } else if (startAngle <= 0 && endAngle < 0) { if (startAngle > endAngle) { renderLeftBackChunk(); renderRightBackChunk(); } else { path.ellipse( centerX, centerY + thickness, radius, radius * distortion, 0, startAngle, endAngle, false ); path.lineTo( centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius * distortion ); params = [ centerX, centerY, radius, radius * distortion, 0, endAngle, startAngle, true ]; if (isDonut) { me.bevelParams.push(params); } path.ellipse.apply(path, params); path.closePath(); } } else { // startAngle >= 0 && endAngle > 0 if (startAngle > endAngle) { path.ellipse( centerX, centerY + thickness, radius, radius * distortion, 0, -Math.PI, 0, false ); path.lineTo( centerX + radius, centerY ); params = [ centerX, centerY, radius, radius * distortion, 0, 0, -Math.PI, true ]; if (isDonut) { me.bevelParams.push(params); } path.ellipse.apply(path, params); path.closePath(); } } } } }, innerFrontRenderer: function(path) { this.rimRenderer(path, this.attr.startRho, true, true); }, innerBackRenderer: function(path) { this.rimRenderer(path, this.attr.startRho, true, false); }, outerFrontRenderer: function(path) { this.rimRenderer(path, this.attr.endRho, false, true); }, outerBackRenderer: function(path) { this.rimRenderer(path, this.attr.endRho, false, false); }});