Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Half donut label offsets #6

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 (
<VisSingleContainer style={{ height: '100%' }} >
<VisDonut
value={d => d}
data={data}
padAngle={0.02}
duration={0}
arcWidth={80}
angleRange={currentAngleRange}
centralLabel="Central Label"
centralSubLabel="Sub-label"
/>
</VisSingleContainer>
)
}
9 changes: 8 additions & 1 deletion packages/ts/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
6 changes: 6 additions & 0 deletions packages/ts/src/components/donut/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export interface DonutConfigInterface<Datum> 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<unknown> = {
Expand Down
63 changes: 57 additions & 6 deletions packages/ts/src/components/donut/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Datum> extends ComponentCore<Datum[], DonutConfigInterface<Datum>> {
static selectors = s
protected _defaultConfig = DonutDefaultConfig as DonutConfigInterface<Datum>
Expand Down Expand Up @@ -68,9 +84,46 @@ export class Donut<Datum> extends ComponentCore<Datum[], DonutConfigInterface<Da
.filter(d => 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)
Expand All @@ -86,8 +139,6 @@ export class Donut<Datum> extends ComponentCore<Datum[], DonutConfigInterface<Da
.value(d => 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<Datum>[] = pieGen(data).map(d => {
const arc = {
...d,
Expand Down Expand Up @@ -123,12 +174,12 @@ export class Donut<Datum> extends ComponentCore<Datum[], DonutConfigInterface<Da

// Label
this.centralLabel
.attr('transform', `translate(${this._width / 2},${this._height / 2})`)
.attr('transform', labelTranslate)
.attr('dy', config.centralSubLabel ? '-0.55em' : null)
.text(config.centralLabel ?? null)

this.centralSubLabel
.attr('transform', `translate(${this._width / 2},${this._height / 2})`)
.attr('transform', labelTranslate)
.attr('dy', config.centralLabel ? '0.55em' : null)
.text(config.centralSubLabel ?? null)

Expand All @@ -137,7 +188,7 @@ export class Donut<Datum> extends ComponentCore<Datum[], DonutConfigInterface<Da
// Background
this.arcBackground.attr('class', s.background)
.attr('visibility', config.showBackground ? null : 'hidden')
.attr('transform', `translate(${this._width / 2},${this._height / 2})`)
.attr('transform', translate)

smartTransition(this.arcBackground, duration)
.attr('d', this.arcGen({
Expand Down