Skip to content

Instantly share code, notes, and snippets.

@np
Created November 22, 2025 22:41
Show Gist options
  • Select an option

  • Save np/ca563b2107e3e0e873b65943cc314dd1 to your computer and use it in GitHub Desktop.

Select an option

Save np/ca563b2107e3e0e873b65943cc314dd1 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Modèles de tarification – plateau central et comparaison</title>
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
margin: 0;
padding: 1rem 2rem;
background: #111;
color: #eee;
}
h1 {
margin-top: 0;
font-size: 1.4rem;
}
.container {
display: flex;
flex-direction: row;
gap: 2rem;
align-items: flex-start;
}
.controls {
min-width: 260px;
max-width: 320px;
background: #1a1a1a;
padding: 1rem;
border-radius: 8px;
border: 1px solid #333;
font-size: 0.9rem;
}
.controls h2 {
font-size: 1rem;
margin-top: 0;
}
.control-group {
margin-bottom: 0.8rem;
}
.control-group label {
display: block;
margin-bottom: 0.2rem;
}
.control-group input[type="range"] {
width: 100%;
}
.value {
font-family: monospace;
font-size: 0.85rem;
color: #ccc;
}
canvas {
background: #000;
border-radius: 8px;
border: 1px solid #333;
}
.legend {
margin-top: 0.5rem;
font-size: 0.85rem;
display: flex;
flex-wrap: wrap;
gap: 2rem;
max-width: 920px;
}
.legend-block {
min-width: 260px;
}
.legend-title {
font-weight: 600;
margin-bottom: 0.3rem;
}
.legend-item {
display: flex;
align-items: flex-start;
margin-bottom: 0.35rem;
}
.legend-color {
width: 12px;
height: 12px;
margin-right: 0.4rem;
margin-top: 0.15rem;
border-radius: 2px;
flex-shrink: 0;
}
.legend-text {
line-height: 1.3;
}
.legend-formula {
font-family: monospace;
font-size: 0.8rem;
color: #ccc;
display: block;
}
.note {
margin-top: 1rem;
font-size: 0.85rem;
color: #aaa;
max-width: 900px;
}
</style>
</head>
<body>
<h1>Modèles de tarification (QF → tarif) : plateau central vs courbes actuelles</h1>
<div class="container">
<div>
<canvas id="chart" width="900" height="500"></canvas>
<div class="legend">
<div class="legend-block">
<div class="legend-title">Modèles “nouvelle grille”</div>
<div class="legend-item">
<span class="legend-color" style="background:#00E5FF;"></span>
<div class="legend-text">
Plateau double-logistique (milieu = plateau)
<span class="legend-formula">
mid = (Lmin + Lmax)/2, x₁ = QF₀ − W, x₂ = QF₀ + W
</span>
<span class="legend-formula">
f(q) = Lmin + (mid − Lmin)·σ(k(q − x₁)) + (Lmax − mid)·σ(k(q − x₂))
</span>
<span class="legend-formula">
σ(t) = 1 / (1 + exp(−t))
</span>
</div>
</div>
</div>
<div class="legend-block">
<div class="legend-title">Tarification actuelle (par enfant, selon fratrie)</div>
<div class="legend-item">
<span class="legend-color" style="background:#B0BEC5;"></span>
<div class="legend-text">
Actuel – 1 enfant (c = 0,35)
<span class="legend-formula">
T₁(q) = clamp(40 + 0,35 · q, Lmin, Lmax)
</span>
</div>
</div>
<div class="legend-item">
<span class="legend-color" style="background:#90CAF9;"></span>
<div class="legend-text">
Actuel – 2 enfants (c = 0,33)
<span class="legend-formula">
T₂(q) = clamp(40 + 0,33 · q, Lmin, Lmax)
</span>
</div>
</div>
<div class="legend-item">
<span class="legend-color" style="background:#CE93D8;"></span>
<div class="legend-text">
Actuel – 3 enfants (c = 0,31)
<span class="legend-formula">
T₃(q) = clamp(40 + 0,31 · q, Lmin, Lmax)
</span>
</div>
</div>
<div class="legend-item">
<span class="legend-color" style="background:#FFAB91;"></span>
<div class="legend-text">
Actuel – 4 enfants et plus (c = 0,29)
<span class="legend-formula">
T₄(q) = clamp(40 + 0,29 · q, Lmin, Lmax)
</span>
</div>
</div>
<div class="legend-item">
<div class="legend-text">
<span class="legend-formula">
clamp(x, Lmin, Lmax) = min(max(x, Lmin), Lmax)
</span>
</div>
</div>
</div>
</div>
<div class="note">
Le modèle “plateau double-logistique” produit une zone centrale presque plate autour du tarif médian (mid),
avec deux transitions lissées vers le plancher (QF bas) et vers le plafond (QF élevés).
Lmin et Lmax contrôlent à la fois les nouveaux modèles et les courbes actuelles (plancher/plafond).
La majoration maternelle (+20&nbsp;€ pour 3–6 ans) et l’adhésion annuelle ne sont pas représentées ici.
</div>
</div>
<div class="controls">
<h2>Paramètres</h2>
<div class="control-group">
<label>Plancher (Lmin)</label>
<input type="range" id="Lmin" min="200" max="350" step="5" value="250" />
<div class="value">Lmin = <span id="LminVal">250</span> €</div>
</div>
<div class="control-group">
<label>Plafond (Lmax)</label>
<input type="range" id="Lmax" min="500" max="800" step="5" value="600" />
<div class="value">Lmax = <span id="LmaxVal">600</span> €</div>
</div>
<div class="control-group">
<label>Pivot (QF₀, centre du plateau / du S)</label>
<input type="range" id="Q0" min="600" max="2000" step="50" value="1250" />
<div class="value">QF₀ = <span id="Q0Val">1250</span></div>
</div>
<div class="control-group">
<label>Pente (k – raideur des transitions)</label>
<input type="range" id="k" min="1" max="30" step="1" value="15" />
<div class="value">k = <span id="kVal">0.015</span></div>
</div>
<div class="control-group">
<label>Largeur du plateau (W, en QF)</label>
<input type="range" id="W" min="50" max="800" step="50" value="500" />
<div class="value">W = <span id="WVal">500</span></div>
</div>
<div class="control-group">
<label>QF minimum / maximum affichés</label>
<input type="range" id="qMin" min="0" max="1000" step="50" value="300" />
<input type="range" id="qMax" min="1000" max="4000" step="50" value="2300" />
<div class="value">
QF ∈ [<span id="qMinVal">300</span> ; <span id="qMaxVal">2300</span>]
</div>
</div>
<div class="note">
Usage typique :
<ul>
<li>Fixer Lmin = 250, Lmax = 600 pour coller à la grille actuelle.</li>
<li>Régler QF₀ autour de 900 pour un plateau centré sur les revenus médians.</li>
<li>Augmenter W pour élargir le plateau où presque tout le monde paie ~mid.</li>
<li>Jouer sur k pour avoir des transitions plus ou moins abruptes sur les extrémités.</li>
</ul>
</div>
</div>
</div>
<script>
const canvas = document.getElementById("chart");
const ctx = canvas.getContext("2d");
const LminInput = document.getElementById("Lmin");
const LmaxInput = document.getElementById("Lmax");
const Q0Input = document.getElementById("Q0");
const kInput = document.getElementById("k");
const WInput = document.getElementById("W");
const qMinInput = document.getElementById("qMin");
const qMaxInput = document.getElementById("qMax");
const LminVal = document.getElementById("LminVal");
const LmaxVal = document.getElementById("LmaxVal");
const Q0Val = document.getElementById("Q0Val");
const kVal = document.getElementById("kVal");
const WVal = document.getElementById("WVal");
const qMinVal = document.getElementById("qMinVal");
const qMaxVal = document.getElementById("qMaxVal");
function sigma(t) {
return 1 / (1 + Math.exp(-t));
}
function logistic(q, Lmin, Lmax, q0, k) {
return Lmin + (Lmax - Lmin) / (1 + Math.exp(-k * (q - q0)));
}
function logisticGen(q, Lmin, Lmax, q0, k, v) {
return Lmin + (Lmax - Lmin) / Math.pow(1 + Math.exp(-k * (q - q0)), v);
}
function arctanF(q, Lmin, Lmax, q0, k) {
return Lmin + (Lmax - Lmin) / Math.PI * (Math.atan(k * (q - q0)) + Math.PI / 2);
}
function tanhF(q, Lmin, Lmax, q0, k) {
const t = k * (q - q0);
const tanh = (2 / (1 + Math.exp(-2 * t))) - 1;
return Lmin + (Lmax - Lmin) / 2 * (1 + tanh);
}
function clamp(x, Lmin, Lmax) {
return Math.min(Math.max(x, Lmin), Lmax);
}
// Tarification actuelle : par enfant
function currentTariff(q, Lmin, Lmax, coeff) {
return clamp(40 + coeff * q, Lmin, Lmax);
}
// Plateau double-logistique :
// mid = (Lmin + Lmax)/2
// x1 = q0 - W, x2 = q0 + W
// f(q) = Lmin + (mid-Lmin)*σ(k(q-x1)) + (Lmax-mid)*σ(k(q-x2))
function plateauDoubleLogistic(q, Lmin, Lmax, q0, k, W) {
const mid = (Lmin + Lmax) / 2;
const x1 = q0 - W;
const x2 = q0 + W;
const s1 = sigma(k * (q - x1));
const s2 = sigma(k * (q - x2));
return Lmin + (mid - Lmin) * s1 + (Lmax - mid) * s2;
}
function draw() {
const Lmin = parseFloat(LminInput.value);
const Lmax = parseFloat(LmaxInput.value);
const q0 = parseFloat(Q0Input.value);
const k = parseFloat(kInput.value) / 1000; // 1..15 → 0.001..0.015
const W = parseFloat(WInput.value); // largeur plateau en QF
const qMin = parseFloat(qMinInput.value);
const qMax = parseFloat(qMaxInput.value);
LminVal.textContent = Lmin.toFixed(0);
LmaxVal.textContent = Lmax.toFixed(0);
Q0Val.textContent = q0.toFixed(0);
kVal.textContent = k.toFixed(3);
WVal.textContent = W.toFixed(0);
qMinVal.textContent = qMin.toFixed(0);
qMaxVal.textContent = qMax.toFixed(0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
const paddingLeft = 60;
const paddingRight = 20;
const paddingTop = 20;
const paddingBottom = 40;
const plotWidth = canvas.width - paddingLeft - paddingRight;
const plotHeight = canvas.height - paddingTop - paddingBottom;
// axes
ctx.strokeStyle = "#444";
ctx.lineWidth = 1;
// X-axis
ctx.beginPath();
ctx.moveTo(paddingLeft, canvas.height - paddingBottom);
ctx.lineTo(canvas.width - paddingRight, canvas.height - paddingBottom);
ctx.stroke();
// Y-axis
ctx.beginPath();
ctx.moveTo(paddingLeft, paddingTop);
ctx.lineTo(paddingLeft, canvas.height - paddingBottom);
ctx.stroke();
// Y ticks (tarif)
const yTicks = 7;
ctx.fillStyle = "#aaa";
ctx.font = "11px system-ui";
for (let i = 0; i <= yTicks; i++) {
const t = i / yTicks;
const val = Lmin + t * (Lmax - Lmin);
const y = canvas.height - paddingBottom - t * plotHeight;
ctx.strokeStyle = "#222";
ctx.beginPath();
ctx.moveTo(paddingLeft, y);
ctx.lineTo(canvas.width - paddingRight, y);
ctx.stroke();
ctx.fillStyle = "#aaa";
ctx.fillText(val.toFixed(0) + "€", 5, y + 4);
}
// X ticks (QF)
const xTicks = 7;
for (let i = 0; i <= xTicks; i++) {
const t = i / xTicks;
const val = qMin + t * (qMax - qMin);
const x = paddingLeft + t * plotWidth;
ctx.strokeStyle = "#222";
ctx.beginPath();
ctx.moveTo(x, paddingTop);
ctx.lineTo(x, canvas.height - paddingBottom);
ctx.stroke();
ctx.fillStyle = "#aaa";
ctx.textAlign = "center";
ctx.fillText(val.toFixed(0), x, canvas.height - paddingBottom + 14);
}
ctx.textAlign = "left";
function mapX(q) {
return paddingLeft + (q - qMin) / (qMax - qMin) * plotWidth;
}
function mapY(tarif) {
const t = (tarif - Lmin) / (Lmax - Lmin);
return canvas.height - paddingBottom - t * plotHeight;
}
const steps = 400;
const qfValues = [];
for (let i = 0; i <= steps; i++) {
const t = i / steps;
qfValues.push(qMin + t * (qMax - qMin));
}
function drawCurve(color, func, lineWidth = 2) {
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
qfValues.forEach((q, idx) => {
const tarif = func(q);
const xPix = mapX(q);
const yPix = mapY(tarif);
if (idx === 0) ctx.moveTo(xPix, yPix);
else ctx.lineTo(xPix, yPix);
});
ctx.stroke();
}
// Plateau double-logistique (milieu = plateau)
drawCurve("#00E5FF", q => plateauDoubleLogistic(q, Lmin, Lmax, q0, k, W), 2.5);
// Tarification actuelle – 4 variantes
drawCurve("#B0BEC5", q => currentTariff(q, Lmin, Lmax, 0.35), 1.5); // 1 enfant
drawCurve("#90CAF9", q => currentTariff(q, Lmin, Lmax, 0.33), 1.5); // 2 enfants
drawCurve("#CE93D8", q => currentTariff(q, Lmin, Lmax, 0.31), 1.5); // 3 enfants
drawCurve("#FFAB91", q => currentTariff(q, Lmin, Lmax, 0.29), 1.5); // 4+ enfants
// Marqueur du centre QF₀ et du milieu de tarif
const midTarif = (Lmin + Lmax) / 2;
const xMid = mapX(q0);
const yMid = mapY(midTarif);
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(xMid, yMid, 3, 0, 2 * Math.PI);
ctx.fill();
ctx.font = "11px system-ui";
ctx.fillText("Pivot QF₀ / mid", xMid + 5, yMid - 5);
}
[LminInput, LmaxInput, Q0Input, kInput, WInput, qMinInput, qMaxInput].forEach(el => {
el.addEventListener("input", draw);
});
draw();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment