PlBarChart
Lengths, compared. A bar encodes how much as length, which is why its axis always starts at zero.
import { PlBarChart } from 'plass-ui';
<PlBarChart series={revenue} categories={regions} />;import 'package:plass_ui/plass_ui.dart';
PlBarChart(series: revenue, categories: regions);Crop the scale and a bar twice as long stops meaning twice as much, and the reader has no way to know it happened. Reach for a line chart when what matters is the shape of a change rather than the size of each value.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| series * | readonly PlassChartSeries[] | — | The data |
| categories | readonly PlassChartCategory[] | — | What the category axis says |
| orientation | 'vertical' | 'horizontal' | 'vertical' | Which way the bars run. horizontal is the right answer whenever the category names are words: it has a whole column for them |
| stacked | boolean | 'full' | false | Puts the series on top of each other instead of beside each other. 'full' makes every bar the same length, so the chart is about share rather than size |
| rounded | boolean | true | Cuts the corners off the data end of each bar. The baseline end stays square: a rounded foot makes the axis look scalloped |
| barSize | number | the size ladder — 24 at md | How thick a bar may get. Below the cap the bars fill their share of the band; above it the leftover stays as air |
| valueLabels | 'none' | 'last' | 'extremes' | 'all' | 'none' | Which values are written on the bars. all is defensible here in a way it is not on a line chart: eight bars with their numbers on them is a chart and a table at once |
| xAxis | PlassChartAxis | — | The category axis |
| yAxis | PlassChartAxis | — | The value axis |
| legend | PlassChartLegend | — | The legend |
| tooltip | PlassChartTooltip | — | The tooltip |
| height | number | string | — | How tall the plot is |
| format | Intl.NumberFormatOptions | — | How a value is written |
| label | string | 'Chart' | What the whole drawing is called |
| empty | ReactNode | — | What is drawn when there is nothing to draw |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Type scale, plot height and the bar thickness cap |
| densityshared | 'default' | 'compact' | 'default' | How much of a category's slot the bars in it may take. Never the height |
| Prop | Type | Default | Description |
|---|---|---|---|
| series * | List<PlassChartSeries> | — | The data |
| categories | List<PlassChartCategory>? | — | What the category axis says |
| orientation | PlassOrientation | PlassOrientation.vertical | Which way the bars run. horizontal is the right answer whenever the category names are words: it has a whole column for them |
| stacking | PlBarStacking | PlBarStacking.grouped | How the series sit relative to each other. Three named states rather than React's boolean | 'full' |
| rounded | bool | true | Cuts the corners off the data end of each bar. The baseline end stays square: a rounded foot makes the axis look scalloped |
| barSize | double? | the size ladder — 24 at md | How thick a bar may get. Below the cap the bars fill their share of the band; above it the leftover stays as air |
| valueLabels | PlassChartValueLabels | PlassChartValueLabels.none | Which values are written on the bars. all is defensible here in a way it is not on a line chart: eight bars with their numbers on them is a chart and a table at once |
| xAxis | PlChartAxis | PlChartAxis() | The category axis |
| yAxis | PlChartAxis | PlChartAxis() | The value axis |
| legend | PlChartLegend | PlChartLegend() | The legend |
| tooltip | PlChartTooltip | PlChartTooltip() | The tooltip |
| height | double? | — | How tall the plot is |
| format | String Function(double)? | — | How a value is written |
| semanticLabel | String? | 'Chart' | What the whole drawing is called |
| empty | Widget? | — | What is drawn when there is nothing to draw |
| sizeshared | PlassSize | PlassSize.md | Type scale, plot height and the bar thickness cap |
| densityshared | PlassDensity | PlassDensity.standard | How much of a category's slot the bars in it may take. Never the height |
The data is the same PlassChartSeries every chart takes. A null is a gap here too, and a bar is simply not drawn for one, which is the distinction that matters most on this chart, because a zero-length bar and a missing bar are the same picture and only one of them is honest.
What the shared axes mean across the library is in prop conventions.
Examples
orientation
horizontal is the right answer whenever the category names are words: it has a whole column for them, where a vertical chart has the width of one bar.
import { PlBarChart } from 'plass-ui';
const sources = [
{
name: 'Sessions',
data: [4820, 3910, 2740, 1980, 1120]
}
];
const channels = [
'Organic search',
'Direct traffic',
'Email campaigns',
'Paid social',
'Referral links'
];
export default function BarChartOrientation() {
return (
<PlBarChart
className="w-full"
series={sources}
categories={channels}
orientation="horizontal"
valueLabels="all"
legend={false}
/>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
const List<PlassChartSeries> _sources = <PlassChartSeries>[
PlassChartSeries(
name: 'Sessions',
data: <PlassChartDatum>[
PlassChartDatum(4820),
PlassChartDatum(3910),
PlassChartDatum(2740),
PlassChartDatum(1980),
PlassChartDatum(1120),
],
),
];
const List<PlassChartCategory> _channels = <PlassChartCategory>[
PlassChartCategory.text('Organic search'),
PlassChartCategory.text('Direct traffic'),
PlassChartCategory.text('Email campaigns'),
PlassChartCategory.text('Paid social'),
PlassChartCategory.text('Referral links'),
];
class BarChartOrientation extends StatelessWidget {
const BarChartOrientation({super.key});
@override
Widget build(BuildContext context) {
return const PlBarChart(
series: _sources,
categories: _channels,
orientation: PlassOrientation.horizontal,
valueLabels: PlassChartValueLabels.all,
legend: PlChartLegend(hidden: true),
);
}
}Everything swaps with it, which band each axis reserves, which way the grid runs, which way the crosshair goes and which end of a bar is rounded.
stacked
Grouped bars answer "which series is bigger here". Stacked bars answer "what is this total made of". They are different questions and the chart should be asked only one of them at a time.
import { useState } from 'react';
import { PlBarChart, PlSegment, PlSegmentedButton } from 'plass-ui';
const plans = [
{ name: 'Free', data: [420, 460, 480, 510] },
{ name: 'Pro', data: [180, 220, 260, 310] },
{ name: 'Team', data: [40, 55, 72, 96] }
];
const quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
type Mode = 'grouped' | 'stacked' | 'full';
export default function BarChartStacked() {
const [mode, setMode] = useState<Mode>('stacked');
return (
<div className="flex w-full flex-col gap-4">
<PlSegmentedButton value={mode} onValueChange={(next) => setMode(next as Mode)}>
<PlSegment value="grouped">grouped</PlSegment>
<PlSegment value="stacked">stacked</PlSegment>
<PlSegment value="full">full</PlSegment>
</PlSegmentedButton>
<PlBarChart
className="w-full"
series={plans}
categories={quarters}
stacked={mode === 'grouped' ? false : mode === 'full' ? 'full' : true}
/>
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
List<PlassChartDatum> _readings(List<double> values) => values.map(PlassChartDatum.new).toList();
final List<PlassChartSeries> _plans = <PlassChartSeries>[
PlassChartSeries(name: 'Free', data: _readings(<double>[420, 460, 480, 510])),
PlassChartSeries(name: 'Pro', data: _readings(<double>[180, 220, 260, 310])),
PlassChartSeries(name: 'Team', data: _readings(<double>[40, 55, 72, 96])),
];
const List<PlassChartCategory> _quarters = <PlassChartCategory>[
PlassChartCategory.text('Q1'),
PlassChartCategory.text('Q2'),
PlassChartCategory.text('Q3'),
PlassChartCategory.text('Q4'),
];
class BarChartStacked extends StatefulWidget {
const BarChartStacked({super.key});
@override
State<BarChartStacked> createState() => _BarChartStackedState();
}
class _BarChartStackedState extends State<BarChartStacked> {
PlBarStacking _stacking = PlBarStacking.stacked;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PlSegmentedButton<PlBarStacking>(
value: _stacking,
onChanged: (PlBarStacking next) => setState(() => _stacking = next),
segments: const <PlSegment<PlBarStacking>>[
PlSegment<PlBarStacking>(value: PlBarStacking.grouped, label: Text('grouped')),
PlSegment<PlBarStacking>(value: PlBarStacking.stacked, label: Text('stacked')),
PlSegment<PlBarStacking>(value: PlBarStacking.full, label: Text('full')),
],
),
const SizedBox(height: 16),
PlBarChart(series: _plans, categories: _quarters, stacking: _stacking),
],
);
}
}'full' makes every bar the same length, so the chart is about share rather than size, exactly as it does on an area chart, and for the same reason it renormalises the data rather than the drawing.
The gap between two stacked segments is taken off the far end of each, so the stack still totals the right length and the seam is the sheet showing through rather than a line drawn on it. A border around a bar is ink that is not data.
Negative values
The two arms are accumulated separately, so a series that dips does not shorten the one above it, and a negative bar grows down from the baseline while the positives grow up.
import { PlBarChart } from 'plass-ui';
const change = [{ name: 'Net change', data: [12, -8, 24, -3, 18, -14, 9] }];
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];
export default function BarChartNegative() {
return (
<PlBarChart
className="w-full"
series={change}
categories={months}
valueLabels="all"
legend={false}
format={{ signDisplay: 'exceptZero' }}
/>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
const List<PlassChartSeries> _change = <PlassChartSeries>[
PlassChartSeries(
name: 'Net change',
data: <PlassChartDatum>[
PlassChartDatum(12),
PlassChartDatum(-8),
PlassChartDatum(24),
PlassChartDatum(-3),
PlassChartDatum(18),
PlassChartDatum(-14),
PlassChartDatum(9),
],
),
];
const List<PlassChartCategory> _months = <PlassChartCategory>[
PlassChartCategory.text('Jan'),
PlassChartCategory.text('Feb'),
PlassChartCategory.text('Mar'),
PlassChartCategory.text('Apr'),
PlassChartCategory.text('May'),
PlassChartCategory.text('Jun'),
PlassChartCategory.text('Jul'),
];
class BarChartNegative extends StatelessWidget {
const BarChartNegative({super.key});
@override
Widget build(BuildContext context) {
return PlBarChart(
series: _change,
categories: _months,
valueLabels: PlassChartValueLabels.all,
legend: const PlChartLegend(hidden: true),
format: (double value) => value > 0 ? '+${value.toInt()}' : '${value.toInt()}',
);
}
}The baseline is redrawn over the bars. Every bar starts there, and under them the line would be half-hidden by the first pixel of each one.
rounded
The corners come off the data end of each bar only. The baseline end stays square: that is where the value starts from, and a rounded foot makes the axis look scalloped.
barSize and density
barSize is a cap, not a width. The band a bar sits in is whatever the plot divided by the category count gives; below the cap the bars fill their share of it, and above it the leftover stays as air. density is the share of the band the bars may take at all.
Accessibility
Everything PlLineChart says applies here: the name and the per-series summary, the legend as real controls, and (on React) the hidden table that carries every number.