UNPKG

@nativescript-community/ui-chart

Version:

A powerful chart / graph plugin, supporting line, bar, pie, radar, bubble, and candlestick charts as well as scaling, panning and animations.

714 lines 36.4 kB
import { Align, Canvas, Direction, LayoutAlignment, RectF, StaticLayout, Style, releaseImage } from '@nativescript-community/ui-canvas'; import { DataRenderer } from './DataRenderer'; import { ValuePosition } from '../data/PieDataSet'; import { ColorTemplate } from '../utils/ColorTemplate'; import { Utils } from '../utils/Utils'; export class PieChartRenderer extends DataRenderer { get centerTextLastBounds() { if (!this.mCenterTextLastBounds) { this.mCenterTextLastBounds = new RectF(0, 0, 0, 0); } return this.mCenterTextLastBounds; } get rectBuffer() { if (!this.mRectBuffer) { this.mRectBuffer = [new RectF(0, 0, 0, 0), new RectF(0, 0, 0, 0)]; } return this.mRectBuffer; } constructor(chart, animator, viewPortHandler) { super(animator, viewPortHandler); this.mChart = chart; } get centerTextPaint() { if (!this.mCenterTextPaint) { this.mCenterTextPaint = Utils.getTemplatePaint('black-fill'); this.mCenterTextPaint.setTextSize(12); } return this.mCenterTextPaint; } get entryLabelsPaint() { if (!this.mEntryLabelsPaint) { this.mEntryLabelsPaint = Utils.getTemplatePaint('white-fill'); this.mEntryLabelsPaint.setTextAlign(Align.CENTER); this.mEntryLabelsPaint.setTextSize(13); } return this.mEntryLabelsPaint; } get transparentCirclePaint() { if (!this.mTransparentCirclePaint) { this.mTransparentCirclePaint = Utils.getTemplatePaint('white-fill'); this.mTransparentCirclePaint.setAlpha(105); } return this.mTransparentCirclePaint; } get holePaint() { if (!this.mHolePaint) { this.mHolePaint = Utils.getTemplatePaint('white-fill'); } return this.mHolePaint; } get valuePaint() { if (!this.mValuePaint) { this.mValuePaint = Utils.getTemplatePaint('value'); this.mValuePaint.setColor('white'); this.mValuePaint.setTextSize(13); } return this.mValuePaint; } drawData(c) { const width = this.mViewPortHandler.chartWidth; const height = this.mViewPortHandler.chartHeight; let drawBitmap = this.mDrawBitmap?.get(); if (!drawBitmap || drawBitmap.width !== width || drawBitmap.height !== height) { if (width > 0 && height > 0) { this.mBitmapCanvas = new Canvas(width, height); drawBitmap = this.mBitmapCanvas.getImage(); this.mDrawBitmap = new WeakRef(drawBitmap); } else { return; } } else { this.mBitmapCanvas.clear(); } let needsBitmapDrawing = false; const pieData = this.mChart.data; for (const set of pieData.dataSets) { needsBitmapDrawing = this.drawDataSet(c, set) || needsBitmapDrawing; } if (needsBitmapDrawing) { const renderPaint = this.renderPaint; c.drawBitmap(drawBitmap, 0, 0, renderPaint); } } calculateMinimumRadiusForSpacedSlice(center, radius, angle, arcStartPointX, arcStartPointY, startAngle, sweepAngle) { const angleMiddle = startAngle + sweepAngle / 2; // Other polet of the arc const arcEndPointX = center.x + radius * Math.cos((startAngle + sweepAngle) * Utils.DEG2RAD); const arcEndPointY = center.y + radius * Math.sin((startAngle + sweepAngle) * Utils.DEG2RAD); // Middle polet on the arc const arcMidPointX = center.x + radius * Math.cos(angleMiddle * Utils.DEG2RAD); const arcMidPointY = center.y + radius * Math.sin(angleMiddle * Utils.DEG2RAD); // This is the base of the contained triangle const basePointsDistance = Math.sqrt(Math.pow(arcEndPointX - arcStartPointX, 2) + Math.pow(arcEndPointY - arcStartPointY, 2)); // After reducing space from both sides of the "slice", // the angle of the contained triangle should stay the same. // So let's find out the height of that triangle. const containedTriangleHeight = (basePointsDistance / 2.0) * Math.tan(((180.0 - angle) / 2.0) * Utils.DEG2RAD); // Now we subtract that from the radius let spacedRadius = radius - containedTriangleHeight; // And now subtract the height of the arc that's between the triangle and the outer circle spacedRadius -= Math.sqrt(Math.pow(arcMidPointX - (arcEndPointX + arcStartPointX) / 2, 2) + Math.pow(arcMidPointY - (arcEndPointY + arcStartPointY) / 2, 2)); return spacedRadius; } /** * Calculates the sliceSpace to use based on visible values and their size compared to the set sliceSpace. * * @param dataSet * @return */ getSliceSpace(dataSet) { if (!dataSet.automaticallyDisableSliceSpacing) { return dataSet.sliceSpace; } const spaceSizeRatio = dataSet.sliceSpace / this.mViewPortHandler.smallestContentExtension; const minValueRatio = (dataSet.yMin / this.mChart.data.getYValueSum()) * 2; const sliceSpace = spaceSizeRatio > minValueRatio ? 0 : dataSet.sliceSpace; return sliceSpace; } drawDataSet(c, dataSet) { const chart = this.mChart; const result = false; let angle = 0; const yKey = dataSet.yProperty; const rotationAngle = chart.rotationAngle; const phaseX = this.animator.phaseX; const phaseY = this.animator.phaseY; const circleBox = chart.circleBox; const entryCount = dataSet.entryCount; const drawAngles = chart.drawAngles; const center = chart.centerCircleBox; const radius = chart.radius; const drawInnerArc = chart.drawHoleEnabled && !chart.drawSlicesUnderHoleEnabled; const userInnerRadius = drawInnerArc ? radius * (chart.holeRadius / 100) : 0; const roundedRadius = (radius - (radius * chart.holeRadius) / 100) / 2; const roundedCircleBox = new RectF(0, 0, 0, 0); const drawRoundedSlices = drawInnerArc && chart.drawRoundedSlices; let visibleAngleCount = 0; for (let j = 0; j < entryCount; j++) { // draw only if the value is greater than zero if (Math.abs(dataSet.getEntryForIndex(j)[yKey]) > Utils.NUMBER_EPSILON) { visibleAngleCount++; } } const sliceSpace = visibleAngleCount <= 1 ? 0 : this.getSliceSpace(dataSet); const customRender = chart.customRenderer; const renderPaint = this.renderPaint; const pathBuffer = Utils.getTempPath(); const previousShader = renderPaint.getShader(); const shader = dataSet.fillShader; if (shader) { renderPaint.setShader(shader); } for (let j = 0; j < entryCount; j++) { const sliceAngle = drawAngles[j]; let innerRadius = userInnerRadius; const e = dataSet.getEntryForIndex(j); // draw only if the value is greater than zero if (!(Math.abs(e[yKey]) > Utils.NUMBER_EPSILON)) { angle += sliceAngle * phaseX; continue; } // Don't draw if it's highlighted, unless the chart uses rounded slices if (chart.needsHighlight(j) && !drawRoundedSlices) { angle += sliceAngle * phaseX; continue; } const accountForSliceSpacing = sliceSpace > 0 && sliceAngle <= 180; renderPaint.setColor(dataSet.getColor(j)); const sliceSpaceAngleOuter = visibleAngleCount === 1 ? 0 : sliceSpace / (Utils.DEG2RAD * radius); const startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2) * phaseY; let sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY; if (sweepAngleOuter < 0) { sweepAngleOuter = 0; } pathBuffer.reset(); if (drawRoundedSlices) { const x = center.x + (radius - roundedRadius) * Math.cos(startAngleOuter * Utils.DEG2RAD); const y = center.y + (radius - roundedRadius) * Math.sin(startAngleOuter * Utils.DEG2RAD); roundedCircleBox.set(x - roundedRadius, y - roundedRadius, x + roundedRadius, y + roundedRadius); } const arcStartPointX = center.x + radius * Math.cos(startAngleOuter * Utils.DEG2RAD); const arcStartPointY = center.y + radius * Math.sin(startAngleOuter * Utils.DEG2RAD); if (sweepAngleOuter >= 360 && sweepAngleOuter % 360 <= Utils.NUMBER_EPSILON) { // Android is doing "mod 360" pathBuffer.addCircle(center.x, center.y, radius, Direction.CW); } else { if (drawRoundedSlices) { pathBuffer.arcTo(roundedCircleBox, startAngleOuter + 180, -180); } pathBuffer.arcTo(circleBox, startAngleOuter, sweepAngleOuter); } // Android API < 21 does not receive floats in addArc, but a RectF if (drawInnerArc && (innerRadius > 0 || accountForSliceSpacing)) { const innerRectBuffer = Utils.getTempRectF(); innerRectBuffer.set(center.x - innerRadius, center.y - innerRadius, center.x + innerRadius, center.y + innerRadius); if (accountForSliceSpacing) { let minSpacedRadius = this.calculateMinimumRadiusForSpacedSlice(center, radius, sliceAngle * phaseY, arcStartPointX, arcStartPointY, startAngleOuter, sweepAngleOuter); if (minSpacedRadius < 0) { minSpacedRadius = -minSpacedRadius; } innerRadius = Math.max(innerRadius, minSpacedRadius); } const sliceSpaceAngleInner = visibleAngleCount === 1 || innerRadius === 0 ? 0 : sliceSpace / (Utils.DEG2RAD * innerRadius); const startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2) * phaseY; let sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY; if (sweepAngleInner < 0) { sweepAngleInner = 0; } const endAngleInner = startAngleInner + sweepAngleInner; if (sweepAngleOuter >= 360 && sweepAngleOuter % 360 <= Utils.NUMBER_EPSILON) { // Android is doing "mod 360" pathBuffer.addCircle(center.x, center.y, innerRadius, Direction.CCW); } else { if (drawRoundedSlices) { const x = center.x + (radius - roundedRadius) * Math.cos(endAngleInner * Utils.DEG2RAD); const y = center.y + (radius - roundedRadius) * Math.sin(endAngleInner * Utils.DEG2RAD); roundedCircleBox.set(x - roundedRadius, y - roundedRadius, x + roundedRadius, y + roundedRadius); pathBuffer.arcTo(roundedCircleBox, endAngleInner, 180); } else { pathBuffer.lineTo(center.x + innerRadius * Math.cos(endAngleInner * Utils.DEG2RAD), center.y + innerRadius * Math.sin(endAngleInner * Utils.DEG2RAD)); } pathBuffer.arcTo(innerRectBuffer, endAngleInner, -sweepAngleInner); } } else { if (sweepAngleOuter % 360 > Utils.NUMBER_EPSILON) { if (accountForSliceSpacing) { const angleMiddle = startAngleOuter + sweepAngleOuter / 2; const sliceSpaceOffset = this.calculateMinimumRadiusForSpacedSlice(center, radius, sliceAngle * phaseY, arcStartPointX, arcStartPointY, startAngleOuter, sweepAngleOuter); const arcEndPointX = center.x + sliceSpaceOffset * Math.cos(angleMiddle * Utils.DEG2RAD); const arcEndPointY = center.y + sliceSpaceOffset * Math.sin(angleMiddle * Utils.DEG2RAD); pathBuffer.lineTo(arcEndPointX, arcEndPointY); } else { pathBuffer.lineTo(center.x, center.y); } } } pathBuffer.close(); if (customRender && customRender.drawSlice) { customRender.drawSlice(c, e, pathBuffer, renderPaint); } else { c.drawPath(pathBuffer, renderPaint); } angle += sliceAngle * phaseX; } renderPaint.setShader(previousShader); return result; } drawValues(c) { const chart = this.mChart; const drawEntryLabels = chart.drawEntryLabels; const data = chart.data; const dataSets = data.dataSets; if (!drawEntryLabels || dataSets.some((d) => d.drawValuesEnabled || d.drawIconsEnabled) === false) { return; } const center = chart.centerCircleBox; // Get whole the radius const radius = chart.radius; let rotationAngle = chart.rotationAngle; const drawAngles = chart.drawAngles; const absoluteAngles = chart.absoluteAngles; const phaseX = this.animator.phaseX; const phaseY = this.animator.phaseY; const roundedRadius = (radius - (radius * chart.holeRadius) / 100) / 2; const holeRadiusPercent = chart.holeRadius / 100; let labelRadiusOffset = (radius / 10) * 3.6; if (chart.drawHoleEnabled) { labelRadiusOffset = (radius - radius * holeRadiusPercent) / 2; if (!chart.drawSlicesUnderHoleEnabled && chart.drawRoundedSlices) { // Add curved circle slice and spacing to rotation angle, so that it sits nicely inside rotationAngle += (roundedRadius * 360) / (Math.PI * 2 * radius); } } const labelRadius = radius - labelRadiusOffset; const yValueSum = data.getYValueSum(); let angle; let xIndex = 0; c.save(); const offset = 5; const paint = this.valuePaint; const entryLabelsPaint = drawEntryLabels ? this.entryLabelsPaint : undefined; const customRender = chart.customRenderer; const valueLinePaint = Utils.getTempPaint(); valueLinePaint.setStyle(Style.STROKE); for (let i = 0; i < dataSets.length; i++) { const dataSet = dataSets[i]; const drawValues = dataSet.drawValuesEnabled; if (!drawValues) { continue; } const yKey = dataSet.yProperty; const xValuePosition = dataSet.xValuePosition; const yValuePosition = dataSet.yValuePosition; // Apply the text-styling defined by the DataSet this.applyValueTextStyle(dataSet); const lineHeight = Utils.calcTextHeight(paint, 'Q') + 4; const formatter = dataSet.valueFormatter; const entryCount = dataSet.entryCount; valueLinePaint.setColor(dataSet.valueLineColor); valueLinePaint.setStrokeWidth(dataSet.valueLineWidth); const sliceSpace = this.getSliceSpace(dataSet); const iconsOffset = dataSet.iconsOffset; const valuesOffset = dataSet.valuesOffset; const isDrawIconsEnabled = dataSet.drawIconsEnabled; for (let j = 0; j < entryCount; j++) { const entry = dataSet.getEntryForIndex(j); if (xIndex === 0) { angle = 0; } else { angle = absoluteAngles[xIndex - 1] * phaseX; } const sliceAngle = drawAngles[xIndex]; const sliceSpaceMiddleAngle = sliceSpace / (Utils.DEG2RAD * labelRadius); // offset needed to center the drawn text in the slice const angleOffset = (sliceAngle - sliceSpaceMiddleAngle / 2) / 2; angle = angle + angleOffset; const transformedAngle = rotationAngle + angle * phaseY; const value = chart.usePercentValues ? (entry[yKey] / yValueSum) * 100 : entry[yKey]; const formattedValue = (formatter.getPieLabel || formatter.getFormattedValue)(value, entry); const entryLabel = entry.label; const sliceXBase = Math.cos(transformedAngle * Utils.DEG2RAD); const sliceYBase = Math.sin(transformedAngle * Utils.DEG2RAD); const drawXOutside = drawEntryLabels && xValuePosition === ValuePosition.OUTSIDE_SLICE; const drawYOutside = drawValues && yValuePosition === ValuePosition.OUTSIDE_SLICE; const drawXInside = drawEntryLabels && xValuePosition === ValuePosition.INSIDE_SLICE; const drawYInside = drawValues && yValuePosition === ValuePosition.INSIDE_SLICE; if (drawXOutside || drawYOutside) { const valueLineLength1 = dataSet.valueLinePart1Length; const valueLineLength2 = dataSet.valueLinePart2Length; const valueLinePart1OffsetPercentage = dataSet.valueLinePart1OffsetPercentage / 100; let pt2x, pt2y; let labelPtx, labelPty; let line1Radius; if (chart.drawHoleEnabled) { line1Radius = (radius - radius * holeRadiusPercent) * valueLinePart1OffsetPercentage + radius * holeRadiusPercent; } else { line1Radius = radius * valueLinePart1OffsetPercentage; } const polyline2Width = dataSet.valueLineVariableLength ? labelRadius * valueLineLength2 * Math.abs(Math.sin(transformedAngle * Utils.DEG2RAD)) : labelRadius * valueLineLength2; const pt0x = line1Radius * sliceXBase + center.x; const pt0y = line1Radius * sliceYBase + center.y; const pt1x = labelRadius * (1 + valueLineLength1) * sliceXBase + center.x; const pt1y = labelRadius * (1 + valueLineLength1) * sliceYBase + center.y; if (transformedAngle % 360.0 >= 90.0 && transformedAngle % 360.0 <= 270.0) { pt2x = pt1x - polyline2Width; pt2y = pt1y; paint.setTextAlign(Align.RIGHT); if (drawXOutside) { entryLabelsPaint.setTextAlign(Align.RIGHT); } labelPtx = pt2x - offset; labelPty = pt2y; } else { pt2x = pt1x + polyline2Width; pt2y = pt1y; paint.setTextAlign(Align.LEFT); if (drawXOutside) { entryLabelsPaint.setTextAlign(Align.LEFT); } labelPtx = pt2x + offset; labelPty = pt2y; } if (dataSet.valueLineColor !== ColorTemplate.COLOR_NONE) { if (dataSet.usingSliceColorAsValueLineColor) { valueLinePaint.setColor(dataSet.getColor(j)); } c.drawLine(pt0x, pt0y, pt1x, pt1y, valueLinePaint); c.drawLine(pt1x, pt1y, pt2x, pt2y, valueLinePaint); } // draw everything, depending on settings if (drawXOutside && drawYOutside) { this.drawValue(c, chart, dataSet, i, entry, j, formattedValue, labelPtx + valuesOffset.x, labelPty + valuesOffset.y, dataSet.getValueTextColor(j), paint, customRender); if (j < data.entryCount && entryLabel) { this.drawEntryLabel(c, entryLabel, labelPtx, labelPty + lineHeight, entryLabelsPaint); } } else if (drawXOutside) { if (j < data.entryCount && entryLabel) { this.drawEntryLabel(c, entryLabel, labelPtx, labelPty + lineHeight / 2, entryLabelsPaint); } } else if (drawYOutside) { this.drawValue(c, chart, dataSet, i, entry, j, formattedValue, labelPtx + valuesOffset.x, labelPty + valuesOffset.y + lineHeight / 2, dataSet.getValueTextColor(j), paint, customRender); } } if (drawXInside || drawYInside) { // calculate the text position const x = labelRadius * sliceXBase + center.x; const y = labelRadius * sliceYBase + center.y; paint.setTextAlign(Align.CENTER); // draw everything, depending on settings if (drawXInside && drawYInside) { this.drawValue(c, chart, dataSet, i, entry, j, formattedValue, x + valuesOffset.x, y + valuesOffset.y, dataSet.getValueTextColor(j), paint, customRender); if (j < data.entryCount && entryLabel) { this.drawEntryLabel(c, entryLabel, x, y + lineHeight, entryLabelsPaint); } } else if (drawXInside) { if (j < data.entryCount && entryLabel) { this.drawEntryLabel(c, entryLabel, x, y + lineHeight / 2, entryLabelsPaint); } } else if (drawYInside) { this.drawValue(c, chart, dataSet, i, entry, j, formattedValue, x + valuesOffset.x, y + valuesOffset.y + lineHeight / 2, dataSet.getValueTextColor(j), paint, customRender); } } if (isDrawIconsEnabled) { const x = (labelRadius + iconsOffset.y) * sliceXBase + center.x; let y = (labelRadius + iconsOffset.y) * sliceYBase + center.y; y += iconsOffset.x; this.drawIcon(c, chart, dataSet, i, entry, j, dataSet.getEntryIcon(entry), x, y, customRender); } xIndex++; } } c.restore(); } /** * Draws an entry label at the specified position. * * @param c * @param label * @param x * @param y */ drawEntryLabel(c, label, x, y, paint) { c.drawText(label.toString(), x, y, paint); } drawExtras(c) { this.drawHole(c); if (this.mDrawBitmap.get()) { c.drawBitmap(this.mDrawBitmap.get(), 0, 0, null); } this.drawCenterText(c); } /** * Draws the hole in the center of the chart and the transparent circle / * hole. */ drawHole(c) { const chart = this.mChart; if (chart.drawHoleEnabled) { const radius = chart.radius; const holeRadius = radius * (chart.holeRadius / 100); const center = chart.centerCircleBox; const paint = this.holePaint; if (ColorTemplate.getColorInstance(paint.getColor()).a > 0) { // draw the hole-circle c.drawCircle(center.x, center.y, holeRadius, paint); } const transparentCirclePaint = this.transparentCirclePaint; // only draw the circle if it can be seen (not covered by the hole) if (ColorTemplate.getColorInstance(transparentCirclePaint.getColor()).a > 0 && chart.transparentCircleRadiusPercent > chart.holeRadius) { const alpha = transparentCirclePaint.getAlpha(); const secondHoleRadius = radius * (chart.transparentCircleRadiusPercent / 100); transparentCirclePaint.setAlpha(alpha * this.animator.phaseX * this.animator.phaseY); // draw the transparent-circle const path = Utils.getTempPath(); path.reset(); path.addCircle(center.x, center.y, secondHoleRadius, Direction.CW); path.addCircle(center.x, center.y, holeRadius, Direction.CCW); c.drawPath(path, transparentCirclePaint); // reset alpha transparentCirclePaint.setAlpha(alpha); } } } /** * Draws the description text in the center of the pie chart makes most * sense when center-hole is enabled. */ drawCenterText(c) { const chart = this.mChart; const centerText = chart.centerText; if (centerText && chart.drawCenterText) { const center = chart.centerCircleBox; const offset = chart.centerTextOffset; const x = center.x + offset.x; const y = center.y + offset.y; const innerRadius = chart.drawHoleEnabled && !chart.drawSlicesUnderHoleEnabled ? chart.radius * (chart.holeRadius / 100) : chart.radius; const rectBuffer = this.rectBuffer; const holeRect = rectBuffer[0]; holeRect.left = x - innerRadius; holeRect.top = y - innerRadius; holeRect.right = x + innerRadius; holeRect.bottom = y + innerRadius; const boundingRect = rectBuffer[1]; boundingRect.set(holeRect); const radiusPercent = chart.centerTextRadiusPercent / 100; if (radiusPercent > 0.0) { boundingRect.inset((boundingRect.width() - boundingRect.width() * radiusPercent) / 2, (boundingRect.height() - boundingRect.height() * radiusPercent) / 2); } const centerTextLastBounds = this.centerTextLastBounds; if (!this.mCenterTextLayout || centerText !== this.mCenterTextLastValue || boundingRect !== centerTextLastBounds) { // Next time we won't recalculate StaticLayout... centerTextLastBounds.set(boundingRect); this.mCenterTextLastValue = centerText; const width = centerTextLastBounds.width(); // If width is 0, it will crash. Always have a minimum of 1 this.mCenterTextLayout = new StaticLayout(centerText, this.centerTextPaint, Math.max(Math.ceil(width), 1), LayoutAlignment.ALIGN_CENTER, 1, 0, false); } //let layoutWidth = Utils.getStaticLayoutMaxWidth(mCenterTextLayout); const layoutHeight = this.mCenterTextLayout.getHeight(); c.save(); if (__ANDROID__) { if (android.os.Build.VERSION.SDK_INT >= 18) { const path = Utils.getTempPath(); path.reset(); path.addOval(holeRect, Direction.CW); c.clipPath(path); } } c.translate(boundingRect.left, boundingRect.top + (boundingRect.height() - layoutHeight) / 2); this.mCenterTextLayout.draw(c); c.restore(); } } drawHighlighted(c, indices) { /* Skip entirely if using rounded circle slices, because it doesn't make sense to highlight * in this way. * TODO: add support for changing slice color with highlighting rather than only shifting the slice */ const chart = this.mChart; const drawInnerArc = chart.drawHoleEnabled && !chart.drawSlicesUnderHoleEnabled; if (drawInnerArc && chart.drawRoundedSlices) { return; } const phaseX = this.animator.phaseX; const phaseY = this.animator.phaseY; let angle; const rotationAngle = chart.rotationAngle; const drawAngles = chart.drawAngles; const absoluteAngles = chart.absoluteAngles; const center = chart.centerCircleBox; const radius = chart.radius; const userInnerRadius = drawInnerArc ? radius * (chart.holeRadius / 100) : 0; const highlightedCircleBox = Utils.getTempRectF(); const customRender = chart.customRenderer; const renderPaint = this.renderPaint; const pathBuffer = Utils.getTempPath(); for (let i = 0; i < indices.length; i++) { // get the index to highlight const high = indices[i]; const index = high.x; if (index >= drawAngles.length) { continue; } const set = chart.data.getDataSetByIndex(high.dataSetIndex); if (!set || !set.highlightEnabled) { continue; } const yKey = set.yProperty; const entryCount = set.entryCount; let visibleAngleCount = 0; for (let j = 0; j < entryCount; j++) { // draw only if the value is greater than zero if (Math.abs(set.getEntryForIndex(j)[yKey]) > Utils.NUMBER_EPSILON) { visibleAngleCount++; } } if (index === 0) { angle = 0; } else { angle = absoluteAngles[index - 1] * phaseX; } const sliceSpace = visibleAngleCount <= 1 ? 0 : set.sliceSpace; const sliceAngle = drawAngles[index]; let innerRadius = userInnerRadius; const shift = set.selectionShift; const highlightedRadius = radius + shift; highlightedCircleBox.set(chart.circleBox); highlightedCircleBox.inset(-shift, -shift); const accountForSliceSpacing = sliceSpace > 0 && sliceAngle <= 180; renderPaint.setColor(set.getColor(index)); const sliceSpaceAngleOuter = visibleAngleCount === 1 ? 0 : sliceSpace / (Utils.DEG2RAD * radius); const sliceSpaceAngleShifted = visibleAngleCount === 1 ? 0 : sliceSpace / (Utils.DEG2RAD * highlightedRadius); const startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2) * phaseY; let sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY; if (sweepAngleOuter < 0) { sweepAngleOuter = 0; } const startAngleShifted = rotationAngle + (angle + sliceSpaceAngleShifted / 2) * phaseY; let sweepAngleShifted = (sliceAngle - sliceSpaceAngleShifted) * phaseY; if (sweepAngleShifted < 0) { sweepAngleShifted = 0; } pathBuffer.reset(); if (sweepAngleOuter >= 360 && sweepAngleOuter % 360 <= Utils.NUMBER_EPSILON) { // Android is doing "mod 360" pathBuffer.addCircle(center.x, center.y, highlightedRadius, Direction.CW); } else { pathBuffer.moveTo(center.x + highlightedRadius * Math.cos(startAngleShifted * Utils.DEG2RAD), center.y + highlightedRadius * Math.sin(startAngleShifted * Utils.DEG2RAD)); pathBuffer.arcTo(highlightedCircleBox, startAngleShifted, sweepAngleShifted); } let sliceSpaceRadius = 0; if (accountForSliceSpacing) { sliceSpaceRadius = this.calculateMinimumRadiusForSpacedSlice(center, radius, sliceAngle * phaseY, center.x + radius * Math.cos(startAngleOuter * Utils.DEG2RAD), center.y + radius * Math.sin(startAngleOuter * Utils.DEG2RAD), startAngleOuter, sweepAngleOuter); } if (drawInnerArc && (innerRadius > 0 || accountForSliceSpacing)) { // Android API < 21 does not receive floats in addArc, but a RectF const innerRectBuffer = Utils.getTempRectF(); innerRectBuffer.set(center.x - innerRadius, center.y - innerRadius, center.x + innerRadius, center.y + innerRadius); if (accountForSliceSpacing) { let minSpacedRadius = sliceSpaceRadius; if (minSpacedRadius < 0) minSpacedRadius = -minSpacedRadius; innerRadius = Math.max(innerRadius, minSpacedRadius); } const sliceSpaceAngleInner = visibleAngleCount === 1 || innerRadius === 0 ? 0 : sliceSpace / (Utils.DEG2RAD * innerRadius); const startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2) * phaseY; let sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY; if (sweepAngleInner < 0) { sweepAngleInner = 0; } const endAngleInner = startAngleInner + sweepAngleInner; if (sweepAngleOuter >= 360 && sweepAngleOuter % 360 <= Utils.NUMBER_EPSILON) { // Android is doing "mod 360" pathBuffer.addCircle(center.x, center.y, innerRadius, Direction.CCW); } else { pathBuffer.lineTo(center.x + innerRadius * Math.cos(endAngleInner * Utils.DEG2RAD), center.y + innerRadius * Math.sin(endAngleInner * Utils.DEG2RAD)); pathBuffer.arcTo(innerRectBuffer, endAngleInner, -sweepAngleInner); } } else { if (sweepAngleOuter % 360 > Utils.NUMBER_EPSILON) { if (accountForSliceSpacing) { const angleMiddle = startAngleOuter + sweepAngleOuter / 2; const arcEndPointX = center.x + sliceSpaceRadius * Math.cos(angleMiddle * Utils.DEG2RAD); const arcEndPointY = center.y + sliceSpaceRadius * Math.sin(angleMiddle * Utils.DEG2RAD); pathBuffer.lineTo(arcEndPointX, arcEndPointY); } else { pathBuffer.lineTo(center.x, center.y); } } } pathBuffer.close(); if (customRender && customRender.drawHighlight) { customRender.drawHighlight(c, high, pathBuffer, renderPaint); } else { c.drawPath(pathBuffer, renderPaint); } } } /** * This gives all pie-slices a rounded edge. * * @param c */ drawRoundedSlices(c) { const chart = this.mChart; if (!chart.drawRoundedSlices) { return; } const dataSet = chart.data.getDataSet(); if (!dataSet.visible) { return; } const yKey = dataSet.yProperty; const phaseX = this.animator.phaseX; const phaseY = this.animator.phaseY; const center = chart.centerCircleBox; const r = chart.radius; // calculate the radius of the "slice-circle" const circleRadius = (r - (r * chart.holeRadius) / 100) / 2; const drawAngles = chart.drawAngles; let angle = chart.rotationAngle; const renderPaint = this.renderPaint; for (let j = 0; j < dataSet.entryCount; j++) { const sliceAngle = drawAngles[j]; const e = dataSet.getEntryForIndex(j); // draw only if the value is greater than zero if (Math.abs(e[yKey]) > Utils.NUMBER_EPSILON) { const x = (r - circleRadius) * Math.cos((angle + sliceAngle) * phaseY * Utils.DEG2RAD) + center.x; const y = (r - circleRadius) * Math.sin((angle + sliceAngle) * phaseY * Utils.DEG2RAD) + center.y; renderPaint.setColor(dataSet.getColor(j)); c.drawCircle(x, y, circleRadius, renderPaint); } angle += sliceAngle * phaseX; } } /** * Releases the drawing bitmap. */ releaseBitmap() { if (this.mBitmapCanvas) { this.mBitmapCanvas.setBitmap(null); this.mBitmapCanvas = null; } if (this.mDrawBitmap) { const drawBitmap = this.mDrawBitmap.get(); if (drawBitmap) { releaseImage(drawBitmap); } this.mDrawBitmap = null; } } } //# sourceMappingURL=PieChartRenderer.js.map