veritatisquam
Version:
Carbon charting components, lerna monorepo
435 lines (372 loc) • 11 kB
text/typescript
// Internal Imports
import { Component } from '../component';
import { DOMUtils } from '../../services';
import {
Roles,
Events,
GaugeTypes,
ArrowDirections,
ColorClassNameTypes,
Alignments,
} from '../../interfaces';
import { Tools } from '../../tools';
// D3 Imports
import { select } from 'd3-selection';
import { arc } from 'd3-shape';
// arrow paths for delta
const ARROW_UP_PATH_STRING = '4,10 8,6 12,10';
const ARROW_DOWN_PATH_STRING = '12,6 8,10 4,6';
export class Gauge extends Component {
type = 'gauge';
// We need to store our arcs so that addEventListeners() can access them
arc: any;
backgroundArc: any;
init() {
const eventsFragment = this.services.events;
}
getValue(): number {
const data = this.model.getData();
const value = data.find((d) => d.group === 'value')?.value ?? null;
return value;
}
getValueRatio(): number {
const value = Tools.clamp(this.getValue(), 0, 100);
return value / 100;
}
getDelta(): number {
const data = this.model.getData();
const delta = data.find((d) => d.group === 'delta')?.value ?? null;
return delta;
}
getArcRatio(): number {
const options = this.getOptions();
const type = Tools.getProperty(options, 'gauge', 'type');
const arcRatio = type === GaugeTypes.FULL ? 1 : 0.5;
return arcRatio;
}
getArcSize(): number {
return this.getArcRatio() * Math.PI * 2;
}
getStartAngle(): number {
const arcSize = this.getArcSize();
if (arcSize === 2 * Math.PI) {
return 0;
}
return -arcSize / 2;
}
// use provided arrow direction or default to using the delta
getArrow(delta): string {
const options = this.getOptions();
const arrowDirection = Tools.getProperty(
options,
'gauge',
'deltaArrow',
'direction'
);
switch (arrowDirection) {
case ArrowDirections.UP:
return ARROW_UP_PATH_STRING;
case ArrowDirections.DOWN:
return ARROW_DOWN_PATH_STRING;
default:
return delta > 0
? ARROW_UP_PATH_STRING
: ARROW_DOWN_PATH_STRING;
}
}
render(animate = true) {
const self = this;
const svg = this.getContainerSVG();
const options = this.getOptions();
const { groupMapsTo } = options.data;
const value = this.getValue();
const valueRatio = this.getValueRatio();
const arcSize = this.getArcSize();
// angles for drawing the gauge
const startAngle = this.getStartAngle();
const rotationAngle = valueRatio * arcSize;
const currentAngle = startAngle + rotationAngle;
const endAngle = startAngle + arcSize;
// Compute the outer radius needed
const radius = this.computeRadius();
const innerRadius = this.getInnerRadius();
// draw the container and arc
this.backgroundArc = arc()
.innerRadius(innerRadius)
.outerRadius(radius)
.startAngle(currentAngle)
.endAngle(endAngle);
this.arc = arc()
.innerRadius(innerRadius)
.outerRadius(radius)
.startAngle(startAngle)
.endAngle(currentAngle);
// draw the container
DOMUtils.appendOrSelect(svg, 'path.arc-background')
.attr('d', this.backgroundArc)
.attr('role', Roles.GROUP);
// Add data arc
const arcValue = svg.selectAll('path.arc-foreground').data([value]);
const arcEnter = arcValue.enter().append('path');
arcEnter
.merge(arcValue)
.attr(
'class',
this.model.getColorClassName({
classNameTypes: [ColorClassNameTypes.FILL],
dataGroupName: 'value',
originalClassName: 'arc-foreground',
})
)
.style('fill', (d) =>
Tools.getProperty(this.getOptions(), 'color', 'scale', 'value')
)
.attr('d', this.arc)
// a11y
.attr('role', Roles.GRAPHICS_SYMBOL)
.attr('aria-roledescription', 'value')
.attr('aria-label', (d) => d.value);
// draw the value and delta to the center
this.drawValueNumber();
this.drawDelta();
arcValue.exit().remove();
const alignment = Tools.getProperty(options, 'gauge', 'alignment');
const { width } = DOMUtils.getSVGElementSize(this.getParent(), {
useAttr: true,
});
// Position gauge
let gaugeTranslateX = radius;
if (alignment === Alignments.CENTER) {
gaugeTranslateX = width / 2;
} else if (alignment === Alignments.RIGHT) {
gaugeTranslateX = width - radius;
}
svg.attr('transform', `translate(${gaugeTranslateX}, ${radius})`);
// Add event listeners
this.addEventListeners();
}
/**
* draws the value number associated with the Gauge component in the center
*/
drawValueNumber() {
const svg = this.getContainerSVG();
const options = this.getOptions();
const arcType = Tools.getProperty(options, 'gauge', 'type');
const value = this.getValue();
const delta = this.getDelta();
// Sizing and positions relative to the radius
const radius = this.computeRadius();
const valueFontSize = Tools.getProperty(
options,
'gauge',
'valueFontSize'
);
// if there is a delta, use the size to center the numbers, otherwise center the valueNumber
const deltaFontSize = Tools.getProperty(
options,
'gauge',
'deltaFontSize'
);
const numberSpacing = Tools.getProperty(
options,
'gauge',
'numberSpacing'
);
// circular gauge without delta should have valueNumber centered
let numbersYPosition = 0;
if (arcType === GaugeTypes.FULL && !delta) {
numbersYPosition = deltaFontSize(radius);
} else if (arcType === GaugeTypes.SEMI && delta) {
// semi circular gauge we want the numbers aligned to the chart container
numbersYPosition = -(deltaFontSize(radius) + numberSpacing);
}
// Add the numbers at the center
const numbersGroup = DOMUtils.appendOrSelect(
svg,
'g.gauge-numbers'
).attr('transform', `translate(0, ${numbersYPosition})`);
// Add the big number
const valueNumberGroup = DOMUtils.appendOrSelect(
numbersGroup,
'g.gauge-value-number'
).attr('transform', 'translate(-10, 0)'); // Optical centering for the presence of the smaller % symbol
const numberFormatter = Tools.getProperty(
options,
'gauge',
'numberFormatter'
);
const valueNumber = valueNumberGroup
.selectAll('text.gauge-value-number')
.data([value]);
valueNumber
.enter()
.append('text')
.attr('class', 'gauge-value-number')
.merge(valueNumber)
.style('font-size', `${valueFontSize(radius)}px`)
.attr('text-anchor', 'middle')
.text((d) => numberFormatter(d));
// add the percentage symbol beside the valueNumber
const {
width: valueNumberWidth,
} = DOMUtils.getSVGElementSize(
DOMUtils.appendOrSelect(svg, 'text.gauge-value-number'),
{ useBBox: true }
);
DOMUtils.appendOrSelect(valueNumberGroup, 'text.gauge-value-symbol')
.style('font-size', `${valueFontSize(radius) / 2}px`)
.attr('x', valueNumberWidth / 2)
.text('%');
}
/**
* adds the delta number for the gauge
*/
drawDelta() {
const self = this;
const svg = this.getContainerSVG();
const options = this.getOptions();
const delta = this.getDelta();
// Sizing and positions relative to the radius
const radius = this.computeRadius();
const deltaFontSize = delta
? Tools.getProperty(options, 'gauge', 'deltaFontSize')
: () => 0;
// use numberFormatter here only if there is a delta supplied
const numberFormatter = delta
? Tools.getProperty(options, 'gauge', 'numberFormatter')
: () => null;
const arrowSize = Tools.getProperty(
options,
'gauge',
'deltaArrow',
'size'
);
const numberSpacing = Tools.getProperty(
options,
'gauge',
'numberSpacing'
);
const numbersGroup = DOMUtils.appendOrSelect(svg, 'g.gauge-numbers');
// Add the smaller number of the delta
const deltaGroup = DOMUtils.appendOrSelect(
numbersGroup,
'g.gauge-delta'
).attr(
'transform',
`translate(0, ${deltaFontSize(radius) + numberSpacing})`
);
const deltaNumber = DOMUtils.appendOrSelect(
deltaGroup,
'text.gauge-delta-number'
);
deltaNumber.data(delta === null ? [] : [delta]);
deltaNumber
.enter()
.append('text')
.classed('gauge-delta-number', true)
.merge(deltaNumber)
.attr('text-anchor', 'middle')
.style('font-size', `${deltaFontSize(radius)}px`)
.text((d) => `${numberFormatter(d)}%`);
// Add the caret for the delta number
const {
width: deltaNumberWidth,
} = DOMUtils.getSVGElementSize(
DOMUtils.appendOrSelect(svg, '.gauge-delta-number'),
{ useBBox: true }
);
// check if delta arrow is disabled
const arrowEnabled = Tools.getProperty(
options,
'gauge',
'deltaArrow',
'enabled'
);
const deltaArrow = deltaGroup
.selectAll('svg.gauge-delta-arrow')
.data(delta !== null && arrowEnabled ? [delta] : []);
deltaArrow
.enter()
.append('svg')
.merge(deltaArrow)
.attr('class', 'gauge-delta-arrow')
.attr('x', -arrowSize(radius) - deltaNumberWidth / 2)
.attr('y', -arrowSize(radius) / 2 - deltaFontSize(radius) * 0.35)
.attr('width', arrowSize(radius))
.attr('height', arrowSize(radius))
.attr('viewBox', '0 0 16 16');
// Needed to correctly size SVG in Firefox
DOMUtils.appendOrSelect(deltaArrow, 'rect.gauge-delta-arrow-backdrop')
.attr('width', '16')
.attr('height', '16')
.attr('fill', 'none');
// Draw the arrow with status
const status = Tools.getProperty(options, 'gauge', 'status');
DOMUtils.appendOrSelect(deltaArrow, 'polygon.gauge-delta-arrow')
.attr(
'class',
status !== null ? `gauge-delta-arrow status--${status}` : ''
)
.attr('points', self.getArrow(delta));
deltaArrow.exit().remove();
deltaNumber.exit().remove();
}
getInnerRadius() {
// Compute the outer radius needed
const radius = this.computeRadius();
const arcWidth = Tools.getProperty(
this.getOptions(),
'gauge',
'arcWidth'
);
return radius - arcWidth;
}
addEventListeners() {
const self = this;
this.parent
.selectAll('path.arc-foreground')
.on('mouseover', function (datum) {
// Dispatch mouse event
self.services.events.dispatchEvent(Events.Gauge.ARC_MOUSEOVER, {
element: select(this),
datum,
});
})
.on('mousemove', function (datum) {
const hoveredElement = select(this);
// Dispatch mouse event
self.services.events.dispatchEvent(Events.Gauge.ARC_MOUSEMOVE, {
element: hoveredElement,
datum,
});
})
.on('click', function (datum) {
// Dispatch mouse event
self.services.events.dispatchEvent(Events.Gauge.ARC_CLICK, {
element: select(this),
datum,
});
})
.on('mouseout', function (datum) {
const hoveredElement = select(this);
// Dispatch mouse event
self.services.events.dispatchEvent(Events.Gauge.ARC_MOUSEOUT, {
element: hoveredElement,
datum,
});
});
}
// Helper functions
protected computeRadius() {
const options = this.getOptions();
const arcType = Tools.getProperty(options, 'gauge', 'type');
const { width, height } = DOMUtils.getSVGElementSize(this.parent, {
useAttrs: true,
});
const radius =
arcType === GaugeTypes.SEMI
? Math.min(width / 2, height)
: Math.min(width / 2, height / 2);
return radius;
}
}