Skip to content

Commit

Permalink
Component | Annotations: New component
Browse files Browse the repository at this point in the history
  • Loading branch information
reb-dev committed Mar 29, 2024
1 parent 33c38f0 commit 071fde1
Show file tree
Hide file tree
Showing 34 changed files with 687 additions and 121 deletions.
3 changes: 3 additions & 0 deletions packages/angular/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export * from './core'
export { VisAreaComponent } from './components/area/area.component'
export { VisAreaModule } from './components/area/area.module'

export { VisAnnotationsComponent } from './components/annotations/annotations.component'
export { VisAnnotationsModule } from './components/annotations/annotations.module'

export { VisAxisComponent } from './components/axis/axis.component'
export { VisAxisModule } from './components/axis/axis.module'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,65 @@
// !!! This code was automatically generated. You should not change it !!!
import { Component, AfterViewInit, Input, SimpleChanges } from '@angular/core'
import { Annotations, AnnotationsConfigInterface, ContainerCore, AnnotationItem } from '@unovis/ts'
import { VisXYComponent } from '../../core'
import { Annotations, AnnotationsConfigInterface, ContainerCore, VisEventType, VisEventCallback, AnnotationItem } from '@unovis/ts'
import { VisGenericComponent } from '../../core'

@Component({
selector: 'vis-annotations',
template: '',
// eslint-disable-next-line no-use-before-define
providers: [{ provide: VisXYComponent, useExisting: VisAnnotationsComponent }],
providers: [{ provide: VisGenericComponent, useExisting: VisAnnotationsComponent }],
})
export class VisAnnotationsComponent implements AnnotationsConfigInterface, AfterViewInit {
/** Animation duration of the data update transitions in milliseconds. Default: `600` */
@Input() duration?: number

/** Events configuration. An object containing properties in the following format:
*
* ```
* {
* \[selectorString]: {
* \[eventType]: callbackFunction
* }
* }
* ```
* e.g.:
* ```
* {
* \[Area.selectors.area]: {
* click: (d) => console.log("Clicked Area", d)
* }
* }
* ``` */
@Input() events?: {
[selector: string]: {
[eventType in VisEventType]?: VisEventCallback
};
}

/** You can set every SVG and HTML visualization object to have a custom DOM attributes, which is useful
* when you want to do unit or end-to-end testing. Attributes configuration object has the following structure:
*
* ```
* {
* \[selectorString]: {
* \[attributeName]: attribute constant value or accessor function
* }
* }
* ```
* e.g.:
* ```
* {
* \[Area.selectors.area]: {
* "test-value": d => d.value
* }
* }
* ``` */
@Input() attributes?: {
[selector: string]: {
[attr: string]: string | number | boolean | ((datum: any) => string | number | boolean);
};
}

/** Legend items. Array of `AnnotationItem`:
* ```
* {
Expand All @@ -24,29 +74,22 @@ export class VisAnnotationsComponent implements AnnotationsConfigInterface, Afte
* To learn more, see our docs https://unovis.dev/docs/auxiliary/Annotations/
* Default: `[]` */
@Input() items: AnnotationItem[] | undefined
@Input() data: Datum[]

component: Annotations | undefined
public componentContainer: ContainerCore | undefined

ngAfterViewInit (): void {
this.component = new Annotations(this.getConfig())

if (this.data) {
this.component.setData(this.data)
this.componentContainer?.render()
}
}

ngOnChanges (changes: SimpleChanges): void {
if (changes.data) { this.component?.setData(this.data) }
this.component?.setConfig(this.getConfig())
this.componentContainer?.render()
}

private getConfig (): AnnotationsConfigInterface {
const { items } = this
const config = { items }
const { duration, events, attributes, items } = this
const config = { duration, events, attributes, items }
const keys = Object.keys(config) as (keyof AnnotationsConfigInterface)[]
keys.forEach(key => { if (config[key] === undefined) delete config[key] })

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
ChordInputLink,
VisEventType,
VisEventCallback,
ColorAccessor,
ChordLinkDatum,
NumericAccessor,
ChordNodeDatum,
ColorAccessor,
ChordLinkDatum,
StringAccessor,
GenericAccessor,
ChordLabelAlignment,
Expand Down Expand Up @@ -75,6 +75,12 @@ export class VisChordDiagramComponent<N extends ChordInputNode, L extends ChordI
};
}

/** Angular range of the diagram. Default: `[0, 2 * Math.PI]` */
@Input() angleRange?: [number, number]

/** Corner radius constant value or accessor function. Default: `2` */
@Input() cornerRadius?: NumericAccessor<ChordNodeDatum<N>>

