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

Container | Core, Single: Setting SVG size in render() #174

Merged
merged 3 commits into from Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/ts/src/components/sankey/index.ts
Expand Up @@ -219,7 +219,7 @@ export class Sankey<

const height = max(values) || config.nodeMinHeight
this._extendedHeight = height + bleed.top + bleed.bottom
this._extendedWidth = (config.nodeWidth + config.nodeHorizontalSpacing) * Object.keys(groupedByColumn).length - config.nodeHorizontalSpacing + bleed.left + bleed.right
this._extendedWidth = Math.max(0, (config.nodeWidth + config.nodeHorizontalSpacing) * Object.keys(groupedByColumn).length - config.nodeHorizontalSpacing + bleed.left + bleed.right)
}

private _prepareLayout (): void {
Expand Down
30 changes: 21 additions & 9 deletions packages/ts/src/containers/single-container/index.ts
Expand Up @@ -23,13 +23,16 @@ export class SingleContainer<Data> extends ContainerCore {
super(element)

if (config) {
this.updateContainer(config)
this.updateContainer(config, true)
this.component = config.component
}

if (data) {
this.setData(data)
this.setData(data, true)
}

// Render if component exists and has data
if (this.component?.datamodel.data) this.render()
}

public setData (data: Data, preventRender?: boolean): void {
Expand Down Expand Up @@ -83,16 +86,21 @@ export class SingleContainer<Data> extends ContainerCore {
return this.width / componentWidth
}

_render (customDuration?: number): void {
_render (duration?: number): void {
const { config, component } = this
super._render(null, true)
super._render(duration)

component.setSize(this.width, this.height)
component.g.attr('transform', `translate(${config.margin.left},${config.margin.top})`)
component.render(duration)

component.g
.attr('transform', `translate(${config.margin.left},${config.margin.top})`)
if (config.tooltip) config.tooltip.update()
}

component.render(customDuration)
// Re-defining the `render()` function to handle different sizing techniques (`Sizing.Extend` and `Sizing.FitWidth`)
// Not calling `super.render()` because we don't want it to interfere with setting the SVG size here.
render (duration = this.config.duration): void {
const { config, component } = this

if (config.sizing === Sizing.Extend || config.sizing === Sizing.FitWidth) {
const fitToWidth = config.sizing === Sizing.FitWidth
Expand All @@ -108,7 +116,7 @@ export class SingleContainer<Data> extends ContainerCore {
const scaledHeight = componentHeight * scale
const animated = currentWidth || currentHeight

smartTransition(this.svg, animated ? (customDuration ?? component.config.duration) : 0)
smartTransition(this.svg, animated ? duration : 0)
.attr('width', scaledWidth)
.attr('height', scaledHeight)
.attr('viewBox', `${0} ${0} ${componentWidth} ${fitToWidth ? scaledHeight : componentHeight}`)
Expand All @@ -119,7 +127,11 @@ export class SingleContainer<Data> extends ContainerCore {
.attr('height', this.config.height || this.containerHeight)
}

if (config.tooltip) config.tooltip.update()
// Schedule the actual rendering in the next frame
cancelAnimationFrame(this._requestedAnimationFrame)
this._requestedAnimationFrame = requestAnimationFrame(() => {
this._render(duration)
})
}

_onResize (): void {
Expand Down
9 changes: 7 additions & 2 deletions packages/ts/src/containers/xy-container/index.ts
Expand Up @@ -74,11 +74,16 @@ export class XYContainer<Datum> extends ContainerCore {
.html('<feColorMatrix type="saturate" in="SourceGraphic" values="1.35"/>')

if (config) {
this.updateContainer(config)
this.updateContainer(config, true)
}

if (data) {
this.setData(data)
this.setData(data, true)
}

// Render if components are present and have data
if (this.components?.some(c => c.datamodel.data)) {
this.render()
}

// Force re-render axes when fonts are loaded
Expand Down
4 changes: 4 additions & 0 deletions packages/ts/src/core/container/config.ts
Expand Up @@ -15,11 +15,15 @@ export interface ContainerConfigInterface {
/** Defines whether components should fit into the container or the container should expand to fit to the component's size. Default: `Sizing.Fit` */
sizing?: Sizing | string;
/** Width in pixels or in CSS units.
* Percentage units `"%"` are not supported here. If you want to set `width` as a percentage, do it via `style`
* of the corresponding DOM element.
* By default, Container automatically fits to the size of the parent element.
* Default: `undefined`
*/
width?: number | string;
/** Height in pixels or in CSS units.
* Percentage units `"%"` are not supported here. If you want to set `height` as a percentage, do it via `style`
* of the corresponding DOM element.
* By default, Container automatically fits to the size of the parent element.
* Default: `undefined`
*/
Expand Down
76 changes: 41 additions & 35 deletions packages/ts/src/core/container/index.ts
Expand Up @@ -6,7 +6,6 @@ import { Sizing } from 'types/component'

// Utils
import { isEqual, clamp } from 'utils/data'
import { getBoundingClientRectObject } from 'utils/misc'

// Config
import { ContainerConfig, ContainerConfigInterface } from './config'
Expand All @@ -18,10 +17,10 @@ export class ContainerCore {
config: ContainerConfig

protected _container: HTMLElement
private _requestedAnimationFrame: number
private _animationFramePromise: Promise<number>
private _containerRect
private _resizeObserver
protected _requestedAnimationFrame: number
private _isFirstRender = true
private _containerSize: { width: number; height: number }
private _resizeObserver: ResizeObserver | undefined

// eslint-disable-next-line @typescript-eslint/naming-convention
static DEFAULT_CONTAINER_HEIGHT = 300
Expand All @@ -39,20 +38,6 @@ export class ContainerCore {
.attr('height', ContainerCore.DEFAULT_CONTAINER_HEIGHT)

this.element = this.svg.node()

// ResizeObserver: Re-render on container resize
this._containerRect = getBoundingClientRectObject(this._container)
this._resizeObserver = new ResizeObserver((entries, observer) => {
const resizedContainerRect = getBoundingClientRectObject(this._container)
const hasSizeChanged = !isEqual(this._containerRect, resizedContainerRect)
// do resize only if element is attached to the DOM
// will come in useful when some ancestor of container becomes detached
if (hasSizeChanged && resizedContainerRect.width && resizedContainerRect.height) {
this._containerRect = resizedContainerRect
this._onResize()
}
})
this._resizeObserver.observe(this._container)
}

updateContainer<T extends ContainerConfigInterface> (config: T): void {
Expand All @@ -62,11 +47,10 @@ export class ContainerCore {
this.config = new ConfigModel().init(config)
}

_render (duration?: number, dontApplySize?: boolean): void {
if (!dontApplySize) {
this.svg
.attr('width', this.config.width || this.containerWidth)
.attr('height', this.config.height || this.containerHeight)
_render (duration?: number): void {
if (this._isFirstRender) {
this._setUpResizeObserver()
this._isFirstRender = false
}

if (this.config.svgDefs) {
Expand All @@ -75,17 +59,22 @@ export class ContainerCore {
}
}

render (duration = this.config.duration): Promise<number> {
cancelAnimationFrame(this._requestedAnimationFrame)
render (duration = this.config.duration): void {
const width = this.config.width || this.containerWidth
const height = this.config.height || this.containerHeight

this._animationFramePromise = new Promise((resolve, reject) => {
this._requestedAnimationFrame = requestAnimationFrame(() => {
this._render(duration)
resolve(this._requestedAnimationFrame)
})
})
// We set SVG size in `render()` instead of `_render()`, because the size values in pixels will become
// available only in the next animation when being accessed via `element.clientWidth` and `element.clientHeight`,
// and we rely on those values when setting width and size of the components.
this.svg
.attr('width', width)
.attr('height', height)

return this._animationFramePromise
// Schedule the actual rendering in the next frame
cancelAnimationFrame(this._requestedAnimationFrame)
this._requestedAnimationFrame = requestAnimationFrame(() => {
this._render(duration)
})
}

get containerWidth (): number {
Expand Down Expand Up @@ -117,13 +106,30 @@ export class ContainerCore {
_onResize (): void {
const { config } = this
const redrawOnResize = config.sizing === Sizing.Fit || config.sizing === Sizing.FitWidth

if (redrawOnResize) this.render(0)
}

_setUpResizeObserver (): void {
const containerRect = this._container.getBoundingClientRect()
this._containerSize = { width: containerRect.width, height: containerRect.height }

this._resizeObserver = new ResizeObserver((entries, observer) => {
const resizedContainerRect = this._container.getBoundingClientRect()
const resizedContainerSize = { width: resizedContainerRect.width, height: resizedContainerRect.height }
const hasSizeChanged = !isEqual(this._containerSize, resizedContainerSize)
// Do resize only if element is attached to the DOM
// will come in useful when some ancestor of container becomes detached
if (hasSizeChanged && resizedContainerSize.width && resizedContainerSize.height) {
this._containerSize = resizedContainerSize
this._onResize()
}
})
this._resizeObserver.observe(this._container)
}

destroy (): void {
cancelAnimationFrame(this._requestedAnimationFrame)
this._resizeObserver.disconnect()
this._resizeObserver?.disconnect()
this.svg.remove()
}
}
14 changes: 11 additions & 3 deletions packages/ts/src/data-models/graph.ts
Expand Up @@ -6,13 +6,17 @@ import { GraphInputLink, GraphInputNode, GraphLinkCore, GraphNodeCore } from 'ty
// Core Data Model
import { CoreDataModel } from './core'

export type GraphData<N extends GraphInputNode, L extends GraphInputLink> = {
nodes: N[];
links?: L[];
}

export class GraphDataModel<
N extends GraphInputNode,
L extends GraphInputLink,
OutNode extends GraphNodeCore<N, L> = GraphNodeCore<N, L>,
OutLink extends GraphLinkCore<N, L> = GraphLinkCore<N, L>,
> extends CoreDataModel<{nodes: N[]; links?: L[]}> {
> extends CoreDataModel<GraphData<N, L>> {
private _nonConnectedNodes: OutNode[]
private _connectedNodes: OutNode[]
private _nodes: OutNode[] = []
Expand All @@ -24,9 +28,13 @@ export class GraphDataModel<
public linkId: ((n: L) => string | undefined) = l => (isString(l.id) || isFinite(l.id as number)) ? `${l.id}` : undefined
public nodeSort: ((a: N, b: N) => number)

// eslint-disable-next-line accessor-pairs
set data (inputData: { nodes: N[]; links?: L[]}) {
get data (): GraphData<N, L> {
return this._data
}

set data (inputData: GraphData<N, L>) {
if (!inputData) return
this._data = inputData
const prevNodes = this.nodes
const prevLinks = this.links

Expand Down
16 changes: 13 additions & 3 deletions packages/ts/src/data-models/map-graph.ts
Expand Up @@ -7,7 +7,13 @@ import { CoreDataModel } from 'data-models/core'
// Types
import { MapLink } from 'types/map'

export class MapGraphDataModel<AreaDatum, PointDatum, LinkDatum> extends CoreDataModel<{ areas?: AreaDatum[]; points?: PointDatum[]; links?: LinkDatum[] }> {
export type MapGraphData<AreaDatum, PointDatum, LinkDatum> = {
areas?: AreaDatum[];
points?: PointDatum[];
links?: LinkDatum[];
}

export class MapGraphDataModel<AreaDatum, PointDatum, LinkDatum> extends CoreDataModel<MapGraphData<AreaDatum, PointDatum, LinkDatum>> {
private _areas: AreaDatum[] = []
private _points: PointDatum[] = []
private _links: MapLink<PointDatum, LinkDatum>[] = []
Expand All @@ -20,9 +26,13 @@ export class MapGraphDataModel<AreaDatum, PointDatum, LinkDatum> extends CoreDat
/* eslint-disable-next-line dot-notation */
public linkTarget: ((l: LinkDatum) => number | string | PointDatum) = l => l['target']

// eslint-disable-next-line accessor-pairs
set data (data: { areas?: AreaDatum[]; points?: PointDatum[]; links?: LinkDatum[] }) {
get data (): MapGraphData<AreaDatum, PointDatum, LinkDatum> {
return this._data
}

set data (data: MapGraphData<AreaDatum, PointDatum, LinkDatum>) {
if (!data) return
this._data = data

this._areas = cloneDeep(data?.areas ?? [])
this._points = cloneDeep(data?.points ?? [])
Expand Down
6 changes: 0 additions & 6 deletions packages/ts/src/utils/misc.ts
Expand Up @@ -3,12 +3,6 @@ import { Rect } from 'types/misc'
import { getString, isString } from 'utils/data'
import toPx from 'to-px'

export const getBoundingClientRectObject = (element: HTMLElement):
{ top: number; right: number; bottom: number; left: number; width: number; height: number; x: number; y: number } => {
const { top, right, bottom, left, width, height, x, y } = element.getBoundingClientRect()
return { top, right, bottom, left, width, height, x, y }
}

export function guid (): string {
const s4 = (): string =>
Math.floor((1 + Math.random()) * 0x10000)
Expand Down