Extending DecoJS - matej-hron/decojs GitHub Wiki

Practical how-tos for common modifications. All file paths are relative to the repository root.

Before-commit checklist

Per CLAUDE.md root of repo:

  1. npm test — all 208 tests must pass.
  2. Bump the version badge: edit css/styles.css, find .version-number::after, update the content string.

Adding a new gas mix

Gases are defined in js/diveSetup.js. Bottom gases go in BOTTOM_GASES (line 40), deco gases in DECO_GASES (line 50):

// js/diveSetup.js:40
export const BOTTOM_GASES = [
    { id: 'air',   name: 'Air',   o2: 0.2098, n2: 0.7902, he: 0 },
    { id: 'ean32', name: 'EAN32', o2: 0.32,   n2: 0.68,   he: 0 },
    // add here:
    { id: 'tx1845', name: 'Trimix 18/45', o2: 0.18, n2: 0.37, he: 0.45 },
];

MOD is derived automatically by calculateMOD() (diveSetup.js:972) whenever the gas is used. Exact integer-metre boundaries are preserved within floating-point tolerance; other values round down conservatively. The he fraction is tracked in the gas record but the decompression algorithm lumps it into the inert-gas calculation via the gas's effective N₂-equivalent; separate helium kinetics are not implemented (see Validation-and-Testing).

The DiveSetupEditor picks new gases up automatically — no UI change is needed.

Switching ZH-L16 variant at runtime

import { setZHL16Variant, ZHL16_VARIANTS } from './js/tissueCompartments.js';

setZHL16Variant(ZHL16_VARIANTS.A); // or .B, or .C (default)

setZHL16Variant() (tissueCompartments.js:182) rebuilds the exported COMPARTMENTS array in place — clearing the existing array and repushing entries. Downstream modules that imported COMPARTMENTS by reference (e.g. decoModel.js, every chart) pick up the change automatically; no reimport is needed.

Recalculate any cached tissue-loading results after switching, since compartment parameters have changed. The three chart classes accept a fresh update(diveSetup) call.

Variant letter meaning: A = original experimental; B = printed tables (moderate conservatism); C = dive computers (most conservative). See Model-01-Compartments.

Adding a new quiz

Per CLAUDE.md:

  1. Create data/quiz-{name}.json.
  2. Create quiz-{name}.html (copy an existing quiz page as template).
  3. Add an entry to the Tests submenu in NAV_ITEMS in js/nav.js.
  4. Add a topic tile to index.html.
  5. Bump the version (see top of this page).

JSON shape:

{
  "title": "Quiz Title",
  "description": "Description",
  "questions": [
    {
      "id": 1,
      "category": "category-slug",
      "question": "Question text?",
      "options": [
        { "key": "a", "text": "Option A" },
        { "key": "b", "text": "Option B" }
      ],
      "correct": "a",
      "explanation": "Why A is correct…"
    }
  ]
}

Quizzes currently use Czech with proper diacritics (háčky, čárky) — matching the SPČR / CMAS exam source. The generic engine in js/quiz.js handles shuffling, category filtering, and scoring with no per-quiz code.

Adding a chart overlay

The cleanest existing example is the tissue-compartment overlay in DiveProfileChart, which plots a dashed line per compartment:

// js/charts/DiveProfileChart.js:949-996
if (this.options.showTissueLoading) {
    COMPARTMENTS.forEach(comp => {
        if (!this.visibleCompartments.has(comp.id)) return;
        const pressureData = results.compartments[comp.id].pressures;
        datasets.push({
            label: `TC${comp.id} (${comp.halfTime}min)`,
            data: results.timePoints.map((t, i) => ({ x: t, y: pressureData[i] })),
            borderColor: comp.color,
            yAxisID: 'yPressure',
            pointRadius: 0,
            borderWidth: 1.5,
            order: 20
        });
    });
}

Pattern to follow:

  1. Add a flag to options in chartTypes.js (DEFAULT_DIVE_PROFILE_OPTIONS, line 156).
  2. In the chart's _render method, gate the new dataset(s) on this.options.yourFlag.
  3. Pull the data from the results object produced by calculateTissueLoading() — its shape is {timePoints, depthPoints, ambientPressures, compartments: {1:{pressures:[]},…}, n2Fractions}.
  4. Assign yAxisID to an existing axis (yDepth / yPressure) or declare a new axis in the Chart.js config.
  5. Use the order field to control z-layering (lower order draws on top).

The chart re-renders on every update(diveSetup) call, so there is no need to mutate Chart.js datasets incrementally.

Adding an i18n locale

  1. Create locales/<lang>.json. Copy locales/en.json as a template and translate values.

  2. Register the code in js/i18n.js at line 16:

    // js/i18n.js:16
    const SUPPORTED_LANGS = ['en', 'cs', 'es', 'de']; // added 'de'
  3. Bump the version.

The language switcher (createLanguageSwitcher in i18n.js) reads from SUPPORTED_LANGS, so the new language appears automatically. Components that listen for the global languagechange event (nav, charts, DiveSetupEditor) re-render with the new strings without a page reload.

Translation keys use dot-notation (e.g. chart.profile.datasetDepth) and the translate(key, fallback) / translate(key, vars) function in i18n.js interpolates {0}, {1} placeholders.

Changing the deco loop discretisation

generateDecoSchedule() in js/decoModel.js (line 899) accepts two discretisation options:

  • stopIncrement — vertical stop grid in metres. Default 3 (standard Bühlmann / decotengu convention).
  • timeIncrement — stop-time quantum in minutes. Default 1.

For continuous-mode rendering (used on the theory pages to draw a smooth GF ramp animation rather than stepped stops):

// continuous deco
generateDecoSchedule(tissues, depth, n2, gfLow, gfHigh, gases, {
    stopIncrement: 0.1,   // metres — finer than the gradient-factor ramp slope
    timeIncrement: 0.1,   // minutes (6 s)
});

Continuous mode enforces MIN_STOP_TIME = 2 min per recorded stop (deco/schedule.js:172) so the output is not drowned in micro-stops. The deco-time cap DECO_STOP_MAX_MINUTES = 300 still applies.

Also tunable on the same call: ascentRate (default 10 m/min), gasSwitchTime (default 0 — minutes held at switch depth), maxPpO2 (default 1.6 — deco-gas MOD ceiling).

When extending profile generation, keep configured deco gases on the generateDecoSchedule path even for NDL dives. The generated gasId waypoints are the shared source for the profile chart, tissue-loading chart, P–P chart, GF chart, and decision audit; bypassing the scheduler would make those surfaces disagree about the breathing gas.

Further reading

⚠️ **GitHub.com Fallback** ⚠️