ag-charts-community
Version:
Advanced Charting / Charts supporting Javascript / Typescript / React / Angular / Vue
568 lines • 22.7 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
const group_1 = require("../../../scene/group");
const line_1 = require("../../../scene/shape/line");
const text_1 = require("../../../scene/shape/text");
const selection_1 = require("../../../scene/selection");
const linearScale_1 = require("../../../scale/linearScale");
const sector_1 = require("../../../scene/shape/sector");
const series_1 = require("./../series");
const label_1 = require("../../label");
const node_1 = require("../../../scene/node");
const angle_1 = require("../../../util/angle");
const number_1 = require("../../../util/number");
const caption_1 = require("../../../caption");
const observable_1 = require("../../../util/observable");
const polarSeries_1 = require("./polarSeries");
const chartAxis_1 = require("../../chartAxis");
const chart_1 = require("../../chart");
class PieHighlightStyle extends series_1.HighlightStyle {
}
var PieNodeTag;
(function (PieNodeTag) {
PieNodeTag[PieNodeTag["Sector"] = 0] = "Sector";
PieNodeTag[PieNodeTag["Callout"] = 1] = "Callout";
PieNodeTag[PieNodeTag["Label"] = 2] = "Label";
})(PieNodeTag || (PieNodeTag = {}));
class PieSeriesLabel extends label_1.Label {
constructor() {
super(...arguments);
this.offset = 3; // from the callout line
this.minAngle = 20; // in degrees
}
}
__decorate([
observable_1.reactive('change')
], PieSeriesLabel.prototype, "offset", void 0);
__decorate([
observable_1.reactive('dataChange')
], PieSeriesLabel.prototype, "minAngle", void 0);
__decorate([
observable_1.reactive('dataChange')
], PieSeriesLabel.prototype, "formatter", void 0);
class PieSeriesCallout extends observable_1.Observable {
constructor() {
super(...arguments);
this.colors = [
'#874349',
'#718661',
'#a48f5f',
'#5a7088',
'#7f637a',
'#5d8692'
];
this.length = 10;
this.strokeWidth = 1;
}
}
__decorate([
observable_1.reactive('change')
], PieSeriesCallout.prototype, "colors", void 0);
__decorate([
observable_1.reactive('change')
], PieSeriesCallout.prototype, "length", void 0);
__decorate([
observable_1.reactive('change')
], PieSeriesCallout.prototype, "strokeWidth", void 0);
class PieSeriesTooltip extends series_1.SeriesTooltip {
}
__decorate([
observable_1.reactive('change')
], PieSeriesTooltip.prototype, "renderer", void 0);
exports.PieSeriesTooltip = PieSeriesTooltip;
class PieTitle extends caption_1.Caption {
constructor() {
super(...arguments);
this.showInLegend = false;
}
}
__decorate([
observable_1.reactive()
], PieTitle.prototype, "showInLegend", void 0);
exports.PieTitle = PieTitle;
class PieSeries extends polarSeries_1.PolarSeries {
constructor() {
super();
this.radiusScale = new linearScale_1.LinearScale();
this.groupSelection = selection_1.Selection.select(this.pickGroup).selectAll();
/**
* The processed data that gets visualized.
*/
this.groupSelectionData = [];
this.angleScale = (() => {
const scale = new linearScale_1.LinearScale();
// Each slice is a ratio of the whole, where all ratios add up to 1.
scale.domain = [0, 1];
// Add 90 deg to start the first pie at 12 o'clock.
scale.range = [-Math.PI, Math.PI].map(angle => angle + Math.PI / 2);
return scale;
})();
// When a user toggles a series item (e.g. from the legend), its boolean state is recorded here.
this.seriesItemEnabled = [];
this.label = new PieSeriesLabel();
this.callout = new PieSeriesCallout();
this.tooltip = new PieSeriesTooltip();
/**
* The key of the numeric field to use to determine the angle (for example,
* a pie slice angle).
*/
this.angleKey = '';
this.angleName = '';
this._fills = [
'#c16068',
'#a2bf8a',
'#ebcc87',
'#80a0c3',
'#b58dae',
'#85c0d1'
];
this._strokes = [
'#874349',
'#718661',
'#a48f5f',
'#5a7088',
'#7f637a',
'#5d8692'
];
this.fillOpacity = 1;
this.strokeOpacity = 1;
this.lineDash = [0];
this.lineDashOffset = 0;
/**
* The series rotation in degrees.
*/
this.rotation = 0;
this.outerRadiusOffset = 0;
this.innerRadiusOffset = 0;
this.strokeWidth = 1;
this.highlightStyle = new PieHighlightStyle();
this.addEventListener('update', this.scheduleUpdate, this);
this.label.addEventListener('change', this.scheduleUpdate, this);
this.label.addEventListener('dataChange', this.scheduleData, this);
this.callout.addEventListener('change', this.scheduleLayout, this);
this.addPropertyListener('data', event => {
if (event.value) {
event.source.seriesItemEnabled = event.value.map(() => true);
}
});
}
set title(value) {
const oldTitle = this._title;
function updateLegend() {
this.fireEvent({ type: 'legendChange' });
}
if (oldTitle !== value) {
if (oldTitle) {
oldTitle.removeEventListener('change', this.scheduleUpdate, this);
oldTitle.addPropertyListener('showInLegend', updateLegend, this);
this.group.removeChild(oldTitle.node);
}
if (value) {
value.node.textBaseline = 'bottom';
value.addEventListener('change', this.scheduleUpdate, this);
value.addPropertyListener('showInLegend', updateLegend, this);
this.group.appendChild(value.node);
}
this._title = value;
this.scheduleUpdate();
}
}
get title() {
return this._title;
}
set fills(values) {
this._fills = values;
this.scheduleUpdate();
}
get fills() {
return this._fills;
}
set strokes(values) {
this._strokes = values;
this.scheduleUpdate();
}
get strokes() {
return this._strokes;
}
onHighlightChange() {
this.updateNodes();
}
setColors(fills, strokes) {
this.fills = fills;
this.strokes = strokes;
this.callout.colors = strokes;
}
getDomain(direction) {
if (direction === chartAxis_1.ChartAxisDirection.X) {
return this.angleScale.domain;
}
else {
return this.radiusScale.domain;
}
}
processData() {
const { angleKey, radiusKey, seriesItemEnabled, angleScale, groupSelectionData, label } = this;
const data = angleKey && this.data ? this.data : [];
const angleData = data.map((datum, index) => seriesItemEnabled[index] && Math.abs(+datum[angleKey]) || 0);
const angleDataTotal = angleData.reduce((a, b) => a + b, 0);
// The ratios (in [0, 1] interval) used to calculate the end angle value for every pie slice.
// Each slice starts where the previous one ends, so we only keep the ratios for end angles.
const angleDataRatios = (() => {
let sum = 0;
return angleData.map(datum => sum += datum / angleDataTotal);
})();
const labelFormatter = label.formatter;
const labelKey = label.enabled && this.labelKey;
let labelData = [];
let radiusData = [];
if (labelKey) {
if (labelFormatter) {
labelData = data.map(datum => labelFormatter({ value: datum[labelKey] }));
}
else {
labelData = data.map(datum => String(datum[labelKey]));
}
}
if (radiusKey) {
const { radiusMin, radiusMax } = this;
const radii = data.map(datum => Math.abs(datum[radiusKey]));
const min = radiusMin !== undefined ? radiusMin : Math.min(...radii);
const max = radiusMax !== undefined ? radiusMax : Math.max(...radii);
const delta = max - min;
radiusData = radii.map(value => delta ? (value - min) / delta : 1);
}
groupSelectionData.length = 0;
const rotation = angle_1.toRadians(this.rotation);
const halfPi = Math.PI / 2;
let datumIndex = 0;
// Simply use reduce here to pair up adjacent ratios.
angleDataRatios.reduce((start, end) => {
const radius = radiusKey ? radiusData[datumIndex] : 1;
const startAngle = angleScale.convert(start) + rotation;
const endAngle = angleScale.convert(end) + rotation;
const midAngle = (startAngle + endAngle) / 2;
const span = Math.abs(endAngle - startAngle);
const midCos = Math.cos(midAngle);
const midSin = Math.sin(midAngle);
const labelMinAngle = angle_1.toRadians(label.minAngle);
const labelVisible = labelKey && span > labelMinAngle;
const midAngle180 = angle_1.normalizeAngle180(midAngle);
// Split the circle into quadrants like so: ⊗
let quadrantStart = -3 * Math.PI / 4; // same as `normalizeAngle180(toRadians(-135))`
let textAlign;
let textBaseline;
if (midAngle180 >= quadrantStart && midAngle180 < (quadrantStart += halfPi)) {
textAlign = 'center';
textBaseline = 'bottom';
}
else if (midAngle180 >= quadrantStart && midAngle180 < (quadrantStart += halfPi)) {
textAlign = 'left';
textBaseline = 'middle';
}
else if (midAngle180 >= quadrantStart && midAngle180 < (quadrantStart += halfPi)) {
textAlign = 'center';
textBaseline = 'hanging';
}
else {
textAlign = 'right';
textBaseline = 'middle';
}
groupSelectionData.push({
series: this,
datum: data[datumIndex],
itemId: datumIndex,
index: datumIndex,
radius,
startAngle,
endAngle,
midAngle,
midCos,
midSin,
label: labelVisible ? {
text: labelData[datumIndex],
textAlign,
textBaseline
} : undefined
});
datumIndex++;
return end;
}, 0);
return true;
}
update() {
this.updatePending = false;
const { radius, innerRadiusOffset, outerRadiusOffset, title } = this;
this.radiusScale.range = [
innerRadiusOffset ? radius + innerRadiusOffset : 0,
radius + (outerRadiusOffset || 0)
];
this.group.translationX = this.centerX;
this.group.translationY = this.centerY;
if (title) {
const outerRadius = Math.max(0, this.radiusScale.range[1]);
if (outerRadius === 0) {
title.node.visible = false;
}
else {
title.node.translationY = -radius - outerRadiusOffset - 2;
title.node.visible = title.enabled;
}
}
this.updateSelections();
this.updateNodes();
}
updateSelections() {
if (!this.nodeDataPending) {
return;
}
this.nodeDataPending = false;
this.updateGroupSelection();
}
updateGroupSelection() {
const updateGroups = this.groupSelection.setData(this.groupSelectionData);
updateGroups.exit.remove();
const enterGroups = updateGroups.enter.append(group_1.Group);
enterGroups.append(sector_1.Sector).each(node => node.tag = PieNodeTag.Sector);
enterGroups.append(line_1.Line).each(node => {
node.tag = PieNodeTag.Callout;
node.pointerEvents = node_1.PointerEvents.None;
});
enterGroups.append(text_1.Text).each(node => {
node.tag = PieNodeTag.Label;
node.pointerEvents = node_1.PointerEvents.None;
});
this.groupSelection = updateGroups.merge(enterGroups);
}
updateNodes() {
if (!this.chart) {
return;
}
this.group.visible = this.visible && this.seriesItemEnabled.indexOf(true) >= 0;
const { fills, strokes, fillOpacity, strokeOpacity, radiusScale, callout, shadow, chart: { highlightedDatum }, highlightStyle: { fill: deprecatedFill, stroke: deprecatedStroke, strokeWidth: deprecatedStrokeWidth, item: { fill: highlightedFill = deprecatedFill, stroke: highlightedStroke = deprecatedStroke, strokeWidth: highlightedDatumStrokeWidth = deprecatedStrokeWidth, } }, angleKey, radiusKey, formatter } = this;
const centerOffsets = [];
const innerRadius = radiusScale.convert(0);
this.groupSelection.selectByTag(PieNodeTag.Sector).each((sector, datum, index) => {
const radius = radiusScale.convert(datum.radius);
const isDatumHighlighted = !!highlightedDatum && highlightedDatum.series === this && datum.itemId === highlightedDatum.itemId;
const fill = isDatumHighlighted && highlightedFill !== undefined ? highlightedFill : fills[index % fills.length];
const stroke = isDatumHighlighted && highlightedStroke !== undefined ? highlightedStroke : strokes[index % strokes.length];
const strokeWidth = isDatumHighlighted && highlightedDatumStrokeWidth !== undefined
? highlightedDatumStrokeWidth
: this.getStrokeWidth(this.strokeWidth);
let format = undefined;
if (formatter) {
format = formatter({
datum: datum.datum,
angleKey,
radiusKey,
fill,
stroke,
strokeWidth,
highlighted: isDatumHighlighted
});
}
// Bring highlighted slice's parent group to front.
const parent = sector.parent && sector.parent.parent;
if (isDatumHighlighted && parent) {
parent.removeChild(sector.parent);
parent.appendChild(sector.parent);
}
sector.innerRadius = Math.max(0, innerRadius);
sector.outerRadius = Math.max(0, radius);
sector.startAngle = datum.startAngle;
sector.endAngle = datum.endAngle;
sector.fill = format && format.fill || fill;
sector.stroke = format && format.stroke || stroke;
sector.strokeWidth = format && format.strokeWidth !== undefined ? format.strokeWidth : strokeWidth;
sector.fillOpacity = fillOpacity;
sector.strokeOpacity = strokeOpacity;
sector.lineDash = this.lineDash;
sector.lineDashOffset = this.lineDashOffset;
sector.fillShadow = shadow;
sector.lineJoin = 'round';
sector.opacity = this.getOpacity();
centerOffsets.push(sector.centerOffset);
});
const { colors: calloutColors, length: calloutLength, strokeWidth: calloutStrokeWidth } = callout;
this.groupSelection.selectByTag(PieNodeTag.Callout).each((line, datum, index) => {
const radius = radiusScale.convert(datum.radius);
const outerRadius = Math.max(0, radius);
if (datum.label && outerRadius !== 0) {
line.strokeWidth = calloutStrokeWidth;
line.stroke = calloutColors[index % calloutColors.length];
line.x1 = datum.midCos * outerRadius;
line.y1 = datum.midSin * outerRadius;
line.x2 = datum.midCos * (outerRadius + calloutLength);
line.y2 = datum.midSin * (outerRadius + calloutLength);
}
else {
line.stroke = undefined;
}
});
{
const { offset, fontStyle, fontWeight, fontSize, fontFamily, color } = this.label;
this.groupSelection.selectByTag(PieNodeTag.Label).each((text, datum, index) => {
const label = datum.label;
const radius = radiusScale.convert(datum.radius);
const outerRadius = Math.max(0, radius);
if (label && outerRadius !== 0) {
const labelRadius = centerOffsets[index] + outerRadius + calloutLength + offset;
text.fontStyle = fontStyle;
text.fontWeight = fontWeight;
text.fontSize = fontSize;
text.fontFamily = fontFamily;
text.text = label.text;
text.x = datum.midCos * labelRadius;
text.y = datum.midSin * labelRadius;
text.fill = color;
text.textAlign = label.textAlign;
text.textBaseline = label.textBaseline;
}
else {
text.fill = undefined;
}
});
}
}
fireNodeClickEvent(event, datum) {
this.fireEvent({
type: 'nodeClick',
event,
series: this,
datum: datum.datum,
angleKey: this.angleKey,
labelKey: this.labelKey,
radiusKey: this.radiusKey
});
}
getTooltipHtml(nodeDatum) {
const { angleKey } = this;
if (!angleKey) {
return '';
}
const { fills, tooltip, angleName, radiusKey, radiusName, labelKey, labelName, } = this;
const { renderer: tooltipRenderer } = tooltip;
const color = fills[nodeDatum.index % fills.length];
const datum = nodeDatum.datum;
const label = labelKey ? `${datum[labelKey]}: ` : '';
const angleValue = datum[angleKey];
const formattedAngleValue = typeof angleValue === 'number' ? number_1.toFixed(angleValue) : angleValue.toString();
const title = this.title ? this.title.text : undefined;
const content = label + formattedAngleValue;
const defaults = {
title,
backgroundColor: color,
content
};
if (tooltipRenderer) {
return chart_1.toTooltipHtml(tooltipRenderer({
datum,
angleKey,
angleValue,
angleName,
radiusKey,
radiusValue: radiusKey ? datum[radiusKey] : undefined,
radiusName,
labelKey,
labelName,
title,
color,
}), defaults);
}
return chart_1.toTooltipHtml(defaults);
}
listSeriesItems(legendData) {
const { labelKey, data } = this;
if (data && data.length && labelKey) {
const { fills, strokes, id } = this;
const titleText = this.title && this.title.showInLegend && this.title.text;
data.forEach((datum, index) => {
let labelParts = [];
titleText && labelParts.push(titleText);
labelParts.push(String(datum[labelKey]));
legendData.push({
id,
itemId: index,
enabled: this.seriesItemEnabled[index],
label: {
text: labelParts.join(' - ')
},
marker: {
fill: fills[index % fills.length],
stroke: strokes[index % strokes.length],
fillOpacity: this.fillOpacity,
strokeOpacity: this.strokeOpacity
}
});
});
}
}
toggleSeriesItem(itemId, enabled) {
this.seriesItemEnabled[itemId] = enabled;
this.scheduleData();
}
}
PieSeries.className = 'PieSeries';
PieSeries.type = 'pie';
__decorate([
observable_1.reactive('dataChange')
], PieSeries.prototype, "angleKey", void 0);
__decorate([
observable_1.reactive('update')
], PieSeries.prototype, "angleName", void 0);
__decorate([
observable_1.reactive('dataChange')
], PieSeries.prototype, "radiusKey", void 0);
__decorate([
observable_1.reactive('update')
], PieSeries.prototype, "radiusName", void 0);
__decorate([
observable_1.reactive('dataChange')
], PieSeries.prototype, "radiusMin", void 0);
__decorate([
observable_1.reactive('dataChange')
], PieSeries.prototype, "radiusMax", void 0);
__decorate([
observable_1.reactive('dataChange')
], PieSeries.prototype, "labelKey", void 0);
__decorate([
observable_1.reactive('update')
], PieSeries.prototype, "labelName", void 0);
__decorate([
observable_1.reactive('layoutChange')
], PieSeries.prototype, "fillOpacity", void 0);
__decorate([
observable_1.reactive('layoutChange')
], PieSeries.prototype, "strokeOpacity", void 0);
__decorate([
observable_1.reactive('update')
], PieSeries.prototype, "lineDash", void 0);
__decorate([
observable_1.reactive('update')
], PieSeries.prototype, "lineDashOffset", void 0);
__decorate([
observable_1.reactive('update')
], PieSeries.prototype, "formatter", void 0);
__decorate([
observable_1.reactive('dataChange')
], PieSeries.prototype, "rotation", void 0);
__decorate([
observable_1.reactive('layoutChange')
], PieSeries.prototype, "outerRadiusOffset", void 0);
__decorate([
observable_1.reactive('dataChange')
], PieSeries.prototype, "innerRadiusOffset", void 0);
__decorate([
observable_1.reactive('layoutChange')
], PieSeries.prototype, "strokeWidth", void 0);
__decorate([
observable_1.reactive('layoutChange')
], PieSeries.prototype, "shadow", void 0);
exports.PieSeries = PieSeries;
//# sourceMappingURL=pieSeries.js.map