# Coordonnées et échelles

Modes : `lineacte` numérique ; `timeacte` temporel. Calendrier repris sans changement d’unité.

## API

```js
import { Scale, scaleCreate, dateToScalar, scalarToDate, dateExtent } from './src/index.js';

const scale = new Scale({ mode: 'lineacte', deb: 0, fin: 100, width: 1000 });
scale.project(25);                    // 250
scale.unproject(250);                 // 25
scale.project(-10);                   // -100 : extrapolation
scale.project(-10, { clamp: true });  // 0
```

`scaleCreate(options)` équivaut à `new Scale(options)`. L’échelle est immuable ; créer une nouvelle échelle après un changement de largeur ou de fenêtre. `width` est une largeur logique strictement positive, généralement en pixels. Les positions vont de 0 à `width`, de gauche à droite. Aucune mesure DOM n’est effectuée.

| API | Résultat |
| --- | --- |
| `new Scale({mode, deb, fin, width, partial})` | Échelle ; mode par défaut `lineacte` |
| `scale.bounds` | Bornes originales, copiées et gelées |
| `scale.scalarBounds` | Bornes converties : Number ou BigInt |
| `scale.toScalar(value)` | Conversion d’une coordonnée selon le mode |
| `scale.project(value, {clamp = false})` | Coordonnée → position X |
| `scale.projectScalar(value, {clamp = false})` | Scalaire déjà converti → X |
| `scale.unproject(x, {clamp = false})` | X → nombre ou objet date complet |
| `scale.projectInterval({deb, fin}, options)` | Géométrie complète et intersection avec la fenêtre |
| `scale.ticks(options)` | Graduations `{value, x, label, unit, align}` |

La fenêtre exige deux bornes renseignées et un intervalle strictement positif après conversion. Un cadre ponctuel accepté par le noyau peut exister, mais `.scale()` refuse sa projection. Largeur nulle, coordonnée non finie, fenêtre inversée ou calcul non représentable déclenchent une erreur explicite.

## Intégration au noyau

```js
const globale = cadre.scale(1000);
const locale = sceneInstance.scale(1000);
const geometrie = acteInstance.project(1000);
```

Une scène sans fenêtre propre suit celle du cadre. Une scène attachée avec `deb` et `fin` possède son échelle propre. L’acte utilise l’échelle de son instance de scène. Les calculs ne modifient ni les sources, ni les attachements, ni les propriétés déclarées.

`cadre.scale(width, {partial})` et `sceneInstance.scale(width, {partial})` acceptent la politique des dates partielles. `acteInstance.project(width, {partial, open_end})` accepte aussi la politique des fins inconnues. Pour projeter beaucoup d’actes, réutiliser une échelle et appeler `scale.projectInterval(acte.source.data)`.

## Calendrier, année zéro et temps profond

Le calendrier est **grégorien prolongé**, avec numérotation astronomique : −2, −1, 0, 1, 2. L’année zéro existe et est bissextile. L’origine interne est le 1er janvier de l’année zéro, à minuit.

La valeur scalaire est un entier de millisecondes en **BigInt**. Les années sont des entiers JavaScript sûrs, de `Number.MIN_SAFE_INTEGER` à `Number.MAX_SAFE_INTEGER`. Les autres champs suivent les limites du noyau. Les calculs ne passent ni par `Date`, ni par un fuseau horaire.

```js
dateToScalar({ an: 0 }); // 0n
dateToScalar({ an: 1 }); // 31622400000n : 366 jours
scalarToDate(0n);
// {an: 0, mois: 1, jour: 1, heure: 0, minute: 0, seconde: 0, milliseconde: 0}
```

Les mois et années ont leurs durées calendaires réelles. La projection est proportionnelle à la durée ; une année bissextile a une longueur légèrement différente. L’année zéro occupe sa place complète entre −1 et +1.

La soustraction des dates se fait en BigInt avant conversion du rapport en Number. Un zoom local à la milliseconde est ainsi possible autour de −4,6 milliards d’années. Les pixels restent des nombres flottants : une vue couvrant des milliards d’années ne peut pas distinguer toutes les millisecondes. `unproject(project(date))` n’est donc pas garanti exact à la milliseconde sur une telle étendue. La conversion directe date → scalaire → date, elle, est exacte.

`unproject()` arrondit à la milliseconde la plus proche, vers le futur en cas d’égalité. Il retourne une date complète, sans prétendre restituer la précision initiale. Les BigInt ne sont pas directement sérialisables par `JSON.stringify` : les exporter sous forme de chaînes décimales si nécessaire, en conservant leur type dans le format de l’application.

Ce calendrier est une convention géométrique. Il ne convertit pas automatiquement les dates historiques juliennes, ne gère pas les secondes intercalaires et ne reconstruit pas un calendrier local ancien.

## Dates partielles

Par défaut, `partial: 'start'` ancre une date au **début de sa précision déclarée** :