/** Node id or index to highlight. Overrides default hover behavior if supplied. Default: `undefined` */
@Input() highlightedNodeId?: number | string

Expand Down Expand Up @@ -108,12 +114,6 @@ export class VisChordDiagramComponent<N extends ChordInputNode, L extends ChordI
/** Pad angle in radians. Default: `0.02` */
@Input() padAngle?: number

/** Corner radius constant value or accessor function. Default: `2` */
@Input() cornerRadius?: NumericAccessor<ChordNodeDatum<N>>

/** Angular range of the diagram. Default: `[0, 2 * Math.PI]` */
@Input() angleRange?: [number, number]

/** The exponent property of the radius scale. Default: `2` */
@Input() radiusScaleExponent?: number
@Input() data: { nodes: N[]; links?: L[] }
Expand All @@ -137,8 +137,8 @@ export class VisChordDiagramComponent<N extends ChordInputNode, L extends ChordI
}

private getConfig (): ChordDiagramConfigInterface<N, L> {
const { duration, events, attributes, highlightedNodeId, highlightedLinkIds, linkColor, linkValue, nodeLevels, nodeWidth, nodeColor, nodeLabel, nodeLabelColor, nodeLabelAlignment, padAngle, cornerRadius, angleRange, radiusScaleExponent } = this
const config = { duration, events, attributes, highlightedNodeId, highlightedLinkIds, linkColor, linkValue, nodeLevels, nodeWidth, nodeColor, nodeLabel, nodeLabelColor, nodeLabelAlignment, padAngle, cornerRadius, angleRange, radiusScaleExponent }
const { duration, events, attributes, angleRange, cornerRadius, highlightedNodeId, highlightedLinkIds, linkColor, linkValue, nodeLevels, nodeWidth, nodeColor, nodeLabel, nodeLabelColor, nodeLabelAlignment, padAngle, radiusScaleExponent } = this
const config = { duration, events, attributes, angleRange, cornerRadius, highlightedNodeId, highlightedLinkIds, linkColor, linkValue, nodeLevels, nodeWidth, nodeColor, nodeLabel, nodeLabelColor, nodeLabelAlignment, padAngle, radiusScaleExponent }
const keys = Object.keys(config) as (keyof ChordDiagramConfigInterface<N, L>)[]
keys.forEach(key => { if (config[key] === undefined) delete config[key] })

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ export class VisNestedDonutComponent<Datum> implements NestedDonutConfigInterfac

/** Show labels for individual segments. Default: `true` */
@Input() showSegmentLabels?: boolean

@Input() data: Datum[]

component: NestedDonut<Datum> | undefined
Expand All @@ -157,8 +156,8 @@ export class VisNestedDonutComponent<Datum> implements NestedDonutConfigInterfac
}

private getConfig (): NestedDonutConfigInterface<Datum> {
const { duration, events, attributes, angleRange, direction, value, centralLabel, centralSubLabel, centralSubLabelWrap, showBackground, sort, layers, layerSettings, layerPadding, cornerRadius, emptySegmentAngle, hideOverflowingSegmentLabels, segmentColor, segmentLabel, segmentLabelColor, showEmptySegments } = this
const config = { duration, events, attributes, angleRange, direction, value, centralLabel, centralSubLabel, centralSubLabelWrap, showBackground, sort, layers, layerSettings, layerPadding, cornerRadius, emptySegmentAngle, hideOverflowingSegmentLabels, segmentColor, segmentLabel, segmentLabelColor, showEmptySegments }
const { duration, events, attributes, angleRange, direction, value, centralLabel, centralSubLabel, centralSubLabelWrap, showBackground, sort, layers, layerSettings, layerPadding, cornerRadius, emptySegmentAngle, hideOverflowingSegmentLabels, segmentColor, segmentLabel, segmentLabelColor, showEmptySegments, showSegmentLabels } = this
const config = { duration, events, attributes, angleRange, direction, value, centralLabel, centralSubLabel, centralSubLabelWrap, showBackground, sort, layers, layerSettings, layerPadding, cornerRadius, emptySegmentAngle, hideOverflowingSegmentLabels, segmentColor, segmentLabel, segmentLabelColor, showEmptySegments, showSegmentLabels }
const keys = Object.keys(config) as (keyof NestedDonutConfigInterface<Datum>)[]
keys.forEach(key => { if (config[key] === undefined) delete config[key] })

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Component, ViewChild, ElementRef, AfterViewInit, Input, OnDestroy, SimpleChanges, ContentChild } from '@angular/core'

