@antv/g2
Version:
the Grammar of Graphics in Javascript
254 lines (240 loc) • 6.34 kB
text/typescript
import { deepMix, isNumber } from '@antv/util';
import { Vector2 } from '@antv/coord';
import { Group } from '@antv/g';
import { filterPrefixObject, isUnset, subObject } from '../utils/helper';
import { CompositeMarkComponent as CC, ShapeComponent as SC } from '../runtime';
import { ColorOptions } from '../shape/point/color';
import { GaugeMark } from '../spec';
import { getTransformOptions } from '../utils/coordinate';
import { Radial } from '../coordinate';
import { applyStyle, getOrigin } from '../shape/utils';
import { select } from '../utils/selection';
import { GaugeRound } from '../shape';
const indicatorShape: SC<ColorOptions> = (options, context) => {
const { shape, radius, ...style } = options;
const pointerStyle = subObject(style, 'pointer');
const pinStyle = subObject(style, 'pin');
const { shape: pointerShape, ...resPointerStyle } = pointerStyle;
const { shape: pinShape, ...resPinStyle } = pinStyle;
const { coordinate, theme } = context;
return (points, value) => {
// Invert points.
const invertedPoints = points.map((p) => coordinate.invert(p));
// Get new coordinate.
const [startAngle, endAngle, innerRadius] = getTransformOptions(
coordinate,
'polar',
) as number[];
const newCoordinate = coordinate.clone();
const { color: stroke } = value;
const newTransformations = Radial({
startAngle,
endAngle,
innerRadius,
outerRadius: radius,
});
newTransformations.push(['cartesian']);
newCoordinate.update({
transformations: newTransformations,
});
const newPoints = invertedPoints.map((p) => newCoordinate.map(p));
const [x, y] = getOrigin(newPoints as Vector2[]);
const [cx, cy] = coordinate.getCenter();
const pointerAttrs = {
x1: x,
y1: y,
x2: cx,
y2: cy,
stroke,
...resPointerStyle,
...style,
};
const pinAttrs = {
cx,
cy,
stroke,
...resPinStyle,
...style,
};
const indicatorGroup = select(new Group());
if (!isUnset(pointerShape)) {
typeof pointerShape === 'function'
? indicatorGroup.append(() =>
pointerShape(newPoints, value, newCoordinate, theme),
)
: indicatorGroup.append('line').call(applyStyle, pointerAttrs).node();
}
if (!isUnset(pinShape)) {
typeof pinShape === 'function'
? indicatorGroup.append(() =>
pinShape(newPoints, value, newCoordinate, theme),
)
: indicatorGroup.append('circle').call(applyStyle, pinAttrs).node();
}
return indicatorGroup.node();
};
};
const DEFAULT_OPTIONS = {
coordinate: {
type: 'radial',
innerRadius: 0.9,
outerRadius: 1,
startAngle: (-11 / 10) * Math.PI,
endAngle: (1 / 10) * Math.PI,
},
axis: {
x: false,
},
legend: false,
tooltip: false,
encode: {
x: 'x',
y: 'y',
color: 'color',
},
scale: {
color: {
range: ['#30BF78', '#D0D0D0'],
},
},
};
const DEFAULT_INDICATOR_OPTIONS = {
style: {
shape: indicatorShape,
lineWidth: 4,
pointerLineCap: 'round',
pinR: 10,
pinFill: '#fff',
radius: 0.6,
},
};
const DEFAULT_TEXT_OPTIONS = {
type: 'text',
style: {
x: '50%',
y: '60%',
textAlign: 'center',
textBaseline: 'middle',
fontSize: 20,
fontWeight: 800,
fill: '#888',
},
};
export type GaugeData =
| {
target?: number;
total?: number;
percent?: number;
name?: string;
thresholds?: number[];
}
| number;
function getGaugeData(data: GaugeData) {
if (isNumber(data)) {
// Percent range [0, 1].
const percent = Math.max(0, Math.min(data, 1));
return {
percent,
target: percent,
total: 1,
};
}
return data;
}
function dataTransform(data: GaugeData, scale) {
const {
name = 'score',
target,
total,
percent,
thresholds = [],
} = getGaugeData(data);
const _target = percent || target;
const _total = percent ? 1 : total;
const newScale = {
y: {
domain: [0, _total],
},
...scale,
};
if (!thresholds.length) {
return {
targetData: [{ x: name, y: _target, color: 'target' }],
totalData: [
{ x: name, y: _target, color: 'target' },
{ x: name, y: _total - _target, color: 'total' },
],
target: _target,
total: _total,
scale: newScale,
};
}
return {
targetData: [{ x: name, y: _target, color: 'target' }],
totalData: thresholds.map((d, i) => ({
x: name,
y: i >= 1 ? d - thresholds[i - 1] : d,
color: i,
})),
target: _target,
total: _total,
scale: newScale,
};
}
function getTextContent(textStyle, { target, total }) {
const { content } = textStyle;
return content ? content(target, total) : target.toString();
}
export type GaugeOptions = Omit<GaugeMark, 'type'>;
export const Gauge: CC<GaugeOptions> = (options) => {
const {
data = {},
scale = {},
style = {},
animate = {},
transform = [],
...resOptions
} = options;
const {
targetData,
totalData,
target,
total,
scale: newScale,
} = dataTransform(data, scale);
const textStyle = subObject(style, 'text');
// pointer + pin
const indicatorStyle = filterPrefixObject(style, ['pointer', 'pin']);
const arcStyle = subObject(style, 'arc');
const shape = arcStyle.shape;
return [
deepMix({}, DEFAULT_OPTIONS, {
type: 'interval',
transform: [{ type: 'stackY' }],
data: totalData,
scale: newScale,
style: shape === 'round' ? { ...arcStyle, shape: GaugeRound } : arcStyle,
animate:
typeof animate === 'object' ? subObject(animate, 'arc') : animate,
...resOptions,
}),
deepMix({}, DEFAULT_OPTIONS, DEFAULT_INDICATOR_OPTIONS, {
type: 'point',
data: targetData,
scale: newScale,
style: indicatorStyle,
animate:
typeof animate === 'object' ? subObject(animate, 'indicator') : animate,
...resOptions,
}),
deepMix({}, DEFAULT_TEXT_OPTIONS, {
style: {
text: getTextContent(textStyle, { target, total }),
...textStyle,
},
animate:
typeof animate === 'object' ? subObject(animate, 'text') : animate,
}),
];
};
Gauge.props = {};