| Donnée | Ancrage |
| --- | --- |
| `{an: 1791}` | Début de l’année 1791 |
| `{an: 1791, mois: 9}` | Début de septembre 1791 |
| `{an: 1791, mois: 9, jour: 3}` | Début du jour indiqué |

Cet ancrage ne devient pas une affirmation historique. Aucun champ n’est ajouté à la source. La même règle s’applique aux deux bornes : une fin `{an: 1800}` est ancrée au début de 1800, sans extension implicite à la fin de l’année.

```js
dateExtent({ an: 2020, mois: 2 });
// {start: BigInt, end: BigInt, precision: 'mois'}
// [start, end) couvre les 29 jours de février 2020.
```

`end` est exclusif. Une date précise à la milliseconde couvre une milliseconde. Cette étendue permettra de représenter l’incertitude ; elle n’est pas appliquée automatiquement aux actes.

Avec `partial: 'reject'`, tous les champs jusqu’à `milliseconde` sont obligatoires. Une paire de dates partielles permise structurellement par le noyau peut devenir inversée ou de largeur nulle après ancrage ; la projection refuse ce résultat.

## Intervalles et fins inconnues

```js
const scale = new Scale({ deb: 10, fin: 20, width: 100 });
scale.projectInterval({ deb: 5, fin: 25 });
// {
//   x: -50, x_fin: 150, width: 200,
//   visible: true, clipped: {x: 0, x_fin: 100, width: 100},
//   point: false, open_end: false
// }
```

`x`, `x_fin`, `width` décrivent l’intervalle complet. `clipped` décrit son intersection avec la fenêtre, ou vaut `null` hors champ. `visible` désigne une intersection géométrique, pas une visibilité décidée par un rideau. Les bords sont inclus pour ce calcul, indépendamment des conventions métier conservées dans les données.

Si `fin` est omis, l’intervalle est ponctuel. Si les deux bornes se projettent au même instant, `point` vaut `true` et la largeur vaut zéro. Le template peut lui donner une forme visible, sans inventer de durée.

Une fin `null` signifie inconnue. Trois politiques :

| `open_end` | Résultat |
| --- | --- |
| `'unknown'` — défaut | X du début ; `x_fin`, `width`, `visible`, `clipped` à `null` |
| `'window'` | Extension de représentation jusqu’au bord droit ; `open_end: true` distingue cette extension d’une fin connue |
| `'error'` | Refus de projeter une borne inconnue |

`'window'` ne signifie ni « en cours aujourd’hui » ni « fin à cette date ». Si le début est après la fenêtre, il n’y a pas d’intersection et la largeur vaut `null`. La source conserve toujours `fin: null`.

## Graduations et centrage

```js
scale.ticks(); // automatique : min_spacing = 90, max_ticks = 1000
scale.ticks({ interval: 1000 }); // numérique
temporalScale.ticks({ interval: { unit: 'month', step: 3 } });
```

Les graduations sont placées sur des multiples réguliers, sans ajout artificiel aux bornes de la fenêtre. Les graduations hors fenêtre sont exclues. Chaque graduation contient par exemple :

```js
{ value: 1000, x: 250, label: '1 000', unit: 'number', align: 'center' }
```

**X représente le centre du texte**, aligné sur la valeur. Le rendu applique cette indication, notamment avec une translation horizontale de −50 %. Aucune largeur de texte n’est mesurée ici.

Mode numérique : intervalle positif imposé ou choix automatique de type 1, 2, 5 × puissance de dix. Les multiples décimaux sont calculés avant conversion en Number, pour éviter les résidus usuels et ne pas perdre la graduation 0.3 avec un pas de 0.1.

Mode chronologique : unités `millisecond`, `second`, `minute`, `hour`, `day`, `month`, `year`, avec `step` entier positif. Les mois et années suivent le calendrier et leur largeur réelle. Les pas fixes d’heures/jours s’alignent sur l’origine de l’année zéro : un pas de 7 jours n’est pas une semaine ISO. Les années s’alignent sur leurs multiples incluant zéro, les mois sur leur indice depuis janvier de l’année zéro.

Le choix automatique dépend de la durée et de la largeur : millisecondes, secondes, minutes, heures, jours, mois, années puis grands multiples d’années. Les libellés français indiquent la date pour éviter les ambiguïtés lors des changements de jour/année. Pour les grands pas annuels, `Ma` et `Ga` indiquent ici une coordonnée annuelle en millions/milliards, avec son signe, et non un âge « avant le présent ».

`min_spacing` est une cible de densité, pas une garantie anti-chevauchement : les mois et les textes ont des largeurs variables. Le rendu complète ce choix par ses mesures. `max_ticks` accepte 2 à 10 000. Une demande dépassant la limite déclenche une erreur explicite. Les intervalles et indices impossibles à représenter sont refusés, sans boucle illimitée ni troncature silencieuse.

## Vérification

Voir [validation.md](validation.md). Les tests couvrent des fenêtres numériques et calendaires ciblées.
