${d.name}
${d.share} %
${d.desc}
`; } nodes.on('mouseenter', function(e,d){ svg.selectAll('.country').classed('dim',true); d3.select(this).classed('dim',false); updateInfo(d); }).on('mouseleave', function(){ svg.selectAll('.country').classed('dim',false); }); ScrollTrigger.create({ trigger:'#ssqCarto', start:'top 75%', onEnter:()=>{ nodes.transition().duration(800).delay((d,i)=>i*60) .attr('opacity',1); } }); })(); /* ============================================================ VIZ 03 — BARRES HORIZONTALES ANIMÉES ============================================================ */ (function(){ const container = document.getElementById('ssqBars'); if(!container) return; const rowsEl = document.getElementById('barsRows'); const ctx = document.getElementById('barsContext'); const tabs = document.querySelectorAll('#barsTabs .viz-bars-year-tab'); const flags = { us: '', ru: '', ua: '', eu: '', ca: '', au: '', ar: '' }; const data = { 1975: { rows: [ {key:'us', name:'الولايات المتحدة', val:48, color:C.sky}, {key:'ca', name:'كندا', val:18, color:C.wheatDeep}, {key:'eu', name:'فرنسا والاتحاد الأوروبي', val:11, color:C.olive}, {key:'au', name:'أستراليا', val:9, color:C.earth}, {key:'ar', name:'الأرجنتين', val:6, color:C.skySoft}, {key:'ru', name:'الاتحاد السوفياتي/روسيا', val:4, color:C.rust}, {key:'ua', name:'أوكرانيا', val:1, color:C.wheat} ], ctx:'الهيمنة الأمريكيةغداة الصدمة النفطية، تُهيمن الولايات المتحدة على قرابة نصف السوق العالمية. فالقمح سلاحٌ دبلوماسيّ من أسلحة الحرب الباردة.' }, 1990: { rows: [ {key:'us', name:'الولايات المتحدة', val:35, color:C.sky}, {key:'eu', name:'فرنسا والاتحاد الأوروبي', val:18, color:C.olive}, {key:'ca', name:'كندا', val:17, color:C.wheatDeep}, {key:'au', name:'أستراليا', val:11, color:C.earth}, {key:'ar', name:'الأرجنتين', val:8, color:C.skySoft}, {key:'ru', name:'الاتحاد السوفياتي/روسيا', val:5, color:C.rust}, {key:'ua', name:'أوكرانيا', val:2, color:C.wheat} ], ctx:'أوروبا تبرزتُحوّل السياسة الفلاحية المشتركة الأوروبية الاتحادَ إلى ثاني مُصدّرٍ عالمي. فتتآكل الحصّة الأمريكية لكنّها تظلّ مُهيمنة.' }, 2005: { rows: [ {key:'us', name:'الولايات المتحدة', val:25, color:C.sky}, {key:'eu', name:'فرنسا والاتحاد الأوروبي', val:16, color:C.olive}, {key:'ca', name:'كندا', val:14, color:C.wheatDeep}, {key:'ru', name:'روسيا', val:13, color:C.rust}, {key:'au', name:'أستراليا', val:11, color:C.earth}, {key:'ar', name:'الأرجنتين', val:9, color:C.skySoft}, {key:'ua', name:'أوكرانيا', val:6, color:C.wheat} ], ctx:'اقتحام البحر الأسودتُعيد روسيا ما بعد السوفياتية الاستثمار في الزراعة فتغدو فاعلاً رئيسياً. وتسلك أوكرانيا المسار التصاعديّ ذاته.' }, 2015: { rows: [ {key:'ru', name:'روسيا', val:18, color:C.rust}, {key:'eu', name:'فرنسا والاتحاد الأوروبي', val:17, color:C.olive}, {key:'us', name:'الولايات المتحدة', val:14, color:C.sky}, {key:'ca', name:'كندا', val:13, color:C.wheatDeep}, {key:'ua', name:'أوكرانيا', val:11, color:C.wheat}, {key:'au', name:'أستراليا', val:10, color:C.earth}, {key:'ar', name:'الأرجنتين', val:7, color:C.skySoft} ], ctx:'يحدث الانقلابتتصدّر روسيا المشهد. ولأوّل مرّةٍ منذ ما بعد الحرب، لم تعد الولايات المتحدة المُصدّر الأول عالمياً للقمح.' }, 2024: { rows: [ {key:'ru', name:'روسيا', val:25, color:C.rust}, {key:'eu', name:'فرنسا والاتحاد الأوروبي', val:17, color:C.olive}, {key:'au', name:'أستراليا', val:13, color:C.earth}, {key:'ca', name:'كندا', val:13, color:C.wheatDeep}, {key:'us', name:'الولايات المتحدة', val:11, color:C.sky}, {key:'ua', name:'أوكرانيا', val:8, color:C.wheat}, {key:'ar', name:'الأرجنتين', val:7, color:C.skySoft} ], ctx:'البحر الأسود في قلب العالمتُرسّخ روسيا صدارتها برُبع التجارة العالمية. فالأمن الغذائي لشمال أفريقيا والشرق الأوسط بات يَعبُر اليوم عبر موانئها.' } }; let currentYear = 1975; let initialized = false; function render(year, animate){ const d = data[year]; const maxVal = 50; ctx.innerHTML = d.ctx; rowsEl.innerHTML = ''; d.rows.forEach((r, i)=>{ const row = document.createElement('div'); row.className = 'viz-bars-row'; row.innerHTML = ` `; rowsEl.appendChild(row); const fill = row.querySelector('.viz-bars-fill'); setTimeout(()=>{ fill.style.width = (r.val/maxVal*100) + '%'; }, animate ? 80 + i*60 : 50); }); } tabs.forEach(tab=>{ tab.addEventListener('click', ()=>{ tabs.forEach(t=>t.classList.remove('active')); tab.classList.add('active'); currentYear = +tab.dataset.y; render(currentYear, true); }); }); ScrollTrigger.create({ trigger:'#ssqBars', start:'top 75%', onEnter:()=>{ if(!initialized){ render(1975, true); initialized = true; } } }); render(1975, false); })(); /* ============================================================ VIZ 04 — CHORD DIAGRAM → Cercle AGRANDI (rayons plus grands) → Labels davantage ÉLOIGNÉS (plus de superposition sur le visuel) ============================================================ */ (function(){ const svg = d3.select('#ssqChord'); if(svg.empty()) return; // viewBox élargi pour accueillir le cercle plus grand + labels bien à l'écart const W=1100, H=920, cx=W/2, cy=H/2; // Rayons AGRANDIS (était 200/182). Le cercle est plus visible. const outerR=280, innerR=258; // Labels DAVANTAGE ÉLOIGNÉS pour éviter toute superposition sur les arcs/rubans const labelR = outerR + 90; const leaderR1 = outerR + 8; const leaderR2 = outerR + 70; const names = ['الغاز', 'اليوريا / الآزوت', 'النفط', 'الفوسفاط', 'الكبريت', 'القمح', 'العملات الصعبة', 'الشحن البحري']; const colors = [C.sky, C.olive, C.ink, C.earthSoft, C.wheatDeep, C.wheat, C.rust, C.skySoft]; const matrix = [ [ 0, 28, 0, 0, 0, 0, 12, 4], [ 0, 0, 0, 0, 0, 30, 8, 6], [ 0, 0, 0, 0, 0, 0, 18, 22], [ 0, 0, 0, 0, 14, 0, 6, 4], [ 0, 0, 0, 0, 0, 0, 4, 3], [ 0, 0, 0, 0, 0, 0, 22, 14], [10, 10, 14, 8, 6, 20, 0, 6], [ 6, 4, 18, 4, 3, 12, 0, 0] ]; const chord = d3.chord().padAngle(0.05).sortSubgroups(d3.descending); const chords = chord(matrix); const g = svg.append('g').attr('transform',`translate(${cx},${cy})`); const arc = d3.arc().innerRadius(innerR).outerRadius(outerR); const ribbon = d3.ribbon().radius(innerR); const arcs = g.append('g').selectAll('path').data(chords.groups).enter().append('path') .attr('class','chord-arc') .attr('d',arc) .attr('fill',(d,i)=>colors[i]) .attr('stroke',C.paper).attr('stroke-width',1.5) .attr('opacity',0); const leaders = g.append('g').selectAll('line').data(chords.groups).enter().append('line') .attr('class','chord-leader') .each(d=>{ d.angle = (d.startAngle + d.endAngle)/2; }) .attr('x1', d=> Math.cos(d.angle - Math.PI/2)*leaderR1) .attr('y1', d=> Math.sin(d.angle - Math.PI/2)*leaderR1) .attr('x2', d=> Math.cos(d.angle - Math.PI/2)*leaderR2) .attr('y2', d=> Math.sin(d.angle - Math.PI/2)*leaderR2) .attr('opacity',0); const labels = g.append('g').selectAll('text').data(chords.groups).enter().append('text') .each(d=>{ d.angle = (d.startAngle + d.endAngle)/2; }) .attr('x', d=> Math.cos(d.angle - Math.PI/2)*labelR) .attr('y', d=> Math.sin(d.angle - Math.PI/2)*labelR) .attr('dy','.35em') .attr('text-anchor', d=>{ const c = Math.cos(d.angle - Math.PI/2); return Math.abs(c) < 0.25 ? 'middle' : (c > 0 ? 'start' : 'end'); }) .attr('fill',(d,i)=>colors[i]) .attr('opacity',0) .text((d,i)=>names[i]); const bands = g.append('g').selectAll('path').data(chords).enter().append('path') .attr('class','chord-band') .attr('d',ribbon) .attr('fill',(d)=>colors[d.source.index]) .attr('fill-opacity',0.55) .attr('stroke',C.paper).attr('stroke-width',0.5) .attr('opacity',0); arcs.on('click', function(e, d){ const idx = d.index; bands.classed('fade', b => b.source.index !== idx && b.target.index !== idx); }); svg.on('click', function(e){ if(e.target.tagName === 'svg') bands.classed('fade', false); }); ScrollTrigger.create({ trigger:'#ssqChord', start:'top 75%', onEnter:()=>{ arcs.transition().duration(800).delay((d,i)=>i*80).attr('opacity',1); leaders.transition().duration(500).delay((d,i)=>500+i*80).attr('opacity',.6); labels.transition().duration(600).delay((d,i)=>500+i*80).attr('opacity',1); bands.transition().duration(1000).delay(1200).attr('opacity',1); } }); })(); /* ============================================================ VIZ 05 — STEPS CARDS + ROULETTE CASH MACHINE ============================================================ */ (function(){ const cards = document.querySelectorAll('.viz-step-card'); if(!cards.length) return; function buildRoller(el){ const target = String(parseInt(el.dataset.target, 10)); const prefix = el.dataset.prefix || ''; const suffix = el.dataset.suffix || ''; el.innerHTML = ''; if(prefix){ const ps = document.createElement('span'); ps.className = 'vsm-static'; ps.textContent = prefix; el.appendChild(ps); } const rollers = []; for(let i=0;i{ const s = document.createElement('span'); s.textContent = n; inner.appendChild(s); }); wrap.appendChild(inner); el.appendChild(wrap); rollers.push({wrap, inner, digit, position: 30 + digit, totalSteps: 40 + digit, colIndex: i}); } if(suffix){ const ss = document.createElement('span'); ss.className = 'vsm-static'; ss.textContent = suffix; el.appendChild(ss); } return rollers; } function spinRollers(rollers){ rollers.forEach((r, idx)=>{ r.inner.style.transition = 'none'; r.inner.style.transform = 'translateY(0)'; void r.inner.offsetHeight; const duration = 1.6 + idx * 0.45; const delay = idx * 0.08; r.inner.style.transition = `transform ${duration}s cubic-bezier(.18,.65,.2,1) ${delay}s`; r.inner.style.transform = `translateY(-${r.position}em)`; }); } const allRollers = []; document.querySelectorAll('.vsm-num[data-roll]').forEach(el=>{ const rollers = buildRoller(el); allRollers.push({card: el.closest('.viz-step-card'), rollers}); }); const observer = new IntersectionObserver((es)=>{ es.forEach(e=>{ if(e.isIntersecting){ cards.forEach(c=>{ const delay = parseInt(c.dataset.delay||'0', 10); setTimeout(()=>{ c.classList.add('in'); const item = allRollers.find(a=>a.card === c); if(item){ setTimeout(()=> spinRollers(item.rollers), 650); } }, delay); }); observer.disconnect(); } }); },{threshold:.15}); observer.observe(cards[0]); })(); /* ============================================================ VIZ 06 — GHOST LINES ============================================================ */ (function(){ const svg = d3.select('#ssqGhost'); if(svg.empty()) return; const W=1100, H=480, m={t:30,r:140,b:50,l:60}; const years = d3.range(2000, 2026); const realPoints = [ [2000,13.5],[2001,11.2],[2002,9.5],[2003,16.8],[2004,15.2], [2005,12.8],[2006,14.5],[2007,11.6],[2008,7.2],[2009,18.3], [2010,15.5],[2011,15.8],[2012,12.4],[2013,17.1],[2014,11.9], [2015,14.8],[2016,8.2],[2017,12.5],[2018,17.6],[2019,15.4], [2020,11.8],[2021,9.7],[2022,8.5],[2023,7.8],[2024,13.2],[2025,14.5] ]; const morocco = years.map((y,i)=> [y, 11 + i*0.28 + Math.sin(i*0.4)*1.2]); const egypt = years.map((y,i)=> [y, 20 + i*0.45 + Math.sin(i*0.3)*0.8]); const irr = years.map((y,i)=> [y, 14 + i*0.48 + Math.sin(i*0.5)*0.7]); const datasets = { real: {data:realPoints, color:C.rust, label:'تونس الفعلية ~14 ق/هـ'}, ma: {data:morocco, color:C.wheatDeep, label:'المسار المغربي ~18'}, eg: {data:egypt, color:C.sky, label:'المسار المصري ~31'}, irr: {data:irr, color:C.olive, label:'ريّ معتدل ~26'} }; const active = new Set(['real']); const x = d3.scaleLinear().domain([2000, 2025]).range([m.l, W-m.r]); const y = d3.scaleLinear().domain([5, 35]).range([H-m.b, m.t]); svg.append('g').attr('transform',`translate(0,${H-m.b})`) .call(d3.axisBottom(x).tickFormat(d3.format('d')).ticks(6)) .selectAll('text').attr('font-family','JetBrains Mono, monospace').attr('font-size',10).attr('fill',C.mute); svg.append('g').attr('transform',`translate(${m.l},0)`) .call(d3.axisLeft(y).ticks(5).tickFormat(d=>d+' ق/هـ')) .selectAll('text').attr('font-family','JetBrains Mono, monospace').attr('font-size',10).attr('fill',C.mute); svg.selectAll('.tick line, .domain').attr('stroke',C.ink).attr('stroke-opacity',.15); y.ticks(5).forEach(t=>{ svg.append('line').attr('x1',m.l).attr('x2',W-m.r).attr('y1',y(t)).attr('y2',y(t)) .attr('stroke',C.ink).attr('stroke-opacity',.06); }); const line = d3.line().x(d=>x(d[0])).y(d=>y(d[1])).curve(d3.curveMonotoneX); const paths = {}; const labels = {}; Object.entries(datasets).forEach(([k, ds])=>{ const isReal = k === 'real'; const path = svg.append('path').datum(ds.data) .attr('fill','none').attr('stroke',ds.color) .attr('stroke-width', isReal ? 3 : 2) .attr('stroke-dasharray', isReal ? null : '4 5') .attr('opacity', isReal ? 0.95 : 0) .attr('d', line); const len = path.node().getTotalLength(); paths[k] = {path, len, isReal}; const last = ds.data[ds.data.length-1]; labels[k] = svg.append('text') .attr('x', x(last[0])+8).attr('y', y(last[1])+4) .attr('font-family','Cormorant Garamond, serif').attr('font-style','italic') .attr('font-size',12).attr('fill',ds.color).attr('font-weight',600) .attr('opacity', isReal ? 1 : 0).text(ds.label); }); ScrollTrigger.create({ trigger:'#ssqGhost', start:'top 75%', onEnter:()=>{ paths.real.path.attr('stroke-dasharray', paths.real.len+' '+paths.real.len) .attr('stroke-dashoffset', paths.real.len) .transition().duration(1800).attr('stroke-dashoffset', 0) .on('end',()=>paths.real.path.attr('stroke-dasharray', null)); } }); document.querySelectorAll('.viz-ghost-toggle').forEach(btn=>{ btn.addEventListener('click',()=>{ const k = btn.dataset.line; if(active.has(k)){ if(k==='real') return; active.delete(k); btn.classList.remove('active'); paths[k].path.transition().duration(400).attr('opacity',0); labels[k].transition().duration(400).attr('opacity',0); } else { active.add(k); btn.classList.add('active'); paths[k].path.attr('stroke-dasharray', '4 5') .attr('opacity',0) .transition().duration(900).attr('opacity',.75); labels[k].transition().duration(600).delay(400).attr('opacity',1); } }); }); })(); /* ============================================================ VIZ 07 — STRIP HEATMAP ============================================================ */ (function(){ const stripBox = document.getElementById('ssqStrip'); const rowsEl = document.getElementById('stripRows'); const yearsEl = document.getElementById('stripYears'); const tip = document.getElementById('stripTip'); const legendEl = document.getElementById('stripLegend'); if(!stripBox) return; const regions = [ {name:'الشمال الغربي', base:0.20, slope:0.025}, {name:'الوطن القبلي', base:0.30, slope:0.030}, {name:'الوسط', base:0.45, slope:0.035}, {name:'الجنوب', base:0.65, slope:0.020} ]; const years = d3.range(2010, 2026); const colorScale = d3.scaleSequential( t => d3.interpolateRgb.gamma(2.2)('#F4EFE6', '#A63D3D')(t) ).domain([0, 1]); function stress(region, year, idx){ const drought = (year===2016||year===2022||year===2023||year===2024) ? 0.18 : 0; const noise = (Math.sin(idx*1.7 + region.base*10)*0.5 + 0.5)*0.08; return Math.min(1, Math.max(0, region.base + region.slope*idx + drought + noise)); } const allCells = []; regions.forEach((reg, ri)=>{ const row = document.createElement('div'); row.className = 'viz-strip-row'; row.innerHTML = `${reg.name}
`;
rowsEl.appendChild(row);
const cellsBox = row.querySelector('.viz-strip-cells');
years.forEach((yr, yi)=>{
const s = stress(reg, yr, yi);
const cell = document.createElement('div');
cell.className = 'viz-strip-cell';
cell.style.background = colorScale(s);
cell.dataset.region = reg.name;
cell.dataset.year = yr;
cellsBox.appendChild(cell);
allCells.push(cell);
cell.addEventListener('mouseenter', ()=>{
const rect = stripBox.getBoundingClientRect();
const cr = cell.getBoundingClientRect();
tip.innerHTML = `${reg.name} · ${yr}الإجهاد المائي: ${Math.round(s*100)}%`;
tip.style.left = (cr.left - rect.left + cr.width/2) + 'px';
tip.style.top = (cr.top - rect.top - 5) + 'px';
tip.style.opacity = 1;
});
cell.addEventListener('mouseleave', ()=>tip.style.opacity = 0);
});
});
years.forEach(y=>{
const yl = document.createElement('div');
yl.textContent = (y % 5 === 0 || y === 2025) ? y : '';
yearsEl.appendChild(yl);
});
[0, 0.25, 0.5, 0.75, 1].forEach(t=>{
const s = document.createElement('span');
s.style.background = colorScale(t);
legendEl.appendChild(s);
});
ScrollTrigger.create({
trigger:'#ssqStrip', start:'top 78%',
onEnter:()=>{
allCells.forEach((c,i)=>{
setTimeout(()=>c.classList.add('in'), i*10);
});
}
});
})();
/* ============================================================
VIZ 08 — WHAT-IF SIMULATOR
============================================================ */
(function(){
const sWheat = document.getElementById('sWheat');
const sGas = document.getElementById('sGas');
const sSub = document.getElementById('sSub');
const sHarv = document.getElementById('sHarv');
if(!sWheat) return;
const vWheat = document.getElementById('vWheat');
const vGas = document.getElementById('vGas');
const vSub = document.getElementById('vSub');
const vHarv = document.getElementById('vHarv');
const out = document.getElementById('simuBudget');
const lbl = document.getElementById('simuLbl');
const needle = document.getElementById('gaugeNeedle');
const flash = document.getElementById('simuFlash');
let prevBudget = null;
let prevTension = null;
function update(){
const wheat = +sWheat.value;
const gas = +sGas.value;
const sub = +sSub.value;
const harv = +sHarv.value;
vWheat.textContent = wheat;
vGas.textContent = gas;
vSub.textContent = sub;
vHarv.textContent = harv;
const deficit = Math.max(0, 37 - harv);
const importCost = deficit * wheat * 100 * 3.1 / 1000;
const fertCost = gas * 60;
const subBudget = Math.round((importCost + fertCost) * (sub/100) * 1.5 + 1200);
if(prevBudget !== null && Math.abs(subBudget - prevBudget) > 5){
const startVal = prevBudget;
const delta = subBudget - startVal;
const duration = 400;
const t0 = performance.now();
function step(t){
const k = Math.min(1, (t - t0)/duration);
const eased = 1 - Math.pow(1-k, 3);
const v = Math.round(startVal + delta*eased);
out.textContent = v.toLocaleString('fr-FR').replace(/,/g,' ') + ' مليون دينار';
if(k < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
} else {
out.textContent = subBudget.toLocaleString('fr-FR').replace(/,/g,' ') + ' مليون دينار';
}
prevBudget = subBudget;
const tension = Math.min(100, Math.max(0,
((wheat-180)/320)*38 + ((gas-3)/22)*30 + (sub/100)*15 + ((22-harv)/14)*17
));
const angle = -90 + (tension/100)*180;
needle.style.transform = `rotate(${angle}deg)`;
if(prevTension !== null && Math.abs(tension - prevTension) > 8){
flash.classList.add('on');
setTimeout(()=>flash.classList.remove('on'), 150);
}
prevTension = tension;
out.className = 'viz-simu-result';
if(tension < 35){
out.classList.add('safe');
lbl.textContent = 'عبءٌ قابلٌ للتحمّل. الهوامش الميزانياتية محفوظة.';
} else if(tension < 65){
out.classList.add('warn');
lbl.textContent = 'توتّرٌ ملموس. مفاضلاتٌ ميزانياتية ضرورية.';
} else {
out.classList.add('danger');
lbl.textContent = 'منطقة أزمة. خطرٌ اجتماعي وماليّ مرتفع.';
}
}
[
{input:sWheat, val:vWheat},
{input:sGas, val:vGas},
{input:sSub, val:vSub},
{input:sHarv, val:vHarv}
].forEach(({input, val})=>{
input.addEventListener('input', ()=>{
val.parentElement.classList.add('pulse');
setTimeout(()=>val.parentElement.classList.remove('pulse'), 350);
update();
});
});
update();
ScrollTrigger.create({
trigger:'#ssqSimu', start:'top 75%',
onEnter:()=>{
if(reduced) return;
needle.style.transform = 'rotate(-90deg)';
setTimeout(()=>{ needle.style.transform = 'rotate(90deg)'; }, 400);
setTimeout(update, 1400);
}
});
})();
/* ============================================================
VIZ 09 — BEESWARM
============================================================ */
(function(){
const svg = d3.select('#ssqBee');
if(svg.empty()) return;
const W=1100, H=460, m={t:50,r:60,b:60,l:120};
const events = [
{date:2010.8, sector:'الحبوب', amp:6, type:'مناخ', name:'جفاف روسيا',
desc:'حظرٌ روسيّ على تصدير القمح. وارتفاعٌ صاروخيّ في الأسعار العالمية.'},
{date:2011.0, sector:'سياسة', amp:5, type:'نزاع', name:'الربيع العربي',
desc:'موجة ارتفاعٍ في أسعار المواد الغذائية، محرّكٌ جزئيّ للانتفاضات.'},
{date:2014.3, sector:'طاقة', amp:5, type:'جيوسياسة',name:'ضمّ القرم',
desc:'أولى التوترات في البحر الأسود. واضطرابٌ في شحن الحبوب.'},
{date:2018.5, sector:'أسمدة', amp:3, type:'سوق', name:'توتّر اليوريا الصينية',
desc:'قيودٌ صينية على تصدير الأسمدة.'},
{date:2020.2, sector:'لوجستيك',amp:8, type:'جائحة', name:'كوفيد-19',
desc:'انهيار الشحن العالمي. وانقطاعاتٌ في سلاسل التزويد.'},
{date:2021.7, sector:'طاقة', amp:7, type:'سوق', name:'الصدمة الغازية الأوروبية',
desc:'سعر الغاز يتضاعف خمس مرّات. واليوريا الأوروبية تنهار.'},
{date:2022.15,sector:'الحبوب', amp:10,type:'نزاع', name:'غزو أوكرانيا',
desc:'البحر الأسود محاصَر. وأسعار القمح تبلغ مستوياتٍ تاريخية.'},
{date:2022.6, sector:'أسمدة', amp:8, type:'نزاع', name:'أزمة الأسمدة العالمية',
desc:'نقصٌ في اليوريا والبوتاس. والفلاحون يُقلّصون كميات الاستعمال.'},
{date:2023.4, sector:'لوجستيك',amp:6, type:'جيوسياسة',name:'البحر الأحمر / الحوثيون',
desc:'هجماتٌ على سفن الشحن. والتحويل عبر رأس الرجاء الصالح.'},
{date:2024.1, sector:'سياسة', amp:5, type:'سوق', name:'قيود الهند',
desc:'الهند تُقيّد صادراتها من الحبوب والأسمدة.'},
{date:2024.7, sector:'طاقة', amp:6, type:'جيوسياسة',name:'توترات إيران',
desc:'ضغطٌ متجدّد على مضيق هرمز. وعلاوات مخاطر.'},
{date:2025.3, sector:'أسمدة', amp:5, type:'سوق', name:'تقييد فوسفاط المغرب',
desc:'إعادة التفاوض على عقود تصدير الفوسفاط.'},
{date:2026.05,sector:'لوجستيك',amp:9, type:'نزاع', name:'إغلاق مضيق هرمز',
desc:'صدمةٌ متراكمة على الغاز واليوريا والشحن وأسعار المواد الغذائية.'}
];
const sectors = ['الحبوب','أسمدة','طاقة','لوجستيك','سياسة'];
const colorByType = {
'مناخ':C.olive, 'نزاع':C.rust, 'جيوسياسة':C.wheatDeep,
'سوق':C.sky, 'جائحة':C.earth
};
const x = d3.scaleLinear().domain([2010, 2026.5]).range([m.l, W-m.r]);
const y = d3.scalePoint().domain(sectors).range([m.t+20, H-m.b]).padding(0.5);
const r = d3.scaleSqrt().domain([1,10]).range([8, 30]);
svg.append('g').attr('transform',`translate(0,${H-m.b+10})`)
.call(d3.axisBottom(x).tickFormat(d3.format('d')).ticks(8))
.selectAll('text').attr('font-family','JetBrains Mono, monospace').attr('font-size',10).attr('fill',C.mute);
sectors.forEach(s=>{
svg.append('text').attr('x',m.l-15).attr('y',y(s)+5).attr('text-anchor','end')
.attr('font-family','Cormorant Garamond, serif').attr('font-style','italic')
.attr('font-size',13).attr('fill',C.ink).attr('font-weight',600).text(s);
svg.append('line').attr('x1',m.l).attr('x2',W-m.r).attr('y1',y(s)).attr('y2',y(s))
.attr('stroke',C.ink).attr('stroke-opacity',.06);
});
svg.selectAll('.tick line, .domain').attr('stroke',C.ink).attr('stroke-opacity',.15);
const legG = svg.append('g').attr('transform',`translate(${m.l},10)`);
let lx = 0;
Object.entries(colorByType).forEach(([k,v])=>{
legG.append('circle').attr('cx',lx).attr('cy',6).attr('r',5).attr('fill',v);
legG.append('text').attr('x',lx+10).attr('y',10)
.attr('font-family','Inter, sans-serif').attr('font-size',11).attr('fill',C.ink).text(k);
lx += k.length*7 + 30;
});
const nodes = events.map(e=>({
...e,
x0: x(e.date), y0: y(e.sector)
}));
const sim = d3.forceSimulation(nodes)
.force('x', d3.forceX(d=>d.x0).strength(0.7))
.force('y', d3.forceY(d=>d.y0).strength(0.4))
.force('collide', d3.forceCollide(d=>r(d.amp)+2))
.stop();
for(let i=0;i<200;i++) sim.tick();
const dotGroups = svg.selectAll('.bee-group').data(nodes).enter().append('g')
.attr('class','bee-group')
.attr('transform',d=>`translate(${d.x},${d.y})`);
const dots = dotGroups.append('circle')
.attr('class','bee-dot')
.attr('r',0)
.attr('fill',d=>colorByType[d.type])
.attr('fill-opacity',.85)
.attr('stroke',C.paper).attr('stroke-width',1.5);
const tip = document.getElementById('beeTip');
const beeBox = svg.node().parentNode;
dotGroups.on('mouseenter', function(e,d){
const rect = beeBox.getBoundingClientRect();
const transform = this.getAttribute('transform');
const match = transform.match(/translate\(([\d.-]+),([\d.-]+)\)/);
const cx_node = parseFloat(match[1]);
const cy_node = parseFloat(match[2]);
const svgEl = svg.node();
const svgRect = svgEl.getBoundingClientRect();
const scaleX = svgRect.width / W;
const scaleY = svgRect.height / H;
const px = svgRect.left - rect.left + cx_node * scaleX;
const py = svgRect.top - rect.top + cy_node * scaleY;
tip.innerHTML = `${d.name}${d.desc}${d.type} · الشدّة ${d.amp}/10`;
tip.style.left = px + 'px';
tip.style.top = (py - r(d.amp) - 6) + 'px';
tip.style.opacity = 1;
}).on('mouseleave',()=>tip.style.opacity=0);
ScrollTrigger.create({
trigger:'#ssqBee', start:'top 75%',
onEnter:()=>{
dots.transition().duration(800).delay((d,i)=>i*70)
.attr('r', d=>r(d.amp));
}
});
})();
/* ============================================================
VIZ 10 — SUNBURST
============================================================ */
(function(){
const svg = d3.select('#ssqBurst');
if(svg.empty()) return;
const W = 1000, H = 1000, cx = W/2, cy = H/2;
const R_INNER = 130;
const R_MID = 270;
const R_OUTER = 460;
const data = {
name:'الصمود',
children:[
{name:'المخزونات الاستراتيجية', desc:'القدرات الوطنية للتخزين، التحجيم، التدوير.', color:C.wheat, children:[
{name:'صوامع مُحدَّثة', desc:'تحديث وتوسيع قدرات التخزين المينائية.'},
{name:'مخزون عازل', desc:'تحجيمٌ أدنى مضادّ للصدمات للحبوب الاستراتيجية.'},
{name:'شبكة جهوية', desc:'لامركزية المخزونات على كامل التراب الوطني.'},
{name:'التتبّع', desc:'منظومة معلوماتٍ آنية حول المخزونات العمومية والخاصة.'}
]},
{name:'التنويع', desc:'تعدّد المزوّدين والمسالك التجارية.', color:C.olive, children:[
{name:'تعدّد المزوّدين', desc:'الحدّ من التبعية لممرٍّ واحد (البحر الأسود).'},
{name:'مسالك بديلة', desc:'تنويعٌ لوجستي: الأطلسي، البحر الأحمر، المتوسط.'},
{name:'عقود طويلة المدى', desc:'اتفاقاتٌ متعدّدة السنوات بضماناتٍ على الكميات.'},
{name:'التحوّط', desc:'أدوات تغطيةٍ في الأسواق الآجلة.'}
]},
{name:'المدخلات الحرجة', desc:'الأسمدة، الكبريت، البذور، التجهيزات.', color:C.earth, children:[
{name:'صناعة الأسمدة', desc:'إعادة بناء القدرات التونسية لإنتاج الأسمدة.'},
{name:'الكبريت والبوتاس', desc:'تأمين التزويد بالمدخلات المنجمية.'},
{name:'بذور محلية', desc:'أصنافٌ صامدة في المناخ شبه القاحل.'},
{name:'صيانة المجمع الكيميائي', desc:'الاستثمار في أدوات المجمع الكيميائي التونسي.'}
]},
{name:'المناخ والمياه', desc:'التكيّف مع ندرة المياه والجفاف.', color:C.sky, children:[
{name:'الريّ بالتنقيط', desc:'تعميم التقنيات المُقتصِدة في الماء.'},
{name:'تحلية المياه', desc:'الجمع بين التحوّل الطاقي والأمن المائي.'},
{name:'أصناف مقاومة', desc:'بحثٌ زراعيّ حول أصناف القمح الصامدة للجفاف.'},
{name:'تأمين مناخي', desc:'آليات لتشارك المخاطر المناخية.'}
]},
{name:'الطاقة الزراعية', desc:'إزالة الكربون وتحقيق الاستقلالية الطاقية للمستثمرات.', color:C.wheatDeep, children:[
{name:'الطاقة الشمسية الزراعية', desc:'نشر الألواح الكهروضوئية في المستثمرات الفلاحية.'},
{name:'النشادر الأخضر', desc:'إنتاجٌ محليّ لأسمدةٍ آزوتية منخفضة الكربون.'},
{name:'النجاعة الطاقية', desc:'تقليص الكثافة الطاقية للسلاسل الزراعية.'},
{name:'تخزين محلي', desc:'بطاريات واستهلاكٌ ذاتيٌّ لامركزي.'}
]},
{name:'العدالة الاجتماعية', desc:'القدرة الشرائية، استهداف الدعم، التماسك الترابي.', color:C.rust, children:[
{name:'دعمٌ مُستهدَف', desc:'إصلاح صندوق الدعم لصالح الفئات الأضعف.'},
{name:'تحويلاتٌ مباشرة', desc:'الاستعاضة بمساعداتٍ نقديةٍ مشروطة.'},
{name:'القدرة الشرائية', desc:'سياسة أجورٍ مرتبطة بالأمن الغذائي.'},
{name:'التنوّع التغذوي', desc:'تنويعٌ بروتيني يتجاوز الحبوب المدعومة.'}
]}
]
};
const root = d3.hierarchy(data).sum(d=>d.children ? 0 : 1);
const partition = d3.partition().size([Math.PI*2, R_OUTER - R_INNER]);
partition(root);
root.descendants().forEach(d=>{
d.y0 += R_INNER;
d.y1 += R_INNER;
});
const arc = d3.arc()
.startAngle(d=>d.x0)
.endAngle(d=>d.x1)
.innerRadius(d=>{
if(d.depth === 0) return 0;
if(d.depth === 1) return R_INNER;
return R_MID;
})
.outerRadius(d=>{
if(d.depth === 0) return 0;
if(d.depth === 1) return R_MID;
return R_OUTER;
})
.padAngle(0.008)
.padRadius(R_INNER);
const g = svg.append('g').attr('transform',`translate(${cx},${cy})`);
function colorFor(d){
if(d.depth === 0) return 'transparent';
let p = d;
while(p.depth > 1) p = p.parent;
const base = p.data.color || C.wheat;
if(d.depth === 1) return base;
return d3.color(base).brighter(0.7).formatHex();
}
const arcs = g.selectAll('path').data(root.descendants().filter(d=>d.depth>0)).enter()
.append('path')
.attr('class','burst-arc')
.attr('d', arc)
.attr('fill', d=>colorFor(d))
.attr('stroke', C.paper).attr('stroke-width', 2)
.attr('opacity', 0);
const defs = svg.append('defs');
root.descendants().filter(d=>d.depth===1).forEach((d,i)=>{
const midR = (R_INNER + R_MID)/2;
const mid = (d.x0 + d.x1)/2;
const flip = mid > Math.PI*0.5 && mid < Math.PI*1.5;
const padA = 0.015;
const startA = (flip ? d.x1 - padA : d.x0 + padA) - Math.PI/2;
const endA = (flip ? d.x0 + padA : d.x1 - padA) - Math.PI/2;
const sx = Math.cos(startA)*midR;
const sy = Math.sin(startA)*midR;
const ex = Math.cos(endA)*midR;
const ey = Math.sin(endA)*midR;
const sweep = flip ? 0 : 1;
const pathId = `burstPath1_${i}`;
defs.append('path')
.attr('id', pathId)
.attr('d', `M ${sx} ${sy} A ${midR} ${midR} 0 0 ${sweep} ${ex} ${ey}`);
g.append('text')
.attr('class','burst-lbl-1')
.attr('font-family','Cormorant Garamond, serif')
.attr('font-style','italic').attr('font-weight',700)
.attr('font-size', 19)
.attr('fill', C.paper)
.attr('opacity', 0)
.style('letter-spacing','0.02em')
.append('textPath')
.attr('href', `#${pathId}`)
.attr('startOffset','50%')
.attr('text-anchor','middle')
.text(d.data.name);
});
const lvl2 = root.descendants().filter(d=>d.depth===2);
lvl2.forEach(d=>{
const midAngle = (d.x0 + d.x1)/2;
const r = (R_MID + R_OUTER)/2;
const flip = midAngle > Math.PI && midAngle < Math.PI*2;
const rotation = midAngle * 180/Math.PI - 90;
const txt = g.append('text')
.attr('class','burst-lbl-2')
.attr('transform', `rotate(${rotation}) translate(${r},0) ${flip?'rotate(180)':''}`)
.attr('text-anchor','middle')
.attr('font-family','Inter, sans-serif')
.attr('font-weight',500)
.attr('font-size', 12)
.attr('fill', C.ink)
.attr('opacity', 0);
const words = d.data.name.split(' ');
if(words.length > 1 && d.data.name.length > 12){
const mid = Math.ceil(words.length/2);
const l1 = words.slice(0, mid).join(' ');
const l2 = words.slice(mid).join(' ');
txt.append('tspan').attr('x', 0).attr('dy','-0.4em').text(l1);
txt.append('tspan').attr('x', 0).attr('dy','1.1em').text(l2);
} else {
txt.attr('dy','.35em').text(d.data.name);
}
});
const center = document.getElementById('burstCenter');
arcs.on('mouseenter', function(e,d){
center.querySelector('h5').textContent = d.data.name;
center.querySelector('p').textContent = d.data.desc || '';
center.querySelector('h5').style.color = d.depth===1 ? colorFor(d) : C.wheatDeep;
}).on('mouseleave',()=>{
center.querySelector('h5').innerHTML = 'الصمودالغذائي'; center.querySelector('h5').style.color = C.ink; center.querySelector('p').textContent = 'مرّر المؤشّر فوق عمودٍ لاستكشاف روافعه.'; }); ScrollTrigger.create({ trigger:'#ssqBurst', start:'top 75%', onEnter:()=>{ arcs.transition().duration(900).delay((d,i)=>d.depth===1 ? i*100 : 600+i*25) .attr('opacity',1); g.selectAll('.burst-lbl-1').transition().duration(600).delay(1200).attr('opacity',1); g.selectAll('.burst-lbl-2').transition().duration(600).delay(1600).attr('opacity',1); } }); })(); })();