ag-charts-community
Version:
Advanced Charting / Charts supporting Javascript / Typescript / React / Angular / Vue
431 lines • 20.3 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
var selection_1 = require("../../scene/selection");
var line_1 = require("../../scene/shape/line");
var angle_1 = require("../../util/angle");
var text_1 = require("../../scene/shape/text");
var bbox_1 = require("../../scene/bbox");
var matrix_1 = require("../../scene/matrix");
// import { Rect } from "../../scene/shape/rect"; debug (bbox)
var bandScale_1 = require("../../scale/bandScale");
var tree_1 = require("../../layout/tree");
var axis_1 = require("../../axis");
var chartAxis_1 = require("../chartAxis");
var GroupedCategoryAxisLabel = /** @class */ (function (_super) {
__extends(GroupedCategoryAxisLabel, _super);
function GroupedCategoryAxisLabel() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.grid = false;
return _this;
}
return GroupedCategoryAxisLabel;
}(axis_1.AxisLabel));
var GroupedCategoryAxis = /** @class */ (function (_super) {
__extends(GroupedCategoryAxis, _super);
function GroupedCategoryAxis() {
var _this = _super.call(this, new bandScale_1.BandScale()) || this;
// Label scale (labels are positionsed between ticks, tick count = label count + 1).
// We don't call is `labelScale` for consistency with other axes.
_this.tickScale = new bandScale_1.BandScale();
_this.longestSeparatorLength = 0;
_this.translation = {
x: 0,
y: 0
};
/**
* Axis rotation angle in degrees.
*/
_this.rotation = 0;
_this.line = {
width: 1,
color: 'rgba(195, 195, 195, 1)'
};
// readonly tick = new AxisTick();
_this.label = new GroupedCategoryAxisLabel();
/**
* The color of the labels.
* Use `undefined` rather than `rgba(0, 0, 0, 0)` to make labels invisible.
*/
_this.labelColor = 'rgba(87, 87, 87, 1)';
var _a = _this, group = _a.group, tickScale = _a.tickScale, scale = _a.scale;
scale.paddingOuter = 0.1;
scale.paddingInner = scale.paddingOuter * 2;
_this.requestedRange = scale.range.slice();
_this.scale = scale;
tickScale.paddingInner = 1;
tickScale.paddingOuter = 0;
_this.gridLineSelection = selection_1.Selection.select(group).selectAll();
_this.axisLineSelection = selection_1.Selection.select(group).selectAll();
_this.separatorSelection = selection_1.Selection.select(group).selectAll();
_this.labelSelection = selection_1.Selection.select(group).selectAll();
return _this;
// this.group.append(this.bboxRect); // debug (bbox)
}
Object.defineProperty(GroupedCategoryAxis.prototype, "domain", {
get: function () {
return this.scale.domain;
},
set: function (domainValues) {
// Prevent duplicate categories.
var values = domainValues.filter(function (s, i, arr) { return arr.indexOf(s) === i; });
this.scale.domain = values;
var tickTree = tree_1.ticksToTree(values);
this.tickTreeLayout = tree_1.treeLayout(tickTree);
var domain = values.slice();
domain.push('');
this.tickScale.domain = domain;
this.resizeTickTree();
},
enumerable: true,
configurable: true
});
Object.defineProperty(GroupedCategoryAxis.prototype, "range", {
get: function () {
return this.requestedRange.slice();
},
set: function (value) {
this.requestedRange = value.slice();
this.updateRange();
},
enumerable: true,
configurable: true
});
GroupedCategoryAxis.prototype.updateRange = function () {
var _a = this, rr = _a.requestedRange, vr = _a.visibleRange, scale = _a.scale;
var span = (rr[1] - rr[0]) / (vr[1] - vr[0]);
var shift = span * vr[0];
var start = rr[0] - shift;
this.tickScale.range = scale.range = [start, start + span];
this.resizeTickTree();
};
GroupedCategoryAxis.prototype.resizeTickTree = function () {
var s = this.scale;
var range = s.domain.length ? [s.convert(s.domain[0]), s.convert(s.domain[s.domain.length - 1])] : s.range;
var layout = this.tickTreeLayout;
var lineHeight = this.lineHeight;
if (layout) {
layout.resize(Math.abs(range[1] - range[0]), layout.depth * lineHeight, (Math.min(range[0], range[1]) || 0) + (s.bandwidth || 0) / 2, -layout.depth * lineHeight, (range[1] - range[0]) < 0);
}
};
Object.defineProperty(GroupedCategoryAxis.prototype, "lineHeight", {
get: function () {
return this.label.fontSize * 1.5;
},
enumerable: true,
configurable: true
});
Object.defineProperty(GroupedCategoryAxis.prototype, "gridLength", {
get: function () {
return this._gridLength;
},
/**
* The length of the grid. The grid is only visible in case of a non-zero value.
*/
set: function (value) {
// Was visible and now invisible, or was invisible and now visible.
if (this._gridLength && !value || !this._gridLength && value) {
this.gridLineSelection = this.gridLineSelection.remove().setData([]);
this.labelSelection = this.labelSelection.remove().setData([]);
}
this._gridLength = value;
},
enumerable: true,
configurable: true
});
/**
* Creates/removes/updates the scene graph nodes that constitute the axis.
* Supposed to be called _manually_ after changing _any_ of the axis properties.
* This allows to bulk set axis properties before updating the nodes.
* The node changes made by this method are rendered on the next animation frame.
* We could schedule this method call automatically on the next animation frame
* when any of the axis properties change (the way we do when properties of scene graph's
* nodes change), but this will mean that we first wait for the next animation
* frame to make changes to the nodes of the axis, then wait for another animation
* frame to render those changes. It's nice to have everything update automatically,
* but this extra level of async indirection will not just introduce an unwanted delay,
* it will also make it harder to reason about the program.
*/
GroupedCategoryAxis.prototype.update = function () {
var _this = this;
var _a = this, group = _a.group, scale = _a.scale, label = _a.label, tickScale = _a.tickScale, requestedRange = _a.requestedRange;
var rangeStart = scale.range[0];
var rangeEnd = scale.range[1];
var rangeLength = Math.abs(rangeEnd - rangeStart);
var bandwidth = (rangeLength / scale.domain.length) || 0;
var parallelLabels = label.parallel;
var rotation = angle_1.toRadians(this.rotation);
var isHorizontal = Math.abs(Math.cos(rotation)) < 1e-8;
var labelRotation = angle_1.normalizeAngle360(angle_1.toRadians(this.label.rotation));
group.translationX = this.translation.x;
group.translationY = this.translation.y;
group.rotation = rotation;
var title = this.title;
// The Text `node` of the Caption is not used to render the title of the grouped category axis.
// The phantom root of the tree layout is used instead.
if (title) {
title.node.visible = false;
}
var lineHeight = this.lineHeight;
// Render ticks and labels.
var tickTreeLayout = this.tickTreeLayout;
var labels = scale.ticks();
var treeLabels = tickTreeLayout ? tickTreeLayout.nodes : [];
var isLabelTree = tickTreeLayout ? tickTreeLayout.depth > 1 : false;
var ticks = tickScale.ticks();
// The side of the axis line to position the labels on.
// -1 = left (default)
// 1 = right
var sideFlag = label.mirrored ? 1 : -1;
// When labels are parallel to the axis line, the `parallelFlipFlag` is used to
// flip the labels to avoid upside-down text, when the axis is rotated
// such that it is in the right hemisphere, i.e. the angle of rotation
// is in the [0, π] interval.
// The rotation angle is normalized, so that we have an easier time checking
// if it's in the said interval. Since the axis is always rendered vertically
// and then rotated, zero rotation means 12 (not 3) o-clock.
// -1 = flip
// 1 = don't flip (default)
var parallelFlipRotation = angle_1.normalizeAngle360(rotation);
var parallelFlipFlag = (!labelRotation && parallelFlipRotation >= 0 && parallelFlipRotation <= Math.PI) ? -1 : 1;
var regularFlipRotation = angle_1.normalizeAngle360(rotation - Math.PI / 2);
// Flip if the axis rotation angle is in the top hemisphere.
var regularFlipFlag = (!labelRotation && regularFlipRotation >= 0 && regularFlipRotation <= Math.PI) ? -1 : 1;
var updateGridLines = this.gridLineSelection.setData(this.gridLength ? ticks : []);
updateGridLines.exit.remove();
var enterGridLines = updateGridLines.enter.append(line_1.Line);
var gridLineSelection = updateGridLines.merge(enterGridLines);
var updateLabels = this.labelSelection.setData(treeLabels);
updateLabels.exit.remove();
var enterLabels = updateLabels.enter.append(text_1.Text);
var labelSelection = updateLabels.merge(enterLabels);
var labelFormatter = label.formatter;
var maxLeafLabelWidth = 0;
labelSelection
.each(function (node, datum, index) {
node.fontStyle = label.fontStyle;
node.fontWeight = label.fontWeight;
node.fontSize = label.fontSize;
node.fontFamily = label.fontFamily;
node.fill = label.color;
node.textBaseline = parallelFlipFlag === -1 ? 'bottom' : 'hanging';
// label.textBaseline = parallelLabels && !labelRotation
// ? (sideFlag * parallelFlipFlag === -1 ? 'hanging' : 'bottom')
// : 'middle';
node.textAlign = 'center';
node.translationX = datum.screenY - label.fontSize * 0.25;
node.translationY = datum.screenX;
if (index === 0) { // use the phantom root as the axis title
if (title && title.enabled && labels.length > 0) {
node.visible = true;
node.text = title.text;
node.fontSize = title.fontSize;
node.fontStyle = title.fontStyle;
node.fontWeight = title.fontWeight;
node.fontFamily = title.fontFamily;
node.textBaseline = 'hanging';
}
else {
node.visible = false;
}
}
else {
node.text = labelFormatter
? labelFormatter({
value: String(datum.label),
index: index
})
: String(datum.label);
node.visible =
datum.screenX >= requestedRange[0] &&
datum.screenX <= requestedRange[1];
}
var bbox = node.computeBBox();
if (bbox && bbox.width > maxLeafLabelWidth) {
maxLeafLabelWidth = bbox.width;
}
});
var labelX = sideFlag * label.padding;
var autoRotation = parallelLabels
? parallelFlipFlag * Math.PI / 2
: (regularFlipFlag === -1 ? Math.PI : 0);
var labelGrid = this.label.grid;
var separatorData = [];
labelSelection.each(function (label, datum, index) {
label.x = labelX;
label.rotationCenterX = labelX;
if (!datum.children.length) {
label.rotation = labelRotation;
label.textAlign = 'end';
label.textBaseline = 'middle';
}
else {
label.translationX -= maxLeafLabelWidth - lineHeight + _this.label.padding;
if (isHorizontal) {
label.rotation = autoRotation;
}
else {
label.rotation = -Math.PI / 2;
}
}
// Calculate positions of label separators for all nodes except the root.
// Each separator is placed to the top of the current label.
if (datum.parent && isLabelTree) {
var y = !datum.children.length
? datum.screenX - bandwidth / 2
: datum.screenX - datum.leafCount * bandwidth / 2;
if (!datum.children.length) {
if ((datum.number !== datum.children.length - 1) || labelGrid) {
separatorData.push({
y: y,
x1: 0,
x2: -maxLeafLabelWidth - _this.label.padding * 2,
toString: function () { return String(index); }
});
}
}
else {
var x = -maxLeafLabelWidth - _this.label.padding * 2 + datum.screenY;
separatorData.push({
y: y,
x1: x + lineHeight,
x2: x,
toString: function () { return String(index); }
});
}
}
});
// Calculate the position of the long separator on the far bottom of the axis.
var minX = 0;
separatorData.forEach(function (d) { return minX = Math.min(minX, d.x2); });
this.longestSeparatorLength = Math.abs(minX);
separatorData.push({
y: Math.max(rangeStart, rangeEnd),
x1: 0,
x2: minX,
toString: function () { return String(separatorData.length); }
});
var updateSeparators = this.separatorSelection.setData(separatorData);
updateSeparators.exit.remove();
var enterSeparators = updateSeparators.enter.append(line_1.Line);
var separatorSelection = updateSeparators.merge(enterSeparators);
this.separatorSelection = separatorSelection;
var epsilon = 0.0000001;
separatorSelection.each(function (line, datum, i) {
line.x1 = datum.x1;
line.x2 = datum.x2;
line.y1 = datum.y;
line.y2 = datum.y;
line.visible = datum.y >= requestedRange[0] - epsilon && datum.y <= requestedRange[1] + epsilon;
line.stroke = _this.tick.color;
line.fill = undefined;
line.strokeWidth = 1;
});
this.gridLineSelection = gridLineSelection;
this.labelSelection = labelSelection;
// Render axis lines.
var lineCount = tickTreeLayout ? tickTreeLayout.depth + 1 : 1;
var lines = [];
for (var i = 0; i < lineCount; i++) {
lines.push(i);
}
var updateAxisLines = this.axisLineSelection.setData(lines);
updateAxisLines.exit.remove();
var enterAxisLines = updateAxisLines.enter.append(line_1.Line);
var axisLineSelection = updateAxisLines.merge(enterAxisLines);
this.axisLineSelection = axisLineSelection;
axisLineSelection.each(function (line, _, index) {
var x = index > 0 ? -maxLeafLabelWidth - _this.label.padding * 2 - (index - 1) * lineHeight : 0;
line.x1 = x;
line.x2 = x;
line.y1 = requestedRange[0];
line.y2 = requestedRange[1];
line.strokeWidth = _this.line.width;
line.stroke = _this.line.color;
line.visible = labels.length > 0 && (index === 0 || (labelGrid && isLabelTree));
});
if (this.gridLength) {
var styles_1 = this.gridStyle;
var styleCount_1 = styles_1.length;
gridLineSelection
.each(function (line, datum, index) {
var y = Math.round(tickScale.convert(datum));
line.x1 = 0;
line.x2 = -sideFlag * _this.gridLength;
line.y1 = y;
line.y2 = y;
line.visible = y >= requestedRange[0] && y <= requestedRange[1] &&
Math.abs(line.parent.translationY - rangeStart) > 1;
var style = styles_1[index % styleCount_1];
line.stroke = style.stroke;
line.strokeWidth = _this.tick.width;
line.lineDash = style.lineDash;
line.fill = undefined;
});
}
// debug (bbox)
// const bbox = this.computeBBox();
// const bboxRect = this.bboxRect;
// bboxRect.x = bbox.x;
// bboxRect.y = bbox.y;
// bboxRect.width = bbox.width;
// bboxRect.height = bbox.height;
};
GroupedCategoryAxis.prototype.computeBBox = function (options) {
var includeTitle = !options || !options.excludeTitle;
var left = Infinity;
var right = -Infinity;
var top = Infinity;
var bottom = -Infinity;
this.labelSelection.each(function (label, _, index) {
// The label itself is rotated, but not translated, the group that
// contains it is. So to capture the group transform in the label bbox
// calculation we combine the transform matrices of the label and the group.
// Depending on the timing of the `axis.computeBBox()` method call, we may
// not have the group's and the label's transform matrices updated yet (because
// the transform matrix is not recalculated whenever a node's transform attributes
// change, instead it's marked for recalculation on the next frame by setting
// the node's `dirtyTransform` flag to `true`), so we force them to update
// right here by calling `computeTransformMatrix`.
if (index > 0 || includeTitle) { // first node is the root (title)
label.computeTransformMatrix();
var matrix = matrix_1.Matrix.flyweight(label.matrix);
var labelBBox = label.computeBBox();
if (labelBBox) {
var bbox = matrix.transformBBox(labelBBox);
left = Math.min(left, bbox.x);
right = Math.max(right, bbox.x + bbox.width);
top = Math.min(top, bbox.y);
bottom = Math.max(bottom, bbox.y + bbox.height);
}
}
});
return new bbox_1.BBox(left, top, Math.max(right - left, this.longestSeparatorLength), bottom - top);
};
// debug (bbox)
// private bboxRect = (() => {
// const rect = new Rect();
// rect.fill = undefined;
// rect.stroke = 'red';
// rect.strokeWidth = 1;
// rect.strokeOpacity = 0.2;
// return rect;
// })();
GroupedCategoryAxis.className = 'GroupedCategoryAxis';
GroupedCategoryAxis.type = 'groupedCategory';
return GroupedCategoryAxis;
}(chartAxis_1.ChartAxis));
exports.GroupedCategoryAxis = GroupedCategoryAxis;
//# sourceMappingURL=groupedCategoryAxis.js.map