Loading...
The band scale is a special type of ordinal scale that maps discrete domains (such as categories, names, etc.) to continuous numerical ranges, allocating equal-width intervals (bands) for each category. Unlike regular ordinal scales, band scales not only focus on point positions but also consider the interval width occupied by each category.
Key characteristics of band scales:
In G2, the band scale is the default x-axis scale for bar charts (interval marks), automatically handling the mapping and layout of categorical data.
Property | Description | Type | Default | Required |
---|---|---|---|---|
domain | Sets the domain array, i.e., possible values of input data | number[] | string[] | Date[] | [] | |
range | Sets the range of data mapping, i.e., the output range | number[] | string[] | [0, 1] | |
unknown | Return value for undefined , NaN , null empty values | any | undefined | |
round | Whether to round the output values | boolean | false | |
paddingInner | Sets inner spacing between categories, in range [0, 1], larger values mean larger spacing | number | 0 | |
paddingOuter | Sets outer spacing at both ends, in range [0, 1], larger values mean larger spacing | number | 0 | |
padding | Shortcut to set both paddingInner and paddingOuter | number | 0 | |
align | Alignment, in range [0, 1], 0 means left-aligned, 0.5 means centered, 1 means right-aligned | number | 0.5 | |
compare | Sorting function for domain mapping | (a: string or number, b: string or number) => number | undefined | |
flex | Sets width allocation ratio for each category | number[] | [] |
The band scale divides the continuous range into equal-width intervals, with each interval corresponding to a discrete value in the domain. The following diagram illustrates the layout principles of the band scale:
|<------------------------------------------- range ------------------------------------------->|| | | | | | ||<--step*PO-->|<----bandWidth---->|<--step*PI-->|<----bandWidth---->|<--step*PI-->|<--step*PO-->|| | ***************** | | ***************** | | || | ******* A ******* | | ******* B ******* | | || | ***************** | | ***************** | | || |<--------------step------------->| ||-----------------------------------------------------------------------------------------------|
Where:
Bandwidth is the actual width occupied by each category in a band scale. It determines the width of bars in bar charts, the height of bars in horizontal bar charts, etc. Bandwidth is the core concept that distinguishes band scales from other scales.
The bandwidth calculation involves multiple parameters, with the specific formula as follows:
step = rangeLength / (domain.length - paddingInner + paddingOuter * 2)bandWidth = step * (1 - paddingInner)
Where:
rangeLength
: The length of the range (range[1] - range[0])domain.length
: The number of categories in the domainpaddingInner
: Inner spacing ratio [0, 1]paddingOuter
: Outer spacing ratio [0, 1]paddingInner
controls the spacing between categories and directly affects bandwidth size:
import { Chart } from '@antv/g2';const data = [{ category: 'A', value: 100 },{ category: 'B', value: 80 },{ category: 'C', value: 120 },{ category: 'D', value: 90 },];// Create three charts showing different paddingInner effectsconst charts = [{ paddingInner: 0, title: 'paddingInner: 0 (Maximum bandwidth)' },{ paddingInner: 0.3, title: 'paddingInner: 0.3 (Medium bandwidth)' },{ paddingInner: 0.8, title: 'paddingInner: 0.8 (Minimum bandwidth)' },];charts.forEach((config, index) => {const container = document.createElement('div');container.style.width = '300px';container.style.height = '200px';container.style.display = 'inline-block';container.style.margin = '10px';document.getElementById('container').appendChild(container);const chart = new Chart({container,autoFit: true,});chart.options({type: 'interval',data,encode: {x: 'category',y: 'value',color: 'category',},scale: {x: {type: 'band',paddingInner: config.paddingInner,paddingOuter: 0.1,},},axis: {x: { title: config.title },y: { title: null },},});chart.render();});
paddingOuter
controls the spacing at both ends and indirectly affects bandwidth:
import { Chart } from '@antv/g2';const data = [{ category: 'A', value: 100 },{ category: 'B', value: 80 },{ category: 'C', value: 120 },{ category: 'D', value: 90 },];const charts = [{ paddingOuter: 0, title: 'paddingOuter: 0' },{ paddingOuter: 0.3, title: 'paddingOuter: 0.3' },{ paddingOuter: 0.6, title: 'paddingOuter: 0.6' },];charts.forEach((config, index) => {const container = document.createElement('div');container.style.width = '300px';container.style.height = '200px';container.style.display = 'inline-block';container.style.margin = '10px';document.getElementById('container').appendChild(container);const chart = new Chart({container,autoFit: true,});chart.options({type: 'interval',data,encode: {x: 'category',y: 'value',color: 'category',},scale: {x: {type: 'band',paddingInner: 0.2,paddingOuter: config.paddingOuter,},},axis: {x: { title: config.title },y: { title: null },},});chart.render();});
The more categories there are, the smaller the bandwidth for each category:
import { Chart } from '@antv/g2';const datasets = [{data: [{ category: 'A', value: 100 },{ category: 'B', value: 80 },],title: '2 Categories',},{data: [{ category: 'A', value: 100 },{ category: 'B', value: 80 },{ category: 'C', value: 120 },{ category: 'D', value: 90 },],title: '4 Categories',},{data: [{ category: 'A', value: 100 },{ category: 'B', value: 80 },{ category: 'C', value: 120 },{ category: 'D', value: 90 },{ category: 'E', value: 110 },{ category: 'F', value: 95 },],title: '6 Categories',},];datasets.forEach((dataset, index) => {const container = document.createElement('div');container.style.width = '300px';container.style.height = '200px';container.style.display = 'inline-block';container.style.margin = '10px';document.getElementById('container').appendChild(container);const chart = new Chart({container,autoFit: true,});chart.options({type: 'interval',data: dataset.data,encode: {x: 'category',y: 'value',color: 'category',},scale: {x: {type: 'band',padding: 0.3, // Fixed spacing ratio},},axis: {x: { title: dataset.title },y: { title: null },},});chart.render();});
In actual development, sometimes you need to get the calculated bandwidth value, which can be done in the following way:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',data: [{ category: 'A', value: 100 },{ category: 'B', value: 80 },{ category: 'C', value: 120 },{ category: 'D', value: 90 },],encode: {x: 'category',y: 'value',color: 'category',},scale: {x: {type: 'band',padding: 0.3,},},});chart.render().then(() => {// Get x-axis scaleconst xScale = chart.getScale().x;// Get bandwidth value - use no-parameter callconst bandWidth = xScale.getBandWidth?.() ?? 0;console.log('Current bandwidth value:', bandWidth);// You can also get bandwidth for specific category (if needed)const categoryABandWidth = xScale.getBandWidth?.(xScale.invert('A')) ?? 0;console.log('Category A bandwidth value:', categoryABandWidth);// Calculate step value (distance between adjacent category centers)const domain = xScale.getOptions()?.domain || [];const range = xScale.getOptions()?.range || [0, 1];const rangeLength = range[1] - range[0];const paddingInner = xScale.getOptions()?.paddingInner || 0;const paddingOuter = xScale.getOptions()?.paddingOuter || 0;const step = rangeLength / (domain.length - paddingInner + paddingOuter * 2);console.log('Current step value:', step);// Display bandwidth information on the chartconst container = chart.getContainer();const info = document.createElement('div');info.style.position = 'absolute';info.style.top = '10px';info.style.left = '10px';info.style.background = 'rgba(0,0,0,0.8)';info.style.color = 'white';info.style.padding = '5px 10px';info.style.borderRadius = '4px';info.style.fontSize = '12px';info.innerHTML = `Bandwidth: ${bandWidth.toFixed(2)}<br>Step: ${step.toFixed(2)}`;container.appendChild(info);});
In bar charts, bandwidth directly determines the width of bars:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',data: [{ month: 'Jan', sales: 1200 },{ month: 'Feb', sales: 1100 },{ month: 'Mar', sales: 1350 },{ month: 'Apr', sales: 1280 },],encode: {x: 'month',y: 'sales',color: 'month',},scale: {x: {type: 'band',padding: 0.2, // Smaller spacing, wider bars},},style: {stroke: '#fff',strokeWidth: 2,},});chart.render();
In horizontal bar charts, bandwidth determines the height of bars:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',coordinate: { transform: [{ type: 'transpose' }] },data: [{ department: 'Sales', count: 45 },{ department: 'Marketing', count: 32 },{ department: 'Technology', count: 28 },{ department: 'HR', count: 15 },],encode: {x: 'department',y: 'count',color: 'department',},scale: {x: {type: 'band',padding: 0.4, // Larger spacing between bars},},});chart.render();
In grouped bar charts, the overall bandwidth is divided equally among the sub-groups:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',data: [{ quarter: 'Q1', type: 'Actual', value: 120 },{ quarter: 'Q1', type: 'Budget', value: 100 },{ quarter: 'Q2', type: 'Actual', value: 140 },{ quarter: 'Q2', type: 'Budget', value: 130 },{ quarter: 'Q3', type: 'Actual', value: 160 },{ quarter: 'Q3', type: 'Budget', value: 150 },],encode: {x: 'quarter',y: 'value',color: 'type',},transform: [{ type: 'dodgeX' }],scale: {x: {type: 'band',padding: 0.3, // Overall bandwidth for each quarter},},});chart.render();
import { Chart } from '@antv/g2';// Simulate bandwidth effects under different container widthsconst widths = [300, 600, 900];const data = Array.from({ length: 8 }, (_, i) => ({category: `Category ${String.fromCharCode(65 + i)}`,value: Math.random() * 100 + 50,}));widths.forEach((width, index) => {const container = document.createElement('div');container.style.width = `${width}px`;container.style.height = '200px';container.style.display = 'inline-block';container.style.margin = '10px';container.style.border = '1px solid #ccc';document.getElementById('container').appendChild(container);const chart = new Chart({container,width,height: 200,});chart.options({type: 'interval',data,encode: {x: 'category',y: 'value',color: 'category',},scale: {x: {type: 'band',padding: 0.2, // Fixed spacing ratio},},axis: {x: { title: `Container width: ${width}px` },y: { title: null },},});chart.render();});
For charts that need to adapt to different screen sizes, you can dynamically adjust padding:
// Dynamically adjust padding based on container widthfunction getResponsivePadding(containerWidth, dataLength) {const baseWidth = containerWidth / dataLength;if (baseWidth > 100) {return 0.6; // Increase spacing when container is very wide} else if (baseWidth > 50) {return 0.4; // Medium width} else {return 0.2; // Reduce spacing when container is narrow}}
By deeply understanding the bandwidth concept, you can better control the visual effects of charts and create both beautiful and practical data visualizations.
The most common application of band scales is in bar charts. By setting padding
, you can control the spacing between bars:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',data: [{ genre: 'Sports', sold: 275 },{ genre: 'Strategy', sold: 115 },{ genre: 'Action', sold: 120 },{ genre: 'Shooter', sold: 350 },{ genre: 'Other', sold: 150 },],encode: {x: 'genre',y: 'sold',color: 'genre',},scale: {x: {type: 'band',padding: 0.5, // Set spacing between bars},},});chart.render();
In grouped bar charts, band scales work together with dodgeX transform to create grouping effects:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',data: [{ month: 'Jan', value: 86.5, type: 'Precipitation' },{ month: 'Feb', value: 86.5, type: 'Precipitation' },{ month: 'Mar', value: 86.5, type: 'Precipitation' },{ month: 'Apr', value: 86.5, type: 'Precipitation' },{ month: 'May', value: 86.5, type: 'Precipitation' },{ month: 'Jun', value: 86.5, type: 'Precipitation' },{ month: 'Jan', value: 30.5, type: 'Evaporation' },{ month: 'Feb', value: 30.5, type: 'Evaporation' },{ month: 'Mar', value: 30.5, type: 'Evaporation' },{ month: 'Apr', value: 30.5, type: 'Evaporation' },{ month: 'May', value: 30.5, type: 'Evaporation' },{ month: 'Jun', value: 30.5, type: 'Evaporation' },],encode: {x: 'month',y: 'value',color: 'type',},transform: [{ type: 'dodgeX' }],scale: {x: {type: 'band',padding: 0.2, // Set spacing between groups},},});chart.render();
Using the flex
property allows setting different width ratios for different categories:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',data: [{ country: 'USA', value: 12394, gdp: 21.4 },{ country: 'China', value: 9608, gdp: 14.7 },{ country: 'Japan', value: 4731, gdp: 5.0 },{ country: 'Germany', value: 3861, gdp: 4.2 },{ country: 'UK', value: 2678, gdp: 2.9 },],encode: {x: 'country',y: 'value',color: 'country',},scale: {x: {type: 'band',padding: 0.4,flex: [2.14, 1.47, 0.5, 0.42, 0.29], // Set different widths based on GDP},},});chart.render();
By transposing the coordinate system, you can create horizontal bar charts where band scales still apply:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',coordinate: { transform: [{ type: 'transpose' }] },data: [{ genre: 'Sports', sold: 275 },{ genre: 'Strategy', sold: 115 },{ genre: 'Action', sold: 120 },{ genre: 'Shooter', sold: 350 },{ genre: 'Other', sold: 150 },],encode: {x: 'genre',y: 'sold',color: 'genre',},scale: {x: {type: 'band',padding: 0.5,},},});chart.render();
Using stackY
transform can create stacked bar charts showing cumulative effects of each part:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',data: [{ quarter: 'Q1', department: 'Sales', value: 120 },{ quarter: 'Q1', department: 'Marketing', value: 100 },{ quarter: 'Q1', department: 'Technology', value: 80 },{ quarter: 'Q2', department: 'Sales', value: 140 },{ quarter: 'Q2', department: 'Marketing', value: 110 },{ quarter: 'Q2', department: 'Technology', value: 90 },{ quarter: 'Q3', department: 'Sales', value: 160 },{ quarter: 'Q3', department: 'Marketing', value: 95 },{ quarter: 'Q3', department: 'Technology', value: 120 },{ quarter: 'Q4', department: 'Sales', value: 180 },{ quarter: 'Q4', department: 'Marketing', value: 100 },{ quarter: 'Q4', department: 'Technology', value: 130 },],encode: {x: 'quarter',y: 'value',color: 'department',},transform: [{ type: 'stackY' }],scale: {x: {type: 'band',padding: 0.3,},},});chart.render();
Automatically adjust bar width based on specified field values, suitable for representing weight or proportional relationships:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',data: [{ region: 'East', sales: 8500, population: 2.4 },{ region: 'South', sales: 6200, population: 1.8 },{ region: 'North', sales: 7800, population: 2.1 },{ region: 'Southwest', sales: 4500, population: 1.2 },{ region: 'Northeast', sales: 3200, population: 0.9 },{ region: 'Northwest', sales: 2800, population: 0.7 },],encode: {x: 'region',y: 'sales',color: 'region',},transform: [{ type: 'flexX', field: 'population' }], // Adjust bar width based on population datascale: {x: {type: 'band',padding: 0.2,},},});chart.render();
When handling time data, band scales can well handle the visualization of time intervals:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',data: [{ month: '2024-01', sales: 1200 },{ month: '2024-02', sales: 1100 },{ month: '2024-03', sales: 1350 },{ month: '2024-04', sales: 1280 },{ month: '2024-05', sales: 1400 },{ month: '2024-06', sales: 1520 },{ month: '2024-07', sales: 1680 },{ month: '2024-08', sales: 1590 },{ month: '2024-09', sales: 1450 },{ month: '2024-10', sales: 1380 },{ month: '2024-11', sales: 1250 },{ month: '2024-12', sales: 1600 },],encode: {x: 'month',y: 'sales',color: (d) => (d.sales > 1500 ? 'high' : d.sales > 1300 ? 'medium' : 'low'),},scale: {x: {type: 'band',padding: 0.1,},color: {domain: ['low', 'medium', 'high'],range: ['#faad14', '#1890ff', '#52c41a'],},},});chart.render();
Displaying categorical data with hierarchical structure:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',data: [{ category: 'Clothing-Men', subcategory: 'Shirts', value: 850 },{ category: 'Clothing-Men', subcategory: 'Pants', value: 750 },{ category: 'Clothing-Men', subcategory: 'Jackets', value: 650 },{ category: 'Clothing-Women', subcategory: 'Dresses', value: 950 },{ category: 'Clothing-Women', subcategory: 'Tops', value: 800 },{ category: 'Clothing-Women', subcategory: 'Skirts', value: 700 },{ category: 'Electronics-Phones', subcategory: 'iPhone', value: 1200 },{ category: 'Electronics-Phones', subcategory: 'Huawei', value: 1100 },{ category: 'Electronics-Phones', subcategory: 'Xiaomi', value: 900 },{ category: 'Electronics-Computers', subcategory: 'Laptops', value: 1500 },{ category: 'Electronics-Computers', subcategory: 'Desktops', value: 800 },{ category: 'Electronics-Computers', subcategory: 'Tablets', value: 600 },],encode: {x: 'category',y: 'value',color: 'subcategory',},transform: [{ type: 'dodgeX' }],scale: {x: {type: 'band',padding: 0.4, // Larger spacing to distinguish different main categoriespaddingInner: 0.3, // Inner spacingpaddingOuter: 0.1, // Outer spacing},},});chart.render();
Using paddingInner and paddingOuter to precisely control spacing, suitable for comparative analysis:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',data: [{ product: 'Product A', current: 320, target: 400 },{ product: 'Product B', current: 280, target: 350 },{ product: 'Product C', current: 410, target: 450 },{ product: 'Product D', current: 180, target: 250 },{ product: 'Product E', current: 350, target: 380 },].flatMap((d) => [{ product: d.product, type: 'Current Sales', value: d.current },{ product: d.product, type: 'Target Sales', value: d.target },]),encode: {x: 'product',y: 'value',color: 'type',},transform: [{ type: 'dodgeX' }],scale: {x: {type: 'band',paddingInner: 0.2, // Smaller intra-group spacingpaddingOuter: 0.3, // Larger inter-group spacing},color: {domain: ['Current Sales', 'Target Sales'],range: ['#1890ff', '#52c41a'],},},});chart.render();
Combine compare function to sort data and use different bar width strategies:
import { Chart } from '@antv/g2';const data = [{ brand: 'Apple', market_share: 23.4, revenue: 365 },{ brand: 'Samsung', market_share: 20.1, revenue: 220 },{ brand: 'Huawei', market_share: 15.8, revenue: 180 },{ brand: 'Xiaomi', market_share: 12.3, revenue: 120 },{ brand: 'OPPO', market_share: 8.9, revenue: 95 },{ brand: 'vivo', market_share: 7.2, revenue: 85 },{ brand: 'Others', market_share: 12.3, revenue: 150 },];const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',data,encode: {x: 'brand',y: 'market_share',color: 'brand',},scale: {x: {type: 'band',padding: 0.2,compare: (a, b) => {// Sort by market share in descending orderconst dataA = data.find((d) => d.brand === a);const dataB = data.find((d) => d.brand === b);return (dataB?.market_share || 0) - (dataA?.market_share || 0);},flex: [2.34, 2.01, 1.58, 1.23, 0.89, 0.72, 1.23], // Set width based on market share},},});chart.render();
Displaying step-by-step cumulative changes in values:
import { Chart } from '@antv/g2';// Waterfall chart data processingconst rawData = [{ name: 'Initial Balance', value: 1000, type: 'start' },{ name: 'Revenue Increase', value: 500, type: 'positive' },{ name: 'Cost Expense', value: -200, type: 'negative' },{ name: 'Tax Expense', value: -150, type: 'negative' },{ name: 'Other Income', value: 100, type: 'positive' },{ name: 'Final Balance', value: 1250, type: 'end' },];// Calculate cumulative valueslet cumulative = 0;const data = rawData.map((d, i) => {if (d.type === 'start' || d.type === 'end') {const result = { ...d, start: 0, end: d.value };cumulative = d.value;return result;} else {const start = cumulative;cumulative += d.value;return { ...d, start, end: cumulative };}});const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'interval',data: data.flatMap((d) => [{ name: d.name, value: d.end - d.start, position: d.start, type: d.type },]),encode: {x: 'name',y: ['position', (d) => d.position + d.value],color: 'type',},scale: {x: {type: 'band',padding: 0.4,},color: {domain: ['start', 'positive', 'negative', 'end'],range: ['#722ed1', '#52c41a', '#ff4d4f', '#1890ff'],},},});chart.render();
Using band scales with faceted layout to display multi-dimensional data:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});chart.options({type: 'facetRect',data: [{ region: 'North', quarter: 'Q1', product: 'Product A', sales: 120 },{ region: 'North', quarter: 'Q1', product: 'Product B', sales: 100 },{ region: 'North', quarter: 'Q1', product: 'Product C', sales: 80 },{ region: 'North', quarter: 'Q2', product: 'Product A', sales: 140 },{ region: 'North', quarter: 'Q2', product: 'Product B', sales: 110 },{ region: 'North', quarter: 'Q2', product: 'Product C', sales: 90 },{ region: 'South', quarter: 'Q1', product: 'Product A', sales: 150 },{ region: 'South', quarter: 'Q1', product: 'Product B', sales: 130 },{ region: 'South', quarter: 'Q1', product: 'Product C', sales: 110 },{ region: 'South', quarter: 'Q2', product: 'Product A', sales: 170 },{ region: 'South', quarter: 'Q2', product: 'Product B', sales: 140 },{ region: 'South', quarter: 'Q2', product: 'Product C', sales: 120 },],encode: { x: 'region', y: 'quarter' },children: [{type: 'interval',encode: {x: 'product',y: 'sales',color: 'product',},scale: {x: {type: 'band',padding: 0.3,},},},],});chart.render();
You can adjust the spacing between bars by setting the padding
property, which indirectly adjusts the width of the bars. The larger the padding
value, the narrower the bars; the smaller the value, the wider the bars.
chart.interval().encode('x', 'type').encode('y', 'sale').scale('x', {type: 'band',padding: 0.5, // Value range is [0, 1]});
bandWidth = 0
, suitable for scatter plots and other charts that only need point positionsThere are two methods:
flex
property to set different width ratios for different categoriesflexX
transform to automatically set bar width based on specified field values// Method 1: Using flex propertychart.interval().encode('x', 'country').encode('y', 'value').scale('x', {type: 'band',flex: [2, 1, 3, 1.5], // Manually set width ratios});// Method 2: Using flexX transformchart.interval().encode('x', 'country').encode('y', 'value').transform({ type: 'flexX', field: 'gdp' }); // Automatically set width based on gdp field