ChartGraph is used for displaying various types of graphs plotted on an x and y axis. It makes it easier to identify and understand patterns over time across different categories, enabling people to make informed decisions quickly.
also known as Bar Graph, Line Graph, Column Graph
Props
Usage guidelines
These are overall guidelines for ChartGraph. For usage guidelines on specific graphs, see:
-
Bar graph usage guidelines
-
Line graph usage guidelines
-
Charts and graphs general guidelines
- To compare data sets
- To show trends, frequency of occurrences and distribution of data across time
- To visually summarize and explain complex data sets and concepts
- To show parts-to-whole. Use a pie or donut chart instead.
- To visualize a series of stages in a sequence that get smaller over time. Use a funnel chart instead.
- To show large data sets that are too hard to show in a chart. Use Table instead.
Best practices
These are overall best practices for ChartGraph. For best practices on specific graphs, see:
-
Bar graph best practices
-
Line graph best practices
-
Charts and graphs general guidelines
Limit the amount of data you show in a graph so that it is readable and easy to follow.
Add a lot of data to a graph that makes it hard to read. If you need to show a lot of data, use Table instead. Another option is using multiple graphs in a grid.
When displaying multiple categories in lines or bars, stick to the default color sequence provided since it has been optimized for color blindness.
Pick colors that are too similar to each other and hard to tell apart, especially for those with visual impairments.
Use a biaxial chart when there is a significant difference between values. A suggestion is a 50%+ difference.
Use a biaxial chart when there is a very minor difference between values being compared. Use a chart with a single axis instead.
Accessibility
Visual patterns
Charts use color to represent discrete categories. However, people with color blindness or other visual impairments may have difficulty telling certain colors apart.
Therefore, ChartGraph provides an accessibility view mode where colors in bars and lines are replaced with visual patterns to help in their interpretation. Bar charts use pattern fills and line charts use series markers with different shapes to help distinguish between data points without using color alone. Each pattern fill and time series shape corresponds to one of the colors in our 12-color categorical palette.
ChartGraph provides visualPatternSelected
and onVisualPatternChange
props to manage the visual state of the component externally. If a person selects Low Vision Features in the settings or enables the visual patterns in one component, other charts can also be enabled at the same time to adapt to a user’s accessibility preferences.
ChartGraph displays an IconButton in the header section that allows to enable the visual pattern from the component itself.
import { useState } from 'react'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('visualPattern'); const data = [ { name: 'A', Color_01: 90, Color_02: 90, Color_03: 90, Color_04: 90, Color_05: 90, Color_06: 90, }, ]; return ( <ChartGraph accessibilityLabel="Example of chart with decal pattern in bars" data={data} elements={[ { type: 'bar', id: 'Color_01' }, { type: 'bar', id: 'Color_02' }, { type: 'bar', id: 'Color_03' }, { type: 'bar', id: 'Color_04' }, { type: 'bar', id: 'Color_05' }, { type: 'bar', id: 'Color_06' }, ]} onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } range={[0, 100]} title="ChartGraph" titleDisplay="hidden" type="bar" visualPatternSelected={visualPatternSelected} /> ); }
For line graphs, shapes are used to help tell categories apart.
import { useState } from 'react'; import { FixedZIndex } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('visualPattern'); const data = [ { name: new Date(2023, 1, 1).getTime(), Color_01: 50, Color_02: 100, Color_03: 150, Color_04: 200, Color_05: 250, Color_06: 300, }, { name: new Date(2023, 5, 15).getTime(), Color_01: 100, Color_02: 150, Color_03: 200, Color_04: 250, Color_05: 300, Color_06: 350, }, { name: new Date(2023, 10, 1).getTime(), Color_01: 150, Color_02: 200, Color_03: 250, Color_04: 300, Color_05: 350, Color_06: 400, }, ]; return ( <ChartGraph accessibilityLabel="Example of chart with decal pattern in lines" data={data} elements={[ { type: 'line', id: 'Color_01' }, { type: 'line', id: 'Color_02' }, { type: 'line', id: 'Color_03' }, { type: 'line', id: 'Color_04' }, { type: 'line', id: 'Color_05' }, { type: 'line', id: 'Color_06' }, ]} modalZIndex={new FixedZIndex(11)} onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } range={{ xAxisBottom: [ new Date(2023, 0, 1).getTime(), new Date(2023, 11, 1).getTime(), ], }} tickFormatter={{ timeseries: (date) => `${new Intl.DateTimeFormat('en-US', { month: 'short' }).format( date )}-${new Intl.DateTimeFormat('en-US', { day: '2-digit' }).format( date )}`, }} title="ChartGraph" titleDisplay="hidden" type="line" visualPatternSelected={visualPatternSelected} /> ); }
ARIA attributes
Charts are presented as images to screen readers. Provide a description using the accessibilityLabel
to provide more context around ChartGraph’s content.
Don't use accessibilityLabel
to describe the ChartGraph content itself. We’re working on adding a Table view feature to access the detailed data of ChartGraph in a more granular and accessible way.
accessibilityLabel
is automatically prefixed with "ChartGraph." to differentiate ChartGraph from other images.
import { useState } from 'react'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const data = [ { name: 'A', Series_01: 50, Series_02: 100, }, { name: 'B', Series_01: 100, Series_02: 200, }, { name: 'C', Series_01: 250, Series_02: 300, }, ]; return ( <ChartGraph accessibilityLabel="Example of Bar chart" data={data} elements={[ { type: 'bar', id: 'Series_01' }, { type: 'bar', id: 'Series_02' }, ]} onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } title="ChartGraph" titleDisplay="hidden" type="bar" visualPatternSelected={visualPatternSelected} /> ); }
Tabular representation
An additional button is available to show chart data as a table so that it’s easier to
- navigate with a screen reader
- read data for those who have difficulty processing visual information
- download data to view in a person’s own tools
The tabular data is also available to download as a .csv file.
Localization
Be sure to localize all text strings. Note that localization can lengthen text by 20 to 30 percent.
To localize data content, use the labelMap
prop. See the example for detailed implementation guidance.
To localize dates in time series, use the tickFormatter.timeseries
prop. See the
time series example for detailed implementation guidance.
import { useState } from 'react'; import { DefaultLabelProvider } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); return ( <DefaultLabelProvider labels={{ ChartGraph: { accessibilityLabelPrefixText: 'ChartGraph', defaultViewText: 'Standard-Ansichtsmodus.', accessibleViewText: 'Ansichtsmodus für Barrierefreiheit.', tabularData: 'Tabellarische Darstellung.', accessibilityLabelDismissModal: 'Tabellendarstellung modal aufheben.', tableSeriesText: 'Reihe.', tableXAxisText: 'x-Achsen-Werte.', tableYAxisText: 'y-Achsen-Werte.', downloadCsvButtonText: 'Als .csv herunterladen.', cancelButtonText: 'Abbrechen.', }, }} > <ChartGraph accessibilityLabel="Beispiel für ein Liniendiagramm" data={[ { name: 'Women', Users: 100, Clickthroughs: 200, }, { name: 'Men', Users: 200, Clickthroughs: 300, }, ]} description="Leistung im Laufe der Zeit. Impressionen geben an, wie oft Ihr Pin auf dem Bildschirm angezeigt wurde." elements={[ { type: 'bar', id: 'Users' }, { type: 'bar', id: 'Clickthroughs' }, ]} labelMap={{ Women: 'Frauen', Men: 'Männer', Users: 'Benutzer', Clickthroughs: 'Durchklicken', }} onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } title="Eindrücke" type="bar" visualPatternSelected={visualPatternSelected} /> </DefaultLabelProvider> ); }
Subcomponents
LegendIcon
LegendIcon should only be used within custom tooltips. See the custom tooltip variant for implementation guidance.
LegendIcon Props
Variants
Bar horizontal
Arrange bars in rows that stack from top to bottom when horizontal space and you have longer text labels. Also known as a "horizontal bar chart".
Props: type="bar"
elements=[{..., type:'bar'}]
layout="horizontal"
import { useState } from 'react'; import { Flex } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const data = [ { name: 'Pacific Northwest', Impressions: 1000, CPM: 1050, CPC: 1000, }, { name: 'Sunbelt', Impressions: 1000, CPM: 1500, CPC: 1000, }, { name: 'Great Lakes', Impressions: 1000, CPM: 1050, CPC: 1000, }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <ChartGraph accessibilityLabel="Example of a vertical bar chart" data={data} elements={[ { type: 'bar', id: 'Impressions' }, { type: 'bar', id: 'CPM' }, { type: 'bar', id: 'CPC' }, ]} layout="horizontal" onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } title="Regions in the US" type="bar" visualPatternSelected={visualPatternSelected} /> </Flex> ); }
Bar column
In a column chart, bars are ordered horizontally. Use this when you have a small amount of data to compare and the horizontal space to do so. This includes text labels. If text labels are long, use Bar horizontal instead.
Props: type="bar"
layout="horizontal"
import { useState } from 'react'; import { Flex } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const data = [ { name: '10-24', Impressions: 1000, CPM: 1050, CPC: 1000, }, { name: '25-50', Impressions: 1000, CPM: 1500, CPC: 1000, }, { name: '50+', Impressions: 1000, CPM: 1050, CPC: 1000, }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <ChartGraph accessibilityLabel="Example of a vertical bar chart" data={data} elements={[ { type: 'bar', id: 'Impressions' }, { type: 'bar', id: 'CPM' }, { type: 'bar', id: 'CPC' }, ]} onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } title="Age" type="bar" visualPatternSelected={visualPatternSelected} /> </Flex> ); }
Stacked bars
Stacked bar charts break bars into smaller categories so that their relationship to the whole can be seen.
Props: type="bar"
stacked={true}
import { useState } from 'react'; import { FixedZIndex, Flex } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const data = [ { name: new Date(2023, 0, 1).getTime(), Awareness: 5000, Consideration: 1000, 'Catalog sales': 4000, Conversions: 2500, }, { name: new Date(2023, 1, 1).getTime(), Awareness: 4000, Consideration: 5000, 'Catalog sales': 1000, Conversions: 2500, }, { name: new Date(2023, 2, 1).getTime(), Awareness: 2500, Consideration: 5000, 'Catalog sales': 1000, Conversions: 4000, }, { name: new Date(2023, 3, 1).getTime(), Awareness: 2500, Consideration: 4000, 'Catalog sales': 1000, Conversions: 5000, }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <ChartGraph accessibilityLabel="Example of stacked bars chart" data={data} elements={[ { type: 'bar', id: 'Awareness' }, { type: 'bar', id: 'Consideration' }, { type: 'bar', id: 'Catalog sales' }, { type: 'bar', id: 'Conversions' }, ]} modalZIndex={new FixedZIndex(11)} onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } range={{ xAxisBottom: ['auto', 'auto'], }} stacked tickFormatter={{ yAxisLeft: (value) => `${value / 1000}K`, timeseries: (date) => `${new Intl.DateTimeFormat('en-US', { month: 'short' }).format( date )}-${new Intl.DateTimeFormat('en-US', { day: '2-digit' }).format( date )}`, }} title="508 campaigns" visualPatternSelected={visualPatternSelected} /> </Flex> ); }
Line
A line graph plots numeric values for categorical data as a line that shows a progression through time.
Props: type="line"
elements=[{..., type:'line'}]
import { useState } from 'react'; import { FixedZIndex, Flex } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const data = [ { name: new Date(2023, 0, 1).getTime(), Engagement: 850000, Saves: 870000, Impressions: 280000, 'Page visits': 5000, }, { name: new Date(2023, 1, 2).getTime(), Engagement: 800000, Saves: 690000, Impressions: 790000, 'Page visits': 9000, }, { name: new Date(2023, 2, 3).getTime(), Engagement: 890000, Saves: 850000, Impressions: 250000, 'Page visits': 8000, }, { name: new Date(2023, 3, 4).getTime(), Engagement: 870000, Saves: 550000, Impressions: 230000, 'Page visits': 7000, }, { name: new Date(2023, 4, 5).getTime(), Engagement: 830000, Saves: 84000, Impressions: 710000, 'Page visits': 3000, }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <ChartGraph accessibilityLabel="Example of line chart" data={data} elements={[ { type: 'line', id: 'Engagement' }, { type: 'line', id: 'Saves' }, { type: 'line', id: 'Impressions' }, { type: 'line', id: 'Page visits' }, ]} modalZIndex={new FixedZIndex(11)} onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } range={{ xAxisBottom: ['auto', 'auto'], }} tickFormatter={{ yAxisLeft: (value) => { if (value >= 1000000) return `${value / 1000000}m`; if (value >= 1000) return `${value / 1000}k`; return value; }, timeseries: (date) => `${new Intl.DateTimeFormat('en-US', { month: 'short' }).format( date )}-${new Intl.DateTimeFormat('en-US', { day: '2-digit' }).format( date )}`, }} title="Performance over time" type="line" visualPatternSelected={visualPatternSelected} /> </Flex> ); }
Combo
This combines a bar graph with a line graph. It is useful to see both categories and a trend or range over time.
Props: type="combo"
elements=[{..., type:'bar' or type:'line'}]
import { useState } from 'react'; import { FixedZIndex, Flex } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const data = [ { name: 'Quarter 1', 'This year': 850000, Profit: 870000, 'Last year': 980000, }, { name: 'Quarter 2', 'This year': 800000, Profit: 690000, 'Last year': 590000, }, { name: 'Quarter 3', 'This year': 890000, Profit: 850000, 'Last year': 950000, }, { name: 'Quarter 4', 'This year': 870000, Profit: 550000, 'Last year': 830000, }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <ChartGraph accessibilityLabel="Example of combo chart" data={data} elements={[ { type: 'bar', id: 'This year' }, { type: 'bar', id: 'Profit' }, { type: 'line', id: 'Last year' }, ]} modalZIndex={new FixedZIndex(11)} onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } tickFormatter={{ yAxisLeft: (value) => { if (value >= 1000000) return `${value / 1000000}m`; if (value >= 1000) return `${value / 1000}k`; return value; }, }} title="Product group sales:" type="combo" visualPatternSelected={visualPatternSelected} /> </Flex> ); }
Biaxial
Biaxial graphs show two y-axis. They're used when either comparing two categories with mixed types of data—for example, clicks vs spend—or when there is a significant difference between values. A suggestion is to move to a biaxial chart if there is a 50%+ difference between compared values.
Props: layout="verticalBiaxial"
layout="horizontalBiaxial"
import { useState } from 'react'; import { FixedZIndex, Flex } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const data = [ { name: new Date(2023, 0, 1).getTime(), Spend: 40000, 'Total ROAS (Checkout)': 570000, }, { name: new Date(2023, 1, 1).getTime(), Spend: 45000, 'Total ROAS (Checkout)': 690000, }, { name: new Date(2023, 2, 1).getTime(), Spend: 55000, 'Total ROAS (Checkout)': 850000, }, { name: new Date(2023, 3, 1).getTime(), Spend: 70000, 'Total ROAS (Checkout)': 550000, }, { name: new Date(2023, 4, 1).getTime(), Spend: 830000, 'Total ROAS (Checkout)': 1000000, }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <ChartGraph accessibilityLabel="Example of line chart" data={data} elements={[ { type: 'line', id: 'Spend', axis: 'left' }, { type: 'line', id: 'Total ROAS (Checkout)', axis: 'right' }, ]} layout="verticalBiaxial" modalZIndex={new FixedZIndex(11)} onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } range={{ xAxisBottom: ['auto', 'auto'] }} tickFormatter={{ yAxisLeft: (value) => { if (value >= 500) return `$${value / 500}k`; return value; }, yAxisRight: (value) => { if (value >= 1000) return `${value / 1000}k`; return value; }, timeseries: (date) => `${new Intl.DateTimeFormat('en-US', { month: 'short' }).format( date )} ${new Intl.DateTimeFormat('en-US', { day: '2-digit' }).format( date )}`, }} title="Performance over last 30 days" type="line" visualPatternSelected={visualPatternSelected} /> </Flex> ); }
Header
ChartGraph's header support a title, description, a HelpButton, and the accessibility features: tabular data and visual patterns switch buttons.
- Title. A title for the graph in case it’s not displayed elsewhere on the screen.
- Description. An optional description is available if more context is needed.
- HelpButton
- Tabular data switch button. Not available yet.
- Visual pattern switch button.
Props: title
description
helpButton
import { Flex, HelpButton } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const data = [ { name: 'Quarter 1', Clicks: 850000, Conversions: 870000, }, { name: 'Quarter 2', Clicks: 800000, Conversions: 690000, }, { name: 'Quarter 3', Clicks: 890000, Conversions: 850000, }, { name: 'Quarter 4', Clicks: 870000, Conversions: 550000, }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <ChartGraph accessibilityLabel="Example of chart with title and description" data={data} description="Includes both web and mobile" elements={[ { type: 'bar', id: 'Clicks', axis: 'left', }, { type: 'line', id: 'Conversions', axis: 'right', }, ]} helpButton={ <HelpButton accessibilityLabel="Click to learn more this ChartGraph" accessibilityPopoverLabel="Expanded information about this ChartGraph" link={{ href: 'https://help.pinterest.com/en/business/article/conversion-insights/', text: 'Read our documentation', accessibilityLabel: 'Visit our Help center', }} text="If you want to learn more about Clicks vs conversions, visit our Help center." /> } layout="verticalBiaxial" onVisualPatternChange={() => {}} title="Clicks vs conversions" visualPatternSelected="disabled" /> </Flex> ); }
Tooltip
For showing more precise details on hover.
Props: renderTooltip
import { useState } from 'react'; import { Flex } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const data = [ { name: 'iOS', '18-30': 850000, '30-50': 870000, '50+': 980000, }, { name: 'Android', '18-30': 800000, '30-50': 690000, '50+': 590000, }, { name: 'Web', '18-30': 890000, '30-50': 850000, '50+': 950000, }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <ChartGraph accessibilityLabel="Example of chart with tooltip" data={data} elements={[ { type: 'bar', id: '18-30' }, { type: 'bar', id: '30-50' }, { type: 'bar', id: '50+' }, ]} initialTicks={3} onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } tickFormatter={{ yAxisLeft: (value) => { if (value >= 1000000) return `${value / 1000000}m`; if (value >= 1000) return `${value / 1000}k`; return value; }, }} title="Views by demographics and device" type="bar" visualPatternSelected={visualPatternSelected} /> </Flex> ); }
import { useState } from 'react'; import { Flex, Text } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const data = [ { name: 'USA', '20-30': 100, '40-50': 200, '50-60': 300, }, { name: 'Europe', '20-30': 200, '40-50': 300, '50-60': 400, }, ]; return ( <Flex direction="column" height="100%" width="100%"> <ChartGraph accessibilityLabel="Example of chart with tooltip" data={data} elements={[ { type: 'bar', id: '20-30', }, { type: 'bar', id: '40-50', }, { type: 'bar', id: '50-60', }, ]} initialTicks={3} onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } renderTooltip={({ label, payload, active }) => active && Array.isArray(payload) ? ( <Flex direction="column" gap={2}> <Flex.Item> {payload.map((payloadData) => ( <Flex key={payloadData.name} alignItems="center" gap={2}> <ChartGraph.LegendIcon payloadData={payloadData} /> <Flex.Item flex="grow"> <Text size="100">{payloadData.name}</Text> </Flex.Item> <Text size="200" weight="bold"> ${payloadData.value} </Text> </Flex> ))} </Flex.Item> <Text color="subtle" size="100"> {label} </Text> </Flex> ) : null } tickFormatter={{ yAxisLeft: (value) => `${value}m` }} title="MAU per regions" type="bar" visualPatternSelected={visualPatternSelected} /> </Flex> ); }
Legend
import { useState } from 'react'; import { FixedZIndex, Flex } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const data = [ { name: new Date(2023, 0, 1).getTime(), California: 1500000, Arizona: 500000, }, { name: new Date(2023, 1, 2).getTime(), California: 1000000, Arizona: 400000, }, { name: new Date(2023, 2, 3).getTime(), California: 1500000, Arizona: 500000, }, { name: new Date(2023, 3, 4).getTime(), California: 1000000, Arizona: 400000, }, { name: new Date(2023, 4, 5).getTime(), California: 1500000, Arizona: 500000, }, { name: new Date(2023, 5, 6).getTime(), California: 1000000, Arizona: 400000, }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <ChartGraph accessibilityLabel="Example of chart with tooltip" data={data} elements={[ { type: 'bar', id: 'California' }, { type: 'bar', id: 'Arizona' }, ]} modalZIndex={new FixedZIndex(11)} onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } tickFormatter={{ yAxisLeft: (value) => { if (value >= 1000000) return `${value / 1000000}m`; if (value >= 1000) return `${value / 1000}k`; return value; }, timeseries: (date) => `${new Intl.DateTimeFormat('en-US', { month: 'short' }).format( date )} ${new Intl.DateTimeFormat('en-US', { day: '2-digit' }).format( date )}`, }} title="Average spend by region" type="bar" visualPatternSelected={visualPatternSelected} /> </Flex> ); }
ReferenceArea
Use to highlight an area in a graph for extra context. A common example is showing when data isn’t available.
Props: referenceAreas
import { Flex } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const data = [ { name: new Date(2023, 0, 1).getTime(), Impressions: 850000, }, { name: new Date(2023, 0, 2).getTime(), Impressions: 800000, }, { name: new Date(2023, 0, 3).getTime(), Impressions: 890000, }, { name: new Date(2023, 0, 4).getTime(), Impressions: 870000, }, { name: new Date(2023, 0, 5).getTime(), Impressions: 830000, }, { name: new Date(2023, 0, 6).getTime(), Impressions: 930000, }, { name: new Date(2023, 0, 7).getTime(), Impressions: 630000, }, { name: new Date(2023, 0, 8).getTime(), Impressions: 730000, }, { name: new Date(2023, 0, 9).getTime(), Impressions: 890000, }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <ChartGraph accessibilityLabel="Example of chart with reference area" data={data} elements={[{ type: 'line', id: 'Impressions' }]} onVisualPatternChange={() => {}} range={{ yAxisLeft: [0, 1000000], xAxisBottom: ['auto', new Date(2023, 0, 10).getTime()], }} referenceAreas={[ { id: 'ExampleBD', label: 'Real-time data not available', x1: new Date(2023, 0, 9).getTime(), x2: new Date(2023, 0, 10).getTime(), yAxisId: 'left', y1: 0, y2: 1000000, }, ]} tickFormatter={{ yAxisLeft: (value) => { if (value >= 1000000) return `${value / 1000000}m`; if (value >= 1000) return `${value / 1000}k`; return value; }, timeseries: (date) => `${new Intl.DateTimeFormat('en-US', { month: 'short' }).format( date )}-${new Intl.DateTimeFormat('en-US', { day: '2-digit' }).format( date )}`, }} title="Impressions over time" type="line" visualPatternSelected="disabled" /> </Flex> ); }
Precision in line graphs
To show exact and accurate data, lines should be rectilinear. When showing trends, forecasts and imprecise data, then lines should be curved to denote that these are just approximations.
Props: elements=[{..., type:'line', precision='estimate'}]
import { FixedZIndex, Flex } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const data = [ { name: new Date(2023, 0, 1).getTime(), 'Actual data': 1000 }, { name: new Date(2023, 1, 1).getTime(), 'Actual data': 1567 }, { name: new Date(2023, 2, 1).getTime(), 'Actual data': 1005 }, { name: new Date(2023, 3, 1).getTime(), 'Actual data': 1003 }, { name: new Date(2023, 4, 1).getTime(), 'Actual data': 1100 }, { name: new Date(2023, 5, 1).getTime(), 'Actual data': 1030 }, { name: new Date(2023, 6, 1).getTime(), 'Actual data': 1089 }, { name: new Date(2023, 7, 1).getTime(), 'Actual data': 1065, Forecast: 1065, }, { name: new Date(2023, 8, 1).getTime(), Forecast: 1089 }, { name: new Date(2023, 9, 1).getTime(), Forecast: 1030 }, { name: new Date(2023, 10, 1).getTime(), Forecast: 1990 }, { name: new Date(2023, 11, 1).getTime(), Forecast: 2034 }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <ChartGraph accessibilityLabel="Example of time series chart" data={data} elements={[ { type: 'line', id: 'Actual data', precision: 'exact' }, { type: 'line', id: 'Forecast', precision: 'estimate' }, ]} modalZIndex={new FixedZIndex(11)} onVisualPatternChange={() => {}} range={{ xAxisBottom: ['auto', 'auto'], }} tickFormatter={{ timeseries: (date) => `${new Intl.DateTimeFormat('en-US', { month: 'short' }).format( date )}-${new Intl.DateTimeFormat('en-US', { day: '2-digit' }).format( date )}`, }} title="Forecast for 2023" type="line" visualPatternSelected="disabled" /> </Flex> ); }
Colors
Colors on data series are set automatically for best contrast.
If different graphs need to be compared simultaneously, see example below, color in time series can be set in the elements
prop setting each color color='01'
individually.
import { useState } from 'react'; import { Flex } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const data = [ { name: 'A', Series_01: 100, }, { name: 'B', Series_01: 200, }, { name: 'C', Series_01: 300, }, ]; return ( <Flex direction="column" width="100%" wrap> <Flex> <ChartGraph accessibilityLabel="Example of line chart color 01" data={data} description="Color 01" elements={[{ type: 'line', id: 'Series_01' }]} initialTicks={3} legend="none" onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } renderTooltip="none" title="ChartGraph A" type="line" visualPatternSelected={visualPatternSelected} /> <ChartGraph accessibilityLabel="Example of line chart color 02" data={data} description="Color 02" elements={[{ type: 'line', id: 'Series_01', color: '02' }]} initialTicks={3} legend="none" onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } renderTooltip="none" title="ChartGraph B" type="line" visualPatternSelected={visualPatternSelected} /> </Flex> <Flex> <ChartGraph accessibilityLabel="Example of line chart color 03" data={data} description="Color 03" elements={[{ type: 'line', id: 'Series_01', color: '03' }]} initialTicks={3} legend="none" onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } renderTooltip="none" title="ChartGraph C" type="line" visualPatternSelected={visualPatternSelected} /> <ChartGraph accessibilityLabel="Example of line chart color 04" data={data} description="Color 04" elements={[{ type: 'line', id: 'Series_01', color: '04' }]} initialTicks={3} legend="none" onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } renderTooltip="none" title="ChartGraph D" type="line" visualPatternSelected={visualPatternSelected} /> </Flex> </Flex> ); }
Layout
ChartGraph supports 4 layouts. Legend positions automatically on each layout for easier comprehension.
Single axis:
- vertical (default)
- horizontal
Dual axis:
- verticalBiaxial
- horizontalBiaxial
Props: layout
import { useState } from 'react'; import { Flex, RadioGroup } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const [type, setType] = useState('bar'); const [layout, setLayout] = useState('vertical'); let axisSeries01; let axisSeries02; if (layout === 'horizontalBiaxial') { axisSeries01 = 'bottom'; axisSeries02 = 'top'; } if (layout === 'verticalBiaxial') { axisSeries01 = 'left'; axisSeries02 = 'right'; } const data = [ { name: 'A', Series_01: 100, Series_02: 200, }, { name: 'B', Series_01: 200, Series_02: 300, }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <Flex justifyContent="between" width="100%" wrap> <RadioGroup direction="row" id="layout-type" legend="ChartGraph type"> <RadioGroup.RadioButton checked={type === 'bar'} id="layout-type-bar" label="Bar" name="bar" onChange={() => setType('bar')} size="sm" value="bar" /> <RadioGroup.RadioButton checked={type === 'line'} id="layout-type-line" label="Line" name="line" onChange={() => setType('line')} size="sm" value="line" /> <RadioGroup.RadioButton checked={type === 'combo'} id="layout-type-combo" label="Combo" name="combo" onChange={() => setType('combo')} size="sm" value="combo" /> </RadioGroup> <RadioGroup direction="row" id="layout_layout" legend="Layout"> <RadioGroup.RadioButton checked={layout === 'horizontal'} id="layout_layout-horizontal" label="Horizontal" name="horizontal" onChange={() => setLayout('horizontal')} size="sm" value="horizontal" /> <RadioGroup.RadioButton checked={layout === 'horizontalBiaxial'} id="layout_layout-horizontalBiaxial" label="HorizontalBiaxial" name="horizontalBiaxial" onChange={() => setLayout('horizontalBiaxial')} size="sm" value="horizontalBiaxial" /> <RadioGroup.RadioButton checked={layout === 'vertical'} id="layout_layout-vertical" label="Vertical" name="vertical" onChange={() => setLayout('vertical')} size="sm" value="vertical" /> <RadioGroup.RadioButton checked={layout === 'verticalBiaxial'} id="layout_layout-verticalBiaxial" label="VerticalBiaxial" name="verticalBiaxial" onChange={() => setLayout('verticalBiaxial')} size="sm" value="verticalBiaxial" /> </RadioGroup> </Flex> <ChartGraph accessibilityLabel="Example of chart with decal custom dimension" data={data} elements={[ { type: type === 'combo' ? 'bar' : type, id: 'Series_01', axis: axisSeries01, }, { type: type === 'combo' ? 'line' : type, id: 'Series_02', axis: axisSeries02, }, ]} layout={layout} legend="none" onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } title="ChartGraph" titleDisplay="hidden" type={type} visualPatternSelected={visualPatternSelected} /> </Flex> ); }
Range
ChartGraph automatically sets the minimum and maximum axis values. The range
prop allows adjusting them in case we need broader range values in the axis.
Props: range
import { useState } from 'react'; import { Flex, RadioGroup } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const [type, setType] = useState('bar'); const data = [ { name: 'A', Percentage: 20, Absolute: 2000, }, { name: 'B', Percentage: 40, Absolute: 3000, }, { name: 'C', Percentage: 80, Absolute: 4000, }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <RadioGroup direction="row" id="range" legend="ChartGraph type"> <RadioGroup.RadioButton checked={type === 'bar'} id="range-bar" label="Bar" name="bar" onChange={() => setType('bar')} size="sm" value="bar" /> <RadioGroup.RadioButton checked={type === 'line'} id="range-line" label="Line" name="line" onChange={() => setType('line')} size="sm" value="line" /> <RadioGroup.RadioButton checked={type === 'combo'} id="range-combo" label="Combo" name="combo" onChange={() => setType('combo')} size="sm" value="combo" /> </RadioGroup> <ChartGraph accessibilityLabel="Example of range in charts" data={data} elements={[ { type: type === 'combo' ? 'bar' : type, id: 'Percentage', axis: 'left', }, { type: type === 'combo' ? 'line' : type, id: 'Absolute', axis: 'right', }, ]} layout="verticalBiaxial" onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } range={{ yAxisLeft: [0, 100] }} title="ChartGraph" titleDisplay="hidden" type={type} visualPatternSelected={visualPatternSelected} /> </Flex> ); }
Responsive
ChartGraph is responsive. ChartGraph's width adjusts to the parent container. In order to render properly, ChartGraph requires a parent container with set dimensions.
For vertical layouts, ChartGraph has a set height based on the amount of ticks (five or three). For horizontal layouts, ChartGraph has a set height.
When ChartGraphs are contained within small containers (under 576px wide), set initialTicks={3}
to prevent ChartGraph to flick.
import { useId, useRef, useState } from 'react'; import { Box, Flex, Label, RadioGroup, Text } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const labelId = useId(); const [width, setWidth] = useState(700); const scrollContainerRef = useRef(); const updateWidth = ({ target }) => { setWidth(Number(target.value)); }; const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const [type, setType] = useState('bar'); const [layout, setLayout] = useState('horizontal'); let axisSeries01; let axisSeries02; if (layout === 'horizontalBiaxial') { axisSeries01 = 'bottom'; axisSeries02 = 'top'; } if (layout === 'verticalBiaxial') { axisSeries01 = 'left'; axisSeries02 = 'right'; } const data = [ { name: 'A', Series_01: 100, Series_02: 200, }, { name: 'B', Series_01: 200, Series_02: 300, }, ]; return ( <Box padding={2} width="100%"> <Flex direction="column" gap={5} width="100%"> <Flex justifyContent="between" width="100%"> <RadioGroup direction="row" id="responsive-type" legend="ChartGraph type" > <RadioGroup.RadioButton checked={type === 'bar'} id="responsive-type-bar" label="Bar" name="bar" onChange={() => setType('bar')} size="sm" value="bar" /> <RadioGroup.RadioButton checked={type === 'line'} id="responsive-type-line" label="Line" name="line" onChange={() => setType('line')} size="sm" value="line" /> <RadioGroup.RadioButton checked={type === 'combo'} id="responsive-type-combo" label="Combo" name="combo" onChange={() => setType('combo')} size="sm" value="combo" /> </RadioGroup> <RadioGroup direction="row" id="responsive_layout" legend="Layout"> <RadioGroup.RadioButton checked={layout === 'horizontal'} id="responsive_layout-horizontal" label="Horizontal" name="horizontal" onChange={() => setLayout('horizontal')} size="sm" value="horizontal" /> <RadioGroup.RadioButton checked={layout === 'horizontalBiaxial'} id="responsive_layout-horizontalBiaxial" label="HorizontalBiaxial" name="horizontalBiaxial" onChange={() => setLayout('horizontalBiaxial')} size="sm" value="horizontalBiaxial" /> <RadioGroup.RadioButton checked={layout === 'vertical'} id="responsive_layout-vertical" label="Vertical" name="vertical" onChange={() => setLayout('vertical')} size="sm" value="vertical" /> <RadioGroup.RadioButton checked={layout === 'verticalBiaxial'} id="responsive_layout-verticalBiaxial" label="VerticalBiaxial" name="verticalBiaxial" onChange={() => setLayout('verticalBiaxial')} size="sm" value="verticalBiaxial" /> </RadioGroup> </Flex> <Flex alignItems="center" direction="column"> <Flex> <Label htmlFor={labelId}> <Text>Container Width</Text> </Label> <Text>{`: ${width}px`}</Text> </Flex> <input defaultValue={800} id={labelId} max={800} min={200} onChange={updateWidth} step={1} style={{ width: '400px', display: 'block', margin: '10px auto' }} type="range" /> </Flex> <div ref={(el) => { scrollContainerRef.current = el; }} style={{ height: '300px', margin: '0 auto', outline: '3px solid #ddd', overflowY: 'scroll', width: `${width}px`, }} > <ChartGraph accessibilityLabel="Example of chart with decal custom dimension" data={data} elements={[ { type: type === 'combo' ? 'bar' : type, id: 'Series_01', axis: axisSeries01, }, { type: type === 'combo' ? 'line' : type, id: 'Series_02', axis: axisSeries02, }, ]} layout={layout} legend="none" onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } title="ChartGraph" titleDisplay="hidden" type={type} visualPatternSelected={visualPatternSelected} /> </div> </Flex> </Box> ); }
Selectors
import { useState } from 'react'; import { TileData } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const dataA = [ { name: 'A', Impressions: 100, }, { name: 'B', Impressions: 200, }, { name: 'C', Impressions: 300, }, ]; const dataB = [ { name: 'A', Engagement: 90, }, { name: 'B', Engagement: 180, }, { name: 'C', Engagement: 250, }, ]; const [selectedId, setSelectedId] = useState('01'); const isSelected = (id) => selectedId === id; const getColor = (value) => { const colorMap = { '01': '01', '02': '02', '03': '03', '04': '04', '05': '05', '06': '06', '07': '07', '08': '08', '09': '09', 10: '10', 11: '11', 12: '12', }; return colorMap[value]; }; return ( <ChartGraph accessibilityLabel="Example of Bar chart" data={selectedId === '01' ? dataA : dataB} description="Description" elements={[ { type: 'bar', id: selectedId === '01' ? 'Impressions' : 'Engagement', color: getColor(selectedId || '01'), }, ]} initialTicks={3} legend="none" onVisualPatternChange={() => {}} title="Title" type="bar" visualPatternSelected="disabled" > <TileData color="01" id="01" onTap={({ id }) => setSelectedId(id)} selected={isSelected('01')} title="Impressions" trend={{ value: 29, accessibilityLabel: 'Trending up' }} value="10M" /> <TileData color="02" id="02" onTap={({ id }) => setSelectedId(id)} selected={isSelected('02')} title="Engagement" trend={{ value: 29, accessibilityLabel: 'Trending up' }} value="2M" /> </ChartGraph> ); }
import { useEffect, useState } from 'react'; import { TagData } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const [elements, setElements] = useState([]); const [selectedId, setSelectedId] = useState(['Campaign Autumn']); const isSelected = (id) => selectedId.includes(id); const getColor = (value) => { const colorMap = { '01': '01', '02': '02', '03': '03', '04': '04', '05': '05', '06': '06', '07': '07', '08': '08', '09': '09', 10: '10', 11: '11', 12: '12', }; return colorMap[value]; }; const handleSelectors = ({ id, selected }) => { if (!selected) { setSelectedId((values) => values.filter((idValue) => idValue !== id)); } if (selected) { setSelectedId((idValues) => [...idValues, id]); } }; useEffect(() => { const elementsArray = [ { type: 'bar', id: 'Campaign Autumn', color: getColor('01'), }, { type: 'bar', id: 'Campaign Winter', color: getColor('02'), }, { type: 'bar', id: 'Campaign Spring', color: getColor('03'), }, ]; const newElements = selectedId .map((idToMap) => elementsArray.filter(({ id }) => id === idToMap)) .flat(); setElements(newElements); }, [selectedId]); return ( <ChartGraph accessibilityLabel="Example of Bar chart" data={[ { name: 'NorthWest', 'Campaign Autumn': 100, 'Campaign Winter': 90, 'Campaign Spring': 10, }, { name: 'Sunbelt', 'Campaign Autumn': 200, 'Campaign Winter': 180, 'Campaign Spring': 50, }, { name: 'East Coast', 'Campaign Autumn': 300, 'Campaign Winter': 250, 'Campaign Spring': 100, }, ]} elements={elements} initialTicks={3} legend="none" onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } title="Clickthroughs per region" type="bar" visualPatternSelected={visualPatternSelected} > <TagData color="01" id="Campaign Autumn" onTap={({ id, selected }) => handleSelectors({ id, selected })} selected={isSelected('Campaign Autumn')} showCheckbox text="Campaign Autumn" /> <TagData color="02" id="Campaign Winter" onTap={({ id, selected }) => handleSelectors({ id, selected })} selected={isSelected('Campaign Winter')} showCheckbox text="Campaign Winter" /> <TagData color="03" id="Campaign Spring" onTap={({ id, selected }) => handleSelectors({ id, selected })} selected={isSelected('Campaign Spring')} showCheckbox text="Campaign Spring" /> </ChartGraph> ); }
Tick format
ChartGraph allows to format the values in the axis' ticks. For example, to translate numeric values to shorter ones (1000 => 1k) or to format dates based on locale.
Props: tickFormatter
.
When localizing dates, use tickFormatter.timestamps
as it traslates the values in the tooltip as well. Avoid using comma separators for dates as .csv files use them for cell separation. tickFormatter.xAxisBottom
overrides tickFormatter.timeseries
when both are present, in case tooltip and x axis present different date formats.
import { ChartGraph } from 'gestalt-charts'; export default function Example() { const data = [ { name: 'A', Series_01: 1000300, }, { name: 'B', Series_01: 2000600, }, { name: 'C', Series_01: 3001200, }, ]; return ( <ChartGraph accessibilityLabel="Example of Bar chart" data={data} elements={[{ type: 'bar', id: 'Series_01' }]} legend="none" onVisualPatternChange={() => {}} tickFormatter={{ yAxisLeft: (value) => `${value / 1000000}M` }} title="ChartGraph" titleDisplay="hidden" type="bar" visualPatternSelected="disabled" /> ); }
Time series
ChartGraph supports timeseries. To enable timeseries, set tickFormatter.timeseries
. Time series charts are only supported in vertical layout.
Props: tickFormatter.timeseries
.
import { useState } from 'react'; import { FixedZIndex, Flex, RadioGroup } from 'gestalt'; import { ChartGraph } from 'gestalt-charts'; export default function Example() { const [visualPatternSelected, setVisualPatternSelected] = useState('default'); const [type, setType] = useState('line'); const data = [ { name: new Date(2023, 0, 1).getTime(), Series_01: 1000, Series_02: 100 }, { name: new Date(2023, 0, 2).getTime(), Series_01: 1005, Series_02: 200 }, { name: new Date(2023, 0, 3).getTime(), Series_01: 1003, Series_02: 150 }, { name: new Date(2023, 0, 4).getTime(), Series_01: 1100, Series_02: 130 }, { name: new Date(2023, 0, 5).getTime(), Series_01: 1030, Series_02: 147 }, { name: new Date(2023, 0, 6).getTime(), Series_01: 1089, Series_02: 189 }, { name: new Date(2023, 0, 7).getTime(), Series_01: 1065, Series_02: 118 }, { name: new Date(2023, 0, 8).getTime(), Series_01: 1090, Series_02: 177 }, ]; return ( <Flex direction="column" gap={2} height="100%" width="100%"> <RadioGroup direction="row" id="timeseries" legend="ChartGraph type"> <RadioGroup.RadioButton checked={type === 'bar'} id="timeseries-bar" label="Bar" name="bar" onChange={() => setType('bar')} size="sm" value="bar" /> <RadioGroup.RadioButton checked={type === 'line'} id="timeseries-line" label="Line" name="line" onChange={() => setType('line')} size="sm" value="line" /> <RadioGroup.RadioButton checked={type === 'combo'} id="timeseries-combo" label="Combo" name="combo" onChange={() => setType('combo')} size="sm" value="combo" /> </RadioGroup> <ChartGraph accessibilityLabel="Example of time series chart" data={data} elements={[ { type: type === 'combo' ? 'bar' : type, id: 'Series_01' }, { type: type === 'combo' ? 'line' : type, id: 'Series_02' }, ]} modalZIndex={new FixedZIndex(11)} onVisualPatternChange={() => setVisualPatternSelected((value) => value === 'default' ? 'visualPattern' : 'default' ) } range={{ xAxisBottom: ['auto', 'auto'], }} tickFormatter={{ timeseries: (date) => `${new Intl.DateTimeFormat('en-US', { month: 'short' }).format( date )}-${new Intl.DateTimeFormat('en-US', { day: '2-digit' }).format( date )}`, }} title="ChartGraph" titleDisplay="hidden" type={type} visualPatternSelected={visualPatternSelected} /> </Flex> ); }
Writing
- Keep labels short so that they don’t wrap and make it hard to read data
- Use abbreviations that are commonly understood and can be translated to all supported languages. For more on abbreviations, see our Content standards.
- Create extra-long labels that have to wrap or truncate
- Use abbreviations that are only understood internally or that don't translate well.
Component quality checklist
Quality item | Status | Status description |
---|---|---|
Figma Library | Ready | Component is available in Figma for web and mobile web. |
Responsive Web | Ready | Component responds to changing viewport sizes in web and mobile web. |
Related
Data visualization guidelines
Principles, use cases and guidelines for charts, graphs and micro-visualizations
TagData
TagData enables people to select multiple categories to compare with each other in a graph or chart.
TileData
TileData enables users to select multiple categories to compare with each other in a graph or chart view, while still being able to see all of the data points.
Table
Tables show data that's more complex and granular.