veritatisquam
Version:
Carbon charting components, lerna monorepo
179 lines (152 loc) • 5.56 kB
text/typescript
// Internal Imports
import { Component } from '../component';
import { Events, ScaleTypes } from '../../interfaces';
import { DOMUtils } from '../../services';
// D3 Imports
import { brushX } from 'd3-brush';
import { event, mouse } from 'd3-selection';
import { scaleTime } from 'd3-scale';
// This class is used for handle brush events in chart
export class ChartBrush extends Component {
static DASH_LENGTH = 4;
type = 'grid-brush';
selectionSelector = 'rect.selection'; // needs to match the class name in d3.brush
frontSelectionSelector = 'rect.frontSelection'; // needs to match the class name in _grid-brush.scss
render(animate = true) {
const svg = this.parent;
// use this area to display selection above all graphs
const frontSelectionArea = this.getContainerSVG();
const backdrop = DOMUtils.appendOrSelect(
svg,
'svg.chart-grid-backdrop'
);
// use this area to handle d3 brush events
const brushArea = DOMUtils.appendOrSelect(backdrop, `g.${this.type}`);
// set an id for rect.selection to be referred
const d3Selection = DOMUtils.appendOrSelect(
brushArea,
this.selectionSelector
);
const { width, height } = DOMUtils.getSVGElementSize(backdrop, {
useAttrs: true,
});
const { cartesianScales } = this.services;
const mainXScaleType = cartesianScales.getMainXScaleType();
const mainXScale = cartesianScales.getMainXScale();
const [xScaleStart, xScaleEnd] = mainXScale.range();
frontSelectionArea.attr('transform', `translate(${xScaleStart},0)`);
const frontSelection = DOMUtils.appendOrSelect(
frontSelectionArea,
this.frontSelectionSelector
);
if (mainXScale && mainXScaleType === ScaleTypes.TIME) {
// get current zoomDomain
let zoomDomain = this.model.get('zoomDomain');
if (zoomDomain === undefined) {
// default to full range with extended domain
zoomDomain = this.services.zoom.getDefaultZoomBarDomain();
if (zoomDomain) {
this.model.set(
{ zoomDomain: zoomDomain },
{ animate: false }
);
}
}
const updateSelectionDash = (selection) => {
// set end drag point to dash
const selectionWidth = selection[1] - selection[0];
let dashArray = '0,' + selectionWidth.toString(); // top (invisible)
// right
const dashCount = Math.floor(height / ChartBrush.DASH_LENGTH);
const totalRightDash = dashCount * ChartBrush.DASH_LENGTH;
for (let i = 0; i < dashCount; i++) {
dashArray += ',' + ChartBrush.DASH_LENGTH; // for each full length dash
}
dashArray += ',' + (height - totalRightDash); // for rest of the right height
// if dash count is even, one more ",0" is needed to make total right dash pattern even
if (dashCount % 2 === 1) {
dashArray += ',0';
}
dashArray += ',' + selectionWidth.toString(); // bottom (invisible)
dashArray += ',' + height.toString(); // left
frontSelection.attr('stroke-dasharray', dashArray);
};
const brushEventHandler = () => {
// selection range: [0, width]
const selection = event.selection;
if (selection === null || selection[0] === selection[1]) {
return;
}
// copy the d3 selection attrs to front selection element
frontSelection
.attr('x', d3Selection.attr('x'))
.attr('y', d3Selection.attr('y'))
.attr('width', d3Selection.attr('width'))
.attr('height', d3Selection.attr('height'))
.style('cursor', 'pointer')
.style('display', null);
updateSelectionDash(selection);
};
// assume max range is [0, width]
const updateZoomDomain = (startPoint, endPoint) => {
// create xScale based on current zoomDomain
const xScale = scaleTime().range([0, width]).domain(zoomDomain);
let newDomain = [
xScale.invert(startPoint),
xScale.invert(endPoint),
];
// if selected start time and end time are the same
// reset to default full range
if (newDomain[0].valueOf() === newDomain[1].valueOf()) {
// same as d3 behavior and zoom bar behavior: set to default full range
newDomain = this.services.zoom.getDefaultZoomBarDomain();
}
// only if zoomDomain needs update
if (
zoomDomain[0].valueOf() !== newDomain[0].valueOf() ||
zoomDomain[1].valueOf() !== newDomain[1].valueOf()
) {
this.services.zoom.handleDomainChange(newDomain);
}
};
const brushed = () => {
// max selection range: [0, width]
const selection = event.selection;
if (selection !== null) {
// updateZoomDomain assumes max range is [0, width]
updateZoomDomain(selection[0], selection[1]);
// clear brush selection
brushArea.call(brush.move, null);
// hide frontSelection
frontSelection.style('display', 'none');
}
};
// leave some space to display selection strokes besides axis
const brush = brushX()
.extent([
[0, 0],
[width - 1, height],
])
.on('start brush end', brushEventHandler)
.on('end.brushed', brushed);
brushArea.call(brush);
const zoomRatio = this.services.zoom.getZoomRatio();
backdrop.on('click', function () {
if (event.shiftKey) {
// clickedX range: [0, width]
const clickedX = mouse(brushArea.node())[0];
let leftPoint = clickedX - (width * zoomRatio) / 2;
if (leftPoint < 0) {
leftPoint = 0;
}
let rightPoint = clickedX + (width * zoomRatio) / 2;
if (rightPoint > width) {
rightPoint = width;
}
// updateZoomDomain assumes max range is [0, width]
updateZoomDomain(leftPoint, rightPoint);
}
});
}
}
}