Loading...
Problem Description
When using AntV G2 to draw charts, manually setting padding
may cause the chart title or other chart components to not display properly or disappear completely.
Related issue: Title not displayed after setting
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'area',padding: 20,title: {align: 'center',title: 'This is a chart title.',subtitle: 'Displayed are sampled values.',},data: [{ country: 'Asia', year: '1750', value: 502 },{ country: 'Asia', year: '1800', value: 635 },{ country: 'Asia', year: '1850', value: 809 },{ country: 'Asia', year: '1900', value: 947 },{ country: 'Asia', year: '1950', value: 1402 },{ country: 'Asia', year: '1999', value: 3634 },{ country: 'Asia', year: '2050', value: 5268 },{ country: 'Africa', year: '1750', value: 106 },{ country: 'Africa', year: '1800', value: 107 },{ country: 'Africa', year: '1850', value: 111 },{ country: 'Africa', year: '1900', value: 133 },{ country: 'Africa', year: '1950', value: 221 },{ country: 'Africa', year: '1999', value: 767 },{ country: 'Africa', year: '2050', value: 1766 },{ country: 'Europe', year: '1750', value: 163 },{ country: 'Europe', year: '1800', value: 203 },{ country: 'Europe', year: '1850', value: 276 },{ country: 'Europe', year: '1900', value: 408 },{ country: 'Europe', year: '1950', value: 547 },{ country: 'Europe', year: '1999', value: 729 },{ country: 'Europe', year: '2050', value: 628 },],encode: {x: 'year',y: 'value',color: 'country',},transform: [{ type: 'stackY' }],style: {fillOpacity: 0.3,},});chart.render();
Cause Analysis
G2 automatically calculates the required spacing for all components by default, but once a fixed padding
value is specified, this automatic adjustment logic is bypassed, potentially causing incomplete component display.
Solutions
There are two ways to solve this problem:
Don't manually set padding
, let G2 automatically calculate the optimal spacing to ensure all components display properly:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'area',title: {align: 'center',title: 'This is a chart title.',subtitle: 'Displayed are sampled values.',},data: [{ country: 'Asia', year: '1750', value: 502 },{ country: 'Asia', year: '1800', value: 635 },{ country: 'Asia', year: '1850', value: 809 },{ country: 'Asia', year: '1900', value: 947 },{ country: 'Asia', year: '1950', value: 1402 },{ country: 'Asia', year: '1999', value: 3634 },{ country: 'Asia', year: '2050', value: 5268 },{ country: 'Africa', year: '1750', value: 106 },{ country: 'Africa', year: '1800', value: 107 },{ country: 'Africa', year: '1850', value: 111 },{ country: 'Africa', year: '1900', value: 133 },{ country: 'Africa', year: '1950', value: 221 },{ country: 'Africa', year: '1999', value: 767 },{ country: 'Africa', year: '2050', value: 1766 },{ country: 'Europe', year: '1750', value: 163 },{ country: 'Europe', year: '1800', value: 203 },{ country: 'Europe', year: '1850', value: 276 },{ country: 'Europe', year: '1900', value: 408 },{ country: 'Europe', year: '1950', value: 547 },{ country: 'Europe', year: '1999', value: 729 },{ country: 'Europe', year: '2050', value: 628 },],encode: {x: 'year',y: 'value',color: 'country',},transform: [{ type: 'stackY' }],style: {fillOpacity: 0.3,},});chart.render();
If you really need to manually set padding
, please ensure sufficient space is reserved for dynamically generated components:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'area',paddingTop: 100,title: {align: 'center',title: 'This is a chart title.',subtitle: 'Displayed are sampled values.',},data: [{ country: 'Asia', year: '1750', value: 502 },{ country: 'Asia', year: '1800', value: 635 },{ country: 'Asia', year: '1850', value: 809 },{ country: 'Asia', year: '1900', value: 947 },{ country: 'Asia', year: '1950', value: 1402 },{ country: 'Asia', year: '1999', value: 3634 },{ country: 'Asia', year: '2050', value: 5268 },{ country: 'Africa', year: '1750', value: 106 },{ country: 'Africa', year: '1800', value: 107 },{ country: 'Africa', year: '1850', value: 111 },{ country: 'Africa', year: '1900', value: 133 },{ country: 'Africa', year: '1950', value: 221 },{ country: 'Africa', year: '1999', value: 767 },{ country: 'Africa', year: '2050', value: 1766 },{ country: 'Europe', year: '1750', value: 163 },{ country: 'Europe', year: '1800', value: 203 },{ country: 'Europe', year: '1850', value: 276 },{ country: 'Europe', year: '1900', value: 408 },{ country: 'Europe', year: '1950', value: 547 },{ country: 'Europe', year: '1999', value: 729 },{ country: 'Europe', year: '2050', value: 628 },],encode: {x: 'year',y: 'value',color: 'country',},transform: [{ type: 'stackY' }],style: {fillOpacity: 0.3,},});chart.render();
You can also pass paddingTop
when creating the Chart
instance, which works exactly the same:
const chart = new Chart({container: 'container',paddingTop: 100,});
Notes
padding
, it's recommended to determine appropriate values through debuggingSee the detailed documentation on Chart Layout.
Problem Description
When drawing stacked area charts or multi-line charts, you need to configure the chart styles. However, when directly specifying stroke colors or stroke opacity in the style, all areas or regions will apply the same style. How do you differentiate styles for different categories?
Solution
When configuring mark styles, not only do we support direct configuration like string
and number
, but also callback functions like string | (datum, index, data, column) => string
. We can customize special styles for different filter conditions based on the parameters in the callback function. Note that the datum
here is the data item corresponding to the mark, which depends on the mark's characteristics Graphic Template. Each graphic corresponds to one or more data items. For example, scatter plots have each graphic corresponding to one data item, while area charts have one graphic corresponding to multiple data items, and datum
will also return multiple data records.
Examples
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'area',data: [{ country: 'Asia', year: '1750', value: 502 },{ country: 'Asia', year: '1800', value: 635 },{ country: 'Asia', year: '1850', value: 809 },{ country: 'Asia', year: '1900', value: 947 },{ country: 'Asia', year: '1950', value: 1402 },{ country: 'Asia', year: '1999', value: 3634 },{ country: 'Asia', year: '2050', value: 5268 },{ country: 'Africa', year: '1750', value: 106 },{ country: 'Africa', year: '1800', value: 107 },{ country: 'Africa', year: '1850', value: 111 },{ country: 'Africa', year: '1900', value: 133 },{ country: 'Africa', year: '1950', value: 221 },{ country: 'Africa', year: '1999', value: 767 },{ country: 'Africa', year: '2050', value: 1766 },{ country: 'Europe', year: '1750', value: 163 },{ country: 'Europe', year: '1800', value: 203 },{ country: 'Europe', year: '1850', value: 276 },{ country: 'Europe', year: '1900', value: 408 },{ country: 'Europe', year: '1950', value: 547 },{ country: 'Europe', year: '1999', value: 729 },{ country: 'Europe', year: '2050', value: 628 },],encode: {x: 'year',y: 'value',color: 'country',},transform: [{ type: 'stackY' }],style: {fillOpacity: 0.3,lineWidth: (datum, index, data, column) =>datum[0].country === 'Asia' ? 2 : 0, // Area marks have default stroke width of 0, need to explicitly set lineWidth to show strokestroke: (datum, index, data, column) =>datum[0].country === 'Asia' ? 'red' : null,},});chart.render();
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'line',data: {type: 'fetch',value:'https://gw.alipayobjects.com/os/bmw-prod/c48dbbb1-fccf-4a46-b68f-a3ddb4908b68.json',},encode: {x: 'date',y: 'value',color: 'type',},axis: {y: {labelFormatter: (v) =>`${v}`.replace(/\d{1,3}(?=(\d{3})+$)/g, (s) => `${s},`),},},scale: { color: { range: ['#30BF78', '#F4664A', '#FAAD14'] } }, // Custom color domain for color channelstyle: {lineDash: (datum, index, data, column) => {if (datum[0].type === 'register') return [4, 4];},lineWidth: (datum, index, data, column) => {if (datum[0].type !== 'register') return 2;},},});chart.render();
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'point',data: {type: 'fetch',value:'https://gw.alipayobjects.com/os/bmw-prod/bd73a175-4417-4749-8b88-bc04d955e899.csv',},encode: {x: 'x',y: 'y',shape: 'category',color: 'category',size: () => 1,},legend: {size: false,},scale: {shape: { range: ['circle', 'plus', 'diamond'] },size: { rangeMin: 5 }, // Set minimum domain for size channel scale to 5}, // Define shape domain for shape channeltransform: [{ type: 'groupX', size: 'sum' }], // Group discrete x channel and map sum to size channelstyle: {fillOpacity: (datum, index, data, column) =>datum.category !== 'setosa' ? 0.8 : 0,stroke: (datum, index, data, column) => {if (datum.category !== 'setosa') {return '#FADC7C';}},lineWidth: (datum, index, data, column) =>datum.category !== 'setosa' ? 1 : 2,},});chart.render();
Problem Description
In business scenarios, tooltip needs to display a lot of information, so enterable: true
is configured to support scrolling when hovering. However, when moving the mouse, tooltip sometimes doesn't close properly, causing obstruction and lag effects on the chart.
Cause Analysis and Solutions
G2's internal algorithm tries to constrain the tooltip within the chart, but if the chart height is too small, even with automatic tooltip position calculation, it will still overflow the chart.
The chart area is too small, and moving directly from inside the tooltip to outside the chart doesn't trigger the tooltip disappear event, which is bound to the chart.
Both issues are caused by the chart area being too small and tooltip taking up too much space (more than half). It's recommended to reduce tooltip area (scrolling is already available) or increase chart area.
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'view',autoFit: true,data: [{ year: '1991', value: 15468 },{ year: '1992', value: 16100 },{ year: '1993', value: 15900 },{ year: '1994', value: 17409 },{ year: '1995', value: 17000 },{ year: '1996', value: 31056 },{ year: '1997', value: 31982 },{ year: '1998', value: 32040 },{ year: '1999', value: 33233 },],children: [{type: 'area',encode: { x: (d) => d.year, y: 'value', shape: 'area' },style: { opacity: 0.2 },axis: { y: { labelFormatter: '~s', title: false } },},{ type: 'line', encode: { x: 'year', y: 'value', shape: 'line' } },],});chart.render();
Solution
Configure the nice
property of the scale that needs adjustment to true, extending the domain range to make the output ticks display more friendly.
({scale: {y: {nice: true,},},});
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'view',autoFit: true,data: [{ year: '1991', value: 15468 },{ year: '1992', value: 16100 },{ year: '1993', value: 15900 },{ year: '1994', value: 17409 },{ year: '1995', value: 17000 },{ year: '1996', value: 31056 },{ year: '1997', value: 31982 },{ year: '1998', value: 32040 },{ year: '1999', value: 33233 },],scale: {y: {nice: true, // Extend y channel scale domain range to make output ticks display more friendly},},children: [{type: 'area',encode: { x: (d) => d.year, y: 'value', shape: 'area' },style: { opacity: 0.2 },axis: { y: { labelFormatter: '~s', title: false } },},{ type: 'line', encode: { x: 'year', y: 'value', shape: 'line' } },],});chart.render();
Problem Description
In business scenarios, you may need the y-axis domain to display opposite to normal coordinate axis, making values increase from top to bottom. In other words, smaller y channel values should appear higher in the chart, suitable for scenarios where smaller numbers represent greater weight, such as rankings.
Solution
[1,0]
. If inversion is needed, adjust to [0,1]
. For better appearance, you can also adjust the x-axis position accordingly.Here's an example of a top-to-bottom bar chart. The same principle applies when creating left-to-right bar charts. (Note that bar charts are column charts with transposed coordinate axis, where left-right corresponds to the x-axis)
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'interval',autoFit: true,data: [{ letter: 'A', frequency: 0.08167 },{ letter: 'B', frequency: 0.01492 },{ letter: 'C', frequency: 0.02782 },{ letter: 'D', frequency: 0.04253 },{ letter: 'E', frequency: 0.12702 },],encode: { x: 'letter', y: 'frequency' },scale: { y: { range: [0, 1] } },axis: { x: { position: 'top' } },});chart.render();
encode.y
, axis.y.labelFormatter
, and other properties.import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'view',autoFit: true,paddingRight: 10,data: [{ month: 'January', rank: 200 },{ month: 'February', rank: 160 },{ month: 'March', rank: 100 },{ month: 'April', rank: 80 },{ month: 'May', rank: 99 },{ month: 'June', rank: 36 },{ month: 'July', rank: 40 },{ month: 'August', rank: 20 },{ month: 'September', rank: 12 },{ month: 'October', rank: 15 },{ month: 'November', rank: 6 },{ month: 'December', rank: 1 },],scale: {y: {nice: true,tickMethod: () => [0, 50, 100, 170, 199],},},axis: {y: {labelFormatter: (d) => `Rank ${200 - d}`,},},children: [{type: 'area',encode: { x: (d) => d.month, y: (d) => 200 - d.rank, shape: 'smooth' },style: { opacity: 0.2 },axis: { y: { labelFormatter: '~s', title: false } },style: {fill: 'l(270) 0:#ffffff 0.9:#7ec2f3 1:#1890ff',fillOpacity: 0.2,},tooltip: false,},{type: 'line',encode: { x: (d) => d.month, y: (d) => 200 - d.rank, shape: 'smooth' },interaction: {tooltip: {render: (event, { title, items }) => `<div style="display: flex; align-items: center;"><span>${title}: Rank </span><h2style="margin-left: 8px;margin-right: 8px;margin-top:4px;font-size: 18px;line-height: 36px;font-weight: 500px">${200 - items[0].value}</h2></div>`,},},style: {lineWidth: 2,},},],});chart.render();
Below is a simple line chart where you can see the x-axis has obvious paddingOuter
with a default value of 0.5
.
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'line',viewStyle: {contentFill: 'l(270) 0:#ffffff 0.5:#7ec2f3 1:#1890ff',},data: [{ year: '1991', value: 3 },{ year: '1992', value: 4 },{ year: '1993', value: 3.5 },{ year: '1994', value: 5 },{ year: '1995', value: 4.9 },{ year: '1996', value: 6 },{ year: '1997', value: 7 },{ year: '1998', value: 9 },{ year: '1999', value: 13 },],labels: [{ text: 'value', style: { dx: -10, dy: -12 } }],encode: { x: 'year', y: 'value' },scale: { y: { domainMin: 0, nice: true } },});chart.render();
Point scale is a band scale with constant bandWidth of 0, internally fixing the following properties:
padding: 0.5, // Internal assignmentpaddingInner: 1, // Cannot be modifiedpaddingOuter: 0.5 // Internal assignment
If you want to customize the paddingOuter
value, you can achieve this by modifying padding
. For example:
(scale: {x: {type: 'point',padding: 0, // Only affects paddingOuter, paddingInner is always 1},});
Through configuration, you can make the spacing at both ends of the line chart equal to 0
.
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'line',viewStyle: {contentFill: 'l(270) 0:#ffffff 0.5:#7ec2f3 1:#1890ff',},data: [{ year: '1991', value: 3 },{ year: '1992', value: 4 },{ year: '1993', value: 3.5 },{ year: '1994', value: 5 },{ year: '1995', value: 4.9 },{ year: '1996', value: 6 },{ year: '1997', value: 7 },{ year: '1998', value: 9 },{ year: '1999', value: 13 },],labels: [{ text: 'value', style: { dx: -10, dy: -12 } }],encode: { x: 'year', y: 'value' },scale: {y: { domainMin: 0, nice: true },x: {padding: 0,},},});chart.render();
There's currently no built-in API for this, so you need to manually trigger legendFilter to achieve it.
import { Chart, ChartEvent } from '@antv/g2';const chart = new Chart({ container: 'container' });chart.options({type: 'interval',data: [{ genre: 'Sports', sold: 100 },{ genre: 'Strategy', sold: 115 },{ genre: 'Action', sold: 120 },{ genre: 'Shooter', sold: 350 },{ genre: 'Other', sold: 150 },],encode: { x: 'genre', y: 'sold', color: 'genre' },});chart.render();chart.on(ChartEvent.AFTER_RENDER, () => {chart.emit('legend:filter', {data: { channel: 'color', values: ['Sports', 'Strategy', 'Action'] },});});
You can set animate: false
to avoid triggering update animations, but there will still be flickering. This will be handled internally through configuration options in the future to achieve better filtering effects.
Problem Description
In certain interactive scenarios, you need to listen for whether the mouse has moved outside the chart container boundaries to execute corresponding business logic, such as hiding tooltips, resetting highlight states, etc.
Solution
You can detect mouse enter and leave states by listening to DOM events on the chart container.
import { Chart, ChartEvent } from '@antv/g2';const chart = new Chart({ container: 'container', autoFit: true });chart.options({type: 'interval',data: [{ genre: 'Sports', sold: 100 },{ genre: 'Strategy', sold: 115 },{ genre: 'Action', sold: 120 },{ genre: 'Shooter', sold: 350 },{ genre: 'Other', sold: 150 },],encode: { x: 'genre', y: 'sold', color: 'genre' },viewStyle: {viewFill: 'blue',viewFillOpacity: 0.3,},});chart.render();let containerMouseEntered = false;chart.on('afterrender', () => {// Get chart container DOM elementconst container = chart.getContainer();// Create status display panelconst statusPanel = document.createElement('div');statusPanel.id = 'mouse-status-panel';statusPanel.style.cssText = `position: absolute;top: 10px;right: 10px;background: rgba(0, 0, 0, 0.8);color: white;padding: 12px;border-radius: 6px;font-family: monospace;font-size: 12px;line-height: 1.4;z-index: 1000;min-width: 220px;`;// Update status displayconst updateStatus = (isInside, eventInfo = {}) => {const status = isInside ? '✅ Mouse inside container' : '❌ Mouse outside container';const containerRect = container.getBoundingClientRect();statusPanel.innerHTML = `<div style="font-weight: bold; margin-bottom: 8px;">${status}</div><div>Container size: ${container.offsetWidth} × ${container.offsetHeight}</div><div>Container position: (${Math.round(containerRect.left)}, ${Math.round(containerRect.top,)})</div>${eventInfo.clientX !== undefined? `<div>Mouse coordinates: (${eventInfo.clientX}, ${eventInfo.clientY})</div>`: ''}${eventInfo.type ? `<div>Event type: ${eventInfo.type}</div>` : ''}<div style="margin-top: 8px; font-size: 11px; opacity: 0.8;">Move mouse over the chart to try!</div>`;};if (container) {// Add status panel to container's parent elementcontainer.parentElement.style.position = 'relative';container.parentElement.appendChild(statusPanel);// Initialize displayupdateStatus(false);// Listen for mouse entering containercontainer.addEventListener('mouseenter', (e) => {containerMouseEntered = true;updateStatus(true, {type: e.type,clientX: e.clientX,clientY: e.clientY,});});// Listen for mouse moving within containercontainer.addEventListener('mousemove', (e) => {if (containerMouseEntered) {updateStatus(true, {type: e.type,clientX: e.clientX,clientY: e.clientY,});}});// Listen for mouse leaving containercontainer.addEventListener('mouseleave', (e) => {if (containerMouseEntered) {containerMouseEntered = false;updateStatus(false, {type: e.type,clientX: e.clientX,clientY: e.clientY,});}});}});
Complete Example
Here's a complete example showing how to control tooltip display and hiding through event triggers. When clicking on an element, the tooltip shows; when clicking on empty area or when the mouse leaves the container, the tooltip hide event is manually triggered.
import { Chart, ChartEvent } from '@antv/g2';const chart = new Chart({ container: 'container', autoFit: true });chart.options({type: 'interval',data: [{ genre: 'Sports', sold: 100 },{ genre: 'Strategy', sold: 115 },{ genre: 'Action', sold: 120 },{ genre: 'Shooter', sold: 350 },{ genre: 'Other', sold: 150 },],encode: { x: 'genre', y: 'sold', color: 'genre' },viewStyle: {viewFill: 'blue',viewFillOpacity: 0.3,},interaction: {tooltip: {disableNative: true, // Disable pointerover and pointerout events.},},});chart.render();let containerMouseEntered = false;chart.on('afterrender', () => {// Get chart container DOM elementconst container = chart.getContainer();if (container) {// Listen for mouse entering containercontainer.addEventListener('mouseenter', (e) => {containerMouseEntered = true;});// Listen for mouse leaving containercontainer.addEventListener('mouseleave', (e) => {if (containerMouseEntered) {containerMouseEntered = false;chart.emit('tooltip:hide');}});}});chart.on('element:click', ({ data }) => chart.emit('tooltip:show', { data }));chart.on('plot:click', () => chart.emit('tooltip:hide'));
Problem Description
When using G2 to draw charts, the default legend position and size may not meet business requirements. You need precise control over the legend's position, alignment, dimensions, and spacing from the chart.
Solution
G2 provides multiple configuration options to precisely control legend size and layout:
Basic Position Configuration
Use position
to set the legend's basic position:
legend: {color: {position: 'top', // 'top' | 'right' | 'left' | 'bottom'}}
Precise Alignment Configuration
Use layout
to configure the legend's precise alignment using Flexbox layout model:
legend: {color: {position: 'top',layout: {justifyContent: 'center', // Main axis alignment: 'flex-start' | 'center' | 'flex-end'alignItems: 'flex-start', // Cross axis alignment: 'flex-start' | 'center' | 'flex-end'flexDirection: 'row', // Main axis direction: 'row' | 'column'}}}
Size Control Configuration
legend: {color: {size: 80, // Legend cross axis sizelength: 300, // Legend main axis lengthcrossPadding: 20, // Distance from chart}}
Complete Example
Here are several common legend layout scenarios:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',height: 400,width: 600,});const container = chart.getContainer();const data = [{ genre: 'Sports', sold: 50 },{ genre: 'Strategy', sold: 115 },{ genre: 'Action', sold: 120 },{ genre: 'Shooter', sold: 350 },{ genre: 'Other', sold: 150 },];chart.options({type: 'interval',data,encode: { x: 'genre', y: 'sold', color: 'genre' },legend: {color: {position: 'top',layout: {justifyContent: 'center', // Horizontal centeralignItems: 'flex-start',},size: 60, // Control legend cross axis sizelength: 250, // Control legend main axis lengthcrossPadding: 20, // Distance from chart},},});// Create layout selectorconst controlPanel = document.createElement('div');controlPanel.style.cssText = `margin-bottom: 16px;padding: 16px;background: #f5f5f5;border-radius: 8px;display: grid;grid-template-columns: 1fr 1fr;gap: 16px;`;// Layout scenario selectorconst sceneContainer = document.createElement('div');sceneContainer.innerHTML = `<label style="display: block; margin-bottom: 8px; font-weight: bold;">Select layout scenario:</label>`;const sceneSelect = document.createElement('select');sceneSelect.style.cssText = 'width: 100%; padding: 4px;';const scenes = [{ label: 'Top center (Dashboard style)', value: 'top-center' },{ label: 'Right vertical center (Detailed chart)', value: 'right-center' },{ label: 'Bottom left aligned (Space saving)', value: 'bottom-start' },{ label: 'Left bottom aligned', value: 'left-end' },{ label: 'Right top aligned (Compact)', value: 'right-start' },];sceneSelect.innerHTML = scenes.map((scene, index) =>`<option value="${scene.value}" ${index === 0 ? 'selected' : ''}>${scene.label}</option>`,).join('');sceneContainer.appendChild(sceneSelect);// Size controlconst sizeContainer = document.createElement('div');sizeContainer.innerHTML = `<label style="display: block; margin-bottom: 8px; font-weight: bold;">Legend size control:</label><div style="margin-bottom: 8px;"><label>crossPadding (Distance from chart): </label><input type="range" id="crossPadding" min="5" max="50" value="20" style="width: 100%;"><span id="crossPaddingValue">20</span></div><div style="margin-bottom: 8px;"><label>size (Cross axis size): </label><input type="range" id="size" min="40" max="200" value="60" style="width: 100%;"><span id="sizeValue">60</span></div><div><label>length (Main axis length): </label><input type="range" id="length" min="40" max="400" value="250" style="width: 100%;"><span id="lengthValue">250</span></div>`;controlPanel.appendChild(sceneContainer);controlPanel.appendChild(sizeContainer);const updateChart = () => {const selectedScene = sceneSelect.value;const crossPadding = parseInt(document.getElementById('crossPadding').value);const size = parseInt(document.getElementById('size').value);const length = parseInt(document.getElementById('length').value);let position, justifyContent;switch (selectedScene) {case 'top-center':position = 'top';justifyContent = 'center';break;case 'right-center':position = 'right';justifyContent = 'center';break;case 'bottom-start':position = 'bottom';justifyContent = 'flex-start';break;case 'left-end':position = 'left';justifyContent = 'flex-end';break;case 'right-start':position = 'right';justifyContent = 'flex-start';break;}chart.options({legend: {color: {position,layout: {justifyContent,alignItems: 'flex-start',},size,length,crossPadding,},},});chart.render();};// Bind eventssceneSelect.addEventListener('change', updateChart);document.addEventListener('DOMContentLoaded', () => {const crossPaddingSlider = document.getElementById('crossPadding');const crossPaddingValue = document.getElementById('crossPaddingValue');const sizeSlider = document.getElementById('size');const sizeValue = document.getElementById('sizeValue');const lengthSlider = document.getElementById('length');const lengthValue = document.getElementById('lengthValue');if (crossPaddingSlider && crossPaddingValue) {crossPaddingSlider.addEventListener('input', (e) => {crossPaddingValue.textContent = e.target.value;updateChart();});}if (sizeSlider && sizeValue) {sizeSlider.addEventListener('input', (e) => {sizeValue.textContent = e.target.value;updateChart();});}if (lengthSlider && lengthValue) {lengthSlider.addEventListener('input', (e) => {lengthValue.textContent = e.target.value;updateChart();});}});// Insert control panelcontainer.insertBefore(controlPanel, container.firstChild);// Initial renderchart.render();// Ensure slider events are properly boundsetTimeout(() => {const crossPaddingSlider = document.getElementById('crossPadding');const crossPaddingValue = document.getElementById('crossPaddingValue');const sizeSlider = document.getElementById('size');const sizeValue = document.getElementById('sizeValue');const lengthSlider = document.getElementById('length');const lengthValue = document.getElementById('lengthValue');if (crossPaddingSlider && crossPaddingValue) {crossPaddingSlider.addEventListener('input', (e) => {crossPaddingValue.textContent = e.target.value;updateChart();});}if (sizeSlider && sizeValue) {sizeSlider.addEventListener('input', (e) => {sizeValue.textContent = e.target.value;updateChart();});}if (lengthSlider && lengthValue) {lengthSlider.addEventListener('input', (e) => {lengthValue.textContent = e.target.value;updateChart();});}}, 100);
See the complete documentation on Legend Component for more configuration options.
Problem Description
In data visualization, it's often necessary to draw line charts containing both actual values and predicted values, where the actual value portion is represented by solid lines and the predicted value portion by dashed lines, so users can clearly distinguish between historical data and prediction data.
Solution
In G2, one line corresponds to one Mark, and you cannot set different styles within the same line. To achieve mixed solid and dashed line effects, you need to:
Core Approach: Group data by type (actual/predicted), use series
encoding to create multiple line segments, then set different styles for different types of line segments through style
callback functions.
Key Configuration:
color
: Used for legend grouping, different groups show different colorsseries
: Used to create multiple line segments, data points with the same series value will be connected as one linestyle.lineDash
callback functionExample Code
import { Chart } from '@antv/g2';const chart = new Chart({ container: 'container' });chart.options({type: 'view',autoFit: true,data: [// Product A actual data{year: '2018',value: 80,product: 'Product A',type: 'Actual',series: 'Product A-Actual',},{year: '2019',value: 95,product: 'Product A',type: 'Actual',series: 'Product A-Actual',},{year: '2020',value: 100,product: 'Product A',type: 'Actual',series: 'Product A-Actual',},{year: '2021',value: 120,product: 'Product A',type: 'Actual',series: 'Product A-Actual',},{year: '2022',value: 110,product: 'Product A',type: 'Actual',series: 'Product A-Actual',},// Product A prediction data (note 2022 connection point duplication){year: '2022',value: 110,product: 'Product A',type: 'Prediction',series: 'Product A-Prediction',},{year: '2023',value: 125,product: 'Product A',type: 'Prediction',series: 'Product A-Prediction',},{year: '2024',value: 140,product: 'Product A',type: 'Prediction',series: 'Product A-Prediction',},{year: '2025',value: 160,product: 'Product A',type: 'Prediction',series: 'Product A-Prediction',},{year: '2026',value: 180,product: 'Product A',type: 'Prediction',series: 'Product A-Prediction',},// Product B actual data{year: '2018',value: 60,product: 'Product B',type: 'Actual',series: 'Product B-Actual',},{year: '2019',value: 70,product: 'Product B',type: 'Actual',series: 'Product B-Actual',},{year: '2020',value: 80,product: 'Product B',type: 'Actual',series: 'Product B-Actual',},{year: '2021',value: 90,product: 'Product B',type: 'Actual',series: 'Product B-Actual',},{year: '2022',value: 95,product: 'Product B',type: 'Actual',series: 'Product B-Actual',},// Product B prediction data{year: '2022',value: 95,product: 'Product B',type: 'Prediction',series: 'Product B-Prediction',},{year: '2023',value: 100,product: 'Product B',type: 'Prediction',series: 'Product B-Prediction',},{year: '2024',value: 110,product: 'Product B',type: 'Prediction',series: 'Product B-Prediction',},{year: '2025',value: 125,product: 'Product B',type: 'Prediction',series: 'Product B-Prediction',},{year: '2026',value: 145,product: 'Product B',type: 'Prediction',series: 'Product B-Prediction',},],encode: {x: 'year',y: 'value',color: 'product', // Used for legend grouping (product dimension)series: 'series', // Used to create line segments (product-type combination)},scale: {x: { range: [0, 1] },y: { nice: true },},axis: {x: { title: 'Year' },y: { title: 'Sales (10k yuan)' },},children: [{type: 'line',encode: { shape: 'smooth' },style: {lineWidth: 2,lineDash: (d) => {// Set line type based on data type: prediction data uses dashed line, actual data uses solid linereturn d[0].type === 'Prediction' ? [4, 4] : null;},},},{type: 'point',encode: { shape: 'circle' },style: { size: 3 },},],});chart.render();
Key Points
Data Structure Design: Each data item includes product
(product), type
(actual/prediction), series
(line segment identifier) fields
Connection Point Handling: 2022 data exists in both actual and prediction groups to ensure line continuity
Encoding Configuration:
color: 'product'
: Group by product, generate legendseries: 'series'
: Group by combination field, create independent line segmentsStyle Callback:
style: {lineDash: (d) => (d[0].type === 'Prediction' ? [4, 4] : null);}
Notes
series
encoding determines which data points will be connected as one linecolor
encoding affects legend display and color mappingd[0]
in style callback function represents the first data point corresponding to the current line segmentimport { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});const data = [{ category: 'Frontend Development', type: 'HTML Structure', score: 3.48 },{ category: 'Frontend Development', type: 'CSS Styling', score: 3.52 },{ category: 'Frontend Development', type: 'JavaScript Programming', score: 3.31 },{ category: 'Frontend Development', type: 'React Framework', score: 3.28 },{ category: 'Backend Development', type: 'Java Programming', score: 3.35 },{ category: 'Backend Development', type: 'Database Design', score: 3.58 },{ category: 'Backend Development', type: 'API Development', score: 3.12 },{ category: 'Backend Development', type: 'Microservice Architecture', score: 3.45 },{ category: 'Data Analysis', type: 'Python Data Processing', score: 3.42 },{ category: 'Data Analysis', type: 'SQL Query Optimization', score: 3.33 },{ category: 'Data Analysis', type: 'Machine Learning Modeling', score: 3.56 },{ category: 'Data Analysis', type: 'Data Visualization', score: 3.39 },{ category: 'Product Design', type: 'User Experience Design', score: 3.47 },{ category: 'Product Design', type: 'Interactive Prototyping', score: 3.24 },{ category: 'Product Design', type: 'Requirements Analysis', score: 3.51 },{ category: 'Product Design', type: 'Competitive Analysis', score: 3.38 },{ category: 'Testing Quality', type: 'Automated Test Scripts', score: 3.44 },{ category: 'Testing Quality', type: 'Performance Testing', score: 3.29 },{ category: 'Testing Quality', type: 'Security Vulnerability Scanning', score: 3.36 },{ category: 'Testing Quality', type: 'Compatibility Verification', score: 3.18 },{ category: 'DevOps Deployment', type: 'Docker Containerization', score: 3.41 },{ category: 'DevOps Deployment', type: 'Kubernetes Orchestration', score: 3.33 },{ category: 'DevOps Deployment', type: 'Monitoring and Alerting', score: 3.27 },{ category: 'DevOps Deployment', type: 'CI/CD Pipeline', score: 3.49 },];chart.options({type: 'interval',autoFit: true,data,encode: {x: 'type',y: 'score',color: (d) => d.category,},coordinate: {transform: [{type: 'transpose',},],},axis: {x: { title: false }, // Hide x-axis title},scale: {color: {range: ['#BAE7FF', '#80C9FE', '#70E3E3', '#ABF5F5', '#FFB3BA', '#FFDFBA'], // Customize colors here},},});chart.render();
Problem Description
When using G2's state management (State) configuration, the configured active
, selected
, and other state styles don't take effect, and the chart's interactive effects don't meet expectations.
Cause Analysis
When the syntax is correct, State configuration not taking effect usually has the following reasons:
Solutions
With multiple Marks, you must configure State at each Mark level separately:
// ❌ Wrong: With multiple marks, state at view level won't propagatechart.options({type: 'view',state: { active: { fill: 'red' } }, // This configuration won't propagate to child markschildren: [{ type: 'line' }, { type: 'point' }],});// ✅ Correct: Configure state at each mark level separatelychart.options({type: 'view',children: [{type: 'line',state: { active: { stroke: 'red', strokeWidth: 2 } },},{type: 'point',state: { active: { fill: 'red', r: 6 } },},],});
With single Mark, you can configure at view level:
// ✅ With single mark, state configuration at view level takes effectchart.options({type: 'view',state: { active: { fill: 'red' } }, // Will propagate to child markchildren: [{ type: 'line' }, // Will inherit state configuration from view],});
Configure directly at Mark level:
// ✅ Configure directly at mark levelchart.options({type: 'line',state: { active: { stroke: 'red', strokeWidth: 2 } },});
State needs to work with interactions to take effect:
chart.options({type: 'interval',state: {active: { fill: 'red' },inactive: { fill: '#aaa' },selected: { fill: 'orange' },unselected: { fill: '#eee' },},// Must configure corresponding interactionsinteraction: {elementHighlight: true, // Enable hover highlightelementSelect: true, // Enable click selection},});
Common interactions and corresponding states:
Interaction | Corresponding State | Description |
---|---|---|
elementHighlight | active/inactive | Hover highlight |
elementSelect | selected/unselected | Click selection |
brushHighlight | active/inactive | Area brush highlight |
legendHighlight | active/inactive | Legend highlight |
elementHighlightByColor | active/inactive | Highlight by color |
elementSelectByColor | selected/unselected | Select by color |
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'interval',data: [{ letter: 'A', frequency: 0.08167 },{ letter: 'B', frequency: 0.01492 },{ letter: 'C', frequency: 0.02782 },],encode: { x: 'letter', y: 'frequency' },state: {// On hover: green fill + black strokeactive: { fill: 'green', stroke: 'black', strokeWidth: 1 },// On selection: red fill (overrides active fill) + keeps active strokeselected: { fill: 'red' },},interaction: { elementHighlight: true, elementSelect: true },});chart.render();
Problem Description
When using G2's state management with both elementHighlight
and elementSelect
interactions enabled, multiple states (like active
and selected
) may take effect simultaneously, but the style behavior doesn't meet expectations.
Cause Analysis
G2 supports multiple states taking effect simultaneously. When the same property is configured by multiple states, the final effective style is selected based on priority. Different states have the following priorities:
selected: 3 (highest)unselected: 3active: 2inactive: 2default: 1 (lowest)
Solutions
Higher priority states will override properties of the same name in lower priority states:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'interval',data: [{ letter: 'A', frequency: 0.08167 },{ letter: 'B', frequency: 0.01492 },{ letter: 'C', frequency: 0.02782 },],encode: { x: 'letter', y: 'frequency' },state: {// On hover: green fill + black strokeactive: { fill: 'green', stroke: 'black', strokeWidth: 1 },// On selection: red fill (overrides active fill) + keeps active strokeselected: { fill: 'red' },},interaction: { elementHighlight: true, elementSelect: true },});chart.render();
Effect explanation:
Avoid configuring the same style properties in different priority states, or ensure high priority states provide complete style configuration:
chart.options({state: {active: {stroke: 'blue',strokeWidth: 2,opacity: 0.8,},selected: {fill: 'orange',stroke: 'black',strokeWidth: 3,// Don't configure opacity, will keep active's opacity effect},},});
For complex state combinations, you can use functions to dynamically calculate styles:
chart.options({state: {active: {fill: (d) => (d.frequency > 0.05 ? 'lightblue' : 'lightgreen'),},selected: {fill: (d) => (d.frequency > 0.05 ? 'darkblue' : 'darkgreen'),strokeWidth: 3,},},});
Problem Description
When using G2 to draw charts in specific business scenarios, data often contains invalid values like null
, undefined
, or empty strings. By default, these null values are also displayed in the tooltip, affecting user experience and data readability.
Solution
You can use interaction.tooltip.filter
configuration to filter out these invalid data items, preventing null values from showing in the tooltip.
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',});chart.options({type: 'view',data: [{ month: 'Jan', city: 'Tokyo', temperature: null },{ month: 'Jan', city: 'London', temperature: 3.9 },{ month: 'Feb', city: 'Tokyo', temperature: 8 },{ month: 'Feb', city: 'London', temperature: 4.2 },{ month: 'Mar', city: 'Tokyo', temperature: 9.5 },{ month: 'Mar', city: 'London', temperature: 5.7 },],encode: { x: 'month', y: 'temperature', color: 'city' },// Filter null and undefined valuesinteraction: {tooltip: {filter: (d) => d.value !== null && d.value !== undefined,},},children: [{type: 'line',encode: { shape: 'smooth' },tooltip: {items: [{ channel: 'y' }],},},{ type: 'point', encode: { shape: 'point' }, tooltip: false },],});chart.render();
Besides filtering null values, you can also filter data in specific value ranges:
// Filter negative values and null valuesinteraction: {tooltip: {filter: (d) => d.value !== null && d.value !== undefined && d.value >= 0,},}// Filter outliers (data outside reasonable range)interaction: {tooltip: {filter: (d) => {if (d.value === null || d.value === undefined) return false;return d.value >= 0 && d.value <= 1000; // Only show values in 0-1000 range},},}
Problem Description
When using G2 to create charts, legend item text may be very long and cannot be fully displayed due to layout space constraints. We need to implement ellipsis for long text while supporting hover interaction to show the complete content.
Solution
G2 provides the poptip
configuration to solve the problem of long legend text. By configuring poptip
, you can display complete tooltip information when legend text is truncated and the user hovers over it.
Key Configuration
itemWidth
: Limit legend item width to trigger text truncationpoptip.render
: Customize tooltip content, supports string or html
poptip.domStyles
: Customize tooltip box stylespoptip.position
: Set tooltip positionpoptip.offset
: Set tooltip offset, recommend setting to [0, positive number] to avoid flickering when triggering poptip
Complete Example
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',height: 300,});chart.options({type: 'interval',data: [{ category: 'This is a very long category name A that exceeds the display range', value: 40 },{ category: 'This is a very long category name B that exceeds the display range', value: 32 },{ category: 'This is a very long category name C that exceeds the display range', value: 28 },],encode: { x: 'category', y: 'value', color: 'category' },coordinate: {transform: [{type: 'transpose',},],},legend: {color: {itemWidth: 120, // Limit width to trigger poptippoptip: {render: (item) => `Full name: ${item.label}`,position: 'top',offset: [0, 20],domStyles: {'.component-poptip': {background: 'rgb(114, 128, 191)',color: '#fff',padding: '12px 16px',borderRadius: '8px',backdropFilter: 'blur(10px)',fontSize: '14px',lineHeight: '1.5',maxWidth: '280px',zIndex: '1000',},'.component-poptip-arrow': {display: 'block',borderTopColor: '#667eea',},'.component-poptip-text': {color: '#fff',lineHeight: '1.5',},},},},},});chart.render();
See the Legend Component documentation for more configuration options.