Skip to content
Open
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
@@ -1,7 +1,17 @@
import { Component, ViewChild, ElementRef, AfterViewInit, Input, OnDestroy, SimpleChanges, ContentChild } from '@angular/core'
import { Component, ViewChild, ElementRef, AfterViewInit, EventEmitter, Input, OnDestroy, Output, SimpleChanges, ContentChild } from '@angular/core'

// Vis
import { ColorFunction, ComponentCore, SingleContainer, SingleContainerConfigInterface, Tooltip, Spacing, Annotations, Sizing } from '@unovis/ts'
import {
ColorFunction,
ComponentCore,
SingleContainer,
SingleContainerConfigInterface,
SingleContainerRenderPayload,
Tooltip,
Spacing,
Annotations,
Sizing,
} from '@unovis/ts'
import { VisCoreComponent } from '../../core'
import { VisTooltipComponent } from '../../components/tooltip/tooltip.component'
import { VisAnnotationsComponent } from '../../components/annotations/annotations.component'
Expand Down Expand Up @@ -54,7 +64,26 @@ export class VisSingleContainerComponent<Data = unknown, C extends ComponentCore
/** Data to be passed to the component. Default: `undefined`. */
@Input() data?: Data

/** Emitted once after the chart's first render completes. */
@Output() load = new EventEmitter<SingleContainerRenderPayload>()
/** Emitted after every render of the chart, including the first. */
@Output() render = new EventEmitter<SingleContainerRenderPayload>()
/** Emitted after every render except the first. */
@Output() redraw = new EventEmitter<SingleContainerRenderPayload>()

chart: SingleContainer<Data>
private _hasLoaded = false

private _onRenderComplete: SingleContainerConfigInterface<Data>['onRenderComplete'] = (svg, margin, containerWidth, containerHeight, componentWidth, componentHeight) => {
const payload: SingleContainerRenderPayload = { svg, margin, containerWidth, containerHeight, componentWidth, componentHeight }
this.render.emit(payload)
if (this._hasLoaded) {
this.redraw.emit(payload)
} else {
this._hasLoaded = true
this.load.emit(payload)
}
}

ngAfterViewInit (): void {
this.chart = new SingleContainer<Data>(this.containerRef.nativeElement, this.getConfig(), this.data)
Expand All @@ -80,7 +109,7 @@ export class VisSingleContainerComponent<Data = unknown, C extends ComponentCore
const tooltip = this.tooltipComponent?.component as Tooltip
const annotations = this.annotationsComponent?.component as Annotations

return { width, height, duration, margin, component, tooltip, ariaLabel, annotations, svgDefs, sizing, colorFunction }
return { width, height, duration, margin, component, tooltip, ariaLabel, annotations, svgDefs, sizing, colorFunction, onRenderComplete: this._onRenderComplete }
}

ngOnDestroy (): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
ContentChild,
ContentChildren,
ElementRef,
EventEmitter,
Input,
OnDestroy,
Output,
QueryList,
SimpleChanges,
ViewChild,
Expand All @@ -24,6 +26,7 @@ import {
Tooltip,
XYContainer,
XYContainerConfigInterface,
XYContainerRenderPayload,
} from '@unovis/ts'
import { VisXYComponent } from '../../core'
import { VisTooltipComponent } from '../../components/tooltip/tooltip.component'
Expand Down Expand Up @@ -135,7 +138,26 @@ export class VisXYContainerComponent<Datum> implements AfterViewInit, AfterConte
* have their individual data. Default: `undefined` */
@Input() data?: Datum[] | undefined = undefined

/** Emitted once after the chart's first render completes. */
@Output() load = new EventEmitter<XYContainerRenderPayload>()
/** Emitted after every render of the chart, including the first. */
@Output() render = new EventEmitter<XYContainerRenderPayload>()
/** Emitted after every render except the first. */
@Output() redraw = new EventEmitter<XYContainerRenderPayload>()

chart: XYContainer<Datum>
private _hasLoaded = false

