Created
November 22, 2025 22:41
-
-
Save np/ca563b2107e3e0e873b65943cc314dd1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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 € 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