Theming
CSS Variables
Overview
In addition to configuration properties, our components also rely on
CSS variables
to supply the values of various SVG attributes such as fill, stroke, opacity, etc. You can override
these variables to further customize your Unovis components.
Every variable has the following naming convention: --vis + label + attribute. For example, the variable named
--vis-area-cursor would apply to the Area component's cursor property.
Note that while our variables follow this convention, it does not guarantee that the value you wish to override is available. Be sure to check the corresponding doc page of the component you want to customize to see the available CSS variables.
Basic Example
Variables can be overridden in your CSS style declarations. Consider the default configuration for sankey, which looks like this:
Now consider the following style declaration. After adding custom-sankey to the container element of the
Sankey component, we will see the following result:
Dark Theme Usage
Our library offers dark theme support which takes effect when the class theme-dark is added to the
document's body element. Every component has a dark version of each color variable labeled with the
prefix --vis-dark. You can opt not to override these if you want to use our default dark theme values,
or override them like so:
Global Variables
The majority of our variables exist on a component level, but there are a few global CSS variables:
Unless overridden explicitly, --vis-color-main corresponds to the first color in the default color palette
Label Styling
Font
The font for labels across all of our components is defined by the --vis-font-family variable. The default font,
Inter, is not imported by default, but you can easily import it yourself from
Google Fonts.
To use a different font, simply redefine the --vis-font-family CSS variable:
Large Sizing
A common theming scenario is the "large size" theme, for when you want larger font sizes for the
labels in your charts. We offer two variations in the form of css classes that you can import directly
from @unovis/ts:
import { styleLargeSize } from '@unovis/ts' // ~1.3x larger
import { styleExtraLargeSize } from '@unovis/ts' // 2x larger
Just add either one to your container's class list to the effects. Consider the following example of a labeled Scatter chart:
className: styleLargeSize
className: styleExtraLargeSize
When using this theme, the following components have caveats:
-
Scatter: If the
labelPositionproperty is set toPosition.Center, point labels will try fit to the point's size. In this case, you will instead need to update thepointSizeproperty to render larger labels. -
Timeline Additionally, you may need to adjust the
rowHeightproperty to accommodate larger labels.
Color Palette
Many of our components use the default color palette for visualizations. You can import the array of hex values directly
from unovis/ts:
import { colors, colorsDark } from '@unovis/ts'
The dark theme palette is slightly different from the regular one. These colors are also defined directly in our CSS variables, labeled --vis-color0, --vis-dark-color0, --vis-color1, --vis-dark-color1, etc.
The full palette looks like this:
Light
Dark
Palette Editor
You can tweak and preview your desired palette using the example StackedBar component below. If you like the result, just copy and paste the corresponding style declaration in the dropdown below.
Alternatively, you can provide a custom color palette in global scope using the UNOVIS_COLORS variable:
window.UNOVIS_COLORS = [...]
// or
globalThis.UNOVIS_COLORS = [...]
This needs to be done before the library is imported, i.e. in your top level JS file or HTML.
Synchronizing Colors Across Charts
By default, every component assigns colors to its series by index: the first series uses --vis-color0,
the second --vis-color1, and so on (see Color Palette). That's fine for a single chart,
but as soon as a dashboard has several charts that share categories, the same category tends to land at a
different index in each chart — and therefore gets a different color. Color synchronization fixes this by
mapping a stable color key to a color through a shared color function, so a category keeps the same color
across every chart, legend, and tooltip.
There are three pieces:
colorKeys— a component property: an array of string keys, one peryaccessor, that labels each series (e.g.['aws', 'azure', 'github']).colorFunction— a function of type(key: string | number) => string, accepted by the container (where it applies to every component inside) and byBulletLegend. Given a key, it returns a color.UnovisColorScale— the default color function, used when you don't provide your own.
How a color is resolved
For each element, Unovis resolves the color in this order:
- The component's own
coloraccessor, if it returns a value — a per‑datum override that always wins. - Otherwise, if the series has a
colorKey, the color function is called with that key. - Otherwise, the color function is called with the series index.
- If nothing matches, the element gets no explicit color.
So colorKeys together with a shared color function means same key → same color, regardless of the chart
type or the order in which series appear.
Defining a color function
A color function is just (key) => color. There are two common ways to build one.
A D3 ordinal scale assigns colors from a range to keys in first‑seen order:
import { Scale } from '@unovis/ts'
const color = Scale.scaleOrdinal()
.range(['#ff8cfd', '#126b7e', '#ff5450', '#23cc00', '#0000ff'])
An explicit color map gives you full control over which key maps to which color, with a fallback for unknown keys:
const colorMap = { aws: '#f0a8b4', google: '#7eb8d4', github: '#b89ef0' }
const color = (key) => colorMap[key] ?? '#cccccc'
Putting it together
Pass the same color function to every container and legend, and give each component a colorKeys
array aligned with its y accessors:
- React
- Angular
- Svelte
- Vue
- Solid
- TypeScript
import { VisXYContainer, VisGroupedBar, VisArea, VisAxis, VisBulletLegend } from '@unovis/react'
import { Scale } from '@unovis/ts'
const color = Scale.scaleOrdinal().range(['#4d8cfd', '#ff6b7e', '#00c19a', '#f4b83e'])
const keys = ['aws', 'azure', 'github']
const y = keys.map(k => (d) => d[k])
// Legend, charts, and tooltips all share the same color function and keys
<VisBulletLegend items={keys.map(name => ({ name, colorKey: name }))} colorFunction={color} />
<VisXYContainer data={data} colorFunction={color}>
<VisGroupedBar x={d => d.x} y={y} colorKeys={keys} />
<VisAxis type="x" />
</VisXYContainer>
<VisXYContainer data={data} colorFunction={color}>
<VisArea x={d => d.x} y={y} colorKeys={keys} />
<VisAxis type="x" />
</VisXYContainer>
import { Scale } from '@unovis/ts'
// In your component class:
color = Scale.scaleOrdinal().range(['#4d8cfd', '#ff6b7e', '#00c19a', '#f4b83e'])
keys = ['aws', 'azure', 'github']
x = (d) => d.x
y = this.keys.map(k => (d) => d[k])
items = this.keys.map(name => ({ name, colorKey: name }))
<vis-bullet-legend [items]="items" [colorFunction]="color"></vis-bullet-legend>
<vis-xy-container [data]="data" [colorFunction]="color">
<vis-grouped-bar [x]="x" [y]="y" [colorKeys]="keys"></vis-grouped-bar>
<vis-axis type="x"></vis-axis>
</vis-xy-container>
<vis-xy-container [data]="data" [colorFunction]="color">
<vis-area [x]="x" [y]="y" [colorKeys]="keys"></vis-area>
<vis-axis type="x"></vis-axis>
</vis-xy-container>
<script>
import { VisXYContainer, VisGroupedBar, VisArea, VisAxis, VisBulletLegend } from '@unovis/svelte'
import { Scale } from '@unovis/ts'
const color = Scale.scaleOrdinal().range(['#4d8cfd', '#ff6b7e', '#00c19a', '#f4b83e'])
const keys = ['aws', 'azure', 'github']
const x = (d) => d.x
const y = keys.map(k => (d) => d[k])
const items = keys.map(name => ({ name, colorKey: name }))
</script>
<VisBulletLegend {items} colorFunction={color} />
<VisXYContainer {data} colorFunction={color}>
<VisGroupedBar {x} {y} colorKeys={keys} />
<VisAxis type="x" />
</VisXYContainer>
<VisXYContainer {data} colorFunction={color}>
<VisArea {x} {y} colorKeys={keys} />
<VisAxis type="x" />
</VisXYContainer>
<script setup lang="ts">
import { VisXYContainer, VisGroupedBar, VisArea, VisAxis, VisBulletLegend } from '@unovis/vue'
import { Scale } from '@unovis/ts'
const color = Scale.scaleOrdinal().range(['#4d8cfd', '#ff6b7e', '#00c19a', '#f4b83e'])
const keys = ['aws', 'azure', 'github']
const x = (d) => d.x
const y = keys.map(k => (d) => d[k])
const items = keys.map(name => ({ name, colorKey: name }))
</script>
<template>
<VisBulletLegend :items="items" :colorFunction="color" />
<VisXYContainer :data="data" :colorFunction="color">
<VisGroupedBar :x="x" :y="y" :colorKeys="keys" />
<VisAxis type="x" />
</VisXYContainer>
<VisXYContainer :data="data" :colorFunction="color">
<VisArea :x="x" :y="y" :colorKeys="keys" />
<VisAxis type="x" />
</VisXYContainer>
</template>
import { VisXYContainer, VisGroupedBar, VisArea, VisAxis, VisBulletLegend } from '@unovis/solid'
import { Scale } from '@unovis/ts'
const color = Scale.scaleOrdinal().range(['#4d8cfd', '#ff6b7e', '#00c19a', '#f4b83e'])
const keys = ['aws', 'azure', 'github']
const y = keys.map(k => (d) => d[k])
// Legend, charts, and tooltips all share the same color function and keys
<VisBulletLegend items={keys.map(name => ({ name, colorKey: name }))} colorFunction={color} />
<VisXYContainer data={data} colorFunction={color}>
<VisGroupedBar x={d => d.x} y={y} colorKeys={keys} />
<VisAxis type="x" />
</VisXYContainer>
<VisXYContainer data={data} colorFunction={color}>
<VisArea x={d => d.x} y={y} colorKeys={keys} />
<VisAxis type="x" />
</VisXYContainer>
import { XYContainer, GroupedBar, Area, Axis, BulletLegend, Scale } from '@unovis/ts'
const color = Scale.scaleOrdinal().range(['#4d8cfd', '#ff6b7e', '#00c19a', '#f4b83e'])
const keys = ['aws', 'azure', 'github']
const y = keys.map(k => (d) => d[k])
// Legend, charts, and tooltips all share the same color function and keys
const legend = new BulletLegend(legendNode, {
items: keys.map(name => ({ name, colorKey: name })),
colorFunction: color,
})
const groupedBar = new XYContainer(node1, {
colorFunction: color,
components: [new GroupedBar({ x: d => d.x, y, colorKeys: keys })],
xAxis: new Axis({ type: 'x' }),
}, data)
const area = new XYContainer(node2, {
colorFunction: color,
components: [new Area({ x: d => d.x, y, colorKeys: keys })],
xAxis: new Axis({ type: 'x' }),
}, data)
A few things to keep in mind:
BulletLegenditems carry their key incolorKey; pass the samecolorFunctionso the bullets match the charts.- The crosshair tooltip picks up
colorKeysfrom the components automatically, so its markers stay in sync too. - Keep
colorKeysthe same length as theyaccessor array.
The example below uses one shared ordinal scale. The second chart lists its series in a different order and the legend, bars, and area all agree on the color of every category:
Overriding the default palette in code
When you don't pass a color function, components fall back to UnovisColorScale — a D3 ordinal scale
whose range is the --vis-color* CSS variables. You can re‑range it once at startup to change the default
palette everywhere, without touching CSS:
import { UnovisColorScale } from '@unovis/ts'
UnovisColorScale.range(['#f0a8b4', '#7eb8d4', '#8ed49a', '#ffdc88', '#b89ef0'])
UnovisColorScale is global: re‑ranging it affects every chart on every page loaded afterwards. If you only
need to recolor the palette (and not sync by key), prefer overriding the --vis-color*
CSS variables instead.
Patterns
Patterns — stripes, dots, cross‑hatching, and so on — add a second visual channel on top of color. They're useful for color‑blind‑safe palettes, grayscale printing, or simply telling series apart in a dense chart. Unovis offers two ways to apply them:
- the
patternaccessor on a component, for explicit per‑series (or per‑datum) control; - the
theme-patternsbody class, which applies a default pattern palette automatically, keyed by series index.
The built‑in pattern definitions are injected into the page for you the first time the library loads, so there's nothing to import or register before using them.
The pattern accessor
Most data components accept a pattern accessor: Area, Line, Scatter, GroupedBar,
StackedBar, Timeline, Donut, NestedDonut, Sankey, and ChordDiagram. It resolves to one
of the built‑in pattern ids, exposed as two enums:
| Enum | Applies to | Values |
|---|---|---|
FillPatternType | Solid shapes (areas, bars, scatter points, donut/sankey segments…) | StripesDiagonal, Dots, StripesVertical, Crosshatch, Waves, Circles |
LinePatternType | The Line component | Circle, Triangle, Diamond, Arrow, Square, Star |
Under the hood, a fill pattern is applied as an SVG
mask over the shape, so it's cut out of the
series' own fill and automatically takes the series color. A line pattern combines an SVG
marker repeated along the path with a
stroke-dasharray; the marker
inherits the line's color.
The accessor follows the component's data shape:
- For multi‑series components (where
yis an array of accessors), the accessor receives the series index as its second argument — return one pattern per series. - For per‑datum components like Scatter points or Donut segments, it receives the datum and its index, so you can vary the pattern by value.
import { VisXYContainer, VisStackedBar, VisLine, VisScatter, VisAxis } from '@unovis/react'
import { FillPatternType, LinePatternType } from '@unovis/ts'
const fillPatterns = [FillPatternType.StripesDiagonal, FillPatternType.Dots, FillPatternType.Crosshatch]
const linePatterns = [LinePatternType.Triangle, LinePatternType.Diamond, LinePatternType.Square]
// A fill pattern per series (StackedBar, Area, GroupedBar, …)
<VisStackedBar x={d => d.x} y={[y0, y1, y2]} pattern={(_d, i) => fillPatterns[i]} />
// A line pattern per series (Line)
<VisLine x={d => d.x} y={[y0, y1, y2]} pattern={(_d, i) => linePatterns[i]} />
// A fill pattern per data point (Scatter)
<VisScatter x={d => d.x} y={d => d.y} pattern={d => fillPatterns[Math.round(d.y) % fillPatterns.length]} />
Fill patterns per series — a stacked bar where each series gets a different FillPatternType
(stripes-diagonal, dots, crosshatch):
Line patterns per series — each line gets a different LinePatternType (triangle, diamond, square):
An explicit pattern accessor always takes precedence over the theme-patterns fallback described below —
the automatic styles only target shapes that don't already carry a pattern. You can therefore enable
theme-patterns globally and still override individual charts with the accessor.
Automatic patterns with theme-patterns
When document.body has the class theme-patterns we automatically apply patterns of two types:
Fill Patterns
Applied automatically to solid shapes (most cases), keyed by series index. Each index maps to a CSS variable
(--vis-pattern-fill0, --vis-pattern-fill1, …) that you can override with any SVG
mask reference.
The default fill‑pattern palette looks like:
Default CSS Variables:
--vis-pattern-fill0: var(--vis-pattern-fill-stripes-diagonal);
--vis-pattern-fill1: var(--vis-pattern-fill-dots);
--vis-pattern-fill2: var(--vis-pattern-fill-stripes-vertical);
--vis-pattern-fill3: var(--vis-pattern-fill-crosshatch);
--vis-pattern-fill4: var(--vis-pattern-fill-waves);
--vis-pattern-fill5: var(--vis-pattern-fill-circles);
Line Patterns
For the Line component and when BulletLegend's bulletShape property is set to "line".
Each series index maps to a marker variable and a dash‑array variable. You can customize these patterns by
assigning any combination of the following variable types:
- Prefixed
--vis-pattern-marker: accepts SVG defs containingsmarkerelements - Variables with the prefix
--vis-pattern-dasharrayto a valid value for the stroke-dasharray property. The default palette looks like:
Default CSS Variables:
--vis-pattern-marker0: var(--vis-pattern-marker-circle);
--vis-pattern-marker1: var(--vis-pattern-marker-triangle);
--vis-pattern-dasharray1: 9 1;
--vis-pattern-marker2: var(--vis-pattern-marker-diamond);
--vis-pattern-dasharray2: 2;
--vis-pattern-marker3: var(--vis-pattern-marker-arrow);
--vis-pattern-dasharray3: 2 3 8 3;
--vis-pattern-marker4: var(--vis-pattern-marker-square);
--vis-pattern-dasharray4: 6;
--vis-pattern-marker5: var(--vis-pattern-marker-star);
--vis-pattern-dasharray5: 1 6;
Customizing the pattern palette
The automatic palette is driven by indexed CSS variables — --vis-pattern-fill{i} for fills, and
--vis-pattern-marker{i} / --vis-pattern-dasharray{i} for lines. Override them to point at your own SVG
<defs>, or to a different dash array. (These variables back the theme-patterns fallback only; charts that
use the pattern accessor ignore them.)
To override default patterns use the following table for reference.
| CSS Variable Prefix | Type | Accepted Value | Example |
|---|---|---|---|
--vis-pattern-fill | Fill | SVG mask from a <defs> element | url(#my-pattern-fill) |
--vis-pattern-marker | Line | SVG marker from a <defs> element | url(#my-line-marker) |
--vis-pattern-dasharray | Line | CSS stroke-dasharray property | 5 10 |
Bordered Segments
For charts with multiple data layers, it might be preferable to have a visual separation of elements.
You can do this by manipulating the stroke and stroke-width variables to create a bordered segment
effect.
For the following components, the stroke property by default is either none or the same color
as its fill. You can tweak the variables accordingly to create the desired effect:
:root {
--stroke: #fff;
--stroke-dark: #292b34;
/* Area */
--vis-area-stroke-width: 1px;
--vis-area-stroke-color: var(--stroke);
--vis-dark-area-stroke: var(--stroke-dark);
/* Donut */
--vis-donut-segment-stroke-width: 1px;
/* StackedBar */
--vis-stacked-bar-stroke-width: 1px;
--vis-stacked-bar-stroke-color: var(--stroke);
--vis-dark-stacked-bar-stroke: var(--stroke-dark);
/* Timeline */
--vis-timeline-line-stroke-width: 1px;
}
Area
Donut
Stacked Bar
Timeline
Gradient Fills with SVG defs
Use the svgDefs property on the container to inject custom SVG definitions — gradients, patterns, clip paths — that any component can reference by id.
Start by building your defs string, then pass the string to svgDefs on the container and reference each gradient by id in the color array.
The example below defines one vertical <linearGradient> per series.
- React
- Angular
- Svelte
- Vue
- Solid
- TypeScript
import { VisXYContainer, VisAxis, VisAxis, VisArea } from '@unovis/react'
function Component(props) {
const data: DataRecord[] = props.data
const svgDefs = `
<linearGradient id="area-grad-0" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.4" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-1" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ef4444" stop-opacity="0.4" />
<stop offset="100%" stop-color="#ef4444" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f59e0b" stop-opacity="0.4" />
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0.02" />
</linearGradient>
`
const x = (d: DataRecord) => d.x
const y = [
(d: DataRecord) => d.y,
(d: DataRecord) => d.y1,
(d: DataRecord) => d.y2
]
const color = [
`url(#area-grad-0)`,
`url(#area-grad-1)`,
`url(#area-grad-2)`
]
const lineColor = [`#3b82f6`, `#ef4444`, `#f59e0b`]
return (
<VisXYContainer svgDefs={svgDefs} data={data}>
<VisAxis type="x"/>
<VisAxis type="y"/>
<VisArea
x={x}
y={y}
color={color}
line={true}
lineWidth={1}
lineColor={lineColor}
/>
</VisXYContainer>
)
}
@Component({
templateUrl: 'template.html'
})
export class Component {
@Input data: DataRecord[];
svgDefs = `
<linearGradient id="area-grad-0" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.4" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-1" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ef4444" stop-opacity="0.4" />
<stop offset="100%" stop-color="#ef4444" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f59e0b" stop-opacity="0.4" />
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0.02" />
</linearGradient>
`
x = (d: DataRecord) => d.x
y = [
(d: DataRecord) => d.y,
(d: DataRecord) => d.y1,
(d: DataRecord) => d.y2
]
color = [
`url(#area-grad-0)`,
`url(#area-grad-1)`,
`url(#area-grad-2)`
]
lineColor = [`#3b82f6`, `#ef4444`, `#f59e0b`]
}
<vis-xy-container [svgDefs]="svgDefs" [data]="data">
<vis-axis type="x"></vis-axis>
<vis-axis type="y"></vis-axis>
<vis-area
[x]="x"
[y]="y"
[color]="color"
[line]="true"
[lineWidth]="1"
[lineColor]="lineColor"
></vis-area>
</vis-xy-container>
<script lang='ts'>
import { VisXYContainer, VisAxis, VisAxis, VisArea } from '@unovis/svelte'
export let data: DataRecord[]
const svgDefs = `
<linearGradient id="area-grad-0" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.4" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-1" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ef4444" stop-opacity="0.4" />
<stop offset="100%" stop-color="#ef4444" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f59e0b" stop-opacity="0.4" />
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0.02" />
</linearGradient>
`
const x = (d: DataRecord) => d.x
const y = [
(d: DataRecord) => d.y,
(d: DataRecord) => d.y1,
(d: DataRecord) => d.y2
]
const color = [
`url(#area-grad-0)`,
`url(#area-grad-1)`,
`url(#area-grad-2)`
]
const lineColor = [`#3b82f6`, `#ef4444`, `#f59e0b`]
</script>
<VisXYContainer {svgDefs} {data}>
<VisAxis type="x"/>
<VisAxis type="y"/>
<VisArea {x} {y} {color} line={true} lineWidth={1} {lineColor}/>
</VisXYContainer>
<script setup lang="ts">
import { VisXYContainer, VisAxis, VisAxis, VisArea } from '@unovis/vue'
const props = defineProps<{ data: DataRecord[] }>()
const svgDefs = `
<linearGradient id="area-grad-0" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.4" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-1" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ef4444" stop-opacity="0.4" />
<stop offset="100%" stop-color="#ef4444" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f59e0b" stop-opacity="0.4" />
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0.02" />
</linearGradient>
`
const x = (d: DataRecord) => d.x
const y = [
(d: DataRecord) => d.y,
(d: DataRecord) => d.y1,
(d: DataRecord) => d.y2
]
const color = [
`url(#area-grad-0)`,
`url(#area-grad-1)`,
`url(#area-grad-2)`
]
const lineColor = [`#3b82f6`, `#ef4444`, `#f59e0b`]
</script>
<template>
<VisXYContainer :svgDefs="svgDefs" :data="data">
<VisAxis type="x" />
<VisAxis type="y" />
<VisArea
:x="x"
:y="y"
:color="color"
:line="true"
:lineWidth="1"
:lineColor="lineColor"
/>
</VisXYContainer>
</template>
import { VisXYContainer, VisAxis, VisAxis, VisArea } from '@unovis/solid'
function Component(props) {
const data: DataRecord[] = () => props.data
const svgDefs = `
<linearGradient id="area-grad-0" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.4" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-1" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ef4444" stop-opacity="0.4" />
<stop offset="100%" stop-color="#ef4444" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f59e0b" stop-opacity="0.4" />
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0.02" />
</linearGradient>
`
const x = (d: DataRecord) => d.x
const y = [
(d: DataRecord) => d.y,
(d: DataRecord) => d.y1,
(d: DataRecord) => d.y2
]
const color = [
`url(#area-grad-0)`,
`url(#area-grad-1)`,
`url(#area-grad-2)`
]
const lineColor = [`#3b82f6`, `#ef4444`, `#f59e0b`]
return (
<VisXYContainer svgDefs={svgDefs} data={data()}>
<VisAxis type="x"/>
<VisAxis type="y"/>
<VisArea
x={x}
y={y}
color={color}
line={true}
lineWidth={1}
lineColor={lineColor}
/>
</VisXYContainer>
)
}
import { XYContainer, Axis, Area } from '@unovis/ts'
import { data, DataRecord } from './data'
const container = new XYContainer<DataRecord>(node, {
svgDefs: `
<linearGradient id="area-grad-0" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.4" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-1" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ef4444" stop-opacity="0.4" />
<stop offset="100%" stop-color="#ef4444" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f59e0b" stop-opacity="0.4" />
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0.02" />
</linearGradient>
`,
xAxis: new Axis({ type: "x" }),
yAxis: new Axis({ type: "y" }),
components: [
new Area<DataRecord>({
x: (d: DataRecord) => d.x,
y: [
(d: DataRecord) => d.y,
(d: DataRecord) => d.y1,
(d: DataRecord) => d.y2
],
color: [
`url(#area-grad-0)`,
`url(#area-grad-1)`,
`url(#area-grad-2)`
],
line: true,
lineWidth: 1,
lineColor: [`#3b82f6`, `#ef4444`, `#f59e0b`]
})
]
}, data)
Alternatively, you can place the defs in a hidden <svg> element anywhere on the same page, and the url(#id) references will still resolve.
- React
- Angular
- Svelte
- Vue
- Solid
- TypeScript
<VisArea ... color={gradientColors}/>
<svg style="position:absolute;width:0;height:0">
<defs>
<linearGradient id="area-grad-0" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.4" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-1" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ef4444" stop-opacity="0.4" />
<stop offset="100%" stop-color="#ef4444" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f59e0b" stop-opacity="0.4" />
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0.02" />
</linearGradient>
</defs>
</svg>
<vis-area ... [color]="gradientColors"></vis-area>
<svg style="position:absolute;width:0;height:0">
<defs>
<linearGradient id="area-grad-0" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.4" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-1" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ef4444" stop-opacity="0.4" />
<stop offset="100%" stop-color="#ef4444" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f59e0b" stop-opacity="0.4" />
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0.02" />
</linearGradient>
</defs>
</svg>
const chart = new Area({ color: gradientColors, ... })
document.body.insertAdjacentHTML('beforeend', `<svg style="position:absolute;width:0;height:0">
<defs>
<linearGradient id="area-grad-0" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.4" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-1" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ef4444" stop-opacity="0.4" />
<stop offset="100%" stop-color="#ef4444" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f59e0b" stop-opacity="0.4" />
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0.02" />
</linearGradient>
</defs>
</svg>`)
SVG Filters (glow, bevel, 3D)
The same svgDefs property can hold custom SVG <filter> definitions for advanced visual effects —
glow, bevel, inner shadow, or neon styling — that aren't possible with plain CSS. Combine it with the
attributes config, available on every component, to apply a filter to the rendered shapes by their
selector.
Because the filter is built on SourceGraphic, it acts as a pure rendering overlay: it enhances what's
already drawn without touching the chart's colors.
import { StackedBar } from '@unovis/ts'
// A bevel filter (inner shadow + top highlight), clipped to each shape.
const svgDefs = `
<filter id="bevel" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur"/>
<feOffset in="blur" dx="0" dy="2" result="offsetBlur"/>
<feComposite in="SourceAlpha" in2="offsetBlur" operator="out" result="innerShadowMask"/>
<feFlood flood-color="black" flood-opacity="0.4" result="shadowColor"/>
<feComposite in="shadowColor" in2="innerShadowMask" operator="in" result="shadow"/>
<feMerge result="merged">
<feMergeNode in="SourceGraphic"/>
<feMergeNode in="shadow"/>
</feMerge>
<feComposite in="merged" in2="SourceGraphic" operator="in"/>
</filter>`
<VisXYContainer data={data} svgDefs={svgDefs}>
<VisStackedBar
x={d => d.x}
y={accessors}
attributes={{ [StackedBar.selectors.bar]: { filter: 'url(#bevel)' } }}
/>
</VisXYContainer>
The filter is set per shape (.bar, .point, .line, .area, .segmentArc, …) rather than on a
wrapping group, so effects like inner shadow and bevel that rely on each shape's own SourceGraphic
render correctly. See the Visual Effects (SVG Filters) gallery example for glow and bevel applied to
bars, lines, and scatter points.