in packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/angular.ts [26:156]
export function angularBullet(
ctx: CanvasRenderingContext2D,
dimensions: BulletPanelDimensions,
style: BulletStyle,
backgroundColor: Color,
spec: BulletSpec,
debug: boolean,
activeValue?: ActiveValue | null,
) {
const { datum, graphArea, scale, ticks, colorBands } = dimensions;
const { radius } = getAngledChartSizing(graphArea.size, spec.subtype);
const [startAngle, endAngle] = scale.range() as [number, number];
const center = {
x: graphArea.center.x,
y: radius + TARGET_SIZE / 2,
};
ctx.translate(GRAPH_PADDING.left, GRAPH_PADDING.top);
const [start, end] = scale.domain() as GenericDomain;
// const counterClockwise = true;
const counterClockwise = startAngle < endAngle && start > end;
const [min, max] = sortNumbers([start, end]) as ContinuousDomain;
const filteredTicks =
spec.subtype !== BulletSubtype.circle
? ticks
: min === ticks.at(0) && max === ticks.at(-1)
? ticks.slice(0, -1)
: max === ticks.at(0) && min === ticks.at(-1)
? ticks.slice(1)
: ticks;
const formatterColorTicks = filteredTicks.map((v) => ({ value: v, formattedValue: datum.tickFormatter(v) }));
// Color bands
colorBands.forEach((band) => {
ctx.beginPath();
ctx.arc(center.x, center.y, radius, band.start, band.end, false);
ctx.lineWidth = BULLET_SIZE;
ctx.strokeStyle = band.color;
ctx.stroke();
});
// Ticks
ctx.beginPath();
ctx.strokeStyle = backgroundColor;
ctx.lineWidth = TICK_WIDTH;
formatterColorTicks
.filter((tick) => tick.value > min && tick.value < max)
.forEach((tick) => {
const bulletWidth = BULLET_SIZE + 4; // TODO fix arbitrary extension
drawPolarLine(ctx, scale(tick.value), radius, bulletWidth, center);
});
ctx.stroke();
// Bar
const confinedValue = clamp(datum.value, min, max);
const adjustedZero = clamp(0, min, max);
ctx.beginPath();
ctx.lineWidth = BAR_SIZE;
ctx.strokeStyle = style.barBackground;
ctx.arc(
center.x,
center.y,
radius,
confinedValue > 0 ? scale(adjustedZero) : scale(confinedValue),
confinedValue > 0 ? scale(confinedValue) : scale(adjustedZero),
counterClockwise,
);
ctx.stroke();
// Target
if (isFiniteNumber(datum.target) && datum.target <= max && datum.target >= min) {
ctx.beginPath();
ctx.strokeStyle = style.barBackground;
ctx.lineWidth = TARGET_STROKE_WIDTH;
drawPolarLine(ctx, scale(datum.target), radius, TARGET_SIZE, center);
ctx.stroke();
}
// Zero baseline
if (isBetween(min, max, true)(0)) {
ctx.beginPath();
ctx.strokeStyle = style.barBackground;
ctx.lineWidth = TICK_WIDTH;
drawPolarLine(ctx, scale(0), radius, BULLET_SIZE, center);
ctx.stroke();
}
const measure = measureText(ctx);
// Assumes mostly homogenous formatting
const maxTickWidth = formatterColorTicks.reduce((acc, t) => {
const { width } = measure(t.formattedValue, TICK_FONT, TICK_FONT_SIZE);
return Math.max(acc, width);
}, 0);
// Tick labels
ctx.fillStyle = style.textColor;
ctx.textBaseline = 'middle';
ctx.font = cssFontShorthand(TICK_FONT, TICK_FONT_SIZE);
formatterColorTicks
.filter((tick) => tick.value >= min && tick.value <= max)
.forEach((tick) => {
ctx.textAlign = 'center';
const textPadding = style.angularTickLabelPadding + maxTickWidth / 2;
const start = scale(tick.value);
const y1 = Math.sin(start) * (radius - BULLET_SIZE / 2 - textPadding);
const x1 = Math.cos(start) * (radius - BULLET_SIZE / 2 - textPadding);
ctx.fillText(tick.formattedValue, center.x + x1, center.y + y1);
});
if (activeValue) {
ctx.beginPath();
ctx.strokeStyle = style.barBackground;
ctx.lineWidth = TARGET_STROKE_WIDTH;
drawPolarLine(ctx, activeValue.value, radius, TARGET_SIZE, center);
ctx.stroke();
}
ctx.beginPath();
if (debug) {
renderDebugPoint(ctx, center.x, center.y); // arch center
}
}