Initial commit
This commit is contained in:
+762
@@ -0,0 +1,762 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Prénoms · France</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Outfit:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg:#f7f4ef;--bg2:#eee9e0;--bg3:#e4ddd2;
|
||||
--ink:#1a1612;--ink2:#5a5048;--ink3:#9a8e82;
|
||||
--accent:#c0392b;--accent2:#e8b4a0;
|
||||
--blue:#2c5282;--blue-l:#bee3f8;
|
||||
--pink:#9b2c5a;--pink-l:#fed7e2;
|
||||
--gold:#b7791f;--gold-l:#fefcbf;
|
||||
--purple:#553c9a;--purple-l:#e9d8fd;
|
||||
--border:rgba(26,22,18,0.1);--card:#ffffff;
|
||||
--header-bg:#1a1612;--header-fg:#f7f4ef;
|
||||
--shadow:0 4px 20px rgba(0,0,0,0.07);
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--bg:#12100e;--bg2:#1c1916;--bg3:#252220;
|
||||
--ink:#f0ebe3;--ink2:#b0a898;--ink3:#6e6458;
|
||||
--accent:#e05545;--accent2:#c07060;
|
||||
--blue:#63b3ed;--blue-l:#1a365d;
|
||||
--pink:#d69ab8;--pink-l:#521b38;
|
||||
--gold:#e2a44a;--gold-l:#3d2e0a;
|
||||
--purple:#b794f4;--purple-l:#2d1f5e;
|
||||
--border:rgba(240,235,227,0.1);--card:#1c1916;
|
||||
--header-bg:#0c0b09;--header-fg:#f0ebe3;
|
||||
--shadow:0 4px 20px rgba(0,0,0,0.3);
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box;}
|
||||
body{font-family:'Outfit',sans-serif;background:var(--bg);color:var(--ink);min-height:100vh;transition:background .3s,color .3s;}
|
||||
header{background:var(--header-bg);color:var(--header-fg);padding:0 20px;display:flex;align-items:center;justify-content:space-between;height:60px;position:sticky;top:0;z-index:200;gap:10px;}
|
||||
.logo{font-family:'Cormorant Garamond',serif;font-size:1.25rem;font-weight:600;white-space:nowrap;flex-shrink:0;}
|
||||
.logo span{color:var(--accent2);font-style:italic;}
|
||||
.header-center{display:flex;background:rgba(255,255,255,0.07);border-radius:100px;padding:3px;gap:2px;}
|
||||
.mode-btn{padding:6px 14px;border-radius:100px;border:none;background:transparent;color:rgba(255,255,255,0.5);font-family:'Outfit',sans-serif;font-size:0.78rem;font-weight:500;cursor:pointer;transition:all .2s;white-space:nowrap;}
|
||||
.mode-btn.active{background:var(--accent);color:#fff;}
|
||||
.theme-toggle{width:34px;height:34px;border-radius:50%;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.07);color:rgba(255,255,255,0.7);cursor:pointer;font-size:0.95rem;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0;}
|
||||
.theme-toggle:hover{background:rgba(255,255,255,0.15);}
|
||||
.page{display:none;}
|
||||
.page.active{display:block;animation:fadeUp .3s ease;}
|
||||
@keyframes fadeUp{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
|
||||
|
||||
/* ── EXPLORER ── */
|
||||
#page-explorer{max-width:860px;margin:0 auto;padding:44px 18px 80px;}
|
||||
.hero-title{font-family:'Cormorant Garamond',serif;font-size:clamp(1.9rem,5vw,3.2rem);font-weight:300;text-align:center;line-height:1.1;margin-bottom:5px;}
|
||||
.hero-title em{font-style:italic;color:var(--accent);}
|
||||
.hero-sub{text-align:center;font-size:0.78rem;color:var(--ink3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:26px;}
|
||||
.search-area{display:flex;flex-direction:column;gap:10px;align-items:center;}
|
||||
.autocomplete-wrap{position:relative;width:100%;max-width:520px;}
|
||||
.search-row{display:flex;align-items:center;width:100%;background:var(--card);border:2px solid var(--border);border-radius:100px;transition:border-color .2s,box-shadow .2s;}
|
||||
.search-row:focus-within{border-color:var(--accent);box-shadow:0 0 0 4px rgba(192,57,43,0.08);}
|
||||
.search-row.blue-focus:focus-within{border-color:var(--blue);box-shadow:0 0 0 4px rgba(44,82,130,0.1);}
|
||||
.search-input{flex:1;border:none;outline:none;background:transparent;padding:12px 16px;font-family:'Cormorant Garamond',serif;font-size:1.2rem;color:var(--ink);min-width:0;}
|
||||
.search-input::placeholder{color:var(--ink3);font-style:italic;}
|
||||
.search-btn{padding:8px 18px;margin:4px;background:var(--accent);color:#fff;border:none;border-radius:100px;font-family:'Outfit',sans-serif;font-size:0.8rem;font-weight:600;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:opacity .2s;}
|
||||
.search-btn:hover{opacity:.88;}
|
||||
.autocomplete-list{position:absolute;top:calc(100% + 6px);left:0;right:0;background:var(--card);border:1px solid var(--border);border-radius:14px;box-shadow:var(--shadow);overflow:hidden;z-index:300;display:none;}
|
||||
.autocomplete-list.open{display:block;}
|
||||
.ac-item{display:flex;align-items:center;justify-content:space-between;padding:9px 15px;cursor:pointer;transition:background .12s;gap:8px;}
|
||||
.ac-item:hover{background:var(--bg2);}
|
||||
.ac-item:not(:last-child){border-bottom:1px solid var(--border);}
|
||||
.ac-name{font-family:'Cormorant Garamond',serif;font-size:1.05rem;color:var(--ink);}
|
||||
.ac-name mark{background:none;color:var(--accent);font-weight:600;}
|
||||
.ac-meta{font-size:0.7rem;color:var(--ink3);}
|
||||
.compare-toggle-row{display:flex;align-items:center;gap:8px;font-size:0.78rem;color:var(--ink3);}
|
||||
.compare-toggle-row label{cursor:pointer;display:flex;align-items:center;gap:6px;}
|
||||
.compare-second{display:none;width:100%;max-width:520px;}
|
||||
.compare-second.open{display:block;}
|
||||
.suggestions{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-top:10px;}
|
||||
.sug-chip{padding:4px 12px;background:var(--card);border:1px solid var(--border);border-radius:100px;font-size:0.78rem;color:var(--ink2);cursor:pointer;transition:all .15s;}
|
||||
.sug-chip:hover{border-color:var(--accent);color:var(--accent);}
|
||||
|
||||
/* result */
|
||||
#explorer-result{margin-top:44px;}
|
||||
.prenom-hero{text-align:center;padding:28px 0 22px;border-bottom:1px solid var(--border);margin-bottom:24px;}
|
||||
.prenom-big{font-family:'Cormorant Garamond',serif;font-size:clamp(2.6rem,8vw,5rem);font-weight:300;letter-spacing:-.02em;line-height:1;margin-bottom:10px;}
|
||||
.prenom-badges{display:flex;gap:6px;justify-content:center;flex-wrap:wrap;}
|
||||
.badge{padding:3px 10px;border-radius:100px;font-size:0.7rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;}
|
||||
.badge-m{background:var(--blue-l);color:var(--blue);}
|
||||
.badge-f{background:var(--pink-l);color:var(--pink);}
|
||||
.badge-mix{background:var(--purple-l);color:var(--purple);}
|
||||
.badge-len{background:var(--bg2);color:var(--ink2);}
|
||||
.stat-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:11px;margin-bottom:20px;}
|
||||
.stat-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px;text-align:center;}
|
||||
.stat-card .val{font-family:'Cormorant Garamond',serif;font-size:1.8rem;font-weight:600;color:var(--ink);display:block;line-height:1;}
|
||||
.stat-card .lbl{font-size:0.65rem;color:var(--ink3);text-transform:uppercase;letter-spacing:.06em;margin-top:5px;display:block;}
|
||||
.peak-row{display:grid;grid-template-columns:1fr 1fr;gap:11px;margin-bottom:20px;}
|
||||
@media(max-width:500px){.peak-row{grid-template-columns:1fr;}}
|
||||
.peak-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px 18px;display:flex;align-items:center;gap:12px;}
|
||||
.peak-icon{font-size:1.4rem;flex-shrink:0;}
|
||||
.peak-year{font-family:'Cormorant Garamond',serif;font-size:1.6rem;font-weight:600;line-height:1;}
|
||||
.peak-desc{font-size:0.68rem;color:var(--ink3);text-transform:uppercase;letter-spacing:.05em;margin-top:3px;}
|
||||
.trend-bar{height:4px;background:var(--bg2);border-radius:2px;margin-top:8px;overflow:hidden;}
|
||||
.trend-fill{height:100%;border-radius:2px;background:linear-gradient(to right,var(--accent2),var(--accent));transition:width .9s ease;}
|
||||
.chart-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:22px;margin-bottom:14px;overflow:hidden;}
|
||||
.chart-card-title{font-family:'Cormorant Garamond',serif;font-size:1.05rem;font-weight:600;color:var(--ink2);margin-bottom:16px;display:flex;align-items:center;justify-content:space-between;gap:8px;}
|
||||
.chart-wrap{position:relative;height:250px;}
|
||||
.share-btn{padding:4px 11px;border-radius:100px;border:1px solid var(--border);background:var(--bg2);font-family:'Outfit',sans-serif;font-size:0.72rem;color:var(--ink2);cursor:pointer;transition:all .15s;white-space:nowrap;flex-shrink:0;}
|
||||
.share-btn:hover{border-color:var(--accent);color:var(--accent);}
|
||||
.share-btn.copied{background:var(--accent);color:#fff;border-color:var(--accent);}
|
||||
|
||||
/* heatmap */
|
||||
.heatmap-scroll-wrap{position:relative;}
|
||||
.heatmap-scroll-wrap::after{content:'›››';position:absolute;top:50%;right:0;transform:translateY(-50%);padding:4px 6px;font-size:0.75rem;color:var(--ink3);pointer-events:none;transition:opacity .3s;background:linear-gradient(to right,transparent 0%,var(--card) 40%);}
|
||||
.heatmap-scroll-wrap.at-end::after{opacity:0;}
|
||||
.heatmap-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;padding-bottom:6px;margin:0 -22px;padding-left:22px;padding-right:22px;}
|
||||
.heatmap-inner{min-width:520px;}
|
||||
.heatmap-decade-labels{display:flex;gap:3px;padding-left:52px;margin-bottom:4px;}
|
||||
.heatmap-decade-lbl{flex:1;font-size:0.58rem;color:var(--ink3);text-align:center;}
|
||||
.heatmap-row{display:flex;align-items:center;gap:3px;margin-bottom:3px;}
|
||||
.heatmap-label{font-size:0.62rem;color:var(--ink3);width:48px;text-align:right;flex-shrink:0;padding-right:5px;}
|
||||
.heatmap-cell{flex:1;height:26px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:0.62rem;font-weight:500;color:#fff;cursor:default;transition:transform .15s;position:relative;}
|
||||
.heatmap-cell:hover{transform:scale(1.06);z-index:1;}
|
||||
.heatmap-cell-0{background:var(--bg2);color:var(--ink3);}
|
||||
|
||||
/* ── FINDER ── */
|
||||
#page-finder{max-width:1080px;margin:0 auto;padding:32px 18px 80px;}
|
||||
.finder-layout{display:grid;grid-template-columns:268px 1fr;gap:22px;align-items:start;}
|
||||
@media(max-width:720px){.finder-layout{grid-template-columns:1fr;}}
|
||||
.finder-sidebar{background:var(--card);border:1px solid var(--border);border-radius:16px;overflow:hidden;position:sticky;top:72px;z-index:10;}
|
||||
@media(max-width:720px){.finder-sidebar{position:static;}}
|
||||
.sidebar-header{background:var(--ink);padding:14px 18px;cursor:pointer;display:flex;align-items:center;justify-content:space-between;}
|
||||
.sidebar-toggle-icon{color:rgba(255,255,255,0.6);font-size:0.8rem;transition:transform .25s;flex-shrink:0;}
|
||||
.finder-sidebar.collapsed .sidebar-toggle-icon{transform:rotate(-90deg);}
|
||||
.finder-sidebar.collapsed .sidebar-body{display:none;}
|
||||
.sidebar-title{font-family:'Cormorant Garamond',serif;font-size:0.98rem;color:var(--header-fg);font-weight:400;}
|
||||
.sidebar-body{padding:16px;}
|
||||
.filter-group{margin-bottom:18px;}
|
||||
.filter-label{font-size:0.66rem;font-weight:600;color:var(--ink3);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;display:block;}
|
||||
.pill-group{display:flex;flex-wrap:wrap;gap:5px;}
|
||||
.pill{padding:5px 11px;border-radius:100px;border:1.5px solid var(--border);background:transparent;font-family:'Outfit',sans-serif;font-size:0.76rem;color:var(--ink2);cursor:pointer;transition:all .15s;}
|
||||
.pill:hover{border-color:var(--ink2);}
|
||||
.pill.active{border-color:var(--accent);background:var(--accent);color:#fff;}
|
||||
.pill.active-blue{border-color:var(--blue);background:var(--blue);color:#fff;}
|
||||
.pill.active-pink{border-color:var(--pink);background:var(--pink);color:#fff;}
|
||||
.pill.active-gold{border-color:var(--gold);background:var(--gold);color:#fff;}
|
||||
.pill.active-mix{border-color:var(--purple);background:var(--purple);color:#fff;}
|
||||
input[type=range]{-webkit-appearance:none;width:100%;height:4px;background:var(--bg3);border-radius:2px;outline:none;cursor:pointer;}
|
||||
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;background:var(--accent);border-radius:50%;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.2);}
|
||||
.range-vals{display:flex;justify-content:space-between;font-size:0.68rem;color:var(--ink3);margin-top:3px;}
|
||||
.filter-input{width:100%;border:1.5px solid var(--border);border-radius:8px;padding:7px 10px;font-family:'Outfit',sans-serif;font-size:0.83rem;color:var(--ink);background:var(--bg);outline:none;transition:border-color .2s;}
|
||||
.filter-input:focus{border-color:var(--accent);}
|
||||
.btn-search-finder{width:100%;padding:11px;background:var(--accent);color:#fff;border:none;border-radius:10px;font-family:'Outfit',sans-serif;font-size:0.9rem;font-weight:600;cursor:pointer;transition:opacity .2s,transform .1s;letter-spacing:.02em;}
|
||||
.btn-search-finder:hover{opacity:.9;transform:translateY(-1px);}
|
||||
.hint{font-size:0.66rem;color:var(--ink3);margin-top:5px;line-height:1.4;}
|
||||
.finder-results-header{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:16px;}
|
||||
.results-title{font-family:'Cormorant Garamond',serif;font-size:1.45rem;font-weight:300;}
|
||||
.results-count{font-size:0.76rem;color:var(--ink3);}
|
||||
.results-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:11px;}
|
||||
.result-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:15px 16px;cursor:pointer;transition:all .2s;position:relative;overflow:hidden;z-index:1;}
|
||||
.result-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--accent);opacity:0;transition:opacity .2s;}
|
||||
.result-card:hover{border-color:rgba(192,57,43,.3);transform:translateY(-2px);box-shadow:var(--shadow);}
|
||||
.result-card:hover::before{opacity:1;}
|
||||
.rc-name{font-family:'Cormorant Garamond',serif;font-size:1.35rem;font-weight:400;margin-bottom:6px;line-height:1;}
|
||||
.rc-meta{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-bottom:7px;}
|
||||
.rc-total{font-size:0.7rem;color:var(--ink3);}
|
||||
.rc-mini-bar{height:3px;background:var(--bg2);border-radius:2px;margin-top:6px;overflow:hidden;}
|
||||
.rc-mini-fill{height:100%;background:linear-gradient(to right,var(--accent2),var(--accent));border-radius:2px;}
|
||||
.pop-label{font-size:0.63rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:100px;}
|
||||
.pop-rare{background:#e6fffa;color:#276749;}
|
||||
.pop-discret{background:var(--blue-l);color:var(--blue);}
|
||||
.pop-connu{background:var(--gold-l);color:var(--gold);}
|
||||
.pop-populaire{background:#fff5f5;color:#c53030;}
|
||||
[data-theme="dark"] .pop-rare{background:#1a3a30;color:#68d391;}
|
||||
[data-theme="dark"] .pop-discret{background:var(--blue-l);color:var(--blue);}
|
||||
[data-theme="dark"] .pop-connu{background:var(--gold-l);color:var(--gold);}
|
||||
[data-theme="dark"] .pop-populaire{background:#3d1010;color:#fc8181;}
|
||||
.state-placeholder{grid-column:1/-1;text-align:center;padding:60px 20px;color:var(--ink3);}
|
||||
.state-placeholder .ph-icon{font-size:2.6rem;margin-bottom:10px;opacity:.4;display:block;}
|
||||
.loading-pulse{animation:pulse 1.2s ease-in-out infinite;display:inline-block;}
|
||||
.top-table{width:100%;border-collapse:collapse;}
|
||||
.top-table th{font-size:0.68rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink3);padding:10px 16px;text-align:left;border-bottom:1px solid var(--border);background:var(--bg2);}
|
||||
.top-table td{padding:11px 16px;border-bottom:1px solid var(--border);font-size:0.88rem;color:var(--ink);}
|
||||
.top-table tr:last-child td{border-bottom:none;}
|
||||
.top-table tr:hover td{background:var(--bg);}
|
||||
.top-table .rang{font-family:'Cormorant Garamond',serif;font-size:1.1rem;color:var(--ink3);width:48px;}
|
||||
.top-table .prenom-link{font-family:'Cormorant Garamond',serif;font-size:1.15rem;color:var(--accent);cursor:pointer;text-decoration:none;}
|
||||
.top-table .prenom-link:hover{text-decoration:underline;}
|
||||
.top-table .bar-cell{width:160px;}
|
||||
.top-mini-bar{height:5px;background:var(--bg2);border-radius:3px;overflow:hidden;margin-top:4px;}
|
||||
.top-mini-fill{height:100%;background:linear-gradient(to right,var(--accent2),var(--accent));border-radius:3px;}
|
||||
@media(max-width:600px){.top-table .bar-cell{display:none;}.top-table th.bar-cell{display:none;}}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="logo">Prénoms <span>de France</span></div>
|
||||
<div class="header-center">
|
||||
<button class="mode-btn active" onclick="switchMode('explorer',this)">🔍 Explorer</button>
|
||||
<button class="mode-btn" onclick="switchMode('finder',this)">✨ Trouver</button>
|
||||
<button class="mode-btn" onclick="switchMode('top',this)">🏆 Classement</button>
|
||||
</div>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" id="theme-btn">🌙</button>
|
||||
</header>
|
||||
|
||||
<div id="page-explorer" class="page active">
|
||||
<div id="page-explorer-inner">
|
||||
<div id="back-to-results" onclick="goBackToFinder()" style="display:none;align-items:center;gap:8px;margin-bottom:18px;cursor:pointer;width:fit-content;padding:7px 14px;border-radius:100px;border:1.5px solid var(--border);background:var(--card);color:var(--ink2);font-size:0.8rem;font-family:'Outfit',sans-serif;transition:all .15s;">
|
||||
<span>←</span> <span>Retour aux résultats</span>
|
||||
</div>
|
||||
<h1 class="hero-title">Explorer un <em>prénom</em></h1>
|
||||
<p class="hero-sub">Données INSEE · France · depuis 1900</p>
|
||||
<div class="search-area">
|
||||
<div class="autocomplete-wrap" id="ac-wrap-1">
|
||||
<div class="search-row">
|
||||
<input class="search-input" id="search-input-1" placeholder="Entrez un prénom…"
|
||||
autocomplete="off" autocorrect="off" spellcheck="false"
|
||||
oninput="onAcInput(1)" onkeydown="onAcKey(event,1)"/>
|
||||
<button class="search-btn" onclick="doExplore()">Analyser</button>
|
||||
</div>
|
||||
<div class="autocomplete-list" id="ac-list-1"></div>
|
||||
</div>
|
||||
<div class="compare-toggle-row">
|
||||
<label><input type="checkbox" id="compare-chk" onchange="toggleCompare()"/> Comparer avec un second prénom</label>
|
||||
</div>
|
||||
<div class="compare-second" id="ac-wrap-2">
|
||||
<div class="search-row blue-focus">
|
||||
<input class="search-input" id="search-input-2" placeholder="Second prénom…"
|
||||
autocomplete="off" autocorrect="off" spellcheck="false"
|
||||
oninput="onAcInput(2)" onkeydown="onAcKey(event,2)"/>
|
||||
<button class="search-btn" style="background:var(--blue)" onclick="doExplore()">Comparer</button>
|
||||
</div>
|
||||
<div class="autocomplete-list" id="ac-list-2"></div>
|
||||
</div>
|
||||
<div class="suggestions">
|
||||
<span class="sug-chip" onclick="quickSearch('Emma')">Emma</span>
|
||||
<span class="sug-chip" onclick="quickSearch('Léo')">Léo</span>
|
||||
<span class="sug-chip" onclick="quickSearch('Marie')">Marie</span>
|
||||
<span class="sug-chip" onclick="quickSearch('Lucas')">Lucas</span>
|
||||
<span class="sug-chip" onclick="quickSearch('Jade')">Jade</span>
|
||||
<span class="sug-chip" onclick="quickSearch('Jean')">Jean</span>
|
||||
<span class="sug-chip" onclick="quickSearch('Camille')">Camille</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="explorer-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-finder" class="page">
|
||||
<div class="finder-layout">
|
||||
<aside class="finder-sidebar">
|
||||
<div class="sidebar-header"><div class="sidebar-title">Critères de recherche</div><span class="sidebar-toggle-icon">▼</span></div>
|
||||
<div class="sidebar-body">
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">Genre</span>
|
||||
<div class="pill-group">
|
||||
<button class="pill active" data-sexe="" onclick="setSexe(this,'')">Tous</button>
|
||||
<button class="pill" data-sexe="1" onclick="setSexe(this,'1')">♂ Garçon</button>
|
||||
<button class="pill" data-sexe="2" onclick="setSexe(this,'2')">♀ Fille</button>
|
||||
<button class="pill" data-sexe="mixte" onclick="setSexe(this,'mixte')">⚥ Mixte</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">Popularité</span>
|
||||
<div class="pill-group">
|
||||
<button class="pill active-gold" id="pop-rare" onclick="togglePop(this,'rare')">Rare</button>
|
||||
<button class="pill" id="pop-discret" onclick="togglePop(this,'discret')">Discret</button>
|
||||
<button class="pill" id="pop-connu" onclick="togglePop(this,'connu')">Connu</button>
|
||||
<button class="pill" id="pop-populaire" onclick="togglePop(this,'populaire')">Populaire</button>
|
||||
</div>
|
||||
<div class="hint">Total toutes années · Rare <2000 · Discret 2k–10k · Connu 10k–50k · Populaire >50k</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">Longueur — <span id="len-disp">3 à 8 lettres</span></span>
|
||||
<input type="range" id="len-min-s" min="1" max="15" value="3" oninput="updateLenDisplay()"/>
|
||||
<div class="range-vals"><span>1</span><span>min</span><span>15</span></div>
|
||||
<input type="range" id="len-max-s" min="1" max="15" value="8" oninput="updateLenDisplay()"/>
|
||||
<div class="range-vals"><span>1</span><span>max</span><span>15</span></div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">Commence par</span>
|
||||
<input type="text" class="filter-input" id="finder-q" placeholder="ex: MA, TH…" maxlength="5" oninput="this.value=this.value.toUpperCase()"/>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">Période de popularité</span>
|
||||
<div class="pill-group">
|
||||
<button class="pill" id="era-ancien" onclick="toggleEra(this,'ancien')">Classique</button>
|
||||
<button class="pill" id="era-retro" onclick="toggleEra(this,'retro')">Rétro 80s</button>
|
||||
<button class="pill active" id="era-recent" onclick="toggleEra(this,'recent')">Actuel</button>
|
||||
</div>
|
||||
<div class="hint" id="era-hint">Actuel : 2010–2023</div>
|
||||
</div>
|
||||
<button class="btn-search-finder" onclick="doFinder()">Rechercher</button>
|
||||
</div>
|
||||
</aside>
|
||||
<div>
|
||||
<div class="finder-results-header">
|
||||
<div class="results-title">Résultats</div>
|
||||
<div class="results-count" id="finder-count"></div>
|
||||
</div>
|
||||
<div class="results-grid" id="finder-grid">
|
||||
<div class="state-placeholder"><span class="ph-icon">✨</span><p>Définissez vos critères et lancez la recherche</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let evoChart=null, compareChart=null;
|
||||
let finderSexe='', finderPop='rare', finderEra='recent';
|
||||
let acTimers=[null,null], acSelected=[-1,-1];
|
||||
const eraMap={ancien:{min:1900,max:1960,label:'Classique (avant 1960)'},retro:{min:1970,max:1995,label:'Rétro 80s (1970–1995)'},recent:{min:2010,max:2023,label:'Actuel (2010–2023)'}};
|
||||
const popMap={rare:{min:0,max:1999},discret:{min:2000,max:9999},connu:{min:10000,max:49999},populaire:{min:50000,max:9999999}};
|
||||
|
||||
// THEME
|
||||
function toggleTheme(){
|
||||
const d=document.documentElement,isDark=d.getAttribute('data-theme')==='dark';
|
||||
d.setAttribute('data-theme',isDark?'light':'dark');
|
||||
document.getElementById('theme-btn').textContent=isDark?'🌙':'☀️';
|
||||
localStorage.setItem('theme',isDark?'light':'dark');
|
||||
}
|
||||
(function(){
|
||||
const s=localStorage.getItem('theme')||'light';
|
||||
document.documentElement.setAttribute('data-theme',s);
|
||||
window.addEventListener('DOMContentLoaded',()=>{document.getElementById('theme-btn').textContent=s==='dark'?'☀️':'🌙';});
|
||||
})();
|
||||
|
||||
// MODE
|
||||
let _prevMode=null;
|
||||
function switchMode(mode,btn){
|
||||
const current=document.querySelector('.page.active')?.id?.replace('page-','');
|
||||
_prevMode=(current&¤t!==mode)?current:null;
|
||||
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
|
||||
document.querySelectorAll('.mode-btn').forEach(b=>b.classList.remove('active'));
|
||||
document.getElementById('page-'+mode).classList.add('active');
|
||||
btn.classList.add('active');
|
||||
// Afficher/masquer le bouton retour
|
||||
const backBtn=document.getElementById('back-to-results');
|
||||
if(backBtn) backBtn.style.display=(mode==='explorer'&&_prevMode==='finder')?'flex':'none';
|
||||
if(mode==='top') loadTop();
|
||||
}
|
||||
|
||||
// SHARE
|
||||
function sharePrenom(prenom){
|
||||
const url=location.origin+location.pathname+'#'+encodeURIComponent(prenom.toUpperCase());
|
||||
navigator.clipboard.writeText(url).then(()=>{
|
||||
const btn=document.getElementById('share-btn-'+prenom);
|
||||
if(btn){btn.textContent='✓ Copié !';btn.classList.add('copied');setTimeout(()=>{btn.textContent='🔗 Partager';btn.classList.remove('copied');},2000);}
|
||||
});
|
||||
}
|
||||
function checkUrlHash(){const h=decodeURIComponent(location.hash.slice(1));if(h){document.getElementById('search-input-1').value=capitalize(h);explorePrenom(h);}}
|
||||
|
||||
// AUTOCOMPLETE
|
||||
function onAcInput(idx){
|
||||
const val=document.getElementById('search-input-'+idx).value.trim();
|
||||
clearTimeout(acTimers[idx-1]);
|
||||
if(val.length<1){closeAc(idx);return;}
|
||||
acTimers[idx-1]=setTimeout(()=>fetchAc(idx,val),160);
|
||||
}
|
||||
async function fetchAc(idx,q){
|
||||
try{const d=await fetch('/api/autocomplete?q='+encodeURIComponent(q)+'&limit=7').then(r=>r.json());renderAc(idx,d,q);}catch(e){}
|
||||
}
|
||||
function renderAc(idx,items,q){
|
||||
const list=document.getElementById('ac-list-'+idx);
|
||||
if(!items.length){closeAc(idx);return;}
|
||||
list.innerHTML='';acSelected[idx-1]=-1;
|
||||
items.forEach((item)=>{
|
||||
const nom=capitalize(item.prenom);
|
||||
const re=new RegExp('^('+escRe(capitalize(q))+')','i');
|
||||
const hl=nom.replace(re,'<mark>$1</mark>');
|
||||
const si=item.sexe===1?'♂':item.sexe===2?'♀':'⚥';
|
||||
const div=document.createElement('div');div.className='ac-item';
|
||||
div.innerHTML='<span class="ac-name">'+hl+'</span><span class="ac-meta">'+si+' · '+fmtK(item.total)+'</span>';
|
||||
div.addEventListener('mousedown',e=>{e.preventDefault();selectAc(idx,item.prenom);});
|
||||
list.appendChild(div);
|
||||
});
|
||||
list.classList.add('open');
|
||||
}
|
||||
function escRe(s){return s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');}
|
||||
function onAcKey(e,idx){
|
||||
const list=document.getElementById('ac-list-'+idx);
|
||||
const items=list.querySelectorAll('.ac-item');
|
||||
if(!items.length){if(e.key==='Enter')doExplore();return;}
|
||||
if(e.key==='ArrowDown'){e.preventDefault();acSelected[idx-1]=Math.min(acSelected[idx-1]+1,items.length-1);highlightAc(idx,items);}
|
||||
else if(e.key==='ArrowUp'){e.preventDefault();acSelected[idx-1]=Math.max(acSelected[idx-1]-1,-1);highlightAc(idx,items);}
|
||||
else if(e.key==='Enter'){if(acSelected[idx-1]>=0){selectAc(idx,items[acSelected[idx-1]].querySelector('.ac-name').textContent);}else{doExplore();}}
|
||||
else if(e.key==='Escape'){closeAc(idx);}
|
||||
}
|
||||
function highlightAc(idx,items){
|
||||
items.forEach((el,i)=>el.style.background=i===acSelected[idx-1]?'var(--bg2)':'');
|
||||
if(acSelected[idx-1]>=0)document.getElementById('search-input-'+idx).value=items[acSelected[idx-1]].querySelector('.ac-name').textContent;
|
||||
}
|
||||
function selectAc(idx,prenom){document.getElementById('search-input-'+idx).value=capitalize(prenom);closeAc(idx);if(idx===1&&!document.getElementById('compare-chk').checked)doExplore();}
|
||||
function closeAc(idx){const l=document.getElementById('ac-list-'+idx);l.classList.remove('open');l.innerHTML='';}
|
||||
document.addEventListener('click',e=>{if(!e.target.closest('.autocomplete-wrap')){closeAc(1);closeAc(2);}});
|
||||
|
||||
// COMPARE TOGGLE
|
||||
function toggleCompare(){
|
||||
const on=document.getElementById('compare-chk').checked;
|
||||
document.getElementById('ac-wrap-2').classList.toggle('open',on);
|
||||
if(!on)closeAc(2);
|
||||
}
|
||||
|
||||
// EXPLORE
|
||||
function quickSearch(p){document.getElementById('search-input-1').value=p;document.getElementById('compare-chk').checked=false;document.getElementById('ac-wrap-2').classList.remove('open');doExplore();}
|
||||
function doExplore(){
|
||||
closeAc(1);closeAc(2);
|
||||
const p1=document.getElementById('search-input-1').value.trim();if(!p1)return;
|
||||
const cmp=document.getElementById('compare-chk').checked;
|
||||
const p2=cmp?document.getElementById('search-input-2').value.trim():null;
|
||||
location.hash=encodeURIComponent(p1.toUpperCase());
|
||||
if(cmp&&p2)doCompare(p1,p2);else explorePrenom(p1);
|
||||
}
|
||||
async function explorePrenom(prenom){
|
||||
const res=document.getElementById('explorer-result');
|
||||
res.style.display='block';
|
||||
res.innerHTML='<div style="text-align:center;padding:60px;color:var(--ink3)"><span class="loading-pulse">Chargement…</span></div>';
|
||||
try{
|
||||
const [evo,hm]=await Promise.all([
|
||||
fetch('/api/evolution/'+encodeURIComponent(prenom.toUpperCase())).then(r=>{if(!r.ok)throw new Error();return r.json();}),
|
||||
fetch('/api/heatmap/'+encodeURIComponent(prenom.toUpperCase())).then(r=>r.ok?r.json():[]),
|
||||
]);
|
||||
renderExplorer(prenom,evo,hm);
|
||||
}catch(e){res.innerHTML='<div style="text-align:center;padding:40px;color:var(--ink3)">Prénom introuvable.</div>';}
|
||||
}
|
||||
function renderExplorer(prenom,evo,hm){
|
||||
const nom=capitalize(prenom),res=document.getElementById('explorer-result');
|
||||
const hasMale=evo.some(d=>d.sexe===1),hasFemale=evo.some(d=>d.sexe===2);
|
||||
const totM=evo.filter(d=>d.sexe===1).reduce((s,d)=>s+d.valeur,0);
|
||||
const totF=evo.filter(d=>d.sexe===2).reduce((s,d)=>s+d.valeur,0);
|
||||
const totAll=totM+totF;
|
||||
const byY={};evo.forEach(d=>{byY[d.periode]=(byY[d.periode]||0)+d.valeur;});
|
||||
const pe=Object.entries(byY).sort((a,b)=>b[1]-a[1])[0];
|
||||
const peakYr=pe?.[0]??'—',peakVal=pe?.[1]??0;
|
||||
const ys=Object.keys(byY).map(Number).sort(),maxY=Math.max(...ys);
|
||||
const r10=ys.filter(y=>y>=maxY-9).reduce((s,y)=>s+(byY[y]||0),0);
|
||||
const p10=ys.filter(y=>y>=maxY-19&&y<maxY-9).reduce((s,y)=>s+(byY[y]||0),0);
|
||||
const tpct=p10>0?Math.round(((r10-p10)/p10)*100):null;
|
||||
let badges='';
|
||||
if(hasMale&&hasFemale)badges+='<span class="badge badge-mix">Mixte</span>';
|
||||
else if(hasMale)badges+='<span class="badge badge-m">♂ Masculin</span>';
|
||||
else if(hasFemale)badges+='<span class="badge badge-f">♀ Féminin</span>';
|
||||
badges+='<span class="badge badge-len">'+prenom.replace(/\s/g,'').length+' lettres</span>';
|
||||
res.innerHTML=`
|
||||
<div class="prenom-hero"><div class="prenom-big">${nom}</div><div class="prenom-badges">${badges}</div></div>
|
||||
<div class="stat-row">
|
||||
<div class="stat-card"><span class="val">${fmt(totAll)}</span><span class="lbl">Naissances totales</span></div>
|
||||
${hasMale?'<div class="stat-card"><span class="val" style="color:var(--blue)">'+fmt(totM)+'</span><span class="lbl">♂ Garçons</span></div>':''}
|
||||
${hasFemale?'<div class="stat-card"><span class="val" style="color:var(--pink)">'+fmt(totF)+'</span><span class="lbl">♀ Filles</span></div>':''}
|
||||
<div class="stat-card"><span class="val">${fmt(peakVal)}</span><span class="lbl">Pic annuel (${peakYr})</span></div>
|
||||
</div>
|
||||
<div class="peak-row">
|
||||
<div class="peak-card"><span class="peak-icon">📅</span><div><div class="peak-year">${peakYr}</div><div class="peak-desc">Année la plus populaire</div><div class="trend-bar"><div class="trend-fill" style="width:100%"></div></div></div></div>
|
||||
<div class="peak-card"><span class="peak-icon">${tpct===null?'📊':tpct>=0?'📈':'📉'}</span><div><div class="peak-year" style="color:${tpct===null?'inherit':tpct>=0?'#276749':'#c53030'}">${tpct===null?'—':(tpct>=0?'+':'')+tpct+'%'}</div><div class="peak-desc">Tendance 10 dernières années</div></div></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-card-title">Évolution des naissances<button class="share-btn" id="share-btn-${nom}" onclick="sharePrenom('${nom}')">🔗 Partager</button></div>
|
||||
<div class="chart-wrap"><canvas id="evo-canvas"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-card-title">Carte de chaleur par décennie</div>
|
||||
<div id="heatmap-scroll"><div id="heatmap-container"></div></div>
|
||||
</div>`;
|
||||
buildEvoChart(evo,nom);buildHeatmap(hm);
|
||||
}
|
||||
|
||||
// COMPARE
|
||||
async function doCompare(p1,p2){
|
||||
const res=document.getElementById('explorer-result');res.style.display='block';
|
||||
res.innerHTML='<div style="text-align:center;padding:60px;color:var(--ink3)"><span class="loading-pulse">Chargement…</span></div>';
|
||||
try{
|
||||
const data=await fetch('/api/compare?a='+encodeURIComponent(p1.toUpperCase())+'&b='+encodeURIComponent(p2.toUpperCase())).then(r=>r.json());
|
||||
renderCompare(p1,p2,data);
|
||||
}catch(e){res.innerHTML='<div style="text-align:center;padding:40px;color:var(--ink3)">Erreur.</div>';}
|
||||
}
|
||||
function renderCompare(p1,p2,data){
|
||||
const n1=capitalize(p1),n2=capitalize(p2);
|
||||
const e1=data[p1.toUpperCase()]||[],e2=data[p2.toUpperCase()]||[];
|
||||
const t1=e1.reduce((s,d)=>s+d.valeur,0),t2=e2.reduce((s,d)=>s+d.valeur,0);
|
||||
const by1={};e1.forEach(d=>{by1[d.periode]=(by1[d.periode]||0)+d.valeur;});
|
||||
const by2={};e2.forEach(d=>{by2[d.periode]=(by2[d.periode]||0)+d.valeur;});
|
||||
const pk1=Object.entries(by1).sort((a,b)=>b[1]-a[1])[0];
|
||||
const pk2=Object.entries(by2).sort((a,b)=>b[1]-a[1])[0];
|
||||
document.getElementById('explorer-result').innerHTML=`
|
||||
<div class="prenom-hero"><div class="prenom-big">${n1} <span style="color:var(--ink3);font-size:.5em">vs</span> ${n2}</div></div>
|
||||
<div class="stat-row">
|
||||
<div class="stat-card"><span class="val" style="color:var(--blue)">${fmt(t1)}</span><span class="lbl">${n1} · total</span></div>
|
||||
<div class="stat-card"><span class="val" style="color:var(--accent)">${fmt(t2)}</span><span class="lbl">${n2} · total</span></div>
|
||||
<div class="stat-card"><span class="val">${pk1?.[0]??'—'}</span><span class="lbl">Pic ${n1}</span></div>
|
||||
<div class="stat-card"><span class="val">${pk2?.[0]??'—'}</span><span class="lbl">Pic ${n2}</span></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-card-title">Comparaison — ${n1} vs ${n2}</div>
|
||||
<div class="chart-wrap"><canvas id="compare-canvas"></canvas></div>
|
||||
</div>`;
|
||||
buildCompareChart(e1,e2,n1,n2);
|
||||
}
|
||||
|
||||
// CHARTS
|
||||
function cColors(){
|
||||
const dark=document.documentElement.getAttribute('data-theme')==='dark';
|
||||
return{grid:dark?'rgba(255,255,255,0.05)':'rgba(0,0,0,0.04)',tick:dark?'#6e6458':'#9a8e82',ttBg:dark?'#1c1916':'#1a1612'};
|
||||
}
|
||||
function cOpts(c){
|
||||
return{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
|
||||
plugins:{legend:{labels:{color:c.tick,font:{family:'Outfit',size:11},usePointStyle:true}},
|
||||
tooltip:{backgroundColor:c.ttBg,titleColor:'#f7f4ef',bodyColor:'#c8bfb0',borderColor:'rgba(255,255,255,0.08)',borderWidth:1,
|
||||
callbacks:{label:ctx=>' '+ctx.dataset.label+' : '+(ctx.parsed.y?.toLocaleString('fr-FR')??'')}}},
|
||||
scales:{x:{ticks:{color:c.tick,font:{family:'Outfit',size:10},maxTicksLimit:12},grid:{color:c.grid}},
|
||||
y:{ticks:{color:c.tick,font:{family:'Outfit',size:10},callback:v=>v>=1000?(v/1000).toFixed(0)+'k':v},grid:{color:c.grid}}}};
|
||||
}
|
||||
function buildEvoChart(evo,nom){
|
||||
const ctx=document.getElementById('evo-canvas').getContext('2d');
|
||||
if(evoChart)evoChart.destroy();
|
||||
const p=[...new Set(evo.map(d=>d.periode))].sort();
|
||||
const hM=evo.some(d=>d.sexe===1),hF=evo.some(d=>d.sexe===2);
|
||||
const c=cColors(),ds=[];
|
||||
if(hM)ds.push({label:'Garçons',data:p.map(x=>{const r=evo.find(d=>d.periode===x&&d.sexe===1);return r?.valeur??null;}),borderColor:'#2c5282',backgroundColor:'rgba(44,82,130,0.08)',borderWidth:2,pointRadius:0,pointHoverRadius:5,fill:true,tension:.4,spanGaps:true});
|
||||
if(hF)ds.push({label:'Filles',data:p.map(x=>{const r=evo.find(d=>d.periode===x&&d.sexe===2);return r?.valeur??null;}),borderColor:'#9b2c5a',backgroundColor:'rgba(155,44,90,0.07)',borderWidth:2,pointRadius:0,pointHoverRadius:5,fill:true,tension:.4,spanGaps:true});
|
||||
if(hM&&hF)ds.push({label:'Total',data:p.map(x=>evo.filter(d=>d.periode===x).reduce((s,d)=>s+d.valeur,0)),borderColor:'#b7791f',backgroundColor:'transparent',borderWidth:1.5,borderDash:[4,3],pointRadius:0,pointHoverRadius:5,fill:false,tension:.4});
|
||||
evoChart=new Chart(ctx,{type:'line',data:{labels:p,datasets:ds},options:cOpts(c)});
|
||||
}
|
||||
function buildCompareChart(e1,e2,n1,n2){
|
||||
const ctx=document.getElementById('compare-canvas').getContext('2d');
|
||||
if(compareChart)compareChart.destroy();
|
||||
const allP=[...new Set([...e1.map(d=>d.periode),...e2.map(d=>d.periode)])].sort();
|
||||
const c=cColors();
|
||||
const ds=[
|
||||
{label:n1,data:allP.map(p=>e1.filter(d=>d.periode===p).reduce((s,d)=>s+d.valeur,0)||null),borderColor:'#2c5282',backgroundColor:'rgba(44,82,130,0.08)',borderWidth:2.5,pointRadius:0,pointHoverRadius:5,fill:true,tension:.4,spanGaps:true},
|
||||
{label:n2,data:allP.map(p=>e2.filter(d=>d.periode===p).reduce((s,d)=>s+d.valeur,0)||null),borderColor:'#c0392b',backgroundColor:'rgba(192,57,43,0.07)',borderWidth:2.5,pointRadius:0,pointHoverRadius:5,fill:true,tension:.4,spanGaps:true}
|
||||
];
|
||||
compareChart=new Chart(ctx,{type:'line',data:{labels:allP,datasets:ds},options:cOpts(c)});
|
||||
}
|
||||
|
||||
// HEATMAP
|
||||
function buildHeatmap(hmData){
|
||||
const container=document.getElementById('heatmap-container');
|
||||
if(!hmData||!hmData.length){container.innerHTML='<p style="color:var(--ink3);font-size:.83rem;padding:8px 0">Pas assez de données.</p>';return;}
|
||||
const decades=[...new Set(hmData.map(d=>d.decade))].sort();
|
||||
const sexes=[...new Set(hmData.map(d=>d.sexe))].sort();
|
||||
const maxVal=Math.max(...hmData.map(d=>d.valeur));
|
||||
const sLabel={1:'♂ Garçons',2:'♀ Filles'};
|
||||
const sColor={1:(a)=>'rgba(44,82,130,'+a+')',2:(a)=>'rgba(155,44,90,'+a+')'};
|
||||
let html='<div class="heatmap-decade-labels">';
|
||||
decades.forEach(d=>{html+='<div class="heatmap-decade-lbl">'+d+'s</div>';});
|
||||
html+='</div>';
|
||||
sexes.forEach(sexe=>{
|
||||
html+='<div class="heatmap-row"><div class="heatmap-label">'+(sLabel[sexe]??sexe)+'</div>';
|
||||
decades.forEach(decade=>{
|
||||
const entry=hmData.find(d=>d.decade===decade&&d.sexe===sexe);
|
||||
const val=entry?.valeur??0;
|
||||
const alpha=val>0?0.15+0.75*(val/maxVal):0;
|
||||
const fmt2=val>=1000?Math.round(val/1000)+'k':val>0?val:'';
|
||||
if(val===0)html+='<div class="heatmap-cell heatmap-cell-0" title="'+decade+'s : 0"></div>';
|
||||
else html+='<div class="heatmap-cell" style="background:'+sColor[sexe](alpha)+'" title="'+decade+'s : '+val.toLocaleString('fr-FR')+'">'+fmt2+'</div>';
|
||||
});
|
||||
html+='</div>';
|
||||
});
|
||||
container.innerHTML='<div class="heatmap-inner">'+html+'</div>';
|
||||
}
|
||||
|
||||
// FINDER
|
||||
function setSexe(btn,val){
|
||||
finderSexe=val;
|
||||
document.querySelectorAll('[data-sexe]').forEach(b=>b.classList.remove('active','active-blue','active-pink','active-mix'));
|
||||
if(val==='1')btn.classList.add('active-blue');
|
||||
else if(val==='2')btn.classList.add('active-pink');
|
||||
else if(val==='mixte')btn.classList.add('active-mix');
|
||||
else btn.classList.add('active');
|
||||
}
|
||||
function togglePop(btn,key){
|
||||
finderPop=key;
|
||||
document.querySelectorAll('[id^="pop-"]').forEach(b=>b.classList.remove('active','active-gold'));
|
||||
btn.classList.add('active-gold');
|
||||
}
|
||||
function toggleEra(btn,key){
|
||||
finderEra=key;
|
||||
document.querySelectorAll('[id^="era-"]').forEach(b=>b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.getElementById('era-hint').textContent=eraMap[key].label;
|
||||
}
|
||||
function updateLenDisplay(){
|
||||
const min=+document.getElementById('len-min-s').value,max=+document.getElementById('len-max-s').value;
|
||||
document.getElementById('len-disp').textContent=min+' à '+max+' lettres';
|
||||
}
|
||||
async function doFinder(){
|
||||
const grid=document.getElementById('finder-grid');
|
||||
grid.innerHTML='<div class="state-placeholder"><span class="loading-pulse">Recherche en cours…</span></div>';
|
||||
document.getElementById('finder-count').textContent='';
|
||||
const era=eraMap[finderEra];
|
||||
const lenMin=+document.getElementById('len-min-s').value,lenMax=+document.getElementById('len-max-s').value;
|
||||
const q=document.getElementById('finder-q').value.trim();
|
||||
const popMin=popMap[finderPop].min;
|
||||
const popMax=popMap[finderPop].max;
|
||||
// popularite filtre sur pic annuel all-time (via peak_min/peak_max, periode all-time)
|
||||
const params=new URLSearchParams({longueur_min:lenMin,longueur_max:lenMax,popularite_min:popMin,popularite_max:popMax,periode_min:era.min,periode_max:era.max,limit:80});
|
||||
if(finderSexe&&finderSexe!=='mixte')params.set('sexe',finderSexe);
|
||||
if(q)params.set('q',q);
|
||||
try{
|
||||
let data=await fetch('/api/search?'+params).then(r=>r.json());
|
||||
if(finderSexe==='mixte'){
|
||||
const counts={};
|
||||
data.forEach(d=>{if(!counts[d.prenom])counts[d.prenom]=new Set();counts[d.prenom].add(d.sexe);});
|
||||
const mx=new Set(Object.entries(counts).filter(([,s])=>s.has(1)&&s.has(2)).map(([n])=>n));
|
||||
const merged={};
|
||||
data.filter(d=>mx.has(d.prenom)).forEach(d=>{if(!merged[d.prenom])merged[d.prenom]={prenom:d.prenom,sexe:'mixte',longueur:d.longueur,total:0};merged[d.prenom].total+=d.total;});
|
||||
data=Object.values(merged).sort((a,b)=>b.total-a.total);
|
||||
}
|
||||
renderFinderResults(data);
|
||||
}catch(e){grid.innerHTML='<div class="state-placeholder"><p>Erreur de connexion.</p></div>';}
|
||||
}
|
||||
function renderFinderResults(data){
|
||||
const grid=document.getElementById('finder-grid');
|
||||
document.getElementById('finder-count').textContent=data.length+' résultat'+(data.length>1?'s':'');
|
||||
if(!data.length){grid.innerHTML='<div class="state-placeholder"><span class="ph-icon">🤔</span><p>Aucun résultat. Élargissez les filtres.</p></div>';return;}
|
||||
const maxT=Math.max(...data.map(d=>d.total_periode||d.total));
|
||||
grid.innerHTML='';
|
||||
data.forEach((item,i)=>{
|
||||
const nom=capitalize(item.prenom),pct=Math.round(((item.total_periode||item.total)/maxT)*100);
|
||||
const sb=item.sexe===1?'<span class="badge badge-m">♂</span>':item.sexe===2?'<span class="badge badge-f">♀</span>':'<span class="badge badge-mix">⚥</span>';
|
||||
const pc=item.total<2000?['pop-rare','Rare']:item.total<10000?['pop-discret','Discret']:item.total<50000?['pop-connu','Connu']:['pop-populaire','Populaire'];
|
||||
const card=document.createElement('div');card.className='result-card';
|
||||
card.style.animationDelay=Math.min(i*12,350)+'ms';
|
||||
card.innerHTML='<div class="rc-name">'+nom+'</div><div class="rc-meta">'+sb+'<span class="pop-label '+pc[0]+'">'+pc[1]+'</span></div><div class="rc-total">'+fmt(item.total)+' naissances</div><div class="rc-mini-bar"><div class="rc-mini-fill" style="width:'+pct+'%"></div></div>';
|
||||
card.addEventListener('click',()=>{
|
||||
document.getElementById('search-input-1').value=nom;
|
||||
document.getElementById('compare-chk').checked=false;
|
||||
document.getElementById('ac-wrap-2').classList.remove('open');
|
||||
switchMode('explorer',document.querySelector('.mode-btn'));
|
||||
explorePrenom(item.prenom);
|
||||
// Afficher bouton retour
|
||||
const backBtn=document.getElementById('back-to-results');
|
||||
if(backBtn) backBtn.style.display='flex';
|
||||
});
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// UTILS
|
||||
function fmt(n){return n?.toLocaleString('fr-FR')??'—';}
|
||||
function fmtK(n){return n>=1000?Math.round(n/1000)+'k':n;}
|
||||
function capitalize(s){return s?s.charAt(0).toUpperCase()+s.slice(1).toLowerCase():'';}
|
||||
|
||||
|
||||
// ── CLASSEMENT ──
|
||||
let topSexe='', topEra='recent', topOrder='desc', topPage=1, topTotal=0, topPerPage=50, topData=null;
|
||||
const topEraMap={all:{min:1900,max:2024},recent:{min:2010,max:2024},retro:{min:1970,max:1995},ancien:{min:1900,max:1960}};
|
||||
function setTopSexe(btn,val){topSexe=val;topPage=1;topData=null;document.querySelectorAll('[data-top-sexe]').forEach(b=>b.classList.remove('active','active-blue','active-pink'));if(val==='1')btn.classList.add('active-blue');else if(val==='2')btn.classList.add('active-pink');else btn.classList.add('active');loadTop();}
|
||||
function setTopEra(btn,val){topEra=val;topPage=1;topData=null;document.querySelectorAll('[data-top-era]').forEach(b=>b.classList.remove('active'));btn.classList.add('active');loadTop();}
|
||||
function setTopOrder(val){topOrder=val;topPage=1;topData=null;document.getElementById('top-order-desc').classList.toggle('active',val==='desc');document.getElementById('top-order-asc').classList.toggle('active',val==='asc');loadTop();}
|
||||
function topChangePage(delta){const maxPage=Math.ceil(topTotal/topPerPage);topPage=Math.max(1,Math.min(maxPage,topPage+delta));topData=null;loadTop();}
|
||||
function renderTopTable(res){
|
||||
const wrap=document.getElementById('top-table-wrap');if(!wrap)return;
|
||||
topTotal=res.total;topData=res;
|
||||
const maxVal=Math.max(...res.data.map(d=>d.total_periode||1));
|
||||
const maxPage=Math.ceil(res.total/topPerPage);
|
||||
document.getElementById('top-count').textContent=res.total.toLocaleString('fr-FR')+' prénoms';
|
||||
document.getElementById('top-page-info').textContent='Page '+res.page+' / '+maxPage;
|
||||
document.getElementById('top-prev').disabled=res.page<=1;
|
||||
document.getElementById('top-next').disabled=res.page>=maxPage;
|
||||
let html='<table class="top-table"><thead><tr><th class="rang">#</th><th>Prénom</th><th>Genre</th><th>Naissances</th><th class="bar-cell"></th></tr></thead><tbody>';
|
||||
res.data.forEach((item,i)=>{
|
||||
const nom=capitalize(item.prenom);
|
||||
const rang=(topOrder==='desc')?(res.page-1)*topPerPage+i+1:res.total-(res.page-1)*topPerPage-i;
|
||||
const si=item.sexe===1?'<span class="badge badge-m">♂</span>':item.sexe===2?'<span class="badge badge-f">♀</span>':'<span class="badge badge-mix">⚥</span>';
|
||||
const total=item.total_periode||item.total;
|
||||
const pct=Math.round((total/maxVal)*100);
|
||||
const medal=rang===1?'🥇 ':rang===2?'🥈 ':rang===3?'🥉 ':'';
|
||||
html+=`<tr><td class="rang" style="color:${rang<=3?'var(--accent)':'var(--ink3)'};font-weight:${rang<=3?700:400}">${medal}${rang}</td><td><span class="prenom-link" onclick="goToPrenom('${item.prenom}')">${nom}</span></td><td>${si}</td><td style="font-family:'Cormorant Garamond',serif;font-size:1.1rem;">${fmt(total)}</td><td class="bar-cell"><div class="top-mini-bar"><div class="top-mini-fill" style="width:${pct}%"></div></div></td></tr>`;
|
||||
});
|
||||
html+='</tbody></table>';wrap.innerHTML=html;
|
||||
}
|
||||
async function loadTop(){
|
||||
const wrap=document.getElementById('top-table-wrap');
|
||||
if(wrap)wrap.innerHTML='<div class="state-placeholder"><span style="font-size:2rem">⏳</span><p style="margin-top:8px;color:var(--ink3)">Chargement…</p></div>';
|
||||
const era=topEraMap[topEra];
|
||||
const params=new URLSearchParams({periode_min:era.min,periode_max:era.max,order:topOrder,page:topPage,per_page:topPerPage});
|
||||
if(topSexe)params.set('sexe',topSexe);
|
||||
try{const res=await fetch('/api/top?'+params).then(r=>r.json());renderTopTable(res);}
|
||||
catch(e){if(wrap)wrap.innerHTML='<div class="state-placeholder"><p>Erreur de connexion.</p></div>';}
|
||||
}
|
||||
function goToPrenom(prenom){
|
||||
document.getElementById('search-input-1').value=capitalize(prenom);
|
||||
switchMode('explorer',document.querySelector('.mode-btn'));
|
||||
explorePrenom(prenom);
|
||||
const backBtn=document.getElementById('back-to-results');
|
||||
if(backBtn)backBtn.style.display='flex';
|
||||
}
|
||||
function goBackToFinder(){switchMode('finder',document.querySelectorAll('.mode-btn')[1]);}
|
||||
|
||||
// INIT
|
||||
|
||||
window.addEventListener('load',()=>{
|
||||
// Heatmap scroll indicator
|
||||
document.addEventListener('scroll', updateHeatmapIndicator, true);
|
||||
function updateHeatmapIndicator(){
|
||||
const el=document.getElementById('heatmap-scroll-el');
|
||||
if(!el) return;
|
||||
const wrap=el.closest('.heatmap-scroll-wrap');
|
||||
if(wrap) wrap.classList.toggle('at-end', el.scrollLeft+el.clientWidth >= el.scrollWidth-4);
|
||||
}
|
||||
// Also update after heatmap is built
|
||||
const origBuild=window.buildHeatmap;
|
||||
setTimeout(updateHeatmapIndicator, 500);
|
||||
|
||||
checkUrlHash();
|
||||
window.addEventListener('hashchange',checkUrlHash);
|
||||
// Mobile: auto-collapse sidebar on small screens
|
||||
const sidebar = document.querySelector('.finder-sidebar');
|
||||
document.querySelector('.sidebar-header').addEventListener('click', () => {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
});
|
||||
if (window.innerWidth <= 720) sidebar.classList.add('collapsed');
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="page-top" class="page">
|
||||
<div style="max-width:860px;margin:0 auto;padding:44px 18px 80px;">
|
||||
<h1 class="hero-title" style="text-align:center;margin-bottom:6px;">Classement des <em>prénoms</em></h1>
|
||||
<p class="hero-sub" style="text-align:center;margin-bottom:28px;">Total de naissances sur la période · Données INSEE</p>
|
||||
|
||||
<!-- Contrôles -->
|
||||
<div style="display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;margin-bottom:20px;">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
|
||||
<!-- Genre -->
|
||||
<div class="pill-group">
|
||||
<button class="pill active" data-top-sexe="" onclick="setTopSexe(this,'')">Tous</button>
|
||||
<button class="pill" data-top-sexe="1" onclick="setTopSexe(this,'1')">♂ Garçons</button>
|
||||
<button class="pill" data-top-sexe="2" onclick="setTopSexe(this,'2')">♀ Filles</button>
|
||||
</div>
|
||||
<!-- Période -->
|
||||
<div class="pill-group">
|
||||
<button class="pill" data-top-era="all" onclick="setTopEra(this,'all')">Toutes années</button>
|
||||
<button class="pill active" data-top-era="recent" onclick="setTopEra(this,'recent')">Actuel (2010–2024)</button>
|
||||
<button class="pill" data-top-era="retro" onclick="setTopEra(this,'retro')">Rétro (1970–1995)</button>
|
||||
<button class="pill" data-top-era="ancien" onclick="setTopEra(this,'ancien')">Classique (avant 1960)</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Ordre -->
|
||||
<div style="display:flex;gap:6px;">
|
||||
<button id="top-order-desc" class="pill active" onclick="setTopOrder('desc')">↓ Plus donnés</button>
|
||||
<button id="top-order-asc" class="pill" onclick="setTopOrder('asc')">↑ Plus rares</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tableau -->
|
||||
<div style="background:var(--card);border:1px solid var(--border);border-radius:14px;overflow:hidden;">
|
||||
<div id="top-table-wrap">
|
||||
<div class="state-placeholder"><span class="ph-icon loading-pulse">⏳</span><p>Chargement…</p></div>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div id="top-pagination" style="display:flex;align-items:center;justify-content:space-between;padding:12px 18px;border-top:1px solid var(--border);gap:10px;flex-wrap:wrap;">
|
||||
<span id="top-count" style="font-size:0.75rem;color:var(--ink3);"></span>
|
||||
<div style="display:flex;gap:6px;align-items:center;">
|
||||
<button id="top-prev" class="pill" onclick="topChangePage(-1)" style="padding:5px 14px;">← Préc.</button>
|
||||
<span id="top-page-info" style="font-size:0.78rem;color:var(--ink2);"></span>
|
||||
<button id="top-next" class="pill" onclick="topChangePage(1)" style="padding:5px 14px;">Suiv. →</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user