Bloc HTML/CSS/JS prêt à intégrer : rendu glassmorphique, animation fluide, grille Y, valeurs modifiables via le tableau DATA, et mise à jour à chaud avec updateFrozenBars() :
<!-- Graphique en bâtons "Frozen Glass" — Exemple + largeur réduite -->
<div class="fg-scene">
<div class="fg-card" id="glassBarChart" aria-label="Graphique en bâtons">
<div class="fg-head">
<h3>Ventes mensuelles (exemple)</h3>
<span class="fg-ymax" id="fgYMax"></span>
</div>
<div class="fg-chart">
<div class="fg-grid" id="fgGrid" aria-hidden="true"></div>
<div class="fg-bars" id="fgBars"></div>
</div>
<div class="fg-legend" id="fgLegend"></div>
</div>
</div>
<style>
/* ===== Scene (fond) ===== */
.fg-scene{
--bg1:#0b1220; --bg2:#0f213a; --bg3:#143a64;
--ice:#ffffff; --ice-05:rgba(255,255,255,.05); --ice-08:rgba(255,255,255,.08);
--ice-12:rgba(255,255,255,.12); --ice-18:rgba(255,255,255,.18); --ice-30:rgba(255,255,255,.30);
--accent:#8bd3ff; /* teinte principale des barres */
--accent-2:#b0e4ff;
--text:#eaf6ff; --muted:#b7c7d8;
--shadow: 0 12px 40px rgba(0,0,0,.35);
--radius: 22px;
--grid: rgba(255,255,255,.10);
--tick: rgba(255,255,255,.18);
position: relative;
padding: 32px;
background:
radial-gradient(1200px 800px at 15% 10%, rgba(139,211,255,.25), transparent 60%),
radial-gradient(1000px 700px at 85% 90%, rgba(84,145,255,.22), transparent 55%),
linear-gradient(140deg, var(--bg1), var(--bg2) 45%, var(--bg3));
min-height: 420px;
display: grid;
place-items: center;
}
/* ===== Carte "frozen glass" (largeur réduite) ===== */
.fg-card{
width: min(780px, 92vw); /* <- moins large */
padding: 22px 22px 16px;
border-radius: var(--radius);
background: linear-gradient(180deg, rgba(255,255,255,.12), rgba(255,255,255,.06));
border: 1px solid var(--ice-18);
box-shadow: var(--shadow);
backdrop-filter: blur(14px) saturate(160%);
-webkit-backdrop-filter: blur(14px) saturate(160%);
color: var(--text);
}
.fg-head{
display:flex; align-items:baseline; justify-content:space-between; gap:16px;
padding: 4px 2px 12px;
}
.fg-head h3{ margin:0; font: 600 18px/1.2 system-ui, Segoe UI, Roboto, Arial, sans-serif; letter-spacing:.2px; }
.fg-ymax{ color: var(--muted); font-size: 12px; }
/* ===== Zone chart ===== */
.fg-chart{
position: relative;
height: clamp(220px, 40vh, 360px);
border-radius: calc(var(--radius) - 6px);
background: linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.04));
border: 1px solid var(--ice-12);
overflow: hidden;
}
/* Grille horizontale */
.fg-grid{
position:absolute; inset:0 0 24px 0; /* laisser la place aux labels X */
background-image:
linear-gradient(to bottom, var(--grid) 1px, transparent 1px);
background-size: 100% calc(20%); /* 5 lignes -> 0%,20%,40%,60%,80% */
pointer-events:none;
}
.fg-grid::after{
content:""; position:absolute; left:0; right:0; bottom:0; height:1px; background: var(--grid);
}
/* Conteneur des barres */
.fg-bars{
position:absolute; inset:0 0 24px 0;
display:flex; align-items:flex-end; gap:clamp(8px, 1.6vw, 14px);
padding: 16px 14px 0 14px;
}
/* Barre + label */
.fg-bar{
flex:1 1 0;
display:flex; flex-direction:column; justify-content:flex-end; align-items:center;
min-width: 28px; max-width: 120px;
}
.fg-bar .fg-stick{
width: 100%;
height: 0%; /* sera animé en JS */
border-radius: 12px 12px 8px 8px;
background:
linear-gradient(180deg, rgba(255,255,255,.55), rgba(255,255,255,.15)) /* givre */,
linear-gradient(180deg, var(--accent), var(--accent-2));
box-shadow:
inset 0 1px 1px rgba(255,255,255,.35),
inset 0 -10px 30px rgba(0,0,0,.15),
0 8px 18px rgba(0,0,0,.28);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
transform-origin: bottom;
transform: scaleY(0);
transition: transform 900ms cubic-bezier(.22,1,.36,1), box-shadow .3s ease;
}
.fg-bar:is(:hover, :focus-within) .fg-stick{
box-shadow:
inset 0 1px 1px rgba(255,255,255,.55),
inset 0 -10px 30px rgba(0,0,0,.09),
0 10px 26px rgba(0,0,0,.35);
}
.fg-value{
margin-bottom: 8px;
font-size: 12px;
color: var(--text);
background: rgba(255,255,255,.12);
border: 1px solid var(--ice-18);
padding: 2px 7px;
border-radius: 10px;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
transform: translateY(6px);
opacity: 0;
transition: opacity .35s ease .3s, transform .35s ease .3s;
pointer-events:none;
}
.fg-bar._in .fg-value{ opacity:1; transform: translateY(0); }
.fg-xlabel{
margin-top: 8px;
font-size: 12px; color: var(--muted);
text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
/* Légende des ticks Y à gauche */
.fg-legend{
display:flex; justify-content:space-between; gap:8px;
padding: 10px 8px 0; color:var(--muted); font-size:11px;
}
/* Fallback si backdrop-filter non supporté */
@supports not ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))){
.fg-card, .fg-chart, .fg-value{ background: rgba(255,255,255,.12); }
}
</style>
<script>
/* ===== VALEURS D'EXEMPLE ===== */
const DATA = [
{ label: "Jan", value: 8 },
{ label: "Fév", value: 12 },
{ label: "Mar", value: 9 },
{ label: "Avr", value: 15 },
{ label: "Mai", value: 19 },
{ label: "Juin", value: 14 },
{ label: "Juil", value: 22 },
{ label: "Août", value: 17 },
{ label: "Sept", value: 20 },
{ label: "Oct", value: 23 },
{ label: "Nov", value: 18 },
{ label: "Déc", value: 25 }
];
// Options rapidement ajustables
const OPTIONS = {
yTicks: 5, // nombre de niveaux sur l'axe Y (grille)
max: null, // fixe manuellement la valeur max (sinon auto)
unit: " k€", // unité affichée
animMs: 900, // durée d'animation d'une barre
delayPerBar: 70 // décalage progressif entre barres (ms)
};
/* ===== Rendu du chart ===== */
(function(){
const root = document.getElementById("glassBarChart");
const grid = document.getElementById("fgGrid");
const bars = document.getElementById("fgBars");
const legend = document.getElementById("fgLegend");
const ymaxEl = document.getElementById("fgYMax");
function fmt(n){ return (Number.isInteger(n) ? n : +n.toFixed(1)) + (OPTIONS.unit||""); }
function computeMax(data, manual){
const maxVal = data.reduce((m,d)=> Math.max(m, d.value||0), 0);
const goal = manual ?? maxVal;
const pow = Math.pow(10, Math.floor(Math.log10(goal||1)));
const n = goal / pow;
const nice = (n<=1? 1 : n<=2? 2 : n<=5? 5 : 10) * pow;
return nice || 1;
}
function render(data){
const max = computeMax(data, OPTIONS.max);
ymaxEl.textContent = "Max: " + fmt(max);
// Ticks Y
legend.innerHTML = "";
for(let i=0;i<=OPTIONS.yTicks;i++){
const v = (max/OPTIONS.yTicks)*i;
const tick = document.createElement("span");
tick.textContent = fmt(v);
legend.appendChild(tick);
}
// Barres
bars.innerHTML = "";
data.forEach((d, idx)=>{
const wrap = document.createElement("div");
wrap.className = "fg-bar";
wrap.style.setProperty("--idx", idx);
const val = document.createElement("div");
val.className = "fg-value";
val.textContent = fmt(d.value);
const stick = document.createElement("div");
stick.className = "fg-stick";
stick.setAttribute("role","img");
stick.setAttribute("aria-label", `${d.label}: ${d.value}${OPTIONS.unit||""} (${Math.round((d.value/max)*100)}%)`);
const xl = document.createElement("div");
xl.className = "fg-xlabel";
xl.textContent = d.label;
wrap.appendChild(val);
wrap.appendChild(stick);
wrap.appendChild(xl);
bars.appendChild(wrap);
const pct = Math.max(0, Math.min(1, (d.value||0) / max));
const delay = OPTIONS.delayPerBar * idx;
stick.style.transitionDuration = OPTIONS.animMs + "ms";
stick.style.transitionDelay = delay + "ms";
requestAnimationFrame(()=>{
wrap.classList.add("_in");
stick.style.transform = "scaleY("+pct+")";
});
});
}
// Relance l'animation lorsque le composant redevient visible
const obs = new IntersectionObserver((entries)=>{
entries.forEach(e=>{
if(e.isIntersecting){
bars.querySelectorAll(".fg-stick").forEach(el=> el.style.transform = "scaleY(0)");
bars.querySelectorAll(".fg-bar").forEach(el=> el.classList.remove("_in"));
setTimeout(()=> render(DATA), 30);
}
});
}, {threshold: .2});
obs.observe(root);
render(DATA);
// API pour mise à jour à chaud
window.updateFrozenBars = function(newData){
if(Array.isArray(newData) && newData.length){
render(newData);
}
};
})();
</script>