@diamovi/react-native-word-cloud
v0.1.0
Published
A WordCloud component for React Native, rendered with react-native-svg.
Maintainers
Readme
@diamovi/react-native-word-cloud
A word cloud component for React Native. Pure JavaScript spiral layout, rendered with react-native-svg. Works with Expo (including Expo Go) and bare React Native, on iOS and Android.
- Deterministic layouts (seeded random, or your own
random) - Weight to font size mapping: linear, sqrt or log
- Rotation ranges with discrete steps, or no rotation at all
- Colors by palette, by category field, or by function
- Press and long-press handlers with the original data row
- Layout engine exported separately for tests and custom renderers
- No native code of its own: only
react-native-svgis required
Installation
bun add @diamovi/react-native-word-cloud react-native-svg
# or
npm install @diamovi/react-native-word-cloud react-native-svg
# or
yarn add @diamovi/react-native-word-cloud react-native-svgreact-native-svg is a peer dependency.
- Expo:
npx expo install react-native-svg. It is included in Expo Go. - Bare React Native: run
cd ios && pod installafter installing it.
Quick start
import { WordCloud } from '@diamovi/react-native-word-cloud'
const data = [
{ text: 'React Native', value: 120 },
{ text: 'TypeScript', value: 95 },
{ text: 'JavaScript', value: 90 },
{ text: 'Expo', value: 80 },
{ text: 'SVG', value: 70 },
{ text: 'Hooks', value: 65 },
]
export function Example() {
return <WordCloud data={data} height={260} />
}data is any array of objects. By default the word is read from text and the
weight from value; change that with wordField and weightField. When
width or height are omitted the component measures its container with
onLayout, so it can be given a style with a height or placed inside a flex
parent.
Examples
All screenshots below come from the app in example/.
Default
Built-in palette, sizes from 12 to 60 px, half of the words rotated 90°.
<WordCloud
data={data}
height={260}
seed={1}
onWordPress={(word) => console.log(word.text, word.value)}
/>Change seed to get a different arrangement of the same data. The same seed
and data always produce the same layout.
Color by category
Each distinct value of colorField gets the next color of the palette. Rotation
is turned off and the font is bold.
const data = [
{ text: 'TypeScript', value: 95, category: 'language' },
{ text: 'Expo', value: 80, category: 'framework' },
{ text: 'SVG', value: 70, category: 'graphics' },
{ text: 'Metro', value: 55, category: 'tooling' },
// ...
]
<WordCloud
data={data}
height={240}
colorField="category"
color={['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD']}
wordStyle={{ rotation: 0, fontWeight: 'bold', padding: 2 }}
/>Report style: custom fields, one color per word, condensed font
Rows shaped as { word, count }, a fixed random so the layout never changes
between renders, and a condensed display font. Impact ships with iOS; on Android
the example bundles Anton (SIL OFL)
as a look-alike.
import { Platform } from 'react-native'
const words = [
{ word: 'coffee', count: 143 },
{ word: 'morning', count: 98 },
{ word: 'espresso', count: 87 },
// ...
]
<WordCloud
data={words}
height={300}
wordField="word"
weightField="count"
colorField="word"
color={['#07c0cb', '#1e92c4', '#6aa72a', '#8945a3', '#c04891', '#e96d3c', '#eebc01', '#e5484d']}
wordStyle={{
rotation: 0,
fontFamily: Platform.select({ ios: 'Impact', default: 'Anton' }),
fontSize: [12, 60],
}}
random={() => 0.5}
onWordPress={(word) => console.log(word.item)} // { word: 'coffee', count: 143 }
/>Emojis only
Any string works as a word, including emoji. Emoji sequences (skin tones, flags, ZWJ combinations) are measured as a single square glyph.
const emojis = [
{ text: '🔥', value: 120 },
{ text: '❤️', value: 110 },
{ text: '😂', value: 95 },
{ text: '🚀', value: 80 },
// ...
]
<WordCloud
data={emojis}
height={240}
seed={3}
wordStyle={{ rotation: 0, fontSize: [18, 64], padding: 3 }}
/>Free rotation, rectangular spiral, log scale
Five rotation steps between -60° and 60°, a rectangular spiral instead of the archimedean one, a logarithmic size scale that flattens big differences in weight, and a color function.
<WordCloud
data={data}
height={240}
spiral="rectangular"
scale="log"
wordStyle={{ rotation: [-60, 60], rotationSteps: 5, fontSize: [10, 44] }}
color={(item) => (item.value > 50 ? '#0F172A' : '#64748B')}
/>Filling the parent
Omit width and height and let the container decide.
<View style={{ flex: 1 }}>
<WordCloud data={data} style={{ flex: 1 }} />
</View>Custom word renderer
renderWord receives the placed word and must return react-native-svg
elements. x and y are the center of the word and dy is the baseline offset
that keeps the glyphs vertically centered.
import { Text as SvgText } from 'react-native-svg'
<WordCloud
data={data}
height={260}
renderWord={(word) => (
<SvgText
x={0}
y={0}
dy={word.dy}
fontSize={word.size}
fill={word.color}
textAnchor="middle"
opacity={0.85}
transform={`translate(${word.x}, ${word.y}) rotate(${word.rotate})`}
>
{word.text.toUpperCase()}
</SvgText>
)}
/>Reacting to the computed layout
<WordCloud
data={data}
height={260}
onLayoutComplete={(words) => {
const dropped = data.length - words.length
if (dropped > 0) console.log(`${dropped} words did not fit`)
}}
/>Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| data | Record<string, any>[] | required | Data rows. |
| wordField | string | 'text' | Field holding the word. |
| weightField | string | 'value' | Field holding the numeric weight. |
| colorField | string | – | Field used to pick a palette color per category. |
| color | string \| string[] \| (item, index) => string | built-in palette | Single color, palette, or resolver function. |
| width / height | number | measured | Canvas size in px. |
| wordStyle | WordStyle | see below | Font and rotation options. |
| spiral | 'archimedean' \| 'rectangular' | 'archimedean' | Placement spiral. |
| scale | 'linear' \| 'sqrt' \| 'log' | 'sqrt' | Weight to font size mapping. |
| seed | number | 42 | Seed for the built-in deterministic PRNG. |
| random | () => number | – | Custom random generator in [0, 1). Overrides seed. |
| measureText | (text, fontSize, fontFamily?, fontWeight?) => number | built-in estimator | Custom text width measurement. |
| dropOverflow | boolean | true | Drop words that do not fit instead of forcing them at the edge. |
| backgroundColor | string | – | Fill behind the words. |
| style | ViewStyle | – | Container style. |
| onWordPress / onWordLongPress | (word: Word, index: number) => void | – | Press handlers. |
| onLayoutComplete | (words: Word[]) => void | – | Called after every layout computation. |
| renderWord | (word: Word, index: number) => ReactElement \| null | – | Custom renderer (must return react-native-svg elements). |
| testID | string | – | Forwarded to the container view. |
wordStyle
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| fontFamily | string | system | Any font available to the app. |
| fontWeight | 'normal' \| 'bold' \| '100'…'900' | 'normal' | |
| fontSize | number \| [min, max] | [12, 60] | Fixed size or range mapped from the weight. |
| rotation | number \| [min, max] | [0, 90] | Degrees. A single number disables random rotation. |
| rotationSteps | number | 2 | Discrete angles inside the rotation range. |
| rotateRatio | number | 0.5 | Share of words (0–1) that get rotated at all. |
| padding | number | 1 | Space in px around each word. |
Word
Every placed word carries:
| Field | Description |
| --- | --- |
| item | The original row from data. |
| text, value | Resolved word and weight. |
| x, y | Center of the word, relative to the canvas. |
| dy | Baseline offset from y that vertically centers the glyphs. |
| size | Font size in px. |
| rotate | Rotation in degrees. |
| color | Resolved color. |
| width, height | Estimated glyph box before rotation. |
| fontFamily, fontWeight | Copied from wordStyle. |
Layout without rendering
The layout engine is exported on its own. It is handy for tests, custom renderers, or computing a layout ahead of time.
import { layoutWordCloud, createRandom } from '@diamovi/react-native-word-cloud'
const words = layoutWordCloud({
width: 400,
height: 300,
data,
wordField: 'text',
weightField: 'value',
random: createRandom(7),
})
// words[0] -> { text: 'React Native', x: 200.8, y: 150.4, size: 60, rotate: 0, color: '#5B8FF9', ... }How the layout works
- Words are sorted by weight, largest first.
- Each weight is mapped to a font size with the chosen
scale. - Starting from the center, each word walks along the spiral until it finds a spot that does not overlap an already placed word and stays inside the canvas.
- A rotated word that does not fit is retried upright. If it still does not
fit, it is dropped (or pinned to the edge with
dropOverflow={false}).
Collision is done with the rotated bounding box of each word's glyphs, which keeps the layout fast enough to run on every render.
Text measurement and fonts
React Native has no synchronous text measurement API, so word widths are estimated from a per-character width table (Latin, accented Latin, CJK and emoji). Heights use the real glyph extent of each word, so lines pack tightly. Estimates are usually within a few percent of the rendered glyphs.
- If a custom font renders wider than expected, increase
wordStyle.padding. - For exact results, pass your own
measureText. - Load custom fonts the same way you would for any
Text: withexpo-fontor by linking them in the native projects. The example app shows bothuseFonts(works in Expo Go) and theexpo-fontconfig plugin.
Development
This repo uses bun. The example/ folder is an Expo app that
bundles the library source directly (see example/metro.config.js), so edits
under src/ reload live.
bun install # installs the library and the example (bun workspaces)
bun run typecheck
bun test
bun run build # emits lib/ (CommonJS + .d.ts)
bun run start # Metro for the example (dev client)
cd example && bun run start:go # Expo Go instead of a dev build
cd example && npx expo prebuild --platform ios --clean && bun run iosReleasing
CI runs typecheck, tests and build on every push to main and on pull
requests. Publishing is done by the Publish to npm workflow, either by
pushing a version tag or by running it manually with a version input:
git tag v0.1.0
git push origin v0.1.0The workflow sets the package version from the tag, publishes to npm and
creates a GitHub release with generated notes. It needs an NPM_TOKEN
repository secret with publish rights on the @diamovi scope.
License
MIT