// Vis
import { ComponentCore, SingleContainer, SingleContainerConfigInterface, Tooltip, Spacing } from '@unovis/ts'
import { ComponentCore, SingleContainer, SingleContainerConfigInterface, Tooltip, Spacing, Annotations } from '@unovis/ts'
import { VisCoreComponent } from '../../core'
import { VisTooltipComponent } from '../../components/tooltip/tooltip.component'
import { VisAnnotationsComponent } from '../../components/annotations/annotations.component'

@Component({
selector: 'vis-single-container',
Expand All @@ -16,6 +17,7 @@ export class VisSingleContainerComponent<Data = unknown, C extends ComponentCore
@ViewChild('container', { static: false }) containerRef: ElementRef
@ContentChild(VisCoreComponent) visComponent: VisCoreComponent
@ContentChild(VisTooltipComponent) tooltipComponent: VisTooltipComponent
@ContentChild(VisAnnotationsComponent) annotationsComponent: VisAnnotationsComponent

/** Width in pixels. By default, Container automatically fits to the size of the parent element. Default: `undefined`. */
@Input() width?: number
Expand Down Expand Up @@ -57,8 +59,9 @@ export class VisSingleContainerComponent<Data = unknown, C extends ComponentCore

const component = this.visComponent?.component as C
const tooltip = this.tooltipComponent?.component as Tooltip
const annotations = this.annotationsComponent?.component as Annotations

return { width, height, duration, margin, component, tooltip, ariaLabel }
return { width, height, duration, margin, component, tooltip, ariaLabel, annotations }
}

ngOnDestroy (): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import {
} from '@angular/core'

// Vis
import { Axis, ContinuousScale, Crosshair, Direction, Spacing, Tooltip, XYContainer, XYContainerConfigInterface } from '@unovis/ts'
import { Annotations, Axis, ContinuousScale, Crosshair, Direction, Spacing, Tooltip, XYContainer, XYContainerConfigInterface } from '@unovis/ts'
import { VisXYComponent } from '../../core'
import { VisTooltipComponent } from '../../components/tooltip/tooltip.component'
import { VisAnnotationsComponent } from '../../components/annotations/annotations.component'

@Component({
selector: 'vis-xy-container',
Expand All @@ -28,6 +29,7 @@ export class VisXYContainerComponent<Datum> implements AfterViewInit, AfterConte
@ViewChild('container', { static: false }) containerRef: ElementRef
@ContentChildren(VisXYComponent) visComponents: QueryList<VisXYComponent>
@ContentChild(VisTooltipComponent) tooltipComponent: VisTooltipComponent
@ContentChild(VisAnnotationsComponent) annotationsComponent: VisAnnotationsComponent

/** Width in pixels. By default, Container automatically fits to the size of the parent element. Default: `undefined`. */
@Input() width?: number
Expand Down Expand Up @@ -148,6 +150,7 @@ export class VisXYContainerComponent<Datum> implements AfterViewInit, AfterConte
const tooltip = this.tooltipComponent?.component as Tooltip
const xAxis = visComponents.find(c => c instanceof Axis && c?.config?.type === 'x') as Axis<Datum>
const yAxis = visComponents.find(c => c instanceof Axis && c?.config?.type === 'y') as Axis<Datum>
const annotations = this.annotationsComponent?.component as Annotations

const components = visComponents.filter(c => !(c instanceof Crosshair) && !(c instanceof Tooltip) && !(c instanceof Axis))

Expand All @@ -162,6 +165,7 @@ export class VisXYContainerComponent<Datum> implements AfterViewInit, AfterConte
yAxis,
tooltip,
crosshair,
annotations,
scaleByDomain,
autoMargin,
xScale,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useEffect, useRef, useState } from 'react'
import { VisXYContainer, VisLine, VisAxis, VisAnnotations, VisLineRef } from '@unovis/react'
import { AnnotationItem } from '@unovis/ts'

export const title = 'Basic Annotations'
export const subTitle = 'Dynamic Data Updates'

export const component = (): JSX.Element => {
const height = 400
const length = 10
const min = 3
const max = 8
const generateData = (): number[] => Array.from({ length }, () => Math.random() * (max - min) + min)

const ref = useRef<VisLineRef<number>>(null)
const [data, setData] = useState<number[]>(generateData)
const [annotations, setAnnotations] = useState<AnnotationItem[]>([])

useEffect(() => {
if (!ref.current?.component) return
const { xScale, yScale } = ref.current.component
const peaks = data.reduce((acc, y, x) => {
acc.min = Math.min(acc.min, y)
acc.max = Math.max(acc.max, y)
acc.total += y
if (x === data.length - 1) {
acc.items.push({
x: 20,
y: yScale(min) + 10,
content: [
{ text: 'Stats', fontSize: 20, fontWeight: 700, color: '#1C72E8' },
{ text: `Min: ${acc.min}`, fontSize: 14 },
{ text: `Max: ${acc.max}`, fontSize: 14 },
{ text: `Range: ~${(acc.max - acc.min).toFixed(2)}`, fontSize: 14 },
{ text: `Mean: ~${(acc.total / data.length).toFixed(2)}`, fontSize: 14 },
],
}, {
x: '102%',
y: yScale(acc.min),
content: 'Min',
verticalAlign: 'middle',
textAlign: 'left',
subject: {
x: () => xScale(data.indexOf(acc.min)),
y: () => yScale(acc.min),
connectorLineStrokeDasharray: '2 2',
},
})
} else if (y > data[x - 1] && y > data[x + 1]) {
acc.items.push({
x: `${(x / 9 * 100)}%`, //
y: yScale(y + 1.5),
content: `Peak: [x: ${x}, y: ${y.toFixed(1)}]`,
verticalAlign: 'bottom',
textAlign: 'center',
subject: {
x: () => xScale(x),
y: () => yScale(y),
radius: 6,
},
})
}
return acc
}, { min: max, max: min, total: 0, items: new Array<AnnotationItem>() })
setAnnotations(peaks.items)
}, [data, ref.current?.component])


return (
<>
<button onClick={() => setData(generateData())}>Update Data</button>
<VisXYContainer data={data} height={height} margin={{ right: 100 }} yDomain={[0, 10]}>
<VisLine ref={ref} x={(_, i) => i as number} y={d => d} />
<VisAxis type='x' numTicks={5} />
<VisAxis type='y' numTicks={5} />
<VisAnnotations items={annotations}/>
</VisXYContainer>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useCallback, useMemo, useEffect } from 'react'
import { VisAnnotations, VisSingleContainer, VisNestedDonut } from '@unovis/react'
import { Annotations, StringAccessor } from '@unovis/ts'

export const title = 'Single Container'
export const subTitle = 'Donut with annotation button'

export const component = (): JSX.Element => {
const data = [
{ group: 'A', subgroup: 'A0', value: 10 },
{ group: 'A', subgroup: 'A0', value: 5 },
{ group: 'A', subgroup: 'A1', value: 2 },
{ group: 'A', subgroup: 'A1', value: 1 },
{ group: 'A', subgroup: 'A1', value: 1 },
{ group: 'A', subgroup: 'A2', value: 15 },
{ group: 'B', value: 15 },
{ group: 'C', subgroup: 'C0', value: 3 },
{ group: 'C', subgroup: 'C1', value: 1 },
]
type Datum = typeof data[0]
const height = 250
const [annotations, setAnnotations] = React.useState<AnnotationItem[]>([])
const [layers, setLayers] = React.useState<StringAccessor<Datum>[]>()
const [expanded, setExpandedState] = React.useState(false)

useEffect(() => {
setAnnotations([{
x: '50%',
y: height / 2 + 15 + 8,
cursor: 'pointer',
textAlign: 'center',
content: { text: expanded ? 'Hide' : 'Show', color: '#3355ee', fontSize: 11 },
}])
if (expanded) {
setLayers([d => d.group, d => d.subgroup ?? null])
} else {
setLayers([d => d.group])
}
}, [expanded])

const events = useMemo(() => ({
[Annotations.selectors.annotationContent]: {
click: () => setExpandedState(!expanded),
},
}), [expanded])
return (
<>
<VisSingleContainer data={data} height={height}>
<VisNestedDonut
showBackground={true}
layers={layers}
value={useCallback((d: Datum) => d.value, [])}
layerSettings={{ width: expanded ? 25 : 50 }}
showSegmentLabels={false}
centralLabel={'Label Text'} centralSubLabel={'Sub-label'}/>
<VisAnnotations events={events} items={annotations}/>
</VisSingleContainer>
</>
)
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { generateNestedData, NestedDatum } from '@src/utils/data'
export const title = 'Interactive Nested Donut'
export const subTitle = 'Click on node to toggle subchart'

const defaultData = generateNestedData(100, 3, ['A1', 'B0', 'B1', 'B2'])
const defaultData = generateNestedData(100, 3)//, ['A1', 'B0', 'B1', 'B2'])

export const component = (): JSX.Element => {
const [data, setData] = useState<NestedDatum[]>(defaultData)
Expand Down

0 comments on commit 071fde1

Please sign in to comment.