MediaWiki:Gadget-wix-interactive.js: Difference between revisions

Content deleted Content added
No edit summary
No edit summary
Line 22:
'premium-matching': initPremiumMatching,
'reserve-sensitivity': initReserveSensitivity,
'ifrs-timeline': initIfrsTimeline,
'liability-waterfall': initLiabilityWaterfall
};
 
Line 1,244 ⟶ 1,245:
// Initial render
render();
}
 
 
/* ================================================================
LIABILITY WATERFALL
Decomposes a single insurance liability number into its IFRS 17
building blocks via a waterfall chart.
Blocks are read from data-wix-blocks JSON; each entry has:
label, short, value, color, titleColor, question, body
================================================================ */
 
function initLiabilityWaterfall( container ) {
 
/* ── Read blocks from data attribute ─────────────────────── */
 
var raw = wix.data( container, 'wix-blocks', '[]' );
var blocks;
try {
blocks = JSON.parse( raw );
} catch ( e ) {
return;
}
if ( !blocks.length ) {
return;
}
 
/* ── Colors (WIX tokens) ─────────────────────────────────── */
 
var styles = getComputedStyle( container );
var colorText = styles.getPropertyValue( '--wix-text-muted' ).trim() || '#54595d';
var colorGrid = styles.getPropertyValue( '--wix-border-subtle' ).trim() || '#eaecf0';
 
/* ── Chart constants ─────────────────────────────────────── */
 
var MAX_VAL = 850;
var PAD = { t: 40, b: 50, l: 20, r: 20 };
 
/* ── State ───────────────────────────────────────────────── */
 
var view = 'old';
var selected = 0;
var hovered = -1;
var hitAreas = [];
var W, H;
 
/* ── Build UI ────────────────────────────────────────────── */
 
wix.empty( container );
 
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
 
// Toggle buttons
var btnOld = wix.el( 'button', {
className: 'wix-pm-mode-btn wix-pm-mode-btn--active',
textContent: 'Old world'
} );
var btnNew = wix.el( 'button', {
className: 'wix-pm-mode-btn',
textContent: 'IFRS 17'
} );
 
wrapper.appendChild( wix.el( 'div', { className: 'wix-pm-modes' }, [ btnOld, btnNew ] ) );
 
// Canvas
var canvas = wix.el( 'canvas', { style: { cursor: 'pointer' } } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-wf-chart' }, [ canvas ] ) );
 
// Detail card
var detailTitle = wix.el( 'div', { className: 'wix-wf-detail-title' } );
var detailBody = wix.el( 'div', { className: 'wix-wf-detail-body' } );
var detailCard = wix.el( 'div', { className: 'wix-wf-detail' }, [ detailTitle, detailBody ] );
wrapper.appendChild( detailCard );
 
 
/* ── Canvas Helpers ──────────────────────────────────────── */
 
function resize() {
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
 
if ( rect.width === 0 ) {
return;
}
 
W = rect.width;
H = 290;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.getContext( '2d' ).setTransform( dpr, 0, 0, dpr, 0, 0 );
}
 
function yForVal( v ) {
return PAD.t + ( 1 - v / MAX_VAL ) * ( H - PAD.t - PAD.b );
}
 
/* ── roundRect polyfill for older browsers ───────────────── */
 
function roundRect( ctx2, x, y, w, h, r ) {
if ( ctx2.roundRect ) {
ctx2.beginPath();
ctx2.roundRect( x, y, w, h, r );
return;
}
ctx2.beginPath();
ctx2.moveTo( x + r, y );
ctx2.lineTo( x + w - r, y );
ctx2.arcTo( x + w, y, x + w, y + r, r );
ctx2.lineTo( x + w, y + h - r );
ctx2.arcTo( x + w, y + h, x + w - r, y + h, r );
ctx2.lineTo( x + r, y + h );
ctx2.arcTo( x, y + h, x, y + h - r, r );
ctx2.lineTo( x, y + r );
ctx2.arcTo( x, y, x + r, y, r );
ctx2.closePath();
}
 
 
/* ── Draw ────────────────────────────────────────────────── */
 
function draw() {
var ctx2 = canvas.getContext( '2d' );
ctx2.clearRect( 0, 0, W, H );
hitAreas = [];
 
if ( view === 'old' ) {
drawOldWorld( ctx2 );
} else {
drawWaterfall( ctx2 );
}
}
 
function drawOldWorld( ctx2 ) {
var bw = Math.min( 160, W * 0.3 );
var x = ( W - bw ) / 2;
var top = yForVal( 800 );
var bot = yForVal( 0 );
var h = bot - top;
 
ctx2.fillStyle = hovered === 0 ? '#7A7A73' : '#888780';
roundRect( ctx2, x, top, bw, h, 6 );
ctx2.fill();
 
ctx2.fillStyle = '#fff';
ctx2.font = '500 15px system-ui, sans-serif';
ctx2.textAlign = 'center';
ctx2.textBaseline = 'middle';
ctx2.fillText( '\u20AC800m', x + bw / 2, top + h / 2 );
 
ctx2.fillStyle = colorText;
ctx2.font = '400 13px system-ui, sans-serif';
ctx2.fillText( 'Insurance liabilities', x + bw / 2, bot + 20 );
 
ctx2.fillStyle = colorGrid;
ctx2.font = '400 12px system-ui, sans-serif';
ctx2.fillText( 'What\u2019s inside?', x + bw / 2, bot + 38 );
 
hitAreas.push( { x: x, y: top, w: bw, h: h, idx: 0 } );
}
 
function drawWaterfall( ctx2 ) {
var n = blocks.length;
var usable = W - PAD.l - PAD.r;
var bw = usable / ( n + ( n - 1 ) * 0.5 );
var gap = bw * 0.5;
var running = 0;
var i, b, x, top, bot, barH, prevRunning, connY, lines, li;
 
for ( i = 0; i < n; i++ ) {
b = blocks[ i ];
x = PAD.l + i * ( bw + gap );
 
if ( i < n - 1 ) {
prevRunning = running;
running += b.value;
if ( b.value >= 0 ) {
top = yForVal( running );
bot = yForVal( prevRunning );
} else {
top = yForVal( prevRunning );
bot = yForVal( running );
}
barH = bot - top;
} else {
// Total bar: full height from 0 to total
top = yForVal( running );
bot = yForVal( 0 );
barH = bot - top;
}
 
// Bar
ctx2.globalAlpha = ( hovered === i || selected === i ) ? 0.85 : 1;
ctx2.fillStyle = b.color;
roundRect( ctx2, x, top, bw, barH, 4 );
ctx2.fill();
ctx2.globalAlpha = 1;
 
// Value label
ctx2.textAlign = 'center';
ctx2.textBaseline = 'middle';
if ( barH > 24 ) {
ctx2.fillStyle = '#fff';
ctx2.font = '500 12px system-ui, sans-serif';
ctx2.fillText( b.short, x + bw / 2, top + barH / 2 );
} else {
ctx2.fillStyle = b.color;
ctx2.font = '500 12px system-ui, sans-serif';
ctx2.fillText( b.short, x + bw / 2, top - 10 );
}
 
// Dashed connector to previous bar
if ( i > 0 && i < n - 1 ) {
connY = b.value >= 0 ? yForVal( running - b.value ) : yForVal( running );
ctx2.strokeStyle = colorGrid;
ctx2.setLineDash( [ 3, 3 ] );
ctx2.lineWidth = 1;
ctx2.beginPath();
ctx2.moveTo( PAD.l + ( i - 1 ) * ( bw + gap ) + bw, connY );
ctx2.lineTo( x, connY );
ctx2.stroke();
ctx2.setLineDash( [] );
}
 
// Connector from last component to total
if ( i === n - 2 ) {
connY = yForVal( running );
ctx2.strokeStyle = colorGrid;
ctx2.setLineDash( [ 3, 3 ] );
ctx2.lineWidth = 1;
ctx2.beginPath();
ctx2.moveTo( x + bw, connY );
ctx2.lineTo( PAD.l + ( n - 1 ) * ( bw + gap ), connY );
ctx2.stroke();
ctx2.setLineDash( [] );
}
 
// X-axis label (supports \n line breaks)
ctx2.fillStyle = colorText;
ctx2.font = '400 11px system-ui, sans-serif';
ctx2.textAlign = 'center';
ctx2.textBaseline = 'top';
lines = b.label.split( '\n' );
for ( li = 0; li < lines.length; li++ ) {
ctx2.fillText( lines[ li ], x + bw / 2, bot + 10 + li * 14 );
}
 
hitAreas.push( { x: x, y: top, w: bw, h: barH, idx: i } );
}
}
 
 
/* ── Detail Card ─────────────────────────────────────────── */
 
function renderDetail() {
if ( view === 'old' ) {
detailTitle.textContent = 'One opaque number';
detailTitle.style.color = '#444441';
detailBody.textContent = 'Under the old rules, this single figure hides everything: expected claims, time value adjustments, uncertainty buffers, and unearned profit. No way to tell what drives it or how it might change.';
return;
}
var b = blocks[ selected ];
detailTitle.textContent = b.question;
detailTitle.style.color = b.titleColor || '';
detailBody.textContent = b.body;
}
 
 
/* ── Hit Testing ─────────────────────────────────────────── */
 
function hitTest( e ) {
var rect = canvas.getBoundingClientRect();
var mx = e.clientX - rect.left;
var my = e.clientY - rect.top;
var i, a;
 
for ( i = hitAreas.length - 1; i >= 0; i-- ) {
a = hitAreas[ i ];
if ( mx >= a.x && mx <= a.x + a.w && my >= a.y && my <= a.y + a.h ) {
return a.idx;
}
}
return -1;
}
 
 
/* ── View Toggle ─────────────────────────────────────────── */
 
function setView( v ) {
view = v;
selected = 0;
hovered = -1;
 
if ( v === 'old' ) {
btnOld.classList.add( 'wix-pm-mode-btn--active' );
btnNew.classList.remove( 'wix-pm-mode-btn--active' );
} else {
btnNew.classList.add( 'wix-pm-mode-btn--active' );
btnOld.classList.remove( 'wix-pm-mode-btn--active' );
}
 
draw();
renderDetail();
}
 
 
/* ── Event Wiring ────────────────────────────────────────── */
 
btnOld.addEventListener( 'click', function () { setView( 'old' ); } );
btnNew.addEventListener( 'click', function () { setView( 'new' ); } );
 
canvas.addEventListener( 'mousemove', function ( e ) {
var h = hitTest( e );
if ( h !== hovered ) {
hovered = h;
canvas.style.cursor = h >= 0 ? 'pointer' : 'default';
draw();
}
} );
 
canvas.addEventListener( 'mouseleave', function () {
hovered = -1;
draw();
} );
 
canvas.addEventListener( 'click', function ( e ) {
var h = hitTest( e );
if ( h >= 0 ) {
selected = h;
renderDetail();
draw();
}
} );
 
window.addEventListener( 'resize', function () {
resize();
draw();
} );
 
// Initial render
resize();
draw();
renderDetail();
}