@progress/kendo-charts
Version:
Kendo UI platform-independent Charts library
529 lines (435 loc) • 19.6 kB
JavaScript
import { drawing as draw, geometry } from '@progress/kendo-drawing';
import PieSegment from './pie-segment';
import PieChartMixin from '../mixins/pie-chart-mixin';
import { ChartElement, Ring, Box, Point } from '../../core';
import { OUTSIDE_END, FADEIN, COLUMN } from '../constants';
import { bindSegments, evalOptions } from '../utils';
import { CIRCLE, RIGHT, CENTER } from '../../common/constants';
import { deepExtend, defined, find, isFunction, last, round, setDefaultOptions, valueOrDefault } from '../../common';
var PIE_SECTOR_ANIM_DELAY = 70;
var PieChart = (function (ChartElement) {
function PieChart(plotArea, options) {
ChartElement.call(this, options);
this.plotArea = plotArea;
this.chartService = plotArea.chartService;
this.points = [];
this.legendItems = [];
this.render();
}
if ( ChartElement ) PieChart.__proto__ = ChartElement;
PieChart.prototype = Object.create( ChartElement && ChartElement.prototype );
PieChart.prototype.constructor = PieChart;
PieChart.prototype.render = function render () {
this.traverseDataPoints(this.addValue.bind(this));
};
PieChart.prototype.traverseDataPoints = function traverseDataPoints (callback) {
var this$1 = this;
var ref = this;
var options = ref.options;
var seriesColors = ref.plotArea.options.seriesColors; if ( seriesColors === void 0 ) seriesColors = [];
var colorsCount = seriesColors.length;
var series = options.series;
var seriesCount = series.length;
for (var seriesIx = 0; seriesIx < seriesCount; seriesIx++) {
var currentSeries = series[seriesIx];
var data = currentSeries.data;
var ref$1 = bindSegments(currentSeries);
var total = ref$1.total;
var points = ref$1.points;
var count = ref$1.count;
var anglePerValue = 360 / total;
var constantAngle = (void 0);
if (!isFinite(anglePerValue)) {
constantAngle = 360 / count;
}
var currentAngle = (void 0);
if (defined(currentSeries.startAngle)) {
currentAngle = currentSeries.startAngle;
} else {
currentAngle = options.startAngle;
}
if (seriesIx !== seriesCount - 1) {
if (currentSeries.labels.position === OUTSIDE_END) {
currentSeries.labels.position = CENTER;
}
}
for (var i = 0; i < points.length; i++) {
var pointData = points[i];
if (!pointData) {
continue;
}
var fields = pointData.fields;
var value = pointData.value;
var visible = pointData.visible;
var angle = value !== 0 ? (constantAngle || (value * anglePerValue)) : 0;
var explode = data.length !== 1 && Boolean(fields.explode);
if (!isFunction(currentSeries.color)) {
currentSeries.color = fields.color || seriesColors[i % colorsCount];
}
callback(value, new Ring(null, 0, 0, currentAngle, angle), {
owner: this$1,
category: defined(fields.category) ? fields.category : "",
pattern: defined(fields.pattern) ? fields.pattern : currentSeries.pattern,
index: i,
series: currentSeries,
seriesIx: seriesIx,
dataItem: data[i],
percentage: total !== 0 ? value / total : 0,
explode: explode,
visibleInLegend: fields.visibleInLegend,
visible: visible,
zIndex: seriesCount - seriesIx,
animationDelay: this$1.animationDelay(i, seriesIx, seriesCount)
});
if (visible !== false) {
currentAngle += angle;
}
}
}
};
PieChart.prototype.evalSegmentOptions = function evalSegmentOptions (options, value, fields) {
var series = fields.series;
evalOptions(options, {
value: value,
series: series,
dataItem: fields.dataItem,
category: fields.category,
percentage: fields.percentage
}, { defaults: series._defaults, excluded: [
"data", "content", "template", "visual", "toggle",
"ariaTemplate", "ariaContent"
] });
};
PieChart.prototype.addValue = function addValue (value, sector, fields) {
var segmentOptions = deepExtend({}, fields.series, { index: fields.index });
segmentOptions.pattern = fields.pattern || segmentOptions.pattern;
this.evalSegmentOptions(segmentOptions, value, fields);
this.createLegendItem(value, segmentOptions, fields);
if (fields.visible === false) {
return;
}
var segment = new PieSegment(value, sector, segmentOptions);
Object.assign(segment, fields);
this.append(segment);
this.points.push(segment);
};
PieChart.prototype.reflow = function reflow (targetBox) {
var ref = this;
var options = ref.options;
var points = ref.points;
var seriesConfigs = ref.seriesConfigs; if ( seriesConfigs === void 0 ) seriesConfigs = [];
var count = points.length;
var box = targetBox.clone();
var space = 5;
var minWidth = Math.min(box.width(), box.height());
var halfMinWidth = minWidth / 2;
var defaultPadding = minWidth - minWidth * 0.85;
var newBox = new Box(box.x1, box.y1, box.x1 + minWidth, box.y1 + minWidth);
var newBoxCenter = newBox.center();
var boxCenter = box.center();
var seriesCount = options.series.length;
var leftSideLabels = [];
var rightSideLabels = [];
var padding = valueOrDefault(options.padding, defaultPadding);
this.targetBox = targetBox;
padding = padding > halfMinWidth - space ? halfMinWidth - space : padding;
newBox.translate(boxCenter.x - newBoxCenter.x, boxCenter.y - newBoxCenter.y);
var radius = halfMinWidth - padding;
var center = new Point(
radius + newBox.x1 + padding,
radius + newBox.y1 + padding
);
for (var i = 0; i < count; i++) {
var segment = points[i];
var sector = segment.sector;
var seriesIndex = segment.seriesIx;
sector.radius = radius;
sector.center = center;
if (seriesConfigs.length) {
var seriesConfig = seriesConfigs[seriesIndex];
sector.innerRadius = seriesConfig.innerRadius;
sector.radius = seriesConfig.radius;
}
if (seriesIndex === seriesCount - 1 && segment.explode) {
sector.center = sector.clone().setRadius(sector.radius * 0.15).point(sector.middle());
}
segment.reflow(newBox);
var label = segment.label;
if (label) {
if (label.options.position === OUTSIDE_END) {
if (seriesIndex === seriesCount - 1) {
if (label.orientation === RIGHT) {
rightSideLabels.push(label);
} else {
leftSideLabels.push(label);
}
}
}
}
}
if (leftSideLabels.length > 0) {
leftSideLabels.sort(this.labelComparator(true));
this.leftLabelsReflow(leftSideLabels);
}
if (rightSideLabels.length > 0) {
rightSideLabels.sort(this.labelComparator(false));
this.rightLabelsReflow(rightSideLabels);
}
this.box = newBox;
};
PieChart.prototype.leftLabelsReflow = function leftLabelsReflow (labels) {
var distances = this.distanceBetweenLabels(labels);
this.distributeLabels(distances, labels);
};
PieChart.prototype.rightLabelsReflow = function rightLabelsReflow (labels) {
var distances = this.distanceBetweenLabels(labels);
this.distributeLabels(distances, labels);
};
PieChart.prototype.distanceBetweenLabels = function distanceBetweenLabels (labels) {
var segment = last(this.points);
var sector = segment.sector;
var count = labels.length - 1;
var lr = sector.radius + segment.options.labels.distance;
var distances = [];
var firstBox = labels[0].box;
var distance = round(firstBox.y1 - (sector.center.y - lr - firstBox.height() - firstBox.height() / 2));
distances.push(distance);
for (var i = 0; i < count; i++) {
var secondBox = labels[i + 1].box;
firstBox = labels[i].box;
distance = round(secondBox.y1 - firstBox.y2);
distances.push(distance);
}
distance = round(sector.center.y + lr - labels[count].box.y2 - labels[count].box.height() / 2);
distances.push(distance);
return distances;
};
PieChart.prototype.distributeLabels = function distributeLabels (distances, labels) {
var this$1 = this;
var count = distances.length;
var left, right, remaining;
for (var i = 0; i < count; i++) {
remaining = -distances[i];
left = right = i;
while (remaining > 0 && (left >= 0 || right < count)) {
remaining = this$1._takeDistance(distances, i, --left, remaining);
remaining = this$1._takeDistance(distances, i, ++right, remaining);
}
}
this.reflowLabels(distances, labels);
};
PieChart.prototype._takeDistance = function _takeDistance (distances, anchor, position, amount) {
var result = amount;
if (distances[position] > 0) {
var available = Math.min(distances[position], result);
result -= available;
distances[position] -= available;
distances[anchor] += available;
}
return result;
};
PieChart.prototype.reflowLabels = function reflowLabels (distances, labels) {
var this$1 = this;
var segment = last(this.points);
var sector = segment.sector;
var labelOptions = segment.options.labels;
var labelsCount = labels.length;
var labelDistance = labelOptions.distance;
var boxY = sector.center.y - (sector.radius + labelDistance) - labels[0].box.height();
var boxX;
distances[0] += 2;
for (var i = 0; i < labelsCount; i++) {
var label = labels[i];
var box = label.box;
boxY += distances[i];
boxX = this$1.hAlignLabel(
box.x2,
sector.clone().expand(labelDistance),
boxY,
boxY + box.height(),
label.orientation === RIGHT);
if (label.orientation === RIGHT) {
if (labelOptions.align !== CIRCLE) {
boxX = sector.radius + sector.center.x + labelDistance;
}
label.reflow(new Box(boxX + box.width(), boxY, boxX, boxY));
} else {
if (labelOptions.align !== CIRCLE) {
boxX = sector.center.x - sector.radius - labelDistance;
}
label.reflow(new Box(boxX - box.width(), boxY, boxX, boxY));
}
boxY += box.height();
}
};
PieChart.prototype.createVisual = function createVisual () {
var this$1 = this;
var ref = this;
var connectors = ref.options.connectors;
var points = ref.points;
var count = points.length;
var space = 4;
ChartElement.prototype.createVisual.call(this);
this._connectorLines = [];
for (var i = 0; i < count; i++) {
var segment = points[i];
var sector = segment.sector;
var label = segment.label;
var angle = sector.middle();
var connectorsColor = (segment.options.connectors || {}).color || connectors.color;
if (label) {
var connectorLine = new draw.Path({
stroke: {
color: connectorsColor,
width: connectors.width
},
animation: {
type: FADEIN,
delay: segment.animationDelay
}
});
if (label.options.position === OUTSIDE_END) {
var box = label.box;
var centerPoint = sector.center;
var start = sector.point(angle);
var middle = new Point(box.x1, box.center().y);
var sr = (void 0), end = (void 0), crossing = (void 0);
start = sector.clone().expand(connectors.padding).point(angle);
connectorLine.moveTo(start.x, start.y);
// TODO: Extract into a method to remove duplication
if (label.orientation === RIGHT) {
end = new Point(box.x1 - connectors.padding, box.center().y);
crossing = intersection(centerPoint, start, middle, end);
middle = new Point(end.x - space, end.y);
crossing = crossing || middle;
crossing.x = Math.min(crossing.x, middle.x);
if (this$1.pointInCircle(crossing, sector.center, sector.radius + space) ||
crossing.x < sector.center.x) {
sr = sector.center.x + sector.radius + space;
if (segment.options.labels.align !== COLUMN) {
if (sr < middle.x) {
connectorLine.lineTo(sr, start.y);
} else {
connectorLine.lineTo(start.x + space * 2, start.y);
}
} else {
connectorLine.lineTo(sr, start.y);
}
connectorLine.lineTo(middle.x, end.y);
} else {
crossing.y = end.y;
connectorLine.lineTo(crossing.x, crossing.y);
}
} else {
end = new Point(box.x2 + connectors.padding, box.center().y);
crossing = intersection(centerPoint, start, middle, end);
middle = new Point(end.x + space, end.y);
crossing = crossing || middle;
crossing.x = Math.max(crossing.x, middle.x);
if (this$1.pointInCircle(crossing, sector.center, sector.radius + space) ||
crossing.x > sector.center.x) {
sr = sector.center.x - sector.radius - space;
if (segment.options.labels.align !== COLUMN) {
if (sr > middle.x) {
connectorLine.lineTo(sr, start.y);
} else {
connectorLine.lineTo(start.x - space * 2, start.y);
}
} else {
connectorLine.lineTo(sr, start.y);
}
connectorLine.lineTo(middle.x, end.y);
} else {
crossing.y = end.y;
connectorLine.lineTo(crossing.x, crossing.y);
}
}
connectorLine.lineTo(end.x, end.y);
this$1._connectorLines.push(connectorLine);
this$1.visual.append(connectorLine);
}
}
}
};
PieChart.prototype.renderVisual = function renderVisual () {
ChartElement.prototype.renderVisual.call(this);
if (find(this.options.series, function (options) { return options.autoFit; })) {
var targetBox = this.targetBox;
var pieCenter = this.box.center();
var bbox = this.visual.bbox();
if (!bbox) {
return;
}
var bboxBottom = bbox.bottomRight();
var scale = Math.min(
(pieCenter.y - targetBox.y1) / (pieCenter.y - bbox.origin.y),
(targetBox.y2 - pieCenter.y) / (bboxBottom.y - pieCenter.y),
(pieCenter.x - targetBox.x1) / (pieCenter.x - bbox.origin.x),
(targetBox.x2 - pieCenter.x) / (bboxBottom.x - pieCenter.x)
);
if (scale < 1) {
this.visual.transform(geometry.transform().scale(scale, scale, [ pieCenter.x, pieCenter.y ]));
}
}
};
PieChart.prototype.labelComparator = function labelComparator (reverse) {
var reverseValue = reverse ? -1 : 1;
return function(a, b) {
var first = (a.parent.sector.middle() + 270) % 360;
var second = (b.parent.sector.middle() + 270) % 360;
return (first - second) * reverseValue;
};
};
PieChart.prototype.hAlignLabel = function hAlignLabel (originalX, sector, y1, y2, direction) {
var radius = sector.radius;
var sector_center = sector.center;
var cx = sector_center.x;
var cy = sector_center.y;
var t = Math.min(Math.abs(cy - y1), Math.abs(cy - y2));
if (t > radius) {
return originalX;
}
return cx + Math.sqrt((radius * radius) - (t * t)) * (direction ? 1 : -1);
};
PieChart.prototype.pointInCircle = function pointInCircle (point, center, radius) {
return Math.pow(center.x - point.x, 2) + Math.pow(center.y - point.y, 2) < Math.pow(radius, 2);
};
PieChart.prototype.formatPointValue = function formatPointValue (point, format) {
return this.chartService.format.auto(format, point.value);
};
PieChart.prototype.animationDelay = function animationDelay (categoryIndex) {
return categoryIndex * PIE_SECTOR_ANIM_DELAY;
};
PieChart.prototype.stackRoot = function stackRoot () {
return this;
};
return PieChart;
}(ChartElement));
function intersection(a1, a2, b1, b2) {
var uat = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x);
var ub = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);
var result;
if (ub !== 0) {
var ua = (uat / ub);
result = new Point(
a1.x + ua * (a2.x - a1.x),
a1.y + ua * (a2.y - a1.y)
);
}
return result;
}
setDefaultOptions(PieChart, {
startAngle: 90,
connectors: {
width: 2,
color: "#939393",
padding: 8
},
inactiveItems: {
markers: {},
labels: {}
}
});
deepExtend(PieChart.prototype, PieChartMixin);
PieChart.prototype.isStackRoot = true;
export default PieChart;