veritatisquam
Version:
Carbon charting components, lerna monorepo
256 lines (226 loc) • 7.33 kB
text/typescript
import { Component } from '../component';
import { Tools } from '../../tools';
import { DOMUtils } from '../../services';
import { ChartModel } from '../../model';
import {
AxisPositions,
Events,
ScaleTypes,
CartesianOrientations,
} from '../../interfaces';
import { select, mouse } from 'd3-selection';
// Carbon position service
import Position, { PLACEMENTS } from '@carbon/utils-position';
// import the settings for the css prefix
import settings from 'carbon-components/es/globals/js/settings';
import {
formatTick,
computeTimeIntervalName,
} from '../../services/time-series';
export class Threshold extends Component {
type = 'threshold';
threshold: any;
thresholdClass: string;
thresholdIdentifierClass: string;
label: any;
positionService = new Position();
constructor(model: ChartModel, services: any, configs: any) {
super(model, services, configs);
}
render(animate = false) {
const { value, fillColor, axisPosition, index } = this.configs;
const chartprefix = Tools.getProperty(
this.getOptions(),
'style',
'prefix'
);
this.thresholdClass = `${settings.prefix}--${chartprefix}--threshold`;
// We can have multiple thresholds, set an unique identifier
this.thresholdIdentifierClass = `${axisPosition}-${index}`;
this.threshold = DOMUtils.appendOrSelect(
this.parent,
`g.${this.thresholdClass}.${this.thresholdIdentifierClass}`
).raise();
// Append threshold hoverable area
const thresholdRect = DOMUtils.appendOrSelect(
this.threshold,
`rect.threshold-hoverable-area`
);
// Append threshold line
const thresholdLine = DOMUtils.appendOrSelect(
this.threshold,
`line.threshold-line`
);
// Set threshold line color from configs options
// If not defined, the line takes the defined CSS color
thresholdLine.style('stroke', fillColor);
const scale = this.services.cartesianScales.getScaleByPosition(
axisPosition
);
const scaleType = this.services.cartesianScales.getScaleTypeByPosition(
axisPosition
);
const mainXScale = this.services.cartesianScales.getMainXScale();
const mainYScale = this.services.cartesianScales.getMainYScale();
const isScaleTypeLabels = scaleType === ScaleTypes.LABELS;
const [xScaleStart, xScaleEnd] = mainXScale.range();
const [yScaleEnd, yScaleStart] = mainYScale.range();
const { cartesianScales } = this.services;
const orientation = cartesianScales.getOrientation();
const getDomainValue = (d) => cartesianScales.getDomainValue(d);
const getRangeValue = (d) => cartesianScales.getRangeValue(d);
const [
getXValue,
getYValue,
] = Tools.flipDomainAndRangeBasedOnOrientation(
getDomainValue,
getRangeValue,
orientation
);
if (
axisPosition === AxisPositions.TOP ||
axisPosition === AxisPositions.BOTTOM
) {
const position =
getXValue(value) + (isScaleTypeLabels ? scale.step() / 2 : 0);
// Position the threshold on the x scale value
this.threshold
.transition(
this.services.transitions.getTransition(
'threshold-update',
animate
)
)
.attr('transform', `translate(${position}, ${yScaleStart})`);
// Set line end point on the y-axis
thresholdLine.attr('y2', yScaleEnd - yScaleStart);
// Set hoverable area width and rotate it
thresholdRect
.attr('width', Math.abs(yScaleEnd - yScaleStart))
.classed('rotate', true);
} else {
const position =
getYValue(value) + (isScaleTypeLabels ? scale.step() / 2 : 0);
// Position the threshold on the y scale value
this.threshold
.transition(
this.services.transitions.getTransition(
'threshold-update',
animate
)
)
.attr('transform', `translate(${xScaleStart}, ${position})`);
// Set line end point on the x-axis
thresholdLine.attr('x2', xScaleEnd - xScaleStart);
// Set hoverable area width
thresholdRect.attr('width', Math.abs(xScaleEnd - xScaleStart));
}
const self = this;
this.services.events.addEventListener(Events.Threshold.SHOW, (e) => {
const hovered = e.detail.hoveredElement.node();
// If is this threshold
if (hovered === self.threshold) {
// Set label position and show it
this.setThresholdLabelPosition();
this.label.classed('hidden', false);
}
});
this.services.events.addEventListener(Events.Threshold.HIDE, (e) => {
this.label.classed('hidden', true);
});
this.appendThresholdLabel();
this.addEventListeners();
}
getFormattedValue() {
const { value, axisPosition } = this.configs;
const options = this.getOptions();
const scaleType = this.services.cartesianScales.getScaleTypeByPosition(
axisPosition
);
// If scale is time, format the threshold date as the ticks format
if (scaleType === ScaleTypes.TIME) {
const isVertical = [
AxisPositions.LEFT,
AxisPositions.RIGHT,
].includes(axisPosition);
const mainXScale = this.services.cartesianScales.getMainXScale();
const mainYScale = this.services.cartesianScales.getMainYScale();
const scale = isVertical ? mainYScale : mainXScale;
const timeScaleOptions = Tools.getProperty(options, 'timeScale');
const timeInterval = computeTimeIntervalName(scale.ticks());
return formatTick(value, 0, timeInterval, timeScaleOptions);
}
return value.toLocaleString('en');
}
appendThresholdLabel() {
const {
value,
valueFormatter,
fillColor,
label = 'Threshold',
} = this.configs;
const holder = select(this.services.domUtils.getHolder());
// Format the threshold value using valueFormatter if defined in user-provided options
const formattedValue = valueFormatter
? valueFormatter(value)
: this.getFormattedValue();
this.label = DOMUtils.appendOrSelect(
holder,
`div.${this.thresholdClass}--label.${this.thresholdIdentifierClass}`
);
this.label
.html(`${label}: ${formattedValue}`)
.classed('hidden', true)
.style('background-color', fillColor);
}
setThresholdLabelPosition() {
const holder = this.services.domUtils.getHolder();
const target = this.label.node();
const mouseRelativePos = mouse(holder);
// Find out whether threshold label should be shown on the left or right side
const bestPlacementOption = this.positionService.findBestPlacementAt(
{
left: mouseRelativePos[0],
top: mouseRelativePos[1],
},
target,
[
PLACEMENTS.RIGHT,
PLACEMENTS.LEFT,
PLACEMENTS.TOP,
PLACEMENTS.BOTTOM,
],
() => ({
width: holder.offsetWidth,
height: holder.offsetHeight,
})
);
// Get coordinates to where label should be positioned
const pos = this.positionService.findPositionAt(
{
left: mouseRelativePos[0],
top: mouseRelativePos[1],
},
target,
bestPlacementOption
);
this.positionService.setElement(target, pos);
}
addEventListeners() {
const self = this;
// Add events to the threshold hoverable area
DOMUtils.appendOrSelect(this.threshold, 'rect')
.on('mouseover mousemove', function () {
self.threshold.classed('active', true);
self.services.events.dispatchEvent(Events.Threshold.SHOW, {
hoveredElement: select(self.threshold),
});
})
.on('mouseout', function () {
self.threshold.classed('active', false);
self.services.events.dispatchEvent(Events.Threshold.HIDE, {
hoveredElement: select(self.threshold),
});
});
}
}