npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-odontogram-chart

v0.1.1

Published

Standard FDI (ISO 3950) interactive odontogram chart component with clinical dental tools for dental EHR systems.

Readme

react-odontogram-chart

npm version npm downloads license

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.

FDI Odontogram

Table of Contents

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-react

react, react-dom, and lucide-react are peer dependencies. For React 19:

npm install react-odontogram-chart react react-dom lucide-react

Quick 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 DentalTool

Constants & 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 kosong

3. 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 dev

Styling

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:3000

Publishing to npmjs

npm run build:lib
npm pack --dry-run   # preview the tarball contents
npm login
npm publish          # auto-runs the build via prepublishOnly

FAQ

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 (2864 px works well).

License

MIT