private _onRenderComplete: XYContainerConfigInterface<Datum>['onRenderComplete'] = (svg, margin, bleed, containerWidth, containerHeight, componentWidth, componentHeight) => {
const payload: XYContainerRenderPayload = { svg, margin, bleed, containerWidth, containerHeight, componentWidth, componentHeight }
this.render.emit(payload)
if (this._hasLoaded) {
this.redraw.emit(payload)
} else {
this._hasLoaded = true
this.load.emit(payload)
}
}

ngAfterViewInit (): void {
this.chart = new XYContainer<Datum>(this.containerRef.nativeElement, this.getConfig(), this.data)
Expand Down Expand Up @@ -207,6 +229,7 @@ export class VisXYContainerComponent<Datum> implements AfterViewInit, AfterConte
yDirection,
ariaLabel,
colorFunction,
onRenderComplete: this._onRenderComplete,
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
<script lang="ts">
import { ComponentCore, SingleContainer, SingleContainerConfigInterface, Tooltip, Annotations } from '@unovis/ts'
import { ComponentCore, SingleContainer, SingleContainerConfigInterface, SingleContainerRenderPayload, Tooltip, Annotations } from '@unovis/ts'
import { arePropsEqual } from '../../utils/props'
import { onDestroy, setContext } from 'svelte'
import { createEventDispatcher, onDestroy, setContext } from 'svelte'

// Generics
type Data = $$Generic
interface $$Props extends SingleContainerConfigInterface<Data> {
data?: Data;
}

const dispatch = createEventDispatcher<{
load: SingleContainerRenderPayload;
render: SingleContainerRenderPayload;
redraw: SingleContainerRenderPayload;
}>()

let hasLoaded = false
const handleRenderComplete: SingleContainerConfigInterface<Data>['onRenderComplete'] = (svg, margin, containerWidth, containerHeight, componentWidth, componentHeight) => {
const userCallback = $$restProps.onRenderComplete as SingleContainerConfigInterface<Data>['onRenderComplete']
userCallback?.(svg, margin, containerWidth, containerHeight, componentWidth, componentHeight)
const payload: SingleContainerRenderPayload = { svg, margin, containerWidth, containerHeight, componentWidth, componentHeight }
dispatch('render', payload)
if (hasLoaded) {
dispatch('redraw', payload)
} else {
hasLoaded = true
dispatch('load', payload)
}
}

// Internal variables
let chart: SingleContainer<Data> | undefined
let ref: HTMLDivElement
Expand Down Expand Up @@ -36,7 +56,7 @@
export { className as class }
let config: SingleContainerConfigInterface<Data>
$: props = $$restProps as SingleContainerConfigInterface<Data>
$: config = { component, tooltip, annotations, ...props }
$: config = { component, tooltip, annotations, ...props, onRenderComplete: handleRenderComplete }

// Helpers
function initChart () {
Expand Down
47 changes: 31 additions & 16 deletions packages/svelte/src/containers/xy-container/xy-container.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
<script lang="ts">
import { XYContainer, XYComponentCore, XYContainerConfigInterface, Tooltip, Crosshair, Axis, Annotations } from '@unovis/ts'
import { onMount, setContext } from 'svelte'
import { XYContainer, XYComponentCore, XYContainerConfigInterface, XYContainerRenderPayload, Tooltip, Crosshair, Axis, Annotations } from '@unovis/ts'
import { createEventDispatcher, onMount, setContext } from 'svelte'

type Datum = $$Generic
interface $$Props extends XYContainerConfigInterface<Datum> {
data?: Datum[];
}

const dispatch = createEventDispatcher<{
load: XYContainerRenderPayload;
render: XYContainerRenderPayload;
redraw: XYContainerRenderPayload;
}>()

let hasLoaded = false
const handleRenderComplete: XYContainerConfigInterface<Datum>['onRenderComplete'] = (svg, margin, bleed, containerWidth, containerHeight, componentWidth, componentHeight) => {
const userCallback = $$restProps.onRenderComplete as XYContainerConfigInterface<Datum>['onRenderComplete']
userCallback?.(svg, margin, bleed, containerWidth, containerHeight, componentWidth, componentHeight)
const payload: XYContainerRenderPayload = { svg, margin, bleed, containerWidth, containerHeight, componentWidth, componentHeight }
dispatch('render', payload)
if (hasLoaded) {
dispatch('redraw', payload)
} else {
hasLoaded = true
dispatch('load', payload)
}
}

// Props
export let data: Datum[] = undefined
export let className = ''
Expand Down Expand Up @@ -40,17 +60,16 @@
$: chart?.setData(data, true)

let animationFrame = 0
const updateContainer = async () => {
// due to the order of events when a component is removed update container can be called
// while a component is being destroyed. This can lead to an error because we trigger an update
// with a destroyed component.
const updateContainer = () => {
// Filter destroyed components — removal order can leave stale refs that crash on update.
config.components = config.components?.filter((e) => !e.isDestroyed())

// we can't use animation frames in a non-browser environment
// No rAF in SSR.
if (typeof requestAnimationFrame === 'undefined') {
chart?.updateContainer({
...config,
...($$restProps as XYContainerConfigInterface<Datum>),
onRenderComplete: handleRenderComplete,
})

return
Expand All @@ -60,26 +79,22 @@
cancelAnimationFrame(animationFrame)
}

// this prevent multiple renders from happening in a single frame
// when a component is first rendered the components will be pushed 1 by 1
// so we don't want to rerender every time a component is added
// Coalesce sibling component pushes into a single frame.
animationFrame = requestAnimationFrame(() => {
chart?.updateContainer({
...config,
...($$restProps as XYContainerConfigInterface<Datum>),
onRenderComplete: handleRenderComplete,
})
animationFrame = 0
})
}

$: {
config
$$restProps
updateContainer()
}
// eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-floating-promises
$: { config; $$restProps; updateContainer() }

onMount(() => {
chart = new XYContainer(ref, config, data)
chart = new XYContainer(ref, { ...config, onRenderComplete: handleRenderComplete }, data)
return () => chart.destroy()
})

Expand Down
3 changes: 0 additions & 3 deletions packages/ts/src/components/topojson-map/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ import { NumericAccessor, ColorAccessor } from '@/types/accessor'
import { Rect } from '@/types/misc'
import { GenericDataRecord } from '@/types/data'

// Styles
import * as s from './style'

// Local Types
import {
AreaLabelDatum,
Expand Down
4 changes: 2 additions & 2 deletions packages/ts/src/containers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { ContainerCore } from './core/container'
export type { ContainerConfigInterface } from './core/container/config'
export { SingleContainer } from './containers/single-container'
export type { SingleContainerConfigInterface } from './containers/single-container/config'
export type { SingleContainerConfigInterface, SingleContainerRenderPayload } from './containers/single-container/config'
export { XYContainer } from './containers/xy-container'
export type { XYContainerConfigInterface } from './containers/xy-container/config'
export type { XYContainerConfigInterface, XYContainerRenderPayload } from './containers/xy-container/config'
9 changes: 9 additions & 0 deletions packages/ts/src/containers/single-container/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ import { Annotations } from '@/components/annotations'
// Types
import { Spacing } from '@/types/spacing'

export type SingleContainerRenderPayload = {
svg: SVGSVGElement;
margin: Spacing;
containerWidth: number;
containerHeight: number;
componentWidth: number;
componentHeight: number;
}

export interface SingleContainerConfigInterface<Datum> extends ContainerConfigInterface {
/** Visualization component. Default: `undefined` */
component?: ComponentCore<Datum>;
Expand Down
9 changes: 8 additions & 1 deletion packages/ts/src/containers/single-container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,14 @@ export class SingleContainer<Data> extends ContainerCore {
config.annotations?.render(duration)

if (config.tooltip) config.tooltip.update()
config.onRenderComplete?.(this.svg.node(), config.margin, this.containerWidth, this.containerHeight, this.width, this.height)
config.onRenderComplete?.(
this.svg.node(),
config.margin,
this.containerWidth,
this.containerHeight,
this.width,
this.height
)
}

// Re-defining the `render()` function to handle different sizing techniques (`Sizing.Extend` and `Sizing.FitWidth`)
Expand Down
10 changes: 10 additions & 0 deletions packages/ts/src/containers/xy-container/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ import { ContinuousScale } from '@/types/scale'
import { Direction } from '@/types/direction'
import { Spacing } from '@/types/spacing'

export type XYContainerRenderPayload = {
svg: SVGSVGElement;
margin: Spacing;
bleed: Spacing;
containerWidth: number;
containerHeight: number;
componentWidth: number;
componentHeight: number;
}

export interface XYContainerConfigInterface<Datum> extends ContainerConfigInterface {
/** An array of visualization components. Default: `[]` */
components?: XYComponentCore<Datum>[];
Expand Down
10 changes: 9 additions & 1 deletion packages/ts/src/containers/xy-container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,15 @@ export class XYContainer<Datum> extends ContainerCore {
config.annotations?.render()

this._firstRender = false
config.onRenderComplete?.(this.svg.node(), margin, this._getBleed(this.components), this.containerWidth, this.containerHeight, this.width, this.height)
config.onRenderComplete?.(
this.svg.node(),
margin,
this._getBleed(this.components),
this.containerWidth,
this.containerHeight,
this.width,
this.height
)
}

private _updateScales<T extends XYComponentCore<Datum>> (...components: T[]): void {
Expand Down
25 changes: 22 additions & 3 deletions packages/vue/src/containers/single-container/index.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
<script setup lang="ts" generic="T">
import { SingleContainer, ComponentCore, SingleContainerConfigInterface, Tooltip, Annotations } from '@unovis/ts'
import { SingleContainer, ComponentCore, SingleContainerConfigInterface, SingleContainerRenderPayload, Tooltip, Annotations } from '@unovis/ts'
import { onUnmounted, ref, provide, watch, toRefs, watchEffect, reactive, toRaw } from 'vue'
import { annotationsAccessorKey, componentAccessorKey, tooltipAccessorKey, } from "../../utils/context"
import { useForwardProps } from "../../utils/props"

const props = defineProps<SingleContainerConfigInterface<T> & { data?: T }>()
const emit = defineEmits<{
load: [payload: SingleContainerRenderPayload];
render: [payload: SingleContainerRenderPayload];
redraw: [payload: SingleContainerRenderPayload];
}>()

const { data } = toRefs(props)
const parsedProps = useForwardProps(props)

Expand All @@ -16,17 +22,30 @@ const config = reactive({
}) as SingleContainerConfigInterface<T>
const elRef = ref<HTMLDivElement>()

let hasLoaded = false
const handleRenderComplete: SingleContainerConfigInterface<T>['onRenderComplete'] = (svg, margin, containerWidth, containerHeight, componentWidth, componentHeight) => {
parsedProps.value.onRenderComplete?.(svg, margin, containerWidth, containerHeight, componentWidth, componentHeight)
const payload: SingleContainerRenderPayload = { svg, margin, containerWidth, containerHeight, componentWidth, componentHeight }
emit('render', payload)
if (hasLoaded) {
emit('redraw', payload)
} else {
hasLoaded = true
emit('load', payload)
}
}

const initChart = () => {
if (chart) return
if (elRef.value && config.component)
chart = new SingleContainer(elRef.value, { ...toRaw(config) }, data.value)
chart = new SingleContainer(elRef.value, { ...toRaw(config), onRenderComplete: handleRenderComplete }, data.value)
}

watchEffect(() => {
initChart()
// watch deep changes in components config
const t = config.component?.config
chart?.updateContainer({ ...toRaw(parsedProps.value), ...toRaw(config) })
chart?.updateContainer({ ...toRaw(parsedProps.value), ...toRaw(config), onRenderComplete: handleRenderComplete })
})

watch(data, () => {
Expand Down
Loading
Loading