@carbon/charts
Version:
Carbon charting components
543 lines • 25.4 kB
JavaScript
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 __());
};
})();
// Internal Imports
import { Component } from '../component';
import { AxisPositions, Events, ScaleTypes, Roles, TruncationTypes, } from '../../interfaces';
import { Tools } from '../../tools';
import { DOMUtils } from '../../services';
import { AxisTitleOrientations, TickRotations } from '../../interfaces/enums';
import * as Configuration from '../../configuration';
import { computeTimeIntervalName, formatTick, isTickPrimary, } from '../../services/time-series';
// D3 Imports
import { select } from 'd3-selection';
import { axisBottom, axisLeft, axisRight, axisTop } from 'd3-axis';
var Axis = /** @class */ (function (_super) {
__extends(Axis, _super);
function Axis(model, services, configs) {
var _this = _super.call(this, model, services, configs) || this;
_this.type = 'axes';
if (configs) {
_this.configs = configs;
}
_this.margins = _this.configs.margins;
return _this;
}
Axis.prototype.render = function (animate) {
if (animate === void 0) { animate = true; }
var axisPosition = this.configs.position;
var options = this.getOptions();
var isAxisVisible = Tools.getProperty(options, 'axes', axisPosition, 'visible');
var svg = this.getContainerSVG();
var _a = DOMUtils.getSVGElementSize(this.parent, {
useAttrs: true,
}), width = _a.width, height = _a.height;
// Add axis into the parent
var container = DOMUtils.appendOrSelect(svg, "g.axis." + axisPosition);
var startPosition, endPosition;
if (axisPosition === AxisPositions.BOTTOM ||
axisPosition === AxisPositions.TOP) {
startPosition = this.configs.axes[AxisPositions.LEFT]
? this.margins.left
: 0;
endPosition = this.configs.axes[AxisPositions.RIGHT]
? width - this.margins.right
: width;
}
else {
startPosition = height - this.margins.bottom;
endPosition = this.margins.top;
}
// Grab the scale off of the Scales service
var scale = this.services.cartesianScales.getScaleByPosition(axisPosition);
if (this.scaleType === ScaleTypes.LABELS) {
scale.rangeRound([startPosition, endPosition]);
}
else {
scale.range([startPosition, endPosition]);
}
// Identify the corresponding d3 axis function
var axisFunction;
switch (axisPosition) {
case AxisPositions.LEFT:
axisFunction = axisLeft;
break;
case AxisPositions.BOTTOM:
axisFunction = axisBottom;
break;
case AxisPositions.RIGHT:
axisFunction = axisRight;
break;
case AxisPositions.TOP:
axisFunction = axisTop;
break;
}
container.attr('aria-label', axisPosition + " axis");
var axisRefExists = !container.select("g.ticks").empty();
var axisRef = DOMUtils.appendOrSelect(container, "g.ticks");
if (!axisRefExists) {
axisRef.attr('role', Roles.GRAPHICS_OBJECT + " " + Roles.GROUP);
axisRef.attr('aria-label', axisPosition + " ticks");
}
// We draw the invisible axis because of the async nature of d3 transitions
// To be able to tell the final width & height of the axis when initiaing the transition
// The invisible axis is updated instantly and without a transition
var invisibleAxisRef = DOMUtils.appendOrSelect(container, "g.ticks.invisible")
.style('opacity', '0')
.style('pointer-events', 'none')
.attr('aria-hidden', true)
.attr('aria-label', "invisible " + axisPosition + " ticks");
var axisOptions = Tools.getProperty(options, 'axes', axisPosition);
var isTimeScaleType = this.scaleType === ScaleTypes.TIME ||
axisOptions.scaleType === ScaleTypes.TIME;
var isVerticalAxis = axisPosition === AxisPositions.LEFT ||
axisPosition === AxisPositions.RIGHT;
// if zoomDomain is available, scale type is time, and axis position isBOTTOM or TOP
// update scale domain to zoomDomain.
var zoomDomain = this.model.get('zoomDomain');
if (zoomDomain && isTimeScaleType && !isVerticalAxis) {
scale.domain(zoomDomain);
}
if (!isAxisVisible) {
axisRef.attr('aria-hidden', true);
return;
}
var axisScaleType = Tools.getProperty(axisOptions, 'scaleType');
var isDataLoading = Tools.getProperty(options, 'data', 'loading');
var numberOfTicksProvided = Tools.getProperty(axisOptions, 'ticks', 'number');
// user can provide custom ticks to be displayed
// ticks need to be in the domain of the axis data
var userProvidedTickValues = Tools.getProperty(axisOptions, 'ticks', 'values');
// get user provided custom values for truncation
var truncationType = Tools.getProperty(axisOptions, 'truncation', 'type');
var truncationThreshold = Tools.getProperty(axisOptions, 'truncation', 'threshold');
var truncationNumCharacter = Tools.getProperty(axisOptions, 'truncation', 'numCharacter');
var isNumberOfTicksProvided = numberOfTicksProvided !== null;
var timeScaleOptions = Tools.getProperty(options, 'timeScale');
// Append to DOM a fake tick to get the right computed font height
var fakeTick = DOMUtils.appendOrSelect(invisibleAxisRef, "g.tick");
var fakeTickText = DOMUtils.appendOrSelect(fakeTick, "text").text('0');
var tickHeight = DOMUtils.getSVGElementSize(fakeTickText.node(), {
useBBox: true,
}).height;
fakeTick.remove();
var scaleType = this.scaleType || axisOptions.scaleType || ScaleTypes.LINEAR;
// Initialize axis object
var axis = axisFunction(scale).tickSizeOuter(0);
if (scale.ticks) {
var numberOfTicks = void 0;
if (isNumberOfTicksProvided) {
numberOfTicks = numberOfTicksProvided;
}
else {
numberOfTicks = Configuration.axis.ticks.number;
if (isVerticalAxis) {
// Set how many ticks based on height
numberOfTicks = this.getNumberOfFittingTicks(height, tickHeight, Configuration.axis.ticks.verticalSpaceRatio);
}
}
// scale continuous
// remove 0 ticks for skeleton
if (scale.ticks().length === 1 && scale.ticks()[0] === 0) {
numberOfTicks = 0;
}
axis.ticks(numberOfTicks);
if (isTimeScaleType) {
if (!scale.ticks(numberOfTicks).length) {
axis.tickValues([]);
}
else {
var addSpaceOnEdges = Tools.getProperty(options, 'timeScale', 'addSpaceOnEdges');
var customDomain = Tools.getProperty(options, 'axes', axisPosition, 'domain');
var tickValues = void 0;
// scale.nice() will change scale domain which causes extra space near chart edge
// so use another scale instance to avoid impacts to original scale
var tempScale = scale.copy();
if (addSpaceOnEdges && !customDomain) {
tempScale.nice(numberOfTicks);
}
tickValues = tempScale.ticks(numberOfTicks);
// Remove labels on the edges
// If there are more than 2 labels to show
if (addSpaceOnEdges &&
tickValues.length > 2 &&
!customDomain) {
tickValues.splice(tickValues.length - 1, 1);
tickValues.splice(0, 1);
}
axis.tickValues(tickValues);
}
}
}
// create the right ticks formatter
var formatter;
var userProvidedFormatter = Tools.getProperty(axisOptions, 'ticks', 'formatter');
if (isTimeScaleType) {
var timeInterval_1 = computeTimeIntervalName(axis.tickValues());
if (userProvidedFormatter === null) {
formatter = function (t, i) {
return formatTick(t, i, axis.tickValues(), timeInterval_1, timeScaleOptions);
};
}
else {
formatter = function (t, i) {
var defaultFormattedValue = formatTick(t, i, axis.tickValues(), timeInterval_1, timeScaleOptions);
return userProvidedFormatter(t, i, defaultFormattedValue);
};
}
}
else {
if (userProvidedFormatter === null) {
if (scaleType === ScaleTypes.LINEAR) {
formatter = function (t) { return t.toLocaleString(); };
}
}
else {
formatter = userProvidedFormatter;
}
}
// Set ticks formatter
axis.tickFormat(formatter);
// prioritize using a custom array of values rather than number of ticks
// if both are provided. custom tick values need to be within the domain of the scale
var _b = this.services.cartesianScales
.getScaleByPosition(axisPosition)
.domain(), lowerBound = _b[0], upperBound = _b[1];
var validTicks;
if (userProvidedTickValues) {
if (isTimeScaleType) {
// sanitize user-provided tick values
userProvidedTickValues.forEach(function (userProvidedTickValue, i) {
if (userProvidedTickValue.getTime === undefined) {
userProvidedTickValues[i] = new Date(userProvidedTickValue);
}
});
// check the supplied ticks are within the time domain
validTicks = userProvidedTickValues.filter(function (tick) {
var tickTimestamp = tick.getTime();
return (tickTimestamp >= new Date(lowerBound).getTime() &&
tickTimestamp <= new Date(upperBound).getTime());
});
}
else if (axisScaleType === ScaleTypes.LABELS) {
var discreteDomain_1 = this.services.cartesianScales
.getScaleByPosition(axisPosition)
.domain();
validTicks = userProvidedTickValues.filter(function (tick) {
return discreteDomain_1.includes(tick);
});
}
else {
// continuous scales
validTicks = userProvidedTickValues.filter(function (tick) { return tick >= lowerBound && tick <= upperBound; });
}
axis.tickValues(validTicks);
}
// Position and transition the axis
switch (axisPosition) {
case AxisPositions.LEFT:
axisRef.attr('transform', "translate(" + this.margins.left + ", 0)");
break;
case AxisPositions.BOTTOM:
axisRef.attr('transform', "translate(0, " + (height - this.margins.bottom) + ")");
break;
case AxisPositions.RIGHT:
axisRef.attr('transform', "translate(" + (width - this.margins.right) + ", 0)");
break;
case AxisPositions.TOP:
axisRef.attr('transform', "translate(0, " + this.margins.top + ")");
break;
}
// Position the axis title
// check that data exists, if they don't, doesn't show the title axis
var isDataEmpty = this.model.isDataEmpty();
if (axisOptions.title) {
var axisTitleRef = DOMUtils.appendOrSelect(container, "text.axis-title").html(isDataEmpty || isDataLoading ? '' : axisOptions.title);
// vertical axes can have override for title orientation
var titleOrientation = Tools.getProperty(axisOptions, 'titleOrientation');
switch (axisPosition) {
case AxisPositions.LEFT:
if (titleOrientation === AxisTitleOrientations.RIGHT) {
axisTitleRef
.attr('transform', 'rotate(90)')
.attr('y', 0)
.attr('x', scale.range()[0] / 2)
.attr('dy', '-0.5em')
.style('text-anchor', 'middle');
}
else {
axisTitleRef
.attr('transform', 'rotate(-90)')
.attr('y', 0)
.attr('x', -(scale.range()[0] / 2))
.attr('dy', '1em')
.style('text-anchor', 'middle');
}
break;
case AxisPositions.BOTTOM:
axisTitleRef
.attr('transform', "translate(" + (this.margins.left / 2 + scale.range()[1] / 2) + ", " + height + ")")
.style('text-anchor', 'middle');
break;
case AxisPositions.RIGHT:
if (titleOrientation === AxisTitleOrientations.LEFT) {
axisTitleRef
.attr('transform', 'rotate(-90)')
.attr('y', width)
.attr('x', -(scale.range()[0] / 2))
.style('text-anchor', 'middle');
}
else {
axisTitleRef
.attr('transform', 'rotate(90)')
.attr('y', -width)
.attr('x', scale.range()[0] / 2)
.attr('dy', '1em')
.style('text-anchor', 'middle');
}
break;
case AxisPositions.TOP:
var titleHeight = DOMUtils.getSVGElementSize(axisTitleRef, {
useBBox: true,
}).height;
axisTitleRef
.attr('transform', "translate(" + (this.margins.left / 2 + scale.range()[1] / 2) + ", " + titleHeight / 2 + ")")
.style('text-anchor', 'middle');
break;
}
}
// Apply new axis to the axis element
if (isTimeScaleType) {
var timeInterval_2 = computeTimeIntervalName(axis.tickValues());
var showDayName_1 = timeScaleOptions.showDayName;
var axisRefSelection = axisRef;
if (animate) {
axisRef = axisRef.transition(this.services.transitions.getTransition('axis-update', animate));
}
axisRef = axisRef.call(axis);
// Manipulate tick labels to make bold those that are in long format
var ticks = axisRefSelection
.selectAll('.tick')
.data(axis.tickValues(), scale)
.order()
.select('text');
ticks.style('font-weight', function (tick, i) {
return isTickPrimary(tick, i, axis.tickValues(), timeInterval_2, showDayName_1)
? 'bold'
: 'normal';
});
}
else {
if (!animate || !axisRefExists) {
axisRef = axisRef.call(axis);
}
else {
axisRef = axisRef
.transition(this.services.transitions.getTransition('axis-update'))
.call(axis);
}
}
invisibleAxisRef.call(axis);
if (axisPosition === AxisPositions.BOTTOM ||
axisPosition === AxisPositions.TOP) {
var shouldRotateTicks = false;
// user could decide if tick rotation is required during zoom domain changing
var tickRotation = Tools.getProperty(axisOptions, 'ticks', 'rotation');
if (tickRotation === TickRotations.ALWAYS) {
shouldRotateTicks = true;
}
else if (tickRotation === TickRotations.NEVER) {
shouldRotateTicks = false;
}
else if (!tickRotation || tickRotation === TickRotations.AUTO) {
// if the option is not set or set to AUTO
// depending on if tick rotation is necessary by calculating space
// If we're dealing with a discrete scale type
// We're able to grab the spacing between the ticks
if (scale.step) {
var textNodes = invisibleAxisRef
.selectAll('g.tick text')
.nodes();
// If any ticks are any larger than the scale step size
shouldRotateTicks = textNodes.some(function (textNode) {
return DOMUtils.getSVGElementSize(textNode, {
useBBox: true,
}).width >= scale.step();
});
}
else {
// When dealing with a continuous scale
// We need to calculate an estimated size of the ticks
var minTickSize = Tools.getProperty(axisOptions, 'ticks', 'rotateIfSmallerThan') || Configuration.axis.ticks.rotateIfSmallerThan;
var ticksNumber = isTimeScaleType
? axis.tickValues().length
: scale.ticks().length;
var estimatedTickSize = width / ticksNumber / 2;
shouldRotateTicks = isTimeScaleType
? estimatedTickSize < minTickSize * 2 // datetime tick could be very long
: estimatedTickSize < minTickSize;
}
}
if (shouldRotateTicks) {
if (!isNumberOfTicksProvided) {
axis.ticks(this.getNumberOfFittingTicks(width, tickHeight, Configuration.axis.ticks.horizontalSpaceRatio));
invisibleAxisRef.call(axis);
axisRef.call(axis);
}
container
.selectAll('g.ticks g.tick text')
.attr('transform', "rotate(-45)")
.style('text-anchor', axisPosition === AxisPositions.TOP ? 'start' : 'end');
}
else {
container
.selectAll('g.ticks g.tick text')
.attr('transform', null)
.style('text-anchor', null);
}
}
// we don't need to show axes on empty state and on skeleton state
// because the Skeleton component draws them
if (isDataLoading) {
container.attr('opacity', 0);
}
else {
container.attr('opacity', 1);
}
axisRef.selectAll('g.tick').attr('aria-label', function (d) { return d; });
invisibleAxisRef.selectAll('g.tick').attr('aria-label', function (d) { return d; });
// truncate the label if it's too long
// only applies to discrete type
if (truncationType !== TruncationTypes.NONE &&
axisScaleType === ScaleTypes.LABELS &&
!userProvidedTickValues) {
var axisTickLabels = this.services.cartesianScales.getScaleDomain(axisPosition);
if (axisTickLabels.length > 0) {
var tick_html = svg
.select("g.axis." + axisPosition + " g.ticks g.tick")
.html();
container.selectAll('g.ticks g.tick').html(tick_html);
container
.selectAll('g.tick text')
.data(axisTickLabels)
.text(function (d) {
if (d.length > truncationThreshold) {
return Tools.truncateLabel(d, truncationType, truncationNumCharacter);
}
else {
return d;
}
});
this.getInvisibleAxisRef()
.selectAll('g.tick text')
.data(axisTickLabels)
.text(function (d) {
if (d.length > truncationThreshold) {
return Tools.truncateLabel(d, truncationType, truncationNumCharacter);
}
else {
return d;
}
});
container
.selectAll('g.ticks')
.html(this.getInvisibleAxisRef().html());
container.selectAll('g.tick text').data(axisTickLabels);
}
}
// Add event listeners to elements drawn
this.addEventListeners();
};
Axis.prototype.addEventListeners = function () {
var svg = this.getContainerSVG();
var axisPosition = this.configs.position;
var container = DOMUtils.appendOrSelect(svg, "g.axis." + axisPosition);
var options = this.getOptions();
var axisOptions = Tools.getProperty(options, 'axes', axisPosition);
var axisScaleType = Tools.getProperty(axisOptions, 'scaleType');
var truncationThreshold = Tools.getProperty(axisOptions, 'truncation', 'threshold');
var isTimeScaleType = this.scaleType === ScaleTypes.TIME ||
axisOptions.scaleType === ScaleTypes.TIME;
var self = this;
container
.selectAll('g.tick text')
.on('mouseover', function (datum) {
// Dispatch mouse event
self.services.events.dispatchEvent(Events.Axis.LABEL_MOUSEOVER, {
element: select(this),
datum: datum,
});
if (axisScaleType === ScaleTypes.LABELS &&
datum.length > truncationThreshold) {
self.services.events.dispatchEvent(Events.Tooltip.SHOW, {
hoveredElement: select(this),
content: datum,
});
}
})
.on('mousemove', function (datum) {
// Dispatch mouse event
self.services.events.dispatchEvent(Events.Axis.LABEL_MOUSEMOVE, {
element: select(this),
datum: datum,
});
if (axisScaleType === ScaleTypes.LABELS &&
datum.length > truncationThreshold) {
self.services.events.dispatchEvent(Events.Tooltip.MOVE);
}
})
.on('click', function (datum) {
// Dispatch mouse event
self.services.events.dispatchEvent(Events.Axis.LABEL_CLICK, {
element: select(this),
datum: datum,
});
})
.on('mouseout', function (datum) {
// Dispatch mouse event
self.services.events.dispatchEvent(Events.Axis.LABEL_MOUSEOUT, {
element: select(this),
datum: datum,
});
if (axisScaleType === ScaleTypes.LABELS) {
self.services.events.dispatchEvent(Events.Tooltip.HIDE);
}
});
};
Axis.prototype.getInvisibleAxisRef = function () {
var axisPosition = this.configs.position;
return this.getContainerSVG().select("g.axis." + axisPosition + " g.ticks.invisible");
};
Axis.prototype.getTitleRef = function () {
var axisPosition = this.configs.position;
return this.getContainerSVG().select("g.axis." + axisPosition + " text.axis-title");
};
Axis.prototype.getNumberOfFittingTicks = function (size, tickSize, spaceRatio) {
var numberOfTicksFit = Math.floor(size / (tickSize * spaceRatio));
return Tools.clamp(numberOfTicksFit, 2, Configuration.axis.ticks.number);
};
Axis.prototype.destroy = function () {
var svg = this.getContainerSVG();
var axisPosition = this.configs.position;
var container = DOMUtils.appendOrSelect(svg, "g.axis." + axisPosition);
// Remove event listeners
container
.selectAll('g.tick text')
.on('mouseover', null)
.on('mousemove', null)
.on('mouseout', null);
};
return Axis;
}(Component));
export { Axis };
//# sourceMappingURL=../../../src/components/axes/axis.js.map