Skip to content
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.
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
Expand Up @@ -22,6 +22,7 @@ import {
Direction,
Spacing,
Tooltip,
XYComponentCore,
XYContainer,
XYContainerConfigInterface,
} from '@unovis/ts'
Expand Down Expand Up @@ -119,6 +120,15 @@ export class VisXYContainerComponent<Datum> implements AfterViewInit, AfterConte
@Input() scaleByDomain?: boolean
/** Enables automatic calculation of chart margins based on the size of the axes. Default: `true` */
@Input() autoMargin?: boolean = true
/** Override the automatically calculated bleed — the extra space (in pixels) the components request
* to fully fit into the container (e.g. half of the point diameter for Scatter), which gets subtracted
* from the scale ranges. Accepts a `Spacing` object or a function that receives the container's
* components (their individual bleed values are available via the `bleed` getter) and returns one.
* The sides you provide replace the calculated values, the sides left `undefined` fall back to them —
* pass `0` explicitly to remove the bleed on a side.
* Useful for synchronizing the scale ranges of multiple charts, since the final bleed is provided
* to the `onRenderComplete` callback. Default: `undefined` */
@Input() bleed?: Spacing | ((components: XYComponentCore<Datum>[]) => Spacing)

/** Alternative text description of the chart for accessibility purposes. It will be applied as an
* `aria-label` attribute to the div element containing your chart. Default: `undefined`.
Expand Down Expand Up @@ -165,7 +175,7 @@ export class VisXYContainerComponent<Datum> implements AfterViewInit, AfterConte

getConfig (): XYContainerConfigInterface<Datum> {
const {
duration, margin, padding, scaleByDomain, autoMargin, width, height,
duration, margin, padding, scaleByDomain, autoMargin, bleed, width, height,
xScale, xDomain, xDomainMinConstraint, xDomainMaxConstraint, xRange,
yScale, yDomain, yDomainMinConstraint, yDomainMaxConstraint, yRange,
yDirection, ariaLabel, colorFunction,
Expand Down Expand Up @@ -194,6 +204,7 @@ export class VisXYContainerComponent<Datum> implements AfterViewInit, AfterConte
annotations,
scaleByDomain,
autoMargin,
bleed,
xScale,
xDomain,
xDomainMinConstraint,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useCallback, useState } from 'react'
import { Spacing } from '@unovis/ts'
import { VisXYContainer, VisScatter, VisLine, VisArea, VisStackedBar, VisAxis } from '@unovis/react'
import { generateXYDataRecords, XYDataRecord } from '@src/utils/data'
import { ExampleViewerDurationProps } from '@src/components/ExampleViewer/index'

export const title = 'Bleed Synchronization'
export const subTitle = 'Sharing bleed between containers'

const data = generateXYDataRecords(25)
const xDomain: [number, number] = [1, 20]

export const component = (props: ExampleViewerDurationProps): React.ReactNode => {
const [syncBleed, setSyncBleed] = useState(true)
const [scatterBleed, setScatterBleed] = useState<Spacing>()

const x = useCallback((d: XYDataRecord) => d.x, [])
const y = useCallback((d: XYDataRecord) => d.y, [])

// The Scatter chart needs the most horizontal space to fit its points, so we take
// its bleed from the `onRenderComplete` callback and pass it to the charts below
const onRenderComplete = useCallback((svg: SVGSVGElement, margin: Spacing, bleed: Spacing): void => {
setScatterBleed(prev => (prev?.left === bleed.left && prev?.right === bleed.right)
? prev
: { left: bleed.left, right: bleed.right }
)
}, [])

// We synchronize the `left` and `right` values only, `top` and `bottom` are left
// undefined so that every chart falls back to its own calculated vertical bleed
const bleed = syncBleed ? scatterBleed : undefined

return (
<>
<label>
<input type='checkbox' checked={syncBleed} onChange={e => setSyncBleed(e.target.checked)}/>
Synchronize bleed
</label>

<VisXYContainer data={data} height={150} xDomain={xDomain} onRenderComplete={onRenderComplete}>
<VisScatter x={x} y={y} size={30} duration={props.duration}/>
<VisAxis type='x' duration={props.duration}/>
<VisAxis type='y' duration={props.duration}/>
</VisXYContainer>

<VisXYContainer data={data} height={150} xDomain={xDomain} bleed={bleed}>
<VisStackedBar x={x} y={y} duration={props.duration}/>
<VisAxis type='x' duration={props.duration}/>
<VisAxis type='y' duration={props.duration}/>
</VisXYContainer>

<VisXYContainer data={data} height={150} xDomain={xDomain} bleed={bleed}>
<VisArea x={x} y={y} duration={props.duration}/>
<VisAxis type='x' duration={props.duration}/>
<VisAxis type='y' duration={props.duration}/>
</VisXYContainer>

<VisXYContainer data={data} height={150} xDomain={xDomain} bleed={bleed}>
<VisLine x={x} y={y} duration={props.duration}/>
<VisAxis type='x' duration={props.duration}/>
<VisAxis type='y' duration={props.duration}/>
</VisXYContainer>
</>
)
}
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 @@ -92,6 +92,15 @@ export interface XYContainerConfigInterface<Datum> extends ContainerConfigInterf
annotations?: Annotations | undefined;
/** Extend the clip path by the specified number of pixels. Default: `2` */
clipPathExtend?: number;
/** Override the automatically calculated bleed — the extra space (in pixels) the components request
* to fully fit into the container (e.g. half of the point diameter for Scatter), which gets subtracted
* from the scale ranges. Accepts a `Spacing` object or a function that receives the container's
* components (their individual bleed values are available via the `bleed` getter) and returns one.
* The sides you provide replace the calculated values, the sides left `undefined` fall back to them —
* pass `0` explicitly to remove the bleed on a side.
* Useful for synchronizing the scale ranges of multiple charts, since the final bleed is provided
* to the `onRenderComplete` callback. Default: `undefined` */
bleed?: Spacing | ((components: XYComponentCore<Datum>[]) => Spacing);
/** Callback function to be called when the chart rendering is complete. Default: `undefined` */
onRenderComplete?: (
svgNode: SVGSVGElement,
Expand Down Expand Up @@ -132,5 +141,6 @@ export const XYContainerDefaultConfig: XYContainerConfigInterface<unknown> = {
scaleByDomain: false,

clipPathExtend: 2,
bleed: undefined,
}

18 changes: 16 additions & 2 deletions packages/ts/src/containers/xy-container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import { Direction } from '@/types/direction'

// Utils
import { clamp, clean, flatten } from '@/utils/data'
import { clamp, clean, flatten, isFunction } from '@/utils/data'
import { guid } from '@/utils/misc'

// Config
Expand Down Expand Up @@ -199,7 +199,7 @@

// Configs are matched to components by array index
if (componentConfigs && componentConfigs.length !== this.components.length) {
console.warn('Unovis | XY Container: The number of provided component configs doesn\'t match the number of components. Configs are matched to components by index, so some components won\'t be updated')

Check warning on line 202 in packages/ts/src/containers/xy-container/index.ts

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 207. Maximum allowed is 200
}

this.components.forEach((c, i) => {
Expand Down Expand Up @@ -459,13 +459,27 @@
}

private _getBleed<T extends XYComponentCore<Datum>> (components: T[]): Spacing {
return components.map(c => c.bleed).reduce((bleed, b) => {
// We request the components' bleed even when the configured `bleed` overrides the result,
// because some components prepare their internal rendering state in the `bleed` getter
const calculatedBleed = components.map(c => c.bleed).reduce((bleed, b) => {
for (const key of Object.keys(bleed)) {
const k = key as keyof Spacing
if (bleed[k] < b[k]) bleed[k] = b[k]
}
return bleed
}, { top: 0, bottom: 0, left: 0, right: 0 })

const configuredBleed = isFunction(this.config.bleed) ? this.config.bleed(components) : this.config.bleed
if (configuredBleed) {
return {
top: configuredBleed.top ?? calculatedBleed.top,
bottom: configuredBleed.bottom ?? calculatedBleed.bottom,
left: configuredBleed.left ?? calculatedBleed.left,
right: configuredBleed.right ?? calculatedBleed.right,
}
}

return calculatedBleed
}

public destroy (): void {
Expand Down
42 changes: 42 additions & 0 deletions packages/website/docs/containers/XY_Container.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,48 @@ const padding = { left: 100, right: 100 }
<XYWrapper className='showBorder' {...xyContainerProps()} containerProps={{ height: 100, padding: { left: 100, right: 100 }}} excludeTabs/>
</div>

### Bleed
Some components need extra space inside the container for their marks to fully fit: half of the point
diameter for _Scatter_, half of the bar width for bar charts at the edges of the domain, and so on. This
space is called **bleed**. Before rendering, the container asks every component how much bleed it needs,
combines the values (taking the maximum for each side), and shrinks the scale ranges accordingly. That's
why the first and the last points of the scatter chart below are shifted inward from the container's
edges instead of being cut in half:

<XYWrapper {...xyContainerProps('Scatter')}
components={[
component('Scatter', { size: 25 }),
{ name: 'Axis', props: { type: 'x' }, key: 'xAxis'},
{ name: 'Axis', props: { type: 'y' }, key: 'yAxis'},
]}
height={125}
excludeTabs
/>

The container reports the bleed it used to the `onRenderComplete` callback, and you can override the
calculated values with the `bleed` property. It accepts a <a href='#sizing'>Spacing</a> object, or a
function that receives the container's components and returns one. The sides you provide replace the
calculated values, the sides left `undefined` fall back to them. Here is the same chart with the
bleed set to zero on every side — the edge points now get clipped:

```ts
const bleed = { top: 0, bottom: 0, left: 0, right: 0 }
```

<XYWrapper {...xyContainerProps('Scatter')}
components={[
component('Scatter', { size: 25 }),
{ name: 'Axis', props: { type: 'x' }, key: 'xAxis'},
{ name: 'Axis', props: { type: 'y' }, key: 'yAxis'},
]}
height={125}
containerProps={{ bleed: { top: 0, bottom: 0, left: 0, right: 0 } }}
excludeTabs
/>

Overriding the bleed is mainly useful for aligning the X values of several charts placed one below
another — see the [Bleed](/docs/guides/bleed) guide for a complete walkthrough.

### Range
The `xRange` and `yRange` determine the screen space your chart contains. By default, an _XY Container_ will
fit to its container. Provide `xRange` with values [`xStart`, `width`] and `yRange` with [`yStart`, `height`]
Expand Down
Loading
Loading