|
|
@@ -0,0 +1,271 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="en">
|
|
|
+<head>
|
|
|
+<meta charset="UTF-8">
|
|
|
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+<title>Ratchet Mechanism</title>
|
|
|
+<style>
|
|
|
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap');
|
|
|
+
|
|
|
+ * { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
+
|
|
|
+ body {
|
|
|
+ width: 1200px;
|
|
|
+ height: 450px;
|
|
|
+ background: #111111;
|
|
|
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header {
|
|
|
+ padding: 28px 60px 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .label {
|
|
|
+ color: #D4532B;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 800;
|
|
|
+ letter-spacing: 2px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ }
|
|
|
+
|
|
|
+ .subtitle {
|
|
|
+ color: #555;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ letter-spacing: 1px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-area {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 0 60px 60px;
|
|
|
+ gap: 0;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bars-wrapper {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ gap: 40px;
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bar-group {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ position: relative;
|
|
|
+ width: 80px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .score {
|
|
|
+ font-size: 36px;
|
|
|
+ font-weight: 800;
|
|
|
+ color: #ffffff;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ line-height: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .score.rollback {
|
|
|
+ color: #C92A2A;
|
|
|
+ text-decoration: line-through;
|
|
|
+ text-decoration-thickness: 3px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bar {
|
|
|
+ width: 80px;
|
|
|
+ border-radius: 4px 4px 0 0;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bar.baseline {
|
|
|
+ background: #444444;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bar.retained {
|
|
|
+ background: #ffffff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bar.rollback-bar {
|
|
|
+ background: transparent;
|
|
|
+ border: 2px dashed #C92A2A;
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bar.highlight {
|
|
|
+ background: #D4532B;
|
|
|
+ }
|
|
|
+
|
|
|
+ .round-label {
|
|
|
+ margin-top: 12px;
|
|
|
+ color: #666666;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* SVG overlay for arrows and ratchet line */
|
|
|
+ .svg-overlay {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+</style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+
|
|
|
+<div class="header">
|
|
|
+ <div class="label">RATCHET MECHANISM</div>
|
|
|
+ <div class="subtitle">— effective baseline only moves up</div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<div class="chart-area" id="chartArea">
|
|
|
+ <div class="bars-wrapper" id="barsWrapper">
|
|
|
+ <!-- bars will be injected by JS -->
|
|
|
+ </div>
|
|
|
+ <svg class="svg-overlay" id="svgOverlay"></svg>
|
|
|
+</div>
|
|
|
+
|
|
|
+<script>
|
|
|
+ const scores = [72, 78, 75, 84, 87];
|
|
|
+ const types = ['baseline', 'retained', 'rollback', 'retained', 'highlight'];
|
|
|
+ const rounds = ['Round 0', 'Round 1', 'Round 2', 'Round 3', 'Round 4'];
|
|
|
+
|
|
|
+ // Effective baseline sequence (ratchet): 72, 78, 78, 84, 87
|
|
|
+ const effectiveBaseline = [72, 78, 78, 84, 87];
|
|
|
+
|
|
|
+ const maxScore = 90;
|
|
|
+ const minScore = 60;
|
|
|
+ const chartHeight = 270; // px available for bars
|
|
|
+
|
|
|
+ function barHeight(score) {
|
|
|
+ return Math.round((score - minScore) / (maxScore - minScore) * chartHeight);
|
|
|
+ }
|
|
|
+
|
|
|
+ const wrapper = document.getElementById('barsWrapper');
|
|
|
+
|
|
|
+ scores.forEach((score, i) => {
|
|
|
+ const group = document.createElement('div');
|
|
|
+ group.className = 'bar-group';
|
|
|
+ group.id = `group-${i}`;
|
|
|
+
|
|
|
+ const scoreEl = document.createElement('div');
|
|
|
+ scoreEl.className = 'score' + (types[i] === 'rollback' ? ' rollback' : '');
|
|
|
+ scoreEl.textContent = score;
|
|
|
+
|
|
|
+ const bar = document.createElement('div');
|
|
|
+ bar.className = 'bar ' + (types[i] === 'rollback' ? 'rollback-bar' : types[i]);
|
|
|
+ const h = barHeight(score);
|
|
|
+ bar.style.height = h + 'px';
|
|
|
+
|
|
|
+ const label = document.createElement('div');
|
|
|
+ label.className = 'round-label';
|
|
|
+ label.textContent = rounds[i];
|
|
|
+
|
|
|
+ group.appendChild(scoreEl);
|
|
|
+ group.appendChild(bar);
|
|
|
+ group.appendChild(label);
|
|
|
+ wrapper.appendChild(group);
|
|
|
+ });
|
|
|
+
|
|
|
+ // Draw arrows and ratchet line after layout
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ const svg = document.getElementById('svgOverlay');
|
|
|
+ const chartArea = document.getElementById('chartArea');
|
|
|
+ const chartRect = chartArea.getBoundingClientRect();
|
|
|
+
|
|
|
+ // Collect bar group positions
|
|
|
+ const groups = [];
|
|
|
+ for (let i = 0; i < 5; i++) {
|
|
|
+ const g = document.getElementById(`group-${i}`);
|
|
|
+ const rect = g.getBoundingClientRect();
|
|
|
+ // top of the bar (not the score label)
|
|
|
+ const bar = g.querySelector('.bar');
|
|
|
+ const barRect = bar.getBoundingClientRect();
|
|
|
+ groups.push({
|
|
|
+ cx: rect.left - chartRect.left + rect.width / 2,
|
|
|
+ barTop: barRect.top - chartRect.top,
|
|
|
+ barBottom: barRect.bottom - chartRect.top,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Arrow heads: connect bar top centers (exclude rollback from arrows, draw arrow anyway between all)
|
|
|
+ const arrowColor = '#D4532B';
|
|
|
+ const arrowGap = 8;
|
|
|
+
|
|
|
+ let svgContent = `
|
|
|
+ <defs>
|
|
|
+ <marker id="arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
|
|
+ <path d="M0,0 L0,6 L8,3 z" fill="${arrowColor}" />
|
|
|
+ </marker>
|
|
|
+ </defs>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // Draw horizontal arrows between consecutive bar centers at mid-height of the lower bar
|
|
|
+ for (let i = 0; i < 4; i++) {
|
|
|
+ const fromX = groups[i].cx + 40 + arrowGap;
|
|
|
+ const toX = groups[i+1].cx - 40 - arrowGap - 8;
|
|
|
+ const higherBarTop = Math.min(groups[i].barTop, groups[i+1].barTop);
|
|
|
+ const lowerBarTop = Math.max(groups[i].barTop, groups[i+1].barTop);
|
|
|
+ const lowerBarBottom = Math.max(groups[i].barBottom, groups[i+1].barBottom);
|
|
|
+ const y = lowerBarTop + (lowerBarBottom - lowerBarTop) * 0.5;
|
|
|
+
|
|
|
+ // Use a slightly raised y to look cleaner
|
|
|
+ const arrowY = Math.min(groups[i].barTop, groups[i+1].barTop) - 18;
|
|
|
+ const clampedY = Math.max(arrowY, 40);
|
|
|
+
|
|
|
+ svgContent += `<line x1="${fromX}" y1="${clampedY}" x2="${toX}" y2="${clampedY}"
|
|
|
+ stroke="${arrowColor}" stroke-width="2" marker-end="url(#arrow)" opacity="0.7"/>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Ratchet line: connects effective baseline tops
|
|
|
+ const effectiveHeights = effectiveBaseline.map(s => barHeight(s));
|
|
|
+ const baselineBottom = groups[0].barBottom; // all bars share same bottom
|
|
|
+
|
|
|
+ // Points for ratchet line
|
|
|
+ const points = groups.map((g, i) => {
|
|
|
+ const effH = effectiveHeights[i];
|
|
|
+ const y = baselineBottom - effH;
|
|
|
+ return { x: g.cx, y };
|
|
|
+ });
|
|
|
+
|
|
|
+ // Draw dashed orange line through effective baseline tops
|
|
|
+ let pathD = `M ${points[0].x} ${points[0].y}`;
|
|
|
+ for (let i = 1; i < points.length; i++) {
|
|
|
+ pathD += ` L ${points[i].x} ${points[i].y}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ svgContent += `<path d="${pathD}" fill="none" stroke="${arrowColor}" stroke-width="2"
|
|
|
+ stroke-dasharray="6,4" opacity="0.9"/>`;
|
|
|
+
|
|
|
+ // Dots at each effective baseline point
|
|
|
+ points.forEach((p, i) => {
|
|
|
+ svgContent += `<circle cx="${p.x}" cy="${p.y}" r="4" fill="${arrowColor}" opacity="0.9"/>`;
|
|
|
+ });
|
|
|
+
|
|
|
+ // Label for the ratchet line
|
|
|
+ svgContent += `<text x="${points[4].x + 10}" y="${points[4].y - 6}" fill="${arrowColor}"
|
|
|
+ font-family="Inter, sans-serif" font-size="11" font-weight="700" letter-spacing="0.5">EFFECTIVE FLOOR</text>`;
|
|
|
+
|
|
|
+ svg.innerHTML = svgContent;
|
|
|
+ });
|
|
|
+ });
|
|
|
+</script>
|
|
|
+</body>
|
|
|
+</html>
|