@carbon/charts
Version:
Carbon charting components
452 lines • 22.1 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 { Tools } from '../../tools';
import { AxisPositions, Events, ScaleTypes, ZoomBarTypes, } from '../../interfaces';
import { DOMUtils } from '../../services';
import * as Configuration from '../../configuration';
// D3 Imports
import { extent } from 'd3-array';
import { brushX } from 'd3-brush';
import { area, line } from 'd3-shape';
import { event } from 'd3-selection';
var ZoomBar = /** @class */ (function (_super) {
__extends(ZoomBar, _super);
function ZoomBar() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.type = 'zoom-bar';
// The minimum selection x range to trigger handler update
// Smaller number may introduce a handler flash during initialization
// Bigger number may not trigger handler update while selection area on chart is very small
_this.MIN_SELECTION_DIFF = 9e-10;
// needs to match the style in _zoom-bar.scss
_this.brushSelector = 'g.zoom-bar-brush';
// Give every zoomBarClip a distinct ID
// so they don't interfere the other zoom bars in a page
_this.clipId = 'zoomBarClip-' + Math.floor(Math.random() * 99999999999);
_this.brush = brushX();
return _this;
}
ZoomBar.prototype.init = function () {
this.services.events.addEventListener(Events.ZoomBar.UPDATE, this.render.bind(this));
// check if pre-defined zoom bar data exists
var definedZoomBarData = Tools.getProperty(this.getOptions(), 'zoomBar', AxisPositions.TOP, 'data');
// load up the zoomBarData into this model
this.model.setZoomBarData(definedZoomBarData);
};
ZoomBar.prototype.render = function (animate) {
var _this = this;
if (animate === void 0) { animate = true; }
var svg = this.getContainerSVG();
var isTopZoomBarLoading = this.services.zoom.isZoomBarLoading(AxisPositions.TOP);
var isTopZoomBarLocked = this.services.zoom.isZoomBarLocked(AxisPositions.TOP);
var zoombarType = Tools.getProperty(this.getOptions(), 'zoomBar', AxisPositions.TOP, 'type');
var zoombarHeight = Configuration.zoomBar.height[zoombarType];
var width = DOMUtils.getSVGElementSize(this.parent, {
useAttrs: true,
}).width;
// initialization is not completed yet
if (width === 0) {
return;
}
// get axes margins
var axesLeftMargin = 0;
var axesMargins = this.model.get('axesMargins');
if (axesMargins && axesMargins.left) {
axesLeftMargin = axesMargins.left;
}
var container = DOMUtils.appendOrSelect(svg, 'svg.zoom-container')
.attr('width', '100%')
.attr('height', zoombarHeight)
.attr('opacity', 1);
var spacer = DOMUtils.appendOrSelect(svg, 'rect.zoom-spacer')
.attr('x', 0)
.attr('y', zoombarHeight)
.attr('width', '100%')
.attr('height', Configuration.zoomBar.spacerHeight)
.attr('opacity', 1)
.attr('fill', 'none');
if (zoombarType === ZoomBarTypes.GRAPH_VIEW) {
// Draw zoombar background rectangle
DOMUtils.appendOrSelect(container, 'rect.zoom-bg')
.attr('x', axesLeftMargin)
.attr('y', 0)
.attr('width', width - axesLeftMargin)
.attr('height', '100%')
.classed('zoom-bg-skeleton', isTopZoomBarLoading);
}
else if (zoombarType === ZoomBarTypes.SLIDER_VIEW) {
// Draw zoombar background line
DOMUtils.appendOrSelect(container, 'rect.zoom-slider-bg')
.attr('x', axesLeftMargin)
.attr('y', zoombarHeight / 2 - 1)
.attr('width', width - axesLeftMargin)
.attr('height', 2)
.classed('zoom-slider-bg-skeleton', isTopZoomBarLoading);
}
if (isTopZoomBarLoading) {
this.renderSkeleton(container, axesLeftMargin, width);
return;
}
var cartesianScales = this.services.cartesianScales;
var mainXScale = cartesianScales.getMainXScale();
var mainYScale = cartesianScales.getMainYScale();
var mainXScaleType = cartesianScales.getMainXScaleType();
if (mainXScale && mainXScaleType === ScaleTypes.TIME) {
var zoomBarData = this.services.zoom.getZoomBarData();
if (Tools.isEmpty(zoomBarData)) {
// if there's no zoom bar data we can't do anything
return;
}
this.xScale = mainXScale.copy();
this.yScale = mainYScale.copy();
var defaultDomain = this.services.zoom.getDefaultZoomBarDomain(zoomBarData);
// add value 0 to the extended domain for zoom bar area graph
zoomBarData = this.compensateDataForDefaultDomain(zoomBarData, defaultDomain);
// get old initialZoomDomain from model
var oldInitialZoomDomain = this.model.get('initialZoomDomain');
// get new initialZoomDomain from option
var newInitialZoomDomain = Tools.getProperty(this.getOptions(), 'zoomBar', AxisPositions.TOP, 'initialZoomDomain');
// change string date to Date object if necessary
if (newInitialZoomDomain &&
newInitialZoomDomain[0] &&
newInitialZoomDomain[1]) {
newInitialZoomDomain[0] = new Date(newInitialZoomDomain[0]);
newInitialZoomDomain[1] = new Date(newInitialZoomDomain[1]);
}
// update initialZoomDomain and set zoomDomain in model only if the option is changed
// not the same object, and both start date and end date are not equal
if (newInitialZoomDomain &&
!(oldInitialZoomDomain &&
oldInitialZoomDomain[0].valueOf() ===
newInitialZoomDomain[0].valueOf() &&
oldInitialZoomDomain[1].valueOf() ===
newInitialZoomDomain[1].valueOf())) {
this.model.set({
// use a new object instead of newInitialZoomDomain
initialZoomDomain: Tools.merge([], newInitialZoomDomain),
zoomDomain: newInitialZoomDomain
? Tools.merge([], newInitialZoomDomain)
: defaultDomain,
}, { skipUpdate: true });
}
else if (newInitialZoomDomain === null && oldInitialZoomDomain != null) {
// if newInitialZoomDomain is set to null (when oldInitialZoomDomain is not null)
// save initialZoomDomain and reset zoom domain to default domain
this.model.set({
initialZoomDomain: null,
zoomDomain: Tools.merge([], defaultDomain)
}, { skipUpdate: true });
}
this.xScale.range([axesLeftMargin, width]).domain(defaultDomain);
// keep max selection range
this.maxSelectionRange = this.xScale.range();
this.yScale
.range([0, zoombarHeight - 6])
.domain(extent(zoomBarData, function (d) { return d.value; }));
var zoomDomain = this.model.get('zoomDomain');
if (zoombarType === ZoomBarTypes.GRAPH_VIEW) {
this.renderZoomBarArea(container, 'path.zoom-graph-area-unselected', zoomBarData, null);
this.updateClipPath(svg, this.clipId, 0, 0, 0, 0);
this.renderZoomBarArea(container, 'path.zoom-graph-area', zoomBarData, this.clipId);
// Draw the zoom bar base line
this.renderZoomBarBaseline(container, axesLeftMargin, width);
}
// Attach brushing event listeners
this.addBrushEventListener(zoomDomain, axesLeftMargin, width);
// Draw the brushing area
var brushArea = DOMUtils.appendOrSelect(svg, this.brushSelector).call(this.brush);
if (zoomDomain === undefined) {
// do nothing, initialization not completed yet
// don't update brushHandle to avoid flash
}
else if (zoomDomain[0].valueOf() === zoomDomain[1].valueOf()) {
brushArea.call(this.brush.move, this.xScale.range()); // default to full range
this.updateBrushHandle(this.getContainerSVG(), this.xScale.range(), this.xScale.domain());
}
else {
var selected = zoomDomain.map(function (domain) {
return _this.xScale(domain);
});
if (selected[1] - selected[0] < this.MIN_SELECTION_DIFF) {
// initialization not completed yet
// don't update brushHandle to avoid flash
}
else {
brushArea.call(this.brush.move, selected); // set brush to correct position
this.updateBrushHandle(this.getContainerSVG(), selected, zoomDomain);
}
}
if (isTopZoomBarLocked) {
this.brush.filter(function () {
return false;
});
// reset all cursor to auto
brushArea.selectAll('rect').attr('cursor', 'auto');
}
}
};
ZoomBar.prototype.addBrushEventListener = function (zoomDomain, axesLeftMargin, width) {
var _this = this;
var brushEventListener = function () {
var selection = event.selection;
// follow d3 behavior: when selection is null, reset default full range
// select behavior is completed, but nothing selected
if (selection === null) {
_this.handleBrushedEvent(zoomDomain, _this.xScale, _this.xScale.range());
}
else if (selection[0] === selection[1]) {
// select behavior is not completed yet, do nothing
}
else {
_this.handleBrushedEvent(zoomDomain, _this.xScale, selection);
}
};
var zoombarType = Tools.getProperty(this.getOptions(), 'zoomBar', AxisPositions.TOP, 'type');
var zoombarHeight = Configuration.zoomBar.height[zoombarType];
// Initialize the d3 brush
this.brush
.extent([
[axesLeftMargin, 0],
[width, zoombarHeight],
])
.on('start brush end', null) // remove old listener first
.on('start brush end', brushEventListener);
};
// brush event listener
ZoomBar.prototype.handleBrushedEvent = function (zoomDomain, scale, selection) {
var newDomain = [
scale.invert(selection[0]),
scale.invert(selection[1]),
];
// update brush handle position
this.updateBrushHandle(this.getContainerSVG(), selection, newDomain);
// be aware that the value of d3.event changes during an event!
// update zoomDomain only if the event comes from mouse/touch event
if (event.sourceEvent != null &&
(event.sourceEvent.type === 'mousemove' ||
event.sourceEvent.type === 'mouseup' ||
event.sourceEvent.type === 'mousedown' ||
event.sourceEvent.type === 'touchstart' ||
event.sourceEvent.type === 'touchmove' ||
event.sourceEvent.type === 'touchend')) {
// only if zoomDomain is never set or needs update
if (zoomDomain === undefined ||
zoomDomain[0] !== newDomain[0] ||
zoomDomain[1] !== newDomain[1]) {
// don't dispatch event for all event types
// let the following code to dispatch necessary events
this.services.zoom.handleDomainChange(newDomain, {
dispatchEvent: false,
});
}
// dispatch selection events
var zoomBarEventType = void 0;
if (event.type === 'start') {
zoomBarEventType = Events.ZoomBar.SELECTION_START;
}
else if (event.type === 'brush') {
zoomBarEventType = Events.ZoomBar.SELECTION_IN_PROGRESS;
}
else if (event.type === 'end') {
zoomBarEventType = Events.ZoomBar.SELECTION_END;
// only dispatch zoom domain change event for triggering api call when event type equals to end
this.services.events.dispatchEvent(Events.ZoomDomain.CHANGE, {
newDomain: newDomain,
});
}
this.services.events.dispatchEvent(zoomBarEventType, {
selection: selection,
newDomain: newDomain,
});
}
};
ZoomBar.prototype.updateBrushHandle = function (svg, selection, domain) {
var self = this;
var handleWidth = Configuration.zoomBar.handleWidth;
var zoombarType = Tools.getProperty(this.getOptions(), 'zoomBar', AxisPositions.TOP, 'type');
var handleHeight = Configuration.zoomBar.height[zoombarType];
var handleXDiff = -handleWidth / 2;
var handleBarWidth = Configuration.zoomBar.handleBarWidth;
var handleBarHeight = zoombarType === ZoomBarTypes.GRAPH_VIEW
? Configuration.zoomBar.handleBarHeight
: 6;
var handleBarXDiff = -handleBarWidth / 2;
var handleYBarDiff = (handleHeight - handleBarHeight) / 2;
// handle
svg.select(this.brushSelector)
.selectAll('rect.handle')
.data([{ type: 'w' }, { type: 'e' }])
.attr('x', function (d) {
if (d.type === 'w') {
// handle should not exceed zoom bar range
return Math.max(selection[0] + handleXDiff, self.maxSelectionRange[0]);
}
else if (d.type === 'e') {
// handle should not exceed zoom bar range
return Math.min(selection[1] + handleXDiff, self.maxSelectionRange[1] - handleWidth);
}
})
.attr('y', 0)
.attr('width', handleWidth)
.attr('height', handleHeight)
.attr('cursor', 'ew-resize')
.style('display', null); // always display
// handle-bar
var handleBars = svg
.select(this.brushSelector)
.selectAll('rect.handle-bar')
.data([{ type: 'w' }, { type: 'e' }]);
// create rect if not exists
handleBars
.enter()
.append('rect')
.attr('class', function (d) {
return 'handle-bar handle-bar--' + d.type;
});
// update positions
handleBars
.attr('x', function (d) {
if (d.type === 'w') {
return Math.max(selection[0] + handleBarXDiff, self.maxSelectionRange[0] - handleXDiff + handleBarXDiff);
}
else if (d.type === 'e') {
return Math.min(selection[1] + handleBarXDiff, self.maxSelectionRange[1] + handleXDiff + handleBarXDiff);
}
})
.attr('y', handleYBarDiff)
.attr('width', handleBarWidth)
.attr('height', handleBarHeight)
.attr('cursor', 'ew-resize');
// Update slider selected area
if (zoombarType === ZoomBarTypes.SLIDER_VIEW) {
this.updateSliderSelectedArea(selection);
}
this.updateClipPath(svg, this.clipId, selection[0], 0, selection[1] - selection[0], handleHeight);
};
ZoomBar.prototype.updateSliderSelectedArea = function (selection) {
var zoombarType = Tools.getProperty(this.getOptions(), 'zoomBar', AxisPositions.TOP, 'type');
var zoombarHeight = Configuration.zoomBar.height[zoombarType];
var width = DOMUtils.getSVGElementSize(this.parent, {
useAttrs: true,
}).width;
// get axes margins
var axesLeftMargin = 0;
var axesMargins = this.model.get('axesMargins');
if (axesMargins && axesMargins.left) {
axesLeftMargin = axesMargins.left;
}
var svg = this.getContainerSVG();
var container = svg.select('svg.zoom-container');
// Draw zoombar background line
DOMUtils.appendOrSelect(container, 'rect.zoom-slider-selected-area')
.attr('x', selection[0])
.attr('y', zoombarHeight / 2 - 1)
.attr('width', selection[1] - selection[0])
.attr('height', 2);
};
ZoomBar.prototype.renderZoomBarArea = function (container, querySelector, data, clipId) {
var cartesianScales = this.services.cartesianScales;
var mainXAxisPosition = cartesianScales.getMainXAxisPosition();
var mainYAxisPosition = cartesianScales.getMainYAxisPosition();
var mainXScaleType = cartesianScales.getMainXScaleType();
var mainYScaleType = cartesianScales.getMainYScaleType();
var accessorFunction = function (scale, scaleType, axisPosition) {
return function (d, i) {
return cartesianScales.getValueFromScale(scale, scaleType, axisPosition, d, i);
};
};
var xAccessor = accessorFunction(this.xScale, mainXScaleType, mainXAxisPosition);
var yAccessor = accessorFunction(this.yScale, mainYScaleType, mainYAxisPosition);
var zoombarType = Tools.getProperty(this.getOptions(), 'zoomBar', AxisPositions.TOP, 'type');
var zoombarHeight = Configuration.zoomBar.height[zoombarType];
var areaGenerator = area()
.x(function (d, i) { return xAccessor(d, i); })
.y0(zoombarHeight)
.y1(function (d, i) { return zoombarHeight - yAccessor(d, i); });
var areaGraph = DOMUtils.appendOrSelect(container, querySelector)
.datum(data)
.attr('d', areaGenerator);
if (clipId) {
areaGraph.attr('clip-path', "url(#" + clipId + ")");
}
};
ZoomBar.prototype.updateClipPath = function (svg, clipId, x, y, width, height) {
var zoomBarClipPath = DOMUtils.appendOrSelect(svg, "clipPath").attr('id', clipId);
DOMUtils.appendOrSelect(zoomBarClipPath, 'rect')
.attr('x', x)
.attr('y', y)
.attr('width', width)
.attr('height', height);
};
// assume the domains in data are already sorted
ZoomBar.prototype.compensateDataForDefaultDomain = function (data, defaultDomain) {
if (!data || data.length < 2) {
return;
}
var zoomBarData = Tools.clone(data);
var domainIdentifier = this.services.cartesianScales.getDomainIdentifier();
var rangeIdentifier = this.services.cartesianScales.getRangeIdentifier();
// if min domain is extended
if (Number(defaultDomain[0]) < Number(zoomBarData[0][domainIdentifier])) {
var newDatum = {};
newDatum[domainIdentifier] = defaultDomain[0];
newDatum[rangeIdentifier] = 0;
zoomBarData.unshift(newDatum);
}
// if max domain is extended
if (Number(defaultDomain[1]) >
Number(zoomBarData[zoomBarData.length - 1][domainIdentifier])) {
var newDatum = {};
newDatum[domainIdentifier] = defaultDomain[1];
newDatum[rangeIdentifier] = 0;
zoomBarData.push(newDatum);
}
return zoomBarData;
};
ZoomBar.prototype.renderZoomBarBaseline = function (container, startX, endX, skeletonClass) {
if (skeletonClass === void 0) { skeletonClass = false; }
var zoombarType = Tools.getProperty(this.model.getOptions(), 'zoomBar', AxisPositions.TOP, 'type');
var zoombarHeight = Configuration.zoomBar.height[zoombarType];
var baselineGenerator = line()([
[startX, zoombarHeight],
[endX, zoombarHeight],
]);
DOMUtils.appendOrSelect(container, 'path.zoom-bg-baseline')
.attr('d', baselineGenerator)
.classed('zoom-bg-baseline-skeleton', skeletonClass);
};
ZoomBar.prototype.renderSkeleton = function (container, startX, endX) {
// need to clear current zoom bar area
this.renderZoomBarArea(container, 'path.zoom-graph-area-unselected', [], null);
this.renderZoomBarArea(container, 'path.zoom-graph-area', [], this.clipId);
// remove brush listener
this.brush.on('start brush end', null);
// clear d3 brush
DOMUtils.appendOrSelect(this.getContainerSVG(), this.brushSelector).html(null);
// re-render baseline because no axis labels in skeleton so the baseline length needs to change
var zoombarType = Tools.getProperty(this.getOptions(), 'zoomBar', AxisPositions.TOP, 'type');
if (zoombarType === ZoomBarTypes.GRAPH_VIEW) {
this.renderZoomBarBaseline(container, startX, endX, true);
}
};
ZoomBar.prototype.destroy = function () {
this.brush.on('start brush end', null); // remove event listener
this.services.events.removeEventListener(Events.ZoomBar.UPDATE, this.render.bind(this));
};
return ZoomBar;
}(Component));
export { ZoomBar };
//# sourceMappingURL=../../../src/components/axes/zoom-bar.js.map