One wide landscape graphic that compares 4 to 12 entities on five to seven measures: two measures as S-curves between parallel scales on the left, joined to a table on the right in which every other number is drawn as dots, squares, icons, a ring, a circle or a bar. Both halves share one row order and one colour per entity.
Glyph table
A table with one row per entity in which every number is drawn as a small form chosen per column (counted dots, squares or icons, a ring, a sized circle, a bar) with the value printed beside it.
- Use when
- Comparing 3 to about 12 entities across 2 to 5 measures with different units, when the table should read as a graphic: profiles of products, cities, teams or species. Pair it with curve rank by passing both the same
order. - Avoid when
- Looking up many exact values, sorting, or more than about 12 rows (use data table); one measure across entities (use bar chart or pictogram chart); a trend over time (use line chart or small multiples).
Preview
import { GlyphTable } from "insightcharts";
export function Example() {
return (
<GlyphTable
caption="Filter coffee sold 310 cups a day at £2.40 a cup"
subtitle="Six drinks at one café: price, cups sold per day, share ordered with oat milk, seconds to prepare and caffeine"
note="Illustrative figures."
name="drink"
nameLabel="Drink"
columns={[
{
field: "price",
label: "Price per cup",
glyph: "bar",
format: { style: "currency", currency: "GBP" },
max: 5,
},
{ field: "cups", label: "Cups sold a day", glyph: "dots", unit: 10 },
{ field: "oat", label: "With oat milk", glyph: "ring", format: "{value}%" },
{ field: "seconds", label: "Seconds to prepare", glyph: "blocks", unit: 10 },
{ field: "caffeine", label: "Caffeine, mg", glyph: "circle" },
]}
rows={[
{
drink: "Filter coffee",
price: 2.4,
cups: 310,
oat: 8,
seconds: 15,
caffeine: 140,
},
{
drink: "Espresso",
price: 2.6,
cups: 145,
oat: null,
seconds: 30,
caffeine: 65,
},
{
drink: "Flat white",
price: 3.6,
cups: 260,
oat: 34,
seconds: 95,
caffeine: 130,
},
{
drink: "Iced latte",
price: 4.2,
cups: 180,
oat: 41,
seconds: 70,
caffeine: 130,
},
{
drink: "Matcha latte",
price: 4.8,
cups: 75,
oat: 57,
seconds: 110,
caffeine: 70,
},
{
drink: "Hot chocolate",
price: 3.9,
cups: 40,
oat: 22,
seconds: 85,
caffeine: 10,
},
]}
/>
);
}{
"type": "glyph-table",
"caption": "Filter coffee sold 310 cups a day at £2.40 a cup",
"subtitle": "Six drinks at one café: price, cups sold per day, share ordered with oat milk, seconds to prepare and caffeine",
"note": "Illustrative figures.",
"name": "drink",
"nameLabel": "Drink",
"columns": [
{
"field": "price",
"label": "Price per cup",
"glyph": "bar",
"format": { "style": "currency", "currency": "GBP" },
"max": 5
},
{ "field": "cups", "label": "Cups sold a day", "glyph": "dots", "unit": 10 },
{ "field": "oat", "label": "With oat milk", "glyph": "ring", "format": "{value}%" },
{ "field": "seconds", "label": "Seconds to prepare", "glyph": "blocks", "unit": 10 },
{ "field": "caffeine", "label": "Caffeine, mg", "glyph": "circle" }
],
"rows": [
{
"drink": "Filter coffee",
"price": 2.4,
"cups": 310,
"oat": 8,
"seconds": 15,
"caffeine": 140
},
{
"drink": "Espresso",
"price": 2.6,
"cups": 145,
"oat": null,
"seconds": 30,
"caffeine": 65
},
{
"drink": "Flat white",
"price": 3.6,
"cups": 260,
"oat": 34,
"seconds": 95,
"caffeine": 130
},
{
"drink": "Iced latte",
"price": 4.2,
"cups": 180,
"oat": 41,
"seconds": 70,
"caffeine": 130
},
{
"drink": "Matcha latte",
"price": 4.8,
"cups": 75,
"oat": 57,
"seconds": 110,
"caffeine": 70
},
{
"drink": "Hot chocolate",
"price": 3.9,
"cups": 40,
"oat": 22,
"seconds": 85,
"caffeine": 10
}
]
}Render with import { Insight } from "insightcharts" and <Insight spec={spec} />. Nested specs resolve by name; nothing else to import.
Composition
Used in
2 infographics use glyph table.
Infographics
2A poster that describes one subject from four sides: the subject as one large silhouette filled to a share (shape-fill), two to four large figures, its place among other things on one scale (scale-ladder), and a table that sets it beside its peers with every number drawn as a small form (glyph-table).
Installation
$ pnpm dlx shadcn@latest add @insightcharts/glyph-tableThe CLI copies the source into your project and needs the @insightcharts registry in components.json; see installation. With the package, import it by name:
import { GlyphTable } from "insightcharts";Examples
Highlighted row, icons, selectable
import { GlyphTable } from "insightcharts";
export function Example() {
return (
<GlyphTable
caption="The night shift had 4 nurses for 28 beds"
subtitle="Three shifts on one hospital ward: nurses on duty, occupied beds, share of call bells answered within 2 minutes"
note="Illustrative figures."
name="shift"
nameLabel="Shift"
highlight="Night"
select
columns={[
{ field: "nurses", label: "Nurses on duty", glyph: "icons", icon: "person" },
{ field: "beds", label: "Occupied beds", glyph: "blocks" },
{
field: "answered",
label: "Bells answered in 2 minutes",
glyph: "ring",
format: "percent",
max: 1,
},
{ field: "handover", label: "Handover, minutes", glyph: "text" },
]}
rows={[
{ shift: "Morning", nurses: 9, beds: 30, answered: 0.91, handover: 25 },
{ shift: "Evening", nurses: 7, beds: 29, answered: 0.84, handover: 20 },
{ shift: "Night", nurses: 4, beds: 28, answered: 0.62, handover: 15 },
]}
/>
);
}{
"type": "glyph-table",
"caption": "The night shift had 4 nurses for 28 beds",
"subtitle": "Three shifts on one hospital ward: nurses on duty, occupied beds, share of call bells answered within 2 minutes",
"note": "Illustrative figures.",
"name": "shift",
"nameLabel": "Shift",
"highlight": "Night",
"select": true,
"columns": [
{ "field": "nurses", "label": "Nurses on duty", "glyph": "icons", "icon": "person" },
{ "field": "beds", "label": "Occupied beds", "glyph": "blocks" },
{
"field": "answered",
"label": "Bells answered in 2 minutes",
"glyph": "ring",
"format": "percent",
"max": 1
},
{ "field": "handover", "label": "Handover, minutes", "glyph": "text" }
],
"rows": [
{ "shift": "Morning", "nurses": 9, "beds": 30, "answered": 0.91, "handover": 25 },
{ "shift": "Evening", "nurses": 7, "beds": 29, "answered": 0.84, "handover": 20 },
{ "shift": "Night", "nurses": 4, "beds": 28, "answered": 0.62, "handover": 15 }
]
}Render with import { Insight } from "insightcharts" and <Insight spec={spec} />. Nested specs resolve by name; nothing else to import.
Ten entities, one colour each
import { GlyphTable } from "insightcharts";
export function Example() {
return (
<GlyphTable
caption="Ten districts by rent, commute, green space and population"
subtitle="Median monthly rent, minutes to the centre, share of land that is park and residents, ten districts of one city"
note="Illustrative figures for an invented city."
name="district"
nameLabel="District"
colorBy="series"
columns={[
{
field: "rent",
label: "Median rent a month",
glyph: "bar",
format: { style: "currency", currency: "EUR", maximumFractionDigits: 0 },
},
{ field: "commute", label: "Minutes to the centre", glyph: "dots" },
{ field: "park", label: "Park land", glyph: "ring", format: "{value}%" },
{ field: "residents", label: "Residents", glyph: "circle", format: "integer" },
]}
rows={[
{ district: "Old Town", rent: 1480, commute: 12, park: 9, residents: 18400 },
{ district: "Harbour", rent: 1390, commute: 15, park: 6, residents: 12100 },
{ district: "Riverside", rent: 1310, commute: 18, park: 22, residents: 21700 },
{ district: "University", rent: 1180, commute: 14, park: 15, residents: 26300 },
{ district: "Market", rent: 1120, commute: 20, park: 4, residents: 9800 },
{ district: "Hillcrest", rent: 1040, commute: 26, park: 31, residents: 15200 },
{ district: "Northgate", rent: 960, commute: 31, park: 12, residents: 30500 },
{ district: "Eastfield", rent: 890, commute: 34, park: 17, residents: 24900 },
{ district: "Mill Quarter", rent: 820, commute: 29, park: 8, residents: 7600 },
{ district: "Lakeside", rent: 760, commute: 41, park: 38, residents: 11300 },
]}
/>
);
}{
"type": "glyph-table",
"caption": "Ten districts by rent, commute, green space and population",
"subtitle": "Median monthly rent, minutes to the centre, share of land that is park and residents, ten districts of one city",
"note": "Illustrative figures for an invented city.",
"name": "district",
"nameLabel": "District",
"colorBy": "series",
"columns": [
{
"field": "rent",
"label": "Median rent a month",
"glyph": "bar",
"format": { "style": "currency", "currency": "EUR", "maximumFractionDigits": 0 }
},
{ "field": "commute", "label": "Minutes to the centre", "glyph": "dots" },
{ "field": "park", "label": "Park land", "glyph": "ring", "format": "{value}%" },
{
"field": "residents",
"label": "Residents",
"glyph": "circle",
"format": "integer"
}
],
"rows": [
{
"district": "Old Town",
"rent": 1480,
"commute": 12,
"park": 9,
"residents": 18400
},
{
"district": "Harbour",
"rent": 1390,
"commute": 15,
"park": 6,
"residents": 12100
},
{
"district": "Riverside",
"rent": 1310,
"commute": 18,
"park": 22,
"residents": 21700
},
{
"district": "University",
"rent": 1180,
"commute": 14,
"park": 15,
"residents": 26300
},
{ "district": "Market", "rent": 1120, "commute": 20, "park": 4, "residents": 9800 },
{
"district": "Hillcrest",
"rent": 1040,
"commute": 26,
"park": 31,
"residents": 15200
},
{
"district": "Northgate",
"rent": 960,
"commute": 31,
"park": 12,
"residents": 30500
},
{
"district": "Eastfield",
"rent": 890,
"commute": 34,
"park": 17,
"residents": 24900
},
{
"district": "Mill Quarter",
"rent": 820,
"commute": 29,
"park": 8,
"residents": 7600
},
{
"district": "Lakeside",
"rent": 760,
"commute": 41,
"park": 38,
"residents": 11300
}
]
}Render with import { Insight } from "insightcharts" and <Insight spec={spec} />. Nested specs resolve by name; nothing else to import.
Stress: 10 rows, long names, very uneven values, missing data
import { GlyphTable } from "insightcharts";
export function Example() {
return (
<GlyphTable
caption="Ten library branches by loans, visits and opening hours"
subtitle="Loans per year, visits per day, hours open per week and share of loans that were e-books, 2024"
note="Illustrative figures."
name="branch"
nameLabel="Branch"
order={["Central Library and City Archive", "Riverside"]}
columns={[
{ field: "loans", label: "Loans a year", glyph: "dots", format: "compact" },
{ field: "visits", label: "Visits a day", glyph: "circle", format: "integer" },
{ field: "hours", label: "Hours open a week", glyph: "bar", max: 84 },
{ field: "ebooks", label: "E-book share", glyph: "ring", format: "{value}%" },
]}
rows={[
{ branch: "Riverside", loans: 88000, visits: 640, hours: 54, ebooks: 31 },
{
branch: "Central Library and City Archive",
loans: 412000,
visits: 3900,
hours: 84,
ebooks: 38,
},
{ branch: "Hillcrest", loans: 61000, visits: 410, hours: 48, ebooks: 22 },
{
branch: "Northgate Community Hub and Learning Centre",
loans: 47500,
visits: 380,
hours: 60,
ebooks: 27,
},
{ branch: "Old Town", loans: 39000, visits: 290, hours: 40, ebooks: 18 },
{ branch: "Mill Quarter", loans: 22000, visits: 150, hours: 32, ebooks: null },
{ branch: "Eastfield", loans: 18400, visits: 120, hours: 30, ebooks: 12 },
{ branch: "Lakeside", loans: 9600, visits: 70, hours: 24, ebooks: 9 },
{
branch: "Harbour reading room",
loans: 2100,
visits: 25,
hours: 12,
ebooks: null,
},
{ branch: "Mobile library van", loans: 900, visits: 40, hours: 18, ebooks: 3 },
]}
/>
);
}{
"type": "glyph-table",
"caption": "Ten library branches by loans, visits and opening hours",
"subtitle": "Loans per year, visits per day, hours open per week and share of loans that were e-books, 2024",
"note": "Illustrative figures.",
"name": "branch",
"nameLabel": "Branch",
"order": ["Central Library and City Archive", "Riverside"],
"columns": [
{ "field": "loans", "label": "Loans a year", "glyph": "dots", "format": "compact" },
{
"field": "visits",
"label": "Visits a day",
"glyph": "circle",
"format": "integer"
},
{ "field": "hours", "label": "Hours open a week", "glyph": "bar", "max": 84 },
{
"field": "ebooks",
"label": "E-book share",
"glyph": "ring",
"format": "{value}%"
}
],
"rows": [
{ "branch": "Riverside", "loans": 88000, "visits": 640, "hours": 54, "ebooks": 31 },
{
"branch": "Central Library and City Archive",
"loans": 412000,
"visits": 3900,
"hours": 84,
"ebooks": 38
},
{ "branch": "Hillcrest", "loans": 61000, "visits": 410, "hours": 48, "ebooks": 22 },
{
"branch": "Northgate Community Hub and Learning Centre",
"loans": 47500,
"visits": 380,
"hours": 60,
"ebooks": 27
},
{ "branch": "Old Town", "loans": 39000, "visits": 290, "hours": 40, "ebooks": 18 },
{
"branch": "Mill Quarter",
"loans": 22000,
"visits": 150,
"hours": 32,
"ebooks": null
},
{ "branch": "Eastfield", "loans": 18400, "visits": 120, "hours": 30, "ebooks": 12 },
{ "branch": "Lakeside", "loans": 9600, "visits": 70, "hours": 24, "ebooks": 9 },
{
"branch": "Harbour reading room",
"loans": 2100,
"visits": 25,
"hours": 12,
"ebooks": null
},
{
"branch": "Mobile library van",
"loans": 900,
"visits": 40,
"hours": 18,
"ebooks": 3
}
]
}Render with import { Insight } from "insightcharts" and <Insight spec={spec} />. Nested specs resolve by name; nothing else to import.
Props
| Prop | Type | Description |
|---|---|---|
| rowsrequired | Row[] | One object per entity. |
| name | string | Field holding the entity name. Defaults to the first key of the first row. |
| nameLabel | string | Heading of the name column. |
| columnsrequired | { field, label?, glyph?: "dots" | "blocks" | "ring" | "circle" | "bar" | "icons" | "text", unit?, format?, max?, icon? }[] | Glyph columns in order. dots, blocks and icons draw one mark per unit (default 1, raised to a round number when the largest value would not fit, and stated in the heading). ring fills to max (default 100, or 1 for fractions). circle is sized by area and bar by length against max (default: the column's largest value). text prints the value alone. |
| order | string[] | Entity names in display order; also assigns colours. Pass the same list to curve-rank to match its curve colours. |
| highlight | string | string[] | Entities drawn in colour; every other row turns grey. |
| colorBy | "series" | "none" | series: one colour per entity; past the size of the theme's palette the extra colours are derived from the theme's own (curve-rank derives the same ones). none: every row in the accent. Default: series while the rows fit the palette (at most 8), none beyond. |
| caption | string | The table's title, as a real <caption>. |
| subtitle | string | Measures, units and period, under the caption. |
| source | string | Shown under the table as "Source: ...". |
| note | string | Method notes or caveats, shown under the table. |
| select | boolean | Let readers click a row (or press Enter on it) to select it; the other rows fade. |
| onSelect | (values: string[]) => void | Called with the selected entity names whenever the selection changes. |
| selected | string[] | Controlled selection: the entity names currently selected. When given, the table shows exactly this list and a click only calls onSelect. Omit it and the table keeps its own selection. |