diff --git a/packages/dev/src/examples/misc/donut/half-donut-full-height/index.tsx b/packages/dev/src/examples/misc/donut/half-donut-full-height/index.tsx
new file mode 100644
index 000000000..3928a5220
--- /dev/null
+++ b/packages/dev/src/examples/misc/donut/half-donut-full-height/index.tsx
@@ -0,0 +1,37 @@
+import React, { useEffect, useState } from 'react'
+import { VisSingleContainer, VisDonut } from '@unovis/react'
+import { DONUT_HALF_ANGLE_RANGES } from '@unovis/ts'
+
+export const title = 'Half Donut: Full Height'
+export const subTitle = 'Testing the resize behavior'
+export const component = (): JSX.Element => {
+ const data = [3, 2, 5, 4, 0, 1]
+
+ const [currentAngleRange, setCurrentAngleRange] = useState(DONUT_HALF_ANGLE_RANGES[0])
+
+ // Cycle through the half-donut angle ranges for testing
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setCurrentAngleRange((prev: [number, number]) => {
+ const currentIndex = DONUT_HALF_ANGLE_RANGES.indexOf(prev)
+ return DONUT_HALF_ANGLE_RANGES[(currentIndex + 1) % DONUT_HALF_ANGLE_RANGES.length]
+ })
+ }, 2000)
+ return () => clearInterval(interval)
+ }, [])
+
+ return (
+
+ d}
+ data={data}
+ padAngle={0.02}
+ duration={0}
+ arcWidth={80}
+ angleRange={currentAngleRange}
+ centralLabel="Central Label"
+ centralSubLabel="Sub-label"
+ />
+
+ )
+}
diff --git a/packages/ts/src/components.ts b/packages/ts/src/components.ts
index 24a8812eb..e45878f83 100644
--- a/packages/ts/src/components.ts
+++ b/packages/ts/src/components.ts
@@ -25,7 +25,14 @@ export { LeafletFlowMap } from './components/leaflet-flow-map'
export { ChordDiagram } from './components/chord-diagram'
export { Graph } from './components/graph'
export { VisControls } from './components/vis-controls'
-export { Donut } from './components/donut'
+export {
+ Donut,
+ DONUT_HALF_ANGLE_RANGE_TOP,
+ DONUT_HALF_ANGLE_RANGE_RIGHT,
+ DONUT_HALF_ANGLE_RANGE_BOTTOM,
+ DONUT_HALF_ANGLE_RANGE_LEFT,
+ DONUT_HALF_ANGLE_RANGES,
+} from './components/donut'
export { FreeBrush } from './components/free-brush'
export { XYLabels } from './components/xy-labels'
export { NestedDonut } from './components/nested-donut'
diff --git a/packages/ts/src/components/donut/config.ts b/packages/ts/src/components/donut/config.ts
index ed3b5e807..538692e42 100644
--- a/packages/ts/src/components/donut/config.ts
+++ b/packages/ts/src/components/donut/config.ts
@@ -42,6 +42,12 @@ export interface DonutConfigInterface extends ComponentConfigInterface {
showBackground?: boolean;
/** Background angle range. When undefined, the value will be taken from `angleRange`. Default: `undefined` */
backgroundAngleRange?: [number, number];
+
+ /** Vertical offset of the half-donut label. */
+ halfDonutLabelOffsetY?: number;
+
+ /** Horizontal offset of the half-donut label. */
+ halfDonutLabelOffsetX?: number;
}
export const DonutDefaultConfig: DonutConfigInterface = {
diff --git a/packages/ts/src/components/donut/index.ts b/packages/ts/src/components/donut/index.ts
index 860668f5b..d9a5cc4c9 100644
--- a/packages/ts/src/components/donut/index.ts
+++ b/packages/ts/src/components/donut/index.ts
@@ -25,6 +25,22 @@ import { createArc, updateArc, removeArc } from './modules/arc'
// Styles
import * as s from './style'
+// Constants that support half donuts
+export const DONUT_HALF_ANGLE_RANGES = Array.from({ length: 4 }, (_, i): [number, number] => {
+ const offset = -Math.PI / 2 + i * Math.PI / 2
+ return [offset, offset + Math.PI]
+})
+
+export const [
+ DONUT_HALF_ANGLE_RANGE_TOP,
+ DONUT_HALF_ANGLE_RANGE_RIGHT,
+ DONUT_HALF_ANGLE_RANGE_BOTTOM,
+ DONUT_HALF_ANGLE_RANGE_LEFT,
+] = DONUT_HALF_ANGLE_RANGES
+
+const DONUT_HALF_LABEL_OFFSET_Y_DEFAULT = 20
+const DONUT_HALF_LABEL_OFFSET_X_DEFAULT = 60
+
export class Donut extends ComponentCore> {
static selectors = s
protected _defaultConfig = DonutDefaultConfig as DonutConfigInterface
@@ -68,9 +84,46 @@ export class Donut extends ComponentCore config.showEmptySegments || getNumber(d.datum, config.value, d.index))
const duration = isNumber(customDuration) ? customDuration : config.duration
- const outerRadius = config.radius || Math.min(this._width - bleed.left - bleed.right, this._height - bleed.top - bleed.bottom) / 2
+
+ // Handle half-donut cases, which adjust the scaling and positioning.
+ // One of these is true if we are dealing with a half-donut.
+ const [
+ isHalfDonutTop,
+ isHalfDonutRight,
+ isHalfDonutBottom,
+ isHalfDonutLeft,
+ ] = DONUT_HALF_ANGLE_RANGES.map(angleRange =>
+ config.angleRange && (
+ config.angleRange[0] === angleRange[0] &&
+ config.angleRange[1] === angleRange[1]
+ )
+ )
+ const isVerticalHalfDonut = isHalfDonutTop || isHalfDonutBottom
+ const isHorizontalHalfDonut = isHalfDonutRight || isHalfDonutLeft
+
+ // Compute the bounding box of the donut,
+ // considering it may be a half-donut
+ const width = this._width * (isHorizontalHalfDonut ? 2 : 1)
+ const height = this._height * (isVerticalHalfDonut ? 2 : 1)
+
+ const outerRadius = config.radius || Math.min(width - bleed.left - bleed.right, height - bleed.top - bleed.bottom) / 2
const innerRadius = config.arcWidth === 0 ? 0 : clamp(outerRadius - config.arcWidth, 0, outerRadius - 1)
+ const translateY = this._height / 2 + (isHalfDonutTop ? outerRadius / 2 : isHalfDonutBottom ? -outerRadius / 2 : 0)
+ const translateX = this._width / 2 + (isHalfDonutLeft ? outerRadius / 2 : isHalfDonutRight ? -outerRadius / 2 : 0)
+ const translate = `translate(${translateX},${translateY})`
+
+ const {
+ halfDonutLabelOffsetY = DONUT_HALF_LABEL_OFFSET_Y_DEFAULT,
+ halfDonutLabelOffsetX = DONUT_HALF_LABEL_OFFSET_X_DEFAULT,
+ } = config
+
+ const labelTranslateY = isHalfDonutTop ? -halfDonutLabelOffsetY : isHalfDonutBottom ? halfDonutLabelOffsetY : 0
+ const labelTranslateX = isHalfDonutLeft ? -halfDonutLabelOffsetX : isHalfDonutRight ? halfDonutLabelOffsetX : 0
+ const labelTranslate = `translate(${translateX + labelTranslateX},${translateY + labelTranslateY})`
+
+ this.arcGroup.attr('transform', translate)
+
this.arcGen
.startAngle(d => d.startAngle)
.endAngle(d => d.endAngle)
@@ -86,8 +139,6 @@ export class Donut extends ComponentCore getNumber(d.datum, config.value, d.index) || 0)
.sort((a, b) => config.sortFunction?.(a.datum, b.datum))
- this.arcGroup.attr('transform', `translate(${this._width / 2},${this._height / 2})`)
-
const arcData: DonutArcDatum[] = pieGen(data).map(d => {
const arc = {
...d,
@@ -123,12 +174,12 @@ export class Donut extends ComponentCore extends ComponentCore