diff --git a/packages/components/examples/report-card/empowerment-metric/segmented-circular-gauge.stories.tsx b/packages/components/examples/report-card/empowerment-metric/segmented-circular-gauge.stories.tsx
new file mode 100644
index 00000000..eb31f27f
--- /dev/null
+++ b/packages/components/examples/report-card/empowerment-metric/segmented-circular-gauge.stories.tsx
@@ -0,0 +1,185 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import {
+ Arc,
+ ArcSegments,
+ ArcSegmentsSweep,
+ ArcUnitLabel,
+ Arcs,
+ Chart,
+} from '../../../src';
+
+const Neutral400 = '#42526E';
+const Neutral100 = '#6B778C';
+const FontWeightNormal = 400;
+const FontWeightBold = 600;
+
+const unitLabelStyle = (at: number, value: number) => {
+ const isPrimaryValue = at === value;
+ const fontWeight = isPrimaryValue ? FontWeightBold : FontWeightNormal;
+ const fill = isPrimaryValue ? Neutral400 : Neutral100;
+ return {
+ fontFamily: '"Inter"',
+ fontSize: '16px',
+ fontStyle: 'normal',
+ fontWeight,
+ fill,
+ };
+};
+
+const ReportCardEmpowermentMetricSegmentedCircularGauge = (props: {
+ valueMin: number;
+ valueMax: number;
+ value: number;
+ valueContinuous: number;
+ radius: number;
+ radiusRatio: number;
+ segments: number;
+ padAngle: number;
+ cornerRadius: number | string;
+}) => {
+ const {
+ valueMin,
+ valueMax,
+ value,
+ valueContinuous,
+ radius,
+ radiusRatio,
+ segments,
+ padAngle,
+ cornerRadius,
+ } = props;
+
+ const startAngleOffset = 0; // -0.05;
+
+ const Neutral80 = '#DFE1E6';
+ const Yellow300 = '#FFE380';
+ const Yellow500 = '#FFAB00';
+
+ return (
+
+
+
+
+
+
+
+
+ Sad
+
+
+ Fair
+
+
+ Okay
+
+
+ Good
+
+
+ Happy
+
+
+
+
+
+
+
+ );
+};
+
+const meta: Meta = {
+ title: 'Examples/Report Card/Empowerment Metric/SegmentedCircularGauge',
+ component: ReportCardEmpowermentMetricSegmentedCircularGauge,
+ args: {
+ valueMin: 1,
+ valueMax: 5,
+ value: 2,
+ radius: 100,
+ radiusRatio: 0.74,
+ padAngle: 0.042,
+ cornerRadius: '1.5px',
+ segments: 52,
+ valueContinuous: 0.9,
+ },
+ argTypes: {
+ value: {
+ control: {
+ type: 'range',
+ min: 1,
+ max: 5,
+ step: 1,
+ },
+ },
+ valueContinuous: {
+ control: {
+ type: 'range',
+ min: 0.9,
+ max: 5,
+ step: 0.1,
+ },
+ },
+ radius: {
+ control: {
+ type: 'range',
+ min: 1,
+ max: 100,
+ step: 1,
+ },
+ },
+ radiusRatio: {
+ control: {
+ type: 'range',
+ min: 0,
+ max: 1,
+ step: 0.01,
+ },
+ },
+ padAngle: {
+ control: {
+ type: 'range',
+ min: 0,
+ max: 0.1,
+ step: 0.001,
+ },
+ },
+ segments: {
+ control: {
+ type: 'range',
+ min: 1,
+ max: 52,
+ step: 1,
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Example: Story = {};
diff --git a/packages/components/src/Arc/Arc.stories.tsx b/packages/components/src/Arc/Arc.stories.tsx
index 8b228b53..5168758c 100644
--- a/packages/components/src/Arc/Arc.stories.tsx
+++ b/packages/components/src/Arc/Arc.stories.tsx
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
import { Arc } from './Arc';
import { ArcSweep } from '../ArcSweep';
import { Chart } from '../Chart';
diff --git a/packages/components/src/ArcCircle/ArcCircle.stories.tsx b/packages/components/src/ArcCircle/ArcCircle.stories.tsx
index 35bff0c0..d7ce2404 100644
--- a/packages/components/src/ArcCircle/ArcCircle.stories.tsx
+++ b/packages/components/src/ArcCircle/ArcCircle.stories.tsx
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
import { ArcCircle } from './ArcCircle';
import { Chart } from '../Chart';
import { generateArc } from '../Arc/generateArc';
diff --git a/packages/components/src/ArcSegments/ArcSegments.stories.tsx b/packages/components/src/ArcSegments/ArcSegments.stories.tsx
new file mode 100644
index 00000000..18e47901
--- /dev/null
+++ b/packages/components/src/ArcSegments/ArcSegments.stories.tsx
@@ -0,0 +1,91 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
+import { ArcSegments } from './ArcSegments';
+import { ArcSegmentsSweep } from '../ArcSegmentsSweep';
+import { Chart } from '../Chart';
+import { generateArc } from '../Arc/generateArc';
+
+const dim = 252 as const;
+
+const meta: Meta = {
+ title: 'ArcSegments',
+ component: ArcSegments,
+ decorators: (Story) => (
+
+
+
+ ),
+ argTypes: {
+ padAngle: {
+ control: {
+ type: 'range',
+ min: 0,
+ max: 0.5,
+ step: 0.005,
+ },
+ },
+ count: {
+ control: {
+ type: 'range',
+ min: 0,
+ max: 50,
+ step: 1,
+ },
+ },
+ cornerRadius: {
+ control: {
+ type: 'text',
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+const circularArc = generateArc({
+ valueMin: 0,
+ valueMax: 126,
+ angleMin: 0,
+ angleMax: 2 * Math.PI,
+ radius: 126,
+ ratio: 0.77,
+ cornerRadius: '50%',
+});
+
+export const Circular: Story = {
+ args: {
+ arc: circularArc,
+ padAngle: 0.05,
+ count: 20,
+ cornerRadius: '2px',
+ children: [
+ ,
+ ,
+ ],
+ },
+};
+
+const radialArc = generateArc({
+ valueMin: 1,
+ valueMax: 3,
+ angleMin: (-1 * Math.PI) / 2,
+ angleMax: Math.PI / 2,
+ radius: 126,
+ ratio: 0.77,
+ cornerRadius: '50%',
+});
+
+export const Radial: Story = {
+ args: {
+ arc: radialArc,
+ padAngle: 0.05,
+ count: 20,
+ cornerRadius: '2px',
+ children: [
+ ,
+ ,
+ ],
+ },
+};
diff --git a/packages/components/src/ArcSegments/ArcSegments.tsx b/packages/components/src/ArcSegments/ArcSegments.tsx
new file mode 100644
index 00000000..8cfcf568
--- /dev/null
+++ b/packages/components/src/ArcSegments/ArcSegments.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { ArcSegmentsProps } from './ArcSegmentsProp';
+import { generateArcSegments } from './generateArcSegments';
+
+export const ArcSegments = (props: ArcSegmentsProps) => {
+ const { arc, children, cornerRadius, count, padAngle } = props;
+
+ if (arc === undefined) {
+ throw Error(
+ 'Oops! `ArcSegments` received `arc: undefined`. Did you mean to either (1) render as a child of `Arc`? or (2) pass `arc` from `generateArc`?'
+ );
+ }
+
+ const arcSegments = generateArcSegments({
+ count,
+ padAngle,
+ cornerRadius,
+ });
+
+ const extraProps = { arc: Object.assign(arc, { segments: arcSegments }) };
+
+ return React.Children.map(children, (child) => {
+ // @ts-expect-error TODO
+ return React.cloneElement(child, extraProps);
+ });
+};
diff --git a/packages/components/src/ArcSegments/ArcSegmentsProp.ts b/packages/components/src/ArcSegments/ArcSegmentsProp.ts
new file mode 100644
index 00000000..edc037e1
--- /dev/null
+++ b/packages/components/src/ArcSegments/ArcSegmentsProp.ts
@@ -0,0 +1,22 @@
+import { ReactNode } from 'react';
+import { ArcOut } from '../Arc/generateArc';
+
+export interface ArcSegmentsProps {
+ /**
+ * The number of segments in the arc.
+ */
+ count: number;
+ /**
+ * The angular separation in radians between adjacent segments.
+ */
+ padAngle: number;
+ /**
+ * The default corner radius of any segment located on this arc.
+ */
+ cornerRadius?: number | string;
+ /**
+ * The arc on which the segments are located.
+ */
+ arc?: ArcOut;
+ children: ReactNode;
+}
diff --git a/packages/components/src/ArcSegments/generateArcSegments.ts b/packages/components/src/ArcSegments/generateArcSegments.ts
new file mode 100644
index 00000000..a847b3cf
--- /dev/null
+++ b/packages/components/src/ArcSegments/generateArcSegments.ts
@@ -0,0 +1,21 @@
+export type ArcsSegmentsIn = {
+ count: number;
+ padAngle: number;
+ cornerRadius?: number | string;
+};
+
+export type ArcSegmentsOut = {
+ count: number;
+ padAngle: number;
+ cornerRadius?: number | string;
+};
+
+export function generateArcSegments(params: ArcsSegmentsIn): ArcSegmentsOut {
+ const arcSegments = {
+ count: params.count,
+ padAngle: params.padAngle,
+ cornerRadius: params.cornerRadius,
+ };
+
+ return arcSegments;
+}
diff --git a/packages/components/src/ArcSegments/index.ts b/packages/components/src/ArcSegments/index.ts
new file mode 100644
index 00000000..04a1da38
--- /dev/null
+++ b/packages/components/src/ArcSegments/index.ts
@@ -0,0 +1,3 @@
+export * from './ArcSegments';
+export * from './ArcSegmentsProp';
+export * from './generateArcSegments';
diff --git a/packages/components/src/ArcSegmentsSweep/ArcSegmentsSweep.stories.tsx b/packages/components/src/ArcSegmentsSweep/ArcSegmentsSweep.stories.tsx
new file mode 100644
index 00000000..81376202
--- /dev/null
+++ b/packages/components/src/ArcSegmentsSweep/ArcSegmentsSweep.stories.tsx
@@ -0,0 +1,155 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
+import { ArcSegmentsSweep } from './ArcSegmentsSweep';
+import { Chart } from '../Chart';
+import { generateArc } from '../Arc/generateArc';
+import { generateArcSegments } from '../ArcSegments/generateArcSegments';
+
+const dim = 252 as const;
+
+const meta: Meta = {
+ title: 'ArcSegmentsSweep',
+ component: ArcSegmentsSweep,
+ decorators: (Story) => (
+
+
+
+ ),
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+const circularArc = {
+ ...generateArc({
+ valueMin: 0,
+ valueMax: 126,
+ angleMin: 0,
+ angleMax: 2 * Math.PI,
+ radius: 126,
+ ratio: 0.77,
+ }),
+ segments: generateArcSegments({
+ cornerRadius: '2px',
+ count: 20,
+ padAngle: 0.02,
+ }),
+};
+
+// circular; min = from = to < max
+export const CircularMin: Story = {
+ args: {
+ from: undefined,
+ to: 0,
+ cornerRadius: undefined,
+ arc: circularArc,
+ },
+};
+
+// circular; min = from < to < max
+export const CircularPartial: Story = {
+ args: {
+ from: undefined,
+ to: 100,
+ cornerRadius: undefined,
+ arc: circularArc,
+ },
+};
+
+// circular; min < from < to < max
+export const CircularPartial2: Story = {
+ args: {
+ from: 20,
+ to: 110,
+ cornerRadius: undefined,
+ arc: circularArc,
+ },
+};
+
+// circular; min < from < to = max
+export const CircularPartial3: Story = {
+ args: {
+ from: 20,
+ to: 126,
+ cornerRadius: undefined,
+ arc: circularArc,
+ },
+};
+
+// circular; min = from < to = max
+export const CircularMax: Story = {
+ args: {
+ from: undefined,
+ to: 126,
+ cornerRadius: undefined,
+ arc: circularArc,
+ },
+};
+
+const radialArc = {
+ ...generateArc({
+ valueMin: 1,
+ valueMax: 3,
+ angleMin: (-1 * Math.PI) / 2,
+ angleMax: Math.PI / 2,
+ radius: 126,
+ ratio: 0.77,
+ cornerRadius: '50%',
+ }),
+ segments: generateArcSegments({
+ cornerRadius: '2px',
+ count: 15,
+ padAngle: 0.02,
+ }),
+};
+
+// radial; min = from = to < max
+export const RadialMin: Story = {
+ args: {
+ from: undefined,
+ to: 1,
+ cornerRadius: undefined,
+ arc: radialArc,
+ },
+};
+
+// radial; min = from < to < max
+export const RadialPartial: Story = {
+ args: {
+ from: undefined,
+ to: 2,
+ cornerRadius: undefined,
+ arc: radialArc,
+ },
+};
+
+// radial; min = from < to < max; cornerRadius = 0
+export const RadialPartialSharp: Story = {
+ args: {
+ from: undefined,
+ to: 2,
+ cornerRadius: 0,
+ arc: radialArc,
+ },
+};
+
+// radial; min = from < to < max; cornerRadius = 4px
+export const RadialPartialSoft: Story = {
+ args: {
+ from: undefined,
+ to: 2,
+ cornerRadius: '8px',
+ arc: radialArc,
+ },
+};
+
+// radial; min = from < to = max
+export const RadialMax: Story = {
+ args: {
+ from: undefined,
+ to: 3,
+ cornerRadius: undefined,
+ arc: radialArc,
+ },
+};
diff --git a/packages/components/src/ArcSegmentsSweep/ArcSegmentsSweep.tsx b/packages/components/src/ArcSegmentsSweep/ArcSegmentsSweep.tsx
new file mode 100644
index 00000000..a351f2cd
--- /dev/null
+++ b/packages/components/src/ArcSegmentsSweep/ArcSegmentsSweep.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { ArcSegmentsSweepProps } from './ArcSegmentsSweepProps';
+import { generateArcSegmentsSweep } from './generateArcSegmentsSweep';
+
+export type ArcSegmentsSweepRef = SVGGElement;
+
+export const ArcSegmentsSweep = React.forwardRef<
+ ArcSegmentsSweepRef,
+ ArcSegmentsSweepProps
+>((props, ref) => {
+ const { from, to, cornerRadius, arc, renderProps, ...other } = props;
+
+ if (arc === undefined) {
+ throw Error(
+ 'Oops! `ArcSegmentsSweep` received `arc: undefined`. Did you mean to either (1) render as a child of `Arc`? or (2) pass `arc` from `generateArc`?'
+ );
+ }
+
+ const sweeps = generateArcSegmentsSweep({
+ from,
+ to,
+ cornerRadius,
+ arc,
+ segments: arc.segments,
+ });
+
+ return (
+
+ {sweeps.map((sweep, i) => {
+ const baseProps = { key: i, d: sweep.d };
+
+ const props =
+ renderProps === undefined
+ ? baseProps
+ : typeof renderProps === 'function'
+ ? renderProps(baseProps, i)
+ : Object.assign(baseProps, renderProps);
+
+ return ;
+ })}
+
+ );
+});
diff --git a/packages/components/src/ArcSegmentsSweep/ArcSegmentsSweepProps.ts b/packages/components/src/ArcSegmentsSweep/ArcSegmentsSweepProps.ts
new file mode 100644
index 00000000..b1b9e208
--- /dev/null
+++ b/packages/components/src/ArcSegmentsSweep/ArcSegmentsSweepProps.ts
@@ -0,0 +1,30 @@
+import React from 'react';
+import { ArcOut } from '../Arc/generateArc';
+import { ArcSegmentsOut } from '../ArcSegments/generateArcSegments';
+
+export interface ArcSegmentsSweepProps {
+ /**
+ * The value from which the arc sweep starts. Defaults to the minimum value.
+ */
+ from?: number;
+ /**
+ * The value from which the arc sweep ends. Defaults to the maximum value.
+ */
+ to?: number;
+ /**
+ * The corner radius of the arc sweep. Defaults to 0. Overrides the corner radius set on the `arc`.
+ */
+ cornerRadius?: number | string;
+ /**
+ * The arc on which the sweep is located.
+ */
+ arc?: ArcOut & {
+ segments: ArcSegmentsOut;
+ };
+ renderProps?:
+ | React.SVGTextElementAttributes
+ | ((
+ props: { d: string },
+ index: number
+ ) => React.SVGTextElementAttributes);
+}
diff --git a/packages/components/src/ArcSegmentsSweep/generateArcSegmentsSweep.ts b/packages/components/src/ArcSegmentsSweep/generateArcSegmentsSweep.ts
new file mode 100644
index 00000000..4bf7f34d
--- /dev/null
+++ b/packages/components/src/ArcSegmentsSweep/generateArcSegmentsSweep.ts
@@ -0,0 +1,78 @@
+import { arc as d3Arc, pie as d3Pie } from 'd3-shape';
+import { ArcOut } from '../Arc/generateArc';
+import { getNumericalValue, getValueSegmentsScale } from '../utils';
+
+type ArcSegmentsSweepIn = {
+ from?: number;
+ to?: number;
+ cornerRadius?: number | string;
+ arc: ArcOut;
+ segments: {
+ count: number;
+ padAngle: number;
+ cornerRadius?: number | string;
+ };
+};
+
+type ArcSegmentsSweepOut = {
+ d: string;
+}[];
+
+export function generateArcSegmentsSweep(
+ params: ArcSegmentsSweepIn
+): ArcSegmentsSweepOut {
+ const segmentScale = getValueSegmentsScale({
+ value: {
+ min: params.arc.value.min,
+ max: params.arc.value.max,
+ },
+ segments: params.segments.count,
+ });
+
+ const from = params.from ?? params.arc.value.min;
+ const to = params.to ?? params.arc.value.max;
+
+ const startSegment = segmentScale(from);
+ const endSegment = segmentScale(to);
+
+ const outerRadius = params.arc.radius;
+ const innerRadius = outerRadius * params.arc.ratio;
+ const width = outerRadius - innerRadius;
+ const cornerRadiusWide =
+ params.cornerRadius ??
+ params.segments.cornerRadius ??
+ DEFAULT_CORNER_RADIUS;
+ const cornerRadius = getNumericalValue(cornerRadiusWide, width);
+
+ const arcGenerator = d3Arc().cornerRadius(cornerRadius);
+
+ // value doesn't matter, because the data is not used
+ const data = Array.from({ length: params.segments.count }, () => 1);
+ const pieGenerator = d3Pie()
+ .padAngle(params.segments.padAngle)
+ .startAngle(params.arc.angle.min)
+ .endAngle(params.arc.angle.max);
+ const pieArcs = pieGenerator(data);
+ const pieArcSweeps = pieArcs
+ .filter(
+ (datum) =>
+ datum.index + 1 >= startSegment && datum.index + 1 <= endSegment
+ )
+ .map(
+ (datum) =>
+ arcGenerator({
+ outerRadius,
+ innerRadius,
+ startAngle: datum.startAngle,
+ endAngle: datum.endAngle,
+ padAngle: datum.padAngle,
+ // cast out `null` because the rendering context is not given
+ }) as string
+ );
+
+ const dMultiple = pieArcSweeps.map((d) => ({ d }));
+
+ return dMultiple;
+}
+
+const DEFAULT_CORNER_RADIUS = 0;
diff --git a/packages/components/src/ArcSegmentsSweep/index.ts b/packages/components/src/ArcSegmentsSweep/index.ts
new file mode 100644
index 00000000..de5bcaf3
--- /dev/null
+++ b/packages/components/src/ArcSegmentsSweep/index.ts
@@ -0,0 +1,3 @@
+export * from './ArcSegmentsSweep';
+export * from './ArcSegmentsSweepProps';
+export * from './generateArcSegmentsSweep';
diff --git a/packages/components/src/ArcSweep/ArcSweep.stories.tsx b/packages/components/src/ArcSweep/ArcSweep.stories.tsx
index 42cd4fcf..51b72244 100644
--- a/packages/components/src/ArcSweep/ArcSweep.stories.tsx
+++ b/packages/components/src/ArcSweep/ArcSweep.stories.tsx
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
import { ArcSweep } from './ArcSweep';
import { Chart } from '../Chart';
import { generateArc } from '../Arc/generateArc';
diff --git a/packages/components/src/Arcs/Arcs.stories.tsx b/packages/components/src/Arcs/Arcs.stories.tsx
index 45e6ed5b..9ef25deb 100644
--- a/packages/components/src/Arcs/Arcs.stories.tsx
+++ b/packages/components/src/Arcs/Arcs.stories.tsx
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
import { Arc } from '../Arc';
import { Arcs } from './Arcs';
import { ArcSweep } from '../ArcSweep';
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts
index 24fb4461..de93513a 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -1,5 +1,7 @@
export * from './Arc';
export * from './ArcCircle';
+export * from './ArcSegments';
+export * from './ArcSegmentsSweep';
export * from './ArcSweep';
export * from './ArcUnitLabel';
export * from './Arcs';
diff --git a/packages/components/src/utils/arc-segments.ts b/packages/components/src/utils/arc-segments.ts
new file mode 100644
index 00000000..495d9063
--- /dev/null
+++ b/packages/components/src/utils/arc-segments.ts
@@ -0,0 +1,34 @@
+import {
+ ScaleLinear as D3ScaleLinear,
+ scaleLinear as d3ScaleLinear,
+} from 'd3-scale';
+import { Value } from './value';
+
+/**
+ * A amount of segments. Non-negative integer.
+ */
+export type Segments = number;
+
+/**
+ * A linear, continuous scale that maps the domain of values to the range of segments.
+ */
+export type ValueSegmentsScale = D3ScaleLinear;
+
+/**
+ * Construct a linear, continuous scale that maps the domain of values to an amount of segments.
+ */
+export const getValueSegmentsScale = (params: {
+ value: {
+ min: Value;
+ max: Value;
+ };
+ segments: Segments;
+}): ValueSegmentsScale => {
+ const domain = [params.value.min, params.value.max];
+
+ const range = [0, params.segments];
+
+ const scale = d3ScaleLinear().domain(domain).range(range);
+
+ return scale;
+};
diff --git a/packages/components/src/utils/index.ts b/packages/components/src/utils/index.ts
index 046e1001..74a92453 100644
--- a/packages/components/src/utils/index.ts
+++ b/packages/components/src/utils/index.ts
@@ -1,5 +1,6 @@
export * from './arc';
export * from './arc-circle';
+export * from './arc-segments';
export * from './chart';
export * from './circle';
export * from './scale';