diff --git a/packages/angular/src/components/radial-bar/radial-bar.component.ts b/packages/angular/src/components/radial-bar/radial-bar.component.ts index e73d1b8ff..412e70a52 100644 --- a/packages/angular/src/components/radial-bar/radial-bar.component.ts +++ b/packages/angular/src/components/radial-bar/radial-bar.component.ts @@ -63,7 +63,8 @@ export class VisRadialBarComponent implements RadialBarConfigInterface d.id ?? i` */ @Input() id?: ((d: Datum, i: number, ...rest) => string | number) - /** Value accessor function. Default: `undefined` */ + /** Value accessor function. Returning `null` or `undefined` marks the value as missing, + * and the corresponding bar will not be rendered. Default: `undefined` */ @Input() value: NumericAccessor /** Maximum value accessor or an array of maximums (indexed by each datum's original position in `data` before sorting). @@ -77,6 +78,12 @@ export class VisRadialBarComponent implements RadialBarConfigInterface number @@ -141,8 +148,8 @@ export class VisRadialBarComponent implements RadialBarConfigInterface { - const { duration, events, attributes, id, value, maxValue, angleRange, padAngle, sortFunction, cornerRadius, color, radius, trackWidth, trackPadding, reverseOrder, centralLabel, centralSubLabel, centralSubLabelWrap, centralLabelOffsetX, centralLabelOffsetY, showBackground, backgroundAngleRange } = this - const config = { duration, events, attributes, id, value, maxValue, angleRange, padAngle, sortFunction, cornerRadius, color, radius, trackWidth, trackPadding, reverseOrder, centralLabel, centralSubLabel, centralSubLabelWrap, centralLabelOffsetX, centralLabelOffsetY, showBackground, backgroundAngleRange } + const { duration, events, attributes, id, value, maxValue, angleRange, padAngle, barMinAngle, sortFunction, cornerRadius, color, radius, trackWidth, trackPadding, reverseOrder, centralLabel, centralSubLabel, centralSubLabelWrap, centralLabelOffsetX, centralLabelOffsetY, showBackground, backgroundAngleRange } = this + const config = { duration, events, attributes, id, value, maxValue, angleRange, padAngle, barMinAngle, sortFunction, cornerRadius, color, radius, trackWidth, trackPadding, reverseOrder, centralLabel, centralSubLabel, centralSubLabelWrap, centralLabelOffsetX, centralLabelOffsetY, showBackground, backgroundAngleRange } const keys = Object.keys(config) as (keyof RadialBarConfigInterface)[] keys.forEach(key => { if (config[key] === undefined) delete config[key] }) diff --git a/packages/dev/src/examples/misc/radial-bar/radial-bar-min-angle/index.tsx b/packages/dev/src/examples/misc/radial-bar/radial-bar-min-angle/index.tsx new file mode 100644 index 000000000..71ea1e8a5 --- /dev/null +++ b/packages/dev/src/examples/misc/radial-bar/radial-bar-min-angle/index.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import { VisSingleContainer, VisRadialBar } from '@unovis/react' +import { ExampleViewerDurationProps } from '@src/components/ExampleViewer/index' + +export const title = 'Radial Bar Min Angle' +export const subTitle = 'Keeping bars with tiny and zero values visible' + +type DataRecord = { key: string; value: number | null } + +const data: DataRecord[] = [ + { key: 'Errors', value: 0.05 }, + { key: 'Warnings', value: 12 }, + { key: 'Info', value: 64 }, + { key: 'Debug', value: 0 }, + { key: 'Trace', value: null }, // Missing data: the bar is not rendered +] + +const cases: { + label: string; + barMinAngle: number; + angleRange?: [number, number]; + padAngle?: number; +}[] = [ + { label: 'barMinAngle: 0 (disabled)', barMinAngle: 0 }, + { label: 'barMinAngle: 0.01 (default)', barMinAngle: 0.01 }, + { label: 'barMinAngle: 0.2', barMinAngle: 0.2 }, + { label: 'barMinAngle: 0.2, padAngle: 0.05', barMinAngle: 0.2, padAngle: 0.05 }, + { label: 'barMinAngle: 0.2, top half', barMinAngle: 0.2, angleRange: [-Math.PI / 2, Math.PI / 2] }, + { label: 'barMinAngle: 0.2, reversed range', barMinAngle: 0.2, angleRange: [0, -2 * Math.PI] }, + { label: 'barMinAngle: 10 → clamped to 2π', barMinAngle: 10 }, +] + +export const component = (props: ExampleViewerDurationProps): React.ReactNode => ( +
+ {cases.map(c => ( +
+
{c.label}
+ + + value={d => d.value} + maxValue={100} + data={data} + duration={props.duration} + angleRange={c.angleRange} + padAngle={c.padAngle} + barMinAngle={c.barMinAngle} + trackWidth={14} + trackPadding={4} + cornerRadius={7} + /> + +
+ ))} +
+) diff --git a/packages/ts/src/components/radial-bar/config.ts b/packages/ts/src/components/radial-bar/config.ts index 094c022d0..b0ab6ec8d 100644 --- a/packages/ts/src/components/radial-bar/config.ts +++ b/packages/ts/src/components/radial-bar/config.ts @@ -7,7 +7,9 @@ import { ColorAccessor, NumericAccessor } from '@/types/accessor' export interface RadialBarConfigInterface extends ComponentConfigInterface { /** Accessor function for getting the unique data record id. Used for more persistent data updates. Default: `(d, i) => d.id ?? i` */ id?: ((d: Datum, i: number, ...any: unknown[]) => string | number); - /** Value accessor function. Default: `undefined` */ + /** Value accessor function. Returning `null` or `undefined` marks the value as missing, + * and the corresponding bar will not be rendered. Default: `undefined` + */ value: NumericAccessor; /** Maximum value accessor or an array of maximums (indexed by each datum's original position in `data` before sorting). * Used to scale each bar's arc length: each bar fills `(value / maxValue) * (angleRange[1] - angleRange[0])`. @@ -18,6 +20,12 @@ export interface RadialBarConfigInterface extends ComponentConfigInterfac angleRange?: [number, number]; /** Pad angle in radians applied between the bar and its end. Default: `0` */ padAngle?: number; + /** Minimum bar angle in radians. Bars with small values, `0` included, will be extended to that angle, + * so that they remain visible. The value is clamped to the length of `angleRange`. + * Bars with missing values (see `value`) are not affected: they're never rendered. + * Set it to `0` to disable. Default: `0.01` (about 1 pixel wide on a ring of a 100 pixel radius) + */ + barMinAngle?: number; /** Custom sort function. Default: `undefined` */ sortFunction?: (a: Datum, b: Datum) => number; /** Corner Radius. Default: `0` */ @@ -53,11 +61,13 @@ export interface RadialBarConfigInterface extends ComponentConfigInterfac export const RadialBarDefaultConfig: RadialBarConfigInterface = { ...ComponentDefaultConfig, - id: (d: unknown, i: number): string | number => (d as { id: string }).id ?? i, + // Optional chaining because a data record itself can be `null` when the data has gaps + id: (d: unknown, i: number): string | number => (d as { id: string })?.id ?? i, value: undefined, maxValue: undefined, angleRange: [0, 2 * Math.PI], padAngle: 0, + barMinAngle: 0.01, sortFunction: undefined, cornerRadius: 0, color: undefined, diff --git a/packages/ts/src/components/radial-bar/index.ts b/packages/ts/src/components/radial-bar/index.ts index 2d3f4880c..b9b8b205f 100644 --- a/packages/ts/src/components/radial-bar/index.ts +++ b/packages/ts/src/components/radial-bar/index.ts @@ -86,6 +86,10 @@ export class RadialBar extends ComponentCore extends ComponentCore getNumber(d.datum, config.value, d.index) ?? 0) + // Resolve per-bar value and max. `null` and `undefined` are treated as missing data: + // such bars are not rendered at all, unlike `0` values, which still get `barMinAngle`. + const values = wrapped.map((d) => { + const value = getNumber(d.datum, config.value, d.index) + return isNumber(value) && isFinite(value) ? value : null + }) const dataMax = max(values) ?? 0 const maxValues = wrapped.map((d) => { const mv = config.maxValue @@ -130,7 +138,11 @@ export class RadialBar extends ComponentCore