react-odontogram-chart
v0.1.1
Published
Standard FDI (ISO 3950) interactive odontogram chart component with clinical dental tools for dental EHR systems.
Maintainers
Readme
react-odontogram-chart
Standard FDI (ISO 3950 / Permenkes RI) interactive odontogram chart component and clinical tools for dental EHR (Rekam Medis Gigi) systems, built with React and TypeScript.
![]()
Table of Contents
- Features
- Installation
- Quick Start
- Core Concepts
- Component API
- Types Reference
- Constants & Utilities
- Usage Examples
- Styling
- Local Demo
- Publishing to npmjs
- FAQ
- License
Features
- Full FDI (ISO 3950 / Permenkes RI) notation for 32 permanent + 20 primary teeth.
- Interactive surface-level annotation — 5-surface layout for molars/premolars, 4-surface for incisors/canines.
- Clinical tools: Caries, Amalgam, Composite, GIC/Sealant, Temporary, Missing, Un-erupted/Impacted, Crown, Root Canal (PSA), Fracture, Implant, Radix, Bridge, Reset.
- Full-tooth condition symbols (X, blue circle, crown frame, zigzag, anchor, triangle, mobility badge, …).
- Per-tooth details drawer: surface editor, full condition, mobility degree (M1–M3), clinical notes, diagnosis code.
- Read-only mode for printing/verification.
- Self-contained CSS — no Tailwind setup required.
- TypeScript types included.
Installation
npm install react-odontogram-chart lucide-reactreact, react-dom, and lucide-react are peer dependencies. For React 19:
npm install react-odontogram-chart react react-dom lucide-reactQuick Start
import { useState } from 'react';
import 'react-odontogram-chart/style.css';
import {
OdontogramChart,
ToolPalette,
ToothDetailsDrawer,
DENTAL_TOOLS,
createEmptyOdontogramData,
} from 'react-odontogram-chart';
import type { SurfaceKey, ToothNumber } from 'react-odontogram-chart';
export default function App() {
const [data, setData] = useState(() => createEmptyOdontogramData());
const [selectedToolId, setSelectedToolId] = useState('caries');
const [selectedTooth, setSelectedTooth] = useState<ToothNumber | null>(null);
const activeTool = DENTAL_TOOLS.find((t) => t.id === selectedToolId) ?? DENTAL_TOOLS[0];
const handleSurfaceClick = (toothNumber: ToothNumber, surface: SurfaceKey) => {
setSelectedTooth(toothNumber);
if (activeTool.category === 'surface' && activeTool.surfaceCondition) {
setData((prev) => ({
...prev,
teeth: {
...prev.teeth,
[toothNumber]: {
...prev.teeth[toothNumber],
surfaces: { ...prev.teeth[toothNumber].surfaces, [surface]: activeTool.surfaceCondition },
},
},
}));
}
};
const handleToothClick = (toothNumber: ToothNumber) => {
setSelectedTooth(toothNumber);
if (activeTool.category === 'tooth' && activeTool.fullCondition) {
setData((prev) => ({
...prev,
teeth: {
...prev.teeth,
[toothNumber]: { ...prev.teeth[toothNumber], fullCondition: activeTool.fullCondition },
},
}));
}
};
return (
<div style={{ padding: 16 }}>
<ToolPalette
selectedToolId={selectedToolId}
onSelectTool={(tool) => setSelectedToolId(tool.id)}
onQuickReset={() => setData(createEmptyOdontogramData())}
/>
<OdontogramChart
teeth={data.teeth}
selectedTooth={selectedTooth}
activeToolCategory={activeTool.category}
onSurfaceClick={handleSurfaceClick}
onToothClick={handleToothClick}
/>
<ToothDetailsDrawer
toothNumber={selectedTooth}
state={selectedTooth ? data.teeth[selectedTooth] : null}
onClose={() => setSelectedTooth(null)}
onUpdateTooth={(updated) =>
setData((prev) => ({
...prev,
teeth: { ...prev.teeth, [updated.toothNumber]: updated },
}))
}
onClearTooth={(toothNumber) => setData(createEmptyOdontogramData())}
/>
</div>
);
}Core Concepts
Data Model
The whole chart is described by the OdontogramData object. Keep it in your state and pass data.teeth to the chart.
interface OdontogramData {
patient: PatientInfo; // name, recordNumber, date, dentistName, clinicName, notes
teeth: Record<ToothNumber, ToothState>; // one entry per tooth (52 teeth)
bridges: BridgeConnection[]; // bridge links between teeth
updatedAt: string; // ISO timestamp
}
interface ToothState {
toothNumber: ToothNumber;
surfaces: Record<SurfaceKey, SurfaceCondition>; // per-surface condition
fullCondition: FullToothCondition; // whole-tooth condition
customColor?: string;
notes?: string; // clinical notes
mobilityDegree?: 1 | 2 | 3; // M1, M2, M3
diagnosisCode?: string; // e.g. 'K02.1'
}createEmptyOdontogramData() returns a full, valid chart with every tooth sound and the patient fields pre-filled — always use it as the starting point.
Dental Tools
DENTAL_TOOLS is an array of DentalTool. Each tool belongs to one of three categories:
| Category | Behavior when clicked | Example tools |
| --- | --- | --- |
| surface | Applies to the clicked surface polygon | Caries, Amalgam, Composite, GIC, Temporary, Reset |
| tooth | Applies to the whole tooth | Missing (X), Unerupted, Crown, Root Canal, Fracture, Implant, Radix |
| bridge | Reserved for bridge connections | Bridge Connector |
Every surface tool has a surfaceCondition, every tooth tool has a fullCondition. Wire these values into your state updater, exactly as in the Quick Start.
Component API
OdontogramChart
Renders the 4-row FDI chart (upper permanent, upper primary, lower primary, lower permanent).
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| teeth | Record<ToothNumber, ToothState> | — | Required. Tooth state map from OdontogramData.teeth. |
| selectedTooth | ToothNumber \| null | null | Currently selected tooth (highlighted). |
| activeToolCategory | 'surface' \| 'tooth' \| 'bridge' | 'surface' | Decides whether a click applies a surface condition or a whole-tooth condition. |
| onSurfaceClick | (toothNumber, surface) => void | — | Required. Fired on surface polygon click. |
| onToothClick | (toothNumber) => void | — | Required. Fired when a tooth is clicked. |
| readOnly | boolean | false | Disables all editing interactions. |
| boxSize | number | 40 | Tooth box size in px (e.g. 56 for larger touch targets). |
| surfaceLayoutMode | '5_surfaces' \| '4_surfaces' \| 'auto' | '5_surfaces' | auto uses 4 surfaces for anterior teeth (1, 2, 3) and 5 for the rest. |
| surfaceLabelType | 'none' \| 'indonesian' \| 'fdi' | 'none' | Surface label display. |
ToolPalette
The clinical tools grid with icons, colors, codes and an active-tool instruction bar.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| selectedToolId | string | — | Id of the active tool (one of DENTAL_TOOLS[].id). |
| onSelectTool | (tool: DentalTool) => void | — | Called when a tool button is pressed. |
| onQuickReset | () => void | — | Called when the "Bersihkan Semua" reset action is pressed. |
ToothDetailsDrawer
A full editor panel for one tooth (surface conditions, full condition, mobility, notes). Renders null when toothNumber or state is null.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| toothNumber | ToothNumber \| null | — | Tooth to edit. |
| state | ToothState \| null | — | Current state of that tooth. |
| onClose | () => void | — | Close handler. |
| onUpdateTooth | (state: ToothState) => void | — | Fired with the updated ToothState on every change. |
| onClearTooth | (toothNumber) => void | — | Fired to reset one tooth to sound. |
ToothBox
Lower-level component rendering a single tooth SVG (used internally by OdontogramChart). Useful if you need a custom grid.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| toothNumber | ToothNumber | — | FDI tooth number. |
| state | ToothState | — | Tooth state. |
| activeToolCategory | 'surface' \| 'tooth' \| 'bridge' | 'surface' | Click behavior category. |
| isSelected | boolean | false | Highlight the tooth. |
| onSurfaceClick | (toothNumber, surface) => void | — | Surface click handler. |
| onToothClick | (toothNumber) => void | — | Tooth click handler. |
| readOnly | boolean | false | Disable editing. |
| boxSize | number | 42 | Box size in px. |
| showToothNumberPosition | 'top' \| 'bottom' | 'top' | Number label position (top for upper arch, bottom for lower). |
| surfaceLayoutMode | '5_surfaces' \| '4_surfaces' \| 'auto' | '5_surfaces' | Surface layout. |
| surfaceLabelType | 'none' \| 'indonesian' \| 'fdi' | 'none' | Surface labels. |
Types Reference
All exported from the package root:
type ToothNumber // union of all FDI numbers (permanent + primary)
type PermanentToothNumber // 18..11, 21..28, 48..41, 31..38
type PrimaryToothNumber // 55..51, 61..65, 85..81, 71..75
type SurfaceKey = 'occlusal' | 'buccal' | 'lingual' | 'mesial' | 'distal'
type SurfaceLayoutMode = '5_surfaces' | '4_surfaces' | 'auto'
type SurfaceLabelType = 'none' | 'indonesian' | 'fdi'
type SurfaceCondition =
| 'sound' | 'caries' | 'amalgam' | 'composite'
| 'gic' | 'temporary' | 'abrasion' | 'non_carious'
type FullToothCondition =
| 'sound' | 'missing' | 'unerupted' | 'impacted' | 'crown'
| 'bridge_abutment' | 'bridge_pontic' | 'root_canal' | 'implant'
| 'fracture' | 'radix' | 'calculus' | 'veneer' | 'mobility'
interface ToothState
interface PatientInfo
interface BridgeConnection
interface OdontogramData
interface DentalToolConstants & Utilities
| Export | Description |
| --- | --- |
| DENTAL_TOOLS: DentalTool[] | Full tools definition (id, name, color, icon, category, conditions, code). |
| ALL_TEETH_META: Record<ToothNumber, ToothMeta> | Per-tooth metadata: quadrant, nameEn, nameId, type, isPrimary, leftIsMesial. |
| ALL_TEETH_NUMBERS: ToothNumber[] | All 52 FDI numbers in chart order. |
| ROW_1_UPPER_PERMANENT, ROW_2_UPPER_PRIMARY, ROW_3_LOWER_PRIMARY, ROW_4_LOWER_PERMANENT | Arch row arrays. |
| createEmptyOdontogramData(): OdontogramData | Blank sound chart with default patient info. |
| PRESET_ODONTOGRAMS: PresetOption[] | empty, typical_adult, pediatric_mixed templates. |
| PresetOption | { id, title, description, getData: () => OdontogramData } |
Usage Examples
1. Read-Only Chart
For printing, PDF export, or viewing a saved record — just set readOnly:
<OdontogramChart
teeth={savedData.teeth}
selectedTooth={null}
activeToolCategory="surface"
onSurfaceClick={() => {}}
onToothClick={() => {}}
readOnly
/>2. Load a Preset
import { PRESET_ODONTOGRAMS, createEmptyOdontogramData } from 'react-odontogram-chart';
const [data, setData] = useState(() => createEmptyOdontogramData());
function loadPreset(id: string) {
const preset = PRESET_ODONTOGRAMS.find((p) => p.id === id);
if (preset) setData(preset.getData());
}
// loadPreset('typical_adult') // karies, amalgam, crown, missing, bridge, impaksi
// loadPreset('pediatric_mixed') // gigi campuran anak
// loadPreset('empty') // chart kosong3. JSON Serialization (Save & Load)
The entire chart is plain JSON — persist it to your backend or localStorage:
// Save
const handleSave = () => {
localStorage.setItem('odontogram', JSON.stringify(data));
};
// Load (validate + fallback)
const loadSaved = (): OdontogramData => {
try {
const raw = localStorage.getItem('odontogram');
if (!raw) return createEmptyOdontogramData();
const parsed = JSON.parse(raw);
// spread over an empty chart so every tooth always exists
const base = createEmptyOdontogramData();
return {
...base,
...parsed,
teeth: { ...base.teeth, ...parsed.teeth },
};
} catch {
return createEmptyOdontogramData();
}
};4. Annotate Programmatically
Set conditions directly without user interaction:
const markCaries = (tooth: ToothNumber, surface: SurfaceKey) => {
setData((prev) => ({
...prev,
teeth: {
...prev.teeth,
[tooth]: {
...prev.teeth[tooth],
surfaces: { ...prev.teeth[tooth].surfaces, [surface]: 'caries' },
notes: 'Ditemukan karies saat skrining.',
},
},
}));
};5. Full Feature Demo
A self-contained playground (tool palette + chart + details drawer) lives in the repository. Run it with:
npm install
npm run devStyling
The package bundles a standalone CSS file (dist/style.css) compiled from Tailwind. No Tailwind setup is needed — import the stylesheet once:
// main.tsx / index.tsx
import 'react-odontogram-chart/style.css';If your app already uses Tailwind v4, skip the CSS import and let your own Tailwind compile the library's classes so you stay on a single stylesheet:
@import "tailwindcss";
@source "../../node_modules/react-odontogram-chart/dist";Dark mode is supported out of the box via prefers-color-scheme.
Local Demo
npm install
npm run dev # open http://localhost:3000Publishing to npmjs
npm run build:lib
npm pack --dry-run # preview the tarball contents
npm login
npm publish # auto-runs the build via prepublishOnlyFAQ
Is Tailwind required? No. The styles are precompiled into style.css and auto-published. Only import the CSS file.
Which React versions are supported? React 18 and 19.
How do I reset a single tooth? Call onClearTooth on the drawer, or spread a sound ToothState into data.teeth[number].
How do I save/load a patient's chart? The data is plain JSON — see Example 3.
The chart is too small / too big. Use the boxSize prop (28–64 px works well).
License
MIT
