Eir
Grid
Transmission System Operator — Republic of Ireland
Demand
—
MW
—
Generation
—
MW
—
Renewables
—
%
Net Export
—
MW
Frequency
—
Hz
Live
—
Connecting to EirGrid Smart Grid Dashboard API…
—
Generation mix
Wind
42%
Gas CCGT
24%
Hydro/pump
14%
Interconn.
12%
Gas OCGT
8%
24h generation — wind vs demand
Loading live data…
Wind
Demand
Gas
System overview
Wind gen.
—
MW
↑ offshore + onshore
Demand
—
MW
System load
Export
—
MW
Net via ICs
Frequency
—
Hz
Nominal ±0.04
Interconnectors
East–West IC
Woodland → Deeside, Wales
EXPORT
— / 500 MW
Greenlink IC
Great Island → Pembroke, Wales
EXPORT
— / 500 MW
Moyle IC
Ballycronan → Auchencrosh, SC
REDUCED
— / 500 MW
Celtic IC
Knockraha → La Martyre, FR
IN DEV
0 / 700 MW
Major generation
Moneypoint
Coal → gas conversion · Clare
915
MW
Poolbeg CCGT
Gas combined cycle · Dublin
468
MW
Aghada CCGT
Gas combined cycle · Cork
428
MW
Turlough Hill
Pumped hydro storage · Wicklow
292
MW
Arklow Bank
Offshore wind · Wexford coast
470
MW
Network status
Moyle reduced capacity
Maintenance — full ops expected 14:00 IST
High wind generation
SW Atlantic — 68% renewables penetration
400 kV ring nominal
All transmission corridors stable
● Telemetry
East–West IC
+500 MW EXPORT
Greenlink IC
+250 MW EXPORT
Moyle IC
⚠ REDUCED — 250 MW
Frequency
50.01 Hz ✓
Wind output
2,340 MW ↑
System demand
4,820 MW
Renewables
68% ↑
Celtic IC
IN DEVELOPMENT — 2027
Moneypoint
915 MW — coal/gas conv.
400 kV ring
ALL CORRIDORS NOMINAL
Net export balance
+750 MW
East–West IC
+500 MW EXPORT
Greenlink IC
+250 MW EXPORT
Moyle IC
⚠ REDUCED — 250 MW
Frequency
50.01 Hz ✓
Wind output
2,340 MW ↑
System demand
4,820 MW
Renewables
68% ↑
Celtic IC
IN DEVELOPMENT — 2027
Moneypoint
915 MW
400 kV ring
NOMINAL
Net export balance
+750 MW
// ── Geographic data ─────────────────────────────────────────────── const IC_NODES = { woodland: {lon:-6.87,lat:53.63,label:'Woodland', col:'#3fffc0',sz:11}, deeside: {lon:-3.07,lat:53.20,label:'Deeside', col:'#3fffc0',sz:9}, greatisland: {lon:-6.97,lat:52.33,label:'Gt. Island', col:'#3fffc0',sz:11}, pembroke: {lon:-4.93,lat:51.68,label:'Pembroke', col:'#3fffc0',sz:9}, ballycronan: {lon:-5.97,lat:54.78,label:'Ballycronan', col:'#ffcc44',sz:10}, auchencrosh: {lon:-4.92,lat:55.10,label:'Auchencrosh', col:'#ffcc44',sz:9}, knockraha: {lon:-8.27,lat:51.93,label:'Knockraha', col:'#b8e8ff',sz:10}, lamartyre: {lon:-4.28,lat:48.42,label:'La Martyre', col:'#b8e8ff',sz:9}, }; const ICS = [ {from:'woodland', to:'deeside', col:'#3fffc0',w:2.2,status:'op', mw:'▶ 500 MW',lbl:'EAST-WEST IC', dl:'0s'}, {from:'greatisland',to:'pembroke', col:'#3fffc0',w:1.8,status:'op', mw:'▶ 250 MW',lbl:'GREENLINK IC', dl:'-.5s'}, {from:'ballycronan',to:'auchencrosh',col:'#ffcc44',w:1.8,status:'red',mw:'▶ 250 MW',lbl:'MOYLE IC', dl:'-.3s'}, {from:'knockraha', to:'lamartyre', col:'#b8e8ff',w:1.5,status:'dev',mw:'DEV 2027', lbl:'CELTIC IC', dl:'0s'}, ]; const SUBSTATIONS = { aghada: {lon:-8.24,lat:51.82,v:400,label:'Aghada', type:'gen', mw:428, fuel:'gas'}, moneypoint: {lon:-9.35,lat:52.62,v:400,label:'Moneypoint', type:'gen', mw:915, fuel:'coal'}, shannon: {lon:-8.63,lat:52.68,v:400,label:'Shannon', type:'sub', mw:0, fuel:null}, poolbeg: {lon:-6.19,lat:53.33,v:400,label:'Poolbeg', type:'gen', mw:468, fuel:'gas'}, northwall: {lon:-6.26,lat:53.36,v:220,label:'N.Wall', type:'gen', mw:182, fuel:'gas'}, turloughhill:{lon:-6.51,lat:53.01,v:400,label:'Turlough Hill',type:'gen',mw:292, fuel:'hydro'}, dunstown: {lon:-6.70,lat:53.35,v:400,label:'Dunstown', type:'sub', mw:0, fuel:null}, woodland_s: {lon:-6.87,lat:53.63,v:400,label:'Woodland', type:'ic', mw:500, fuel:null}, clonsast: {lon:-7.22,lat:53.22,v:400,label:'Clonsast', type:'sub', mw:0, fuel:null}, portlaoise: {lon:-7.30,lat:53.04,v:400,label:'Portlaoise', type:'sub', mw:0, fuel:null}, ballycliric: {lon:-8.41,lat:53.30,v:400,label:'Ballycliric', type:'sub', mw:0, fuel:null}, flagford: {lon:-8.09,lat:53.88,v:400,label:'Flagford', type:'sub', mw:0, fuel:null}, louth: {lon:-6.53,lat:53.93,v:400,label:'Louth', type:'sub', mw:0, fuel:null}, bellacorick: {lon:-9.69,lat:54.10,v:220,label:'Bellacorick', type:'wind',mw:170, fuel:'wind'}, screeb: {lon:-9.58,lat:53.33,v:220,label:'Screeb', type:'sub', mw:0, fuel:null}, cashla: {lon:-9.17,lat:53.26,v:220,label:'Cashla', type:'sub', mw:0, fuel:null}, athlone: {lon:-7.95,lat:53.42,v:220,label:'Athlone', type:'sub', mw:0, fuel:null}, arklow: {lon:-5.98,lat:52.80,v:220,label:'Arklow', type:'wind',mw:470, fuel:'wind'}, gt_island_s: {lon:-6.97,lat:52.33,v:400,label:'Gt.Island', type:'ic', mw:250, fuel:null}, knockraha_s: {lon:-8.27,lat:51.93,v:400,label:'Knockraha', type:'ic', mw:0, fuel:null}, cushlanig: {lon:-8.07,lat:52.08,v:220,label:'Cushlaning', type:'sub', mw:0, fuel:null}, }; const TX400 = [ ['woodland_s','dunstown'],['dunstown','poolbeg'],['poolbeg','gt_island_s'], ['gt_island_s','knockraha_s'],['knockraha_s','aghada'], ['aghada','moneypoint'],['moneypoint','ballycliric'],['ballycliric','flagford'], ['flagford','louth'],['louth','woodland_s'], ['dunstown','clonsast'],['clonsast','portlaoise'],['portlaoise','aghada'], ['portlaoise','gt_island_s'],['dunstown','turloughhill'],['turloughhill','clonsast'], ]; const TX220 = [ ['moneypoint','shannon'],['shannon','athlone'],['athlone','flagford'], ['athlone','cashla'],['cashla','screeb'],['screeb','bellacorick'], ['bellacorick','flagford'],['clonsast','northwall'],['northwall','poolbeg'], ['portlaoise','cushlanig'],['cushlanig','gt_island_s'],['poolbeg','arklow'],['arklow','gt_island_s'], ]; const OFFSHORE = [ {lon:-9.5,lat:51.4,rx:1.1,ry:.7,label:'CELTIC SEA',mw:'1,200 MW'}, {lon:-5.4,lat:54.1,rx:.8,ry:.55,label:'N. IRISH SEA',mw:'800 MW'}, ]; const TOOLTIPS = { woodland: {title:'Woodland Station', rows:[['Voltage','400 kV'],['IC flow','500 MW export'],['Status','Operational']]}, deeside: {title:'Deeside, Wales', rows:[['Role','EW IC terminal GB'],['Import','500 MW'],['Status','Operational']]}, greatisland: {title:'Great Island', rows:[['Voltage','400 kV'],['IC flow','250 MW export'],['Status','Operational']]}, pembroke: {title:'Pembroke, Wales', rows:[['Role','Greenlink terminal GB'],['Import','250 MW'],['Status','Operational']]}, ballycronan: {title:'Ballycronan More', rows:[['Voltage','400 kV'],['IC flow','250 MW export'],['Status','⚠ Reduced']]}, auchencrosh: {title:'Auchencrosh, SC', rows:[['Role','Moyle terminal SC'],['Import','250 MW'],['Status','Reduced']]}, knockraha: {title:'Knockraha, Cork', rows:[['Voltage','400 kV'],['Celtic IC','700 MW planned'],['Status','In dev 2027']]}, lamartyre: {title:'La Martyre, FR', rows:[['Role','Celtic IC terminal'],['Capacity','700 MW planned'],['Status','In dev 2027']]}, poolbeg: {title:'Poolbeg CCGT', rows:[['Type','Gas CCGT'],['Output','468 MW'],['Voltage','400 / 220 kV']]}, aghada: {title:'Aghada CCGT', rows:[['Type','Gas CCGT'],['Output','428 MW'],['Voltage','400 kV']]}, moneypoint: {title:'Moneypoint', rows:[['Type','Coal → gas conv.'],['Capacity','915 MW'],['Voltage','400 kV']]}, turloughhill:{title:'Turlough Hill', rows:[['Type','Pumped hydro'],['Capacity','292 MW'],['Voltage','400 kV']]}, arklow: {title:'Arklow Bank Wind', rows:[['Type','Offshore wind'],['Output','470 MW'],['Voltage','220 kV']]}, bellacorick: {title:'Bellacorick Wind', rows:[['Type','Onshore wind'],['Output','170 MW'],['Voltage','220 kV']]}, gt_island_s: {title:'Great Island Sub.', rows:[['Voltage','400 kV'],['IC role','Greenlink terminal'],['Flow','250 MW export']]}, woodland_s: {title:'Woodland Station', rows:[['Voltage','400 kV'],['IC role','EW IC terminal'],['Flow','500 MW export']]}, knockraha_s: {title:'Knockraha Sub.', rows:[['Voltage','400 kV'],['IC role','Celtic IC terminal'],['Status','In development']]}, }; // ── Build map ───────────────────────────────────────────────────── async function buildMap(){ const container = document.getElementById('map-container'); const W = container.clientWidth, H = container.clientHeight; const svg = d3.select('#map-svg').attr('viewBox',`0 0 ${W} ${H}`); svg.selectAll('*').remove(); // Full regional view: Ireland centred, GB + France visible const proj = d3.geoMercator() .center([-5.8, 52.8]) .scale(Math.min(W,H) * 4.2) .translate([W * 0.38, H * 0.46]); const path = d3.geoPath().projection(proj); // ── DEFS ── const defs = svg.append('defs'); // Blueprint glow filters — cyan-tinted, not green [ ['gf-g','#3fffc0',3], // green/operational ['gf-a','#ffcc44',2.5],// amber/warning ['gf-b','#b8e8ff',3], // blue/dev ['gf-c','#7ecfff',4], // main cyan ['gf-p','#c4a3ff',2.5],// purple/hydro ['gf-o','#ffab66',2.5],// orange/coal ].forEach(([id,col,sd])=>{ const f=defs.append('filter').attr('id',id).attr('x','-60%').attr('y','-60%').attr('width','220%').attr('height','220%'); f.append('feGaussianBlur').attr('in','SourceGraphic').attr('stdDeviation',sd).attr('result','b'); const m=f.append('feMerge'); m.append('feMergeNode').attr('in','b'); m.append('feMergeNode').attr('in','SourceGraphic'); }); // Sea gradient — deep navy const rg=defs.append('radialGradient').attr('id','sea-rg').attr('cx','40%').attr('cy','48%').attr('r','65%'); rg.append('stop').attr('offset','0%').attr('stop-color','#0d2248').attr('stop-opacity',.9); rg.append('stop').attr('offset','100%').attr('stop-color','#071428').attr('stop-opacity',0); // ── BACKGROUND ── svg.append('rect').attr('width',W).attr('height',H).attr('fill','#07111e'); // Blueprint crosshatch on map const bpPat=defs.append('pattern').attr('id','bp-grid').attr('width',40).attr('height',40).attr('patternUnits','userSpaceOnUse'); bpPat.append('path').attr('d','M40 0L0 0 0 40').attr('fill','none').attr('stroke','rgba(120,180,255,0.055)').attr('stroke-width',.5); svg.append('rect').attr('width',W).attr('height',H).attr('fill','url(#bp-grid)'); svg.append('ellipse').attr('cx',W*.38).attr('cy',H*.46).attr('rx',W*.7).attr('ry',H*.7).attr('fill','url(#sea-rg)'); // Lat/lon reference lines — blueprint style, more visible const gridG=svg.append('g').attr('opacity',.1); for(let la=46;la<=58;la+=2){ const pts=d3.range(-12,4,.3).map(lo=>{const p=proj([lo,la]);return p?p.join(','):null}).filter(Boolean); if(pts.length>1){ gridG.append('polyline').attr('points',pts.join(' ')).attr('fill','none').attr('stroke','#7ecfff').attr('stroke-width',.4); // Lat label const lp=proj([-12,la]); if(lp) gridG.append('text').attr('x',lp[0]+3).attr('y',lp[1]-2) .attr('font-family','Share Tech Mono,monospace').attr('font-size',7).attr('fill','rgba(126,207,255,.35)').text(la+'°N'); } } for(let lo=-12;lo<=4;lo+=3){ const pts=d3.range(46,58,.3).map(la=>{const p=proj([lo,la]);return p?p.join(','):null}).filter(Boolean); if(pts.length>1){ gridG.append('polyline').attr('points',pts.join(' ')).attr('fill','none').attr('stroke','#7ecfff').attr('stroke-width',.4); const lp=proj([lo,57.5]); if(lp) gridG.append('text').attr('x',lp[0]-3).attr('y',lp[1]-3).attr('text-anchor','middle') .attr('font-family','Share Tech Mono,monospace').attr('font-size',7).attr('fill','rgba(126,207,255,.35)').text(lo+'°'); } } // ── COUNTRIES ── let world=null; try{ world=await d3.json('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json'); } catch(e){ console.warn('world-atlas load failed',e); } if(world){ const features=topojson.feature(world,world.objects.countries).features; const keep=new Set([372,826,250]); // Land fills — Ireland slightly brighter, blueprint blue-grey for others const fills={372:'#132d4a',826:'#0c1e30',250:'#0b1c2e'}; const strokeGlows={372:'rgba(126,207,255,.22)',826:'rgba(126,207,255,.1)',250:'rgba(126,207,255,.09)'}; // Shadow base svg.append('g').selectAll('path') .data(features.filter(f=>keep.has(+f.id))) .enter().append('path').attr('d',path) .attr('fill','rgba(0,0,0,.3)').attr('transform','translate(2,2)'); // Fill svg.append('g').selectAll('path') .data(features.filter(f=>keep.has(+f.id))) .enter().append('path').attr('d',path) .attr('fill',d=>fills[+d.id]||'#0c1e30') .attr('stroke','#1a4060').attr('stroke-width',.8); // Coastline glow svg.append('g').selectAll('path') .data(features.filter(f=>keep.has(+f.id))) .enter().append('path').attr('d',path).attr('fill','none') .attr('stroke',d=>strokeGlows[+d.id]).attr('stroke-width',2.5) .attr('filter','url(#gf-c)'); // Inner vignette on land svg.append('g').selectAll('path') .data(features.filter(f=>keep.has(+f.id))) .enter().append('path').attr('d',path).attr('fill','none') .attr('stroke',d=>d.id===372?'rgba(126,207,255,.12)':'rgba(126,207,255,.06)').attr('stroke-width',6); } // ── OFFSHORE ZONES ── const ozG=svg.append('g'); OFFSHORE.forEach(oz=>{ const [cx,cy]=proj([oz.lon,oz.lat]); const [ex]=proj([oz.lon+oz.rx,oz.lat]); const [,ey]=proj([oz.lon,oz.lat-oz.ry]); const rx=Math.abs(ex-cx), ry=Math.abs(ey-cy); // Outer glow ring ozG.append('ellipse').attr('cx',cx).attr('cy',cy).attr('rx',rx+8).attr('ry',ry+6) .attr('fill','none').attr('stroke','rgba(126,207,255,.06)').attr('stroke-width',8); ozG.append('ellipse').attr('cx',cx).attr('cy',cy).attr('rx',rx).attr('ry',ry) .attr('fill','rgba(126,207,255,.04)').attr('stroke','rgba(126,207,255,.22)') .attr('stroke-width',.8).attr('stroke-dasharray','6 5'); // Corner crosshair marks [[-rx,0],[rx,0],[0,-ry],[0,ry]].forEach(([dx,dy])=>{ ozG.append('line').attr('x1',cx+dx-4).attr('y1',cy+dy).attr('x2',cx+dx+4).attr('y2',cy+dy) .attr('stroke','rgba(126,207,255,.35)').attr('stroke-width',.8); ozG.append('line').attr('x1',cx+dx).attr('y1',cy+dy-4).attr('x2',cx+dx).attr('y2',cy+dy+4) .attr('stroke','rgba(126,207,255,.35)').attr('stroke-width',.8); }); ozG.append('text').attr('x',cx).attr('y',cy-10).attr('text-anchor','middle') .attr('font-family','Share Tech Mono,monospace').attr('font-size',9) .attr('fill','rgba(126,207,255,.55)').attr('letter-spacing','.14em').text(oz.label); ozG.append('text').attr('x',cx).attr('y',cy+5).attr('text-anchor','middle') .attr('font-family','Share Tech Mono,monospace').attr('font-size',9.5) .attr('fill','rgba(63,255,192,.6)').attr('font-weight','600').text(oz.mw); // Turbine icons [-1,0,1].forEach((off,j)=>{ const tx=cx+off*rx*.28, ty=cy+18; const tg=ozG.append('g').attr('transform',`translate(${tx},${ty})`); tg.append('line').attr('x1',0).attr('y1',0).attr('x2',0).attr('y2',11) .attr('stroke','rgba(126,207,255,.45)').attr('stroke-width',1.1); const bg=tg.append('g').attr('class','blade-grp').style('animation-delay',`${-j*1.1}s`); [0,120,240].forEach(a=>{ const r=(a-90)*Math.PI/180; bg.append('line').attr('x1',0).attr('y1',0).attr('x2',Math.cos(r)*10).attr('y2',Math.sin(r)*10) .attr('stroke','rgba(126,207,255,.6)').attr('stroke-width',1); }); }); }); // ── 220 kV TRANSMISSION LINES ── const tx220G=svg.append('g'); TX220.forEach(([a,b])=>{ const A=SUBSTATIONS[a], B=SUBSTATIONS[b]; if(!A||!B) return; const [x1,y1]=proj([A.lon,A.lat]), [x2,y2]=proj([B.lon,B.lat]); tx220G.append('line').attr('x1',x1).attr('y1',y1).attr('x2',x2).attr('y2',y2) .attr('stroke','rgba(126,207,255,.18)').attr('stroke-width',5).attr('stroke-linecap','round'); tx220G.append('line').attr('x1',x1).attr('y1',y1).attr('x2',x2).attr('y2',y2) .attr('stroke','rgba(126,207,255,.28)').attr('stroke-width',1).attr('stroke-linecap','round'); tx220G.append('line').attr('x1',x1).attr('y1',y1).attr('x2',x2).attr('y2',y2) .attr('class','flow-a tx220').attr('stroke','rgba(126,207,255,.7)').attr('stroke-width',1.2) .style('animation-delay',`${(A.lon+B.lat)*.2}s`); }); // ── 400 kV TRANSMISSION LINES ── const tx400G=svg.append('g'); TX400.forEach(([a,b])=>{ const A=SUBSTATIONS[a], B=SUBSTATIONS[b]; if(!A||!B) return; const [x1,y1]=proj([A.lon,A.lat]), [x2,y2]=proj([B.lon,B.lat]); // Outer glow tx400G.append('line').attr('x1',x1).attr('y1',y1).attr('x2',x2).attr('y2',y2) .attr('stroke','rgba(63,255,192,.08)').attr('stroke-width',10).attr('stroke-linecap','round'); tx400G.append('line').attr('x1',x1).attr('y1',y1).attr('x2',x2).attr('y2',y2) .attr('stroke','rgba(63,255,192,.2)').attr('stroke-width',3.5).attr('stroke-linecap','round'); // Conductor tx400G.append('line').attr('x1',x1).attr('y1',y1).attr('x2',x2).attr('y2',y2) .attr('stroke','rgba(63,255,192,.35)').attr('stroke-width',1.3).attr('stroke-linecap','round'); // Animated power flow tx400G.append('line').attr('x1',x1).attr('y1',y1).attr('x2',x2).attr('y2',y2) .attr('class','flow-a tx').attr('stroke','rgba(63,255,192,.9)').attr('stroke-width',1.5) .attr('filter','url(#gf-g)').style('animation-delay',`${(A.lat*B.lon)*.05}s`); }); // ── INTERCONNECTORS ── const icG=svg.append('g'); ICS.forEach(ic=>{ const A=IC_NODES[ic.from], B=IC_NODES[ic.to]; const [x1,y1]=proj([A.lon,A.lat]), [x2,y2]=proj([B.lon,B.lat]); const mx=(x1+x2)/2, my=(y1+y2)/2; const dx=x2-x1, dy=y2-y1, len=Math.sqrt(dx*dx+dy*dy); const cpx=mx-(dy/len)*len*.12, cpy=my+(dx/len)*len*.12; const d=`M${x1},${y1} Q${cpx},${cpy} ${x2},${y2}`; const gfId=ic.col==='#3fffc0'?'gf-g':ic.col==='#ffcc44'?'gf-a':'gf-b'; // Layered glow icG.append('path').attr('d',d).attr('fill','none').attr('stroke',ic.col).attr('stroke-width',12).attr('opacity',.04); icG.append('path').attr('d',d).attr('fill','none').attr('stroke',ic.col).attr('stroke-width',5).attr('opacity',.1); icG.append('path').attr('d',d).attr('fill','none').attr('stroke',ic.col).attr('stroke-width',ic.w).attr('opacity',.22); const cls='flow-a'+(ic.status==='red'?' slow':'')+(ic.status==='dev'?' dev':''); const ap=icG.append('path').attr('d',d).attr('class',cls) .attr('stroke',ic.col).attr('stroke-width',ic.w) .attr('opacity',ic.status==='dev'?.25:.9).style('animation-delay',ic.dl); if(ic.status!=='dev') ap.attr('filter',`url(#${gfId})`); // Label pill — blueprint style with corners const lg=icG.append('g').attr('transform',`translate(${cpx},${cpy})`); lg.append('rect').attr('x',-36).attr('y',-16).attr('width',72).attr('height',27).attr('rx',0) .attr('fill','rgba(5,14,30,.9)').attr('stroke',ic.col).attr('stroke-width',.6).attr('stroke-opacity',.5); // Corner marks on label [[-36,-16,4],[36,-16,4]].forEach(([bx,by,s])=>{ const sign=bx<0?1:-1; lg.append('path').attr('d',`M${bx+sign*s},${by} L${bx},${by} L${bx},${by+s}`) .attr('fill','none').attr('stroke',ic.col).attr('stroke-width',.7).attr('opacity',.6); }); lg.append('text').attr('y',-4).attr('text-anchor','middle') .attr('font-family','Share Tech Mono,monospace').attr('font-size',7).attr('fill',ic.col).attr('opacity',.6).attr('letter-spacing','.1em').text(ic.lbl); lg.append('text').attr('y',8).attr('text-anchor','middle') .attr('font-family','Barlow Condensed,sans-serif').attr('font-size',10.5).attr('font-weight','700').attr('fill',ic.col).attr('letter-spacing','.05em').text(ic.mw); }); // ── SUBSTATION NODES ── const tooltip=document.getElementById('tooltip'); function drawSub(id, n, px, py){ const isGen=n.type==='gen'||n.type==='wind'; const isIC=n.type==='ic'; if(isIC) return; const col=n.fuel==='gas'?'#b8e8ff':n.fuel==='hydro'?'#c4a3ff':n.fuel==='wind'?'#3fffc0':n.fuel==='coal'?'#ffab66':'rgba(126,207,255,.5)'; const sz=isGen?7:3.5; const g=svg.append('g').attr('transform',`translate(${px},${py})`).style('cursor','pointer'); if(isGen){ // Pulsing outer ring const ring=g.append('circle').attr('r',sz+5).attr('fill','none').attr('stroke',col).attr('stroke-width',.7).attr('class','nr') .style('animation-delay',`${Math.random()*3}s`); // Square frame (blueprint aesthetic for generators) const hs=sz+1; g.append('rect').attr('x',-hs).attr('y',-hs).attr('width',hs*2).attr('height',hs*2) .attr('fill','#071428').attr('stroke',col).attr('stroke-width',1); // Core dot g.append('circle').attr('r',sz*.45).attr('fill',col).attr('opacity',.95) .attr('filter',`url(#${col==='#b8e8ff'?'gf-b':col==='#c4a3ff'?'gf-p':col==='#ffab66'?'gf-o':'gf-g'})`); // Label + MW for major stations if(n.mw>=280){ g.append('text').attr('y',sz+14).attr('text-anchor','middle') .attr('font-family','Share Tech Mono,monospace').attr('font-size',8).attr('fill',col).attr('opacity',.75).text(n.label); g.append('text').attr('y',sz+24).attr('text-anchor','middle') .attr('font-family','Barlow Condensed,sans-serif').attr('font-size',9.5).attr('font-weight','600').attr('fill',col).attr('opacity',.6).text(n.mw+' MW'); } } else { // Substation: small diamond g.append('rect').attr('x',-4).attr('y',-4).attr('width',8).attr('height',8) .attr('transform','rotate(45)').attr('fill','#071428').attr('stroke','rgba(126,207,255,.4)').attr('stroke-width',.7); g.append('rect').attr('x',-1.5).attr('y',-1.5).attr('width',3).attr('height',3) .attr('transform','rotate(45)').attr('fill','rgba(126,207,255,.5)'); } const ttData=TOOLTIPS[id]||{title:n.label,rows:[['Voltage',n.v+' kV'],['Type',n.type]]}; g.on('mouseenter',()=>{ let h=`
${ttData.title}
`; ttData.rows.forEach(r=>{h+=`
${r[0]}
${r[1]}
`;}); tooltip.innerHTML=h; tooltip.classList.add('show'); }).on('mousemove',e=>{ const r=container.getBoundingClientRect(); let tx=e.clientX-r.left+14, ty=e.clientY-r.top-14; if(tx+210>r.width) tx=e.clientX-r.left-225; tooltip.style.left=tx+'px'; tooltip.style.top=ty+'px'; }).on('mouseleave',()=>tooltip.classList.remove('show')); } Object.entries(SUBSTATIONS).forEach(([id,n])=>{ const [px,py]=proj([n.lon,n.lat]); drawSub(id,n,px,py); }); // ── IC TERMINAL NODES ── Object.entries(IC_NODES).forEach(([id,n])=>{ const [px,py]=proj([n.lon,n.lat]); const g=svg.append('g').attr('transform',`translate(${px},${py})`).style('cursor','pointer'); const gfId=n.col==='#3fffc0'?'gf-g':n.col==='#ffcc44'?'gf-a':'gf-b'; // Pulsing ring g.append('circle').attr('r',n.sz+5).attr('fill','none').attr('stroke',n.col).attr('stroke-width',.7).attr('class','nr') .style('animation-delay',`${Math.random()*3}s`); // Outer circle g.append('circle').attr('r',n.sz).attr('fill','#071428').attr('stroke',n.col).attr('stroke-width',1.2); // Inner core g.append('circle').attr('r',n.sz*.42).attr('fill',n.col).attr('opacity',.95).attr('filter',`url(#${gfId})`); // Label g.append('text').attr('y',n.sz+12).attr('text-anchor','middle') .attr('font-family','Share Tech Mono,monospace').attr('font-size',8).attr('fill',n.col).attr('opacity',.7).attr('letter-spacing','.07em').text(n.label); const ttData=TOOLTIPS[id]||{title:n.label,rows:[]}; g.on('mouseenter',()=>{ let h=`
${ttData.title}
`; ttData.rows.forEach(r=>{h+=`
${r[0]}
${r[1]}
`;}); tooltip.innerHTML=h; tooltip.classList.add('show'); }).on('mousemove',e=>{ const r=container.getBoundingClientRect(); let tx=e.clientX-r.left+14, ty=e.clientY-r.top-14; if(tx+210>r.width) tx=e.clientX-r.left-225; tooltip.style.left=tx+'px'; tooltip.style.top=ty+'px'; }).on('mouseleave',()=>tooltip.classList.remove('show')); }); // ── COUNTRY LABELS ── [ {text:'IRELAND', lon:-8.0, lat:53.1, sz:13, col:'rgba(126,207,255,.4)'}, {text:'GREAT', lon:-1.8, lat:54.0, sz:10, col:'rgba(126,207,255,.25)'}, {text:'BRITAIN', lon:-1.8, lat:53.5, sz:10, col:'rgba(126,207,255,.25)'}, {text:'N. IRELAND', lon:-6.6, lat:54.65,sz:8.5,col:'rgba(126,207,255,.3)'}, {text:'WALES', lon:-3.7, lat:52.3, sz:8.5,col:'rgba(126,207,255,.25)'}, {text:'SCOTLAND', lon:-4.0, lat:56.7, sz:8.5,col:'rgba(126,207,255,.25)'}, {text:'FRANCE', lon:-2.0, lat:47.8, sz:10, col:'rgba(126,207,255,.22)'}, ].forEach(l=>{ const [lx,ly]=proj([l.lon,l.lat]); svg.append('text').attr('x',lx).attr('y',ly).attr('text-anchor','middle') .attr('font-family','Barlow Condensed,sans-serif').attr('font-weight','700').attr('font-size',l.sz) .attr('fill',l.col).attr('letter-spacing','.18em').text(l.text); }); // ── VOLTAGE LEGEND ── const legG=svg.append('g').attr('transform',`translate(14,${H-110})`); legG.append('rect').attr('x',-6).attr('y',-14).attr('width',152).attr('height',104).attr('rx',0) .attr('fill','rgba(5,14,30,.88)').attr('stroke','rgba(126,207,255,.18)').attr('stroke-width',.8); // Corner marks [[-6,-14,6],[146,-14,6]].forEach(([bx,by,s])=>{ const sign=bx<0?1:-1; legG.append('path').attr('d',`M${bx+sign*s},${by} L${bx},${by} L${bx},${by+s}`) .attr('fill','none').attr('stroke','rgba(126,207,255,.4)').attr('stroke-width',.8); }); legG.append('text').attr('x',3).attr('y',2) .attr('font-family','Share Tech Mono,monospace').attr('font-size',7.5).attr('fill','rgba(126,207,255,.4)').attr('letter-spacing','.18em').text('VOLTAGE KEY'); [ {col:'rgba(63,255,192,.9)', dash:'8,14', lbl:'400 kV active flow', y:18}, {col:'rgba(126,207,255,.7)',dash:'5,11', lbl:'220 kV active flow', y:34}, {col:'#b8e8ff', circle:true, lbl:'Gas / CCGT station', y:50}, {col:'#3fffc0', circle:true, lbl:'Wind / renewable', y:66}, {col:'#c4a3ff', circle:true, lbl:'Hydro / storage', y:82}, ].forEach(it=>{ if(it.circle){ legG.append('rect').attr('x',0).attr('y',it.y-8).attr('width',8).attr('height',8) .attr('fill',it.col).attr('opacity',.85); } else { legG.append('line').attr('x1',0).attr('y1',it.y-4).attr('x2',26).attr('y2',it.y-4) .attr('stroke',it.col).attr('stroke-width',1.8).attr('stroke-dasharray',it.dash); } legG.append('text').attr('x',it.circle?14:32).attr('y',it.y) .attr('font-family','Share Tech Mono,monospace').attr('font-size',8.5).attr('fill','rgba(126,207,255,.6)').text(it.lbl); }); // ── SCALE BAR ── const [sx1]=proj([-9,53.5]), [sx2]=proj([-7,53.5]); const sbW=sx2-sx1; const sbG=svg.append('g').attr('transform',`translate(14,${H-8})`); sbG.append('line').attr('x1',0).attr('y1',0).attr('x2',sbW).attr('y2',0) .attr('stroke','rgba(126,207,255,.35)').attr('stroke-width',1); [[0,-3,0,3],[sbW,-3,sbW,3]].forEach(([x1,y1,x2,y2])=> sbG.append('line').attr('x1',x1).attr('y1',y1).attr('x2',x2).attr('y2',y2) .attr('stroke','rgba(126,207,255,.35)').attr('stroke-width',1)); sbG.append('text').attr('x',sbW/2).attr('y',-6).attr('text-anchor','middle') .attr('font-family','Share Tech Mono,monospace').attr('font-size',8.5).attr('fill','rgba(126,207,255,.4)').text('≈ 130 km'); } buildMap(); let rTimer; window.addEventListener('resize',()=>{ clearTimeout(rTimer); rTimer=setTimeout(buildMap,150); });