MediaWiki:Gadget-wix-interactive.js: Difference between revisions
Appearance
Content deleted Content added
No edit summary |
No edit summary |
||
| Line 10: | Line 10: | ||
- "pool-simulator" Risk pooling comparison (Bordeaux vs Normandy) |
- "pool-simulator" Risk pooling comparison (Bordeaux vs Normandy) |
||
- "insurer-engines" Two-engine profit simulator (underwriting + investment) |
- "insurer-engines" Two-engine profit simulator (underwriting + investment) |
||
- "risk-adjustment" Brittany storm confidence-level risk adjustment chart |
|||
================================================================ */ |
================================================================ */ |
||
| Line 26: | Line 27: | ||
'prob-weighted': initProbWeighted, |
'prob-weighted': initProbWeighted, |
||
'discount-rate': initDiscountRate, |
'discount-rate': initDiscountRate, |
||
'balance-sheet': initBalanceSheet |
'balance-sheet': initBalanceSheet, |
||
'risk-adjustment': initRiskAdjustment |
|||
}; |
}; |
||
| Line 2,111: | Line 2,113: | ||
var padTop = 30; |
var padTop = 30; |
||
var padBot = 10; |
var padBot = 10; |
||
var padL = 10; |
|||
var bracketZone = 150; |
var bracketZone = 150; |
||
var colGap = |
var colGap = 12; |
||
var colW = Math.min( 180, ( W |
var colW = Math.min( 180, ( W - bracketZone - colGap ) / 2 ); |
||
var chartH = H - padTop - padBot; |
var chartH = H - padTop - padBot; |
||
var totalW = colW * 2 + colGap + bracketZone; |
|||
var padL = Math.max( 10, ( W - totalW ) / 2 ); |
|||
var xA = padL; |
var xA = padL; |
||
var xL = padL + colW + colGap; |
var xL = padL + colW + colGap; |
||
| Line 2,255: | Line 2,258: | ||
draw(); |
draw(); |
||
renderDetail(); |
renderDetail(); |
||
} |
|||
/* ================================================================ |
|||
RISK ADJUSTMENT — data-wix-module="risk-adjustment" |
|||
Brittany storm: lognormal claim distribution with interactive |
|||
confidence slider. Shows best estimate, percentile threshold, |
|||
and the risk adjustment gap on a PDF chart. |
|||
================================================================ */ |
|||
function initRiskAdjustment( container ) { |
|||
/* ── Constants ────────────────────────────────────────────── */ |
|||
var MU = 1.7118; |
|||
var SIG = 0.40; |
|||
var MEAN = 6.0; |
|||
var XMIN = 1, XMAX = 18, STEPS = 400; |
|||
/* ── Math helpers ────────────────────────────────────────── */ |
|||
function lognormPDF( x ) { |
|||
if ( x <= 0 ) return 0; |
|||
var lx = Math.log( x ); |
|||
return Math.exp( -0.5 * Math.pow( ( lx - MU ) / SIG, 2 ) ) / |
|||
( x * SIG * Math.sqrt( 2 * Math.PI ) ); |
|||
} |
|||
function ratApprox( t ) { |
|||
var c = [ 2.515517, 0.802853, 0.010328 ]; |
|||
var d = [ 1.432788, 0.189269, 0.001308 ]; |
|||
return t - ( c[0] + c[1]*t + c[2]*t*t ) / |
|||
( 1 + d[0]*t + d[1]*t*t + d[2]*t*t*t ); |
|||
} |
|||
function normInv( p ) { |
|||
if ( p <= 0 ) return -Infinity; |
|||
if ( p >= 1 ) return Infinity; |
|||
if ( p < 0.5 ) return -ratApprox( Math.sqrt( -2 * Math.log( p ) ) ); |
|||
if ( p > 0.5 ) return ratApprox( Math.sqrt( -2 * Math.log( 1 - p ) ) ); |
|||
return 0; |
|||
} |
|||
function lognormCDFInv( p ) { |
|||
return Math.exp( MU + SIG * normInv( p ) ); |
|||
} |
|||
/* ── Chart colors (from WIX tokens) ──────────────────────── */ |
|||
var styles = getComputedStyle( container ); |
|||
var colorAccent = styles.getPropertyValue( '--wix-accent' ).trim() || '#1a5276'; |
|||
var colorWarn = styles.getPropertyValue( '--wix-wrong' ).trim() || '#922b21'; |
|||
var colorGrid = styles.getPropertyValue( '--wix-border-subtle' ).trim() || '#eaecf0'; |
|||
var colorAxis = styles.getPropertyValue( '--wix-text-muted' ).trim() || '#54595d'; |
|||
/* ── Build UI ────────────────────────────────────────────── */ |
|||
wix.empty( container ); |
|||
var slider = wix.el( 'input', { type: 'range', min: '50', max: '95', value: '75', step: '1' } ); |
|||
var sliderVal = wix.el( 'span', { className: 'wix-ra-slider-val', textContent: '75%' } ); |
|||
var elBE = wix.el( 'div', { className: 'wix-ra-stat-val', textContent: '\u20AC6.0m' } ); |
|||
var elPct = wix.el( 'div', { className: 'wix-ra-stat-val', textContent: '\u20AC7.2m' } ); |
|||
var elRA = wix.el( 'div', { className: 'wix-ra-stat-val wix-ra-stat-val--accent', textContent: '\u20AC1.2m' } ); |
|||
var canvas = wix.el( 'canvas' ); |
|||
var explain = wix.el( 'p', { className: 'wix-ra-explain' } ); |
|||
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } ); |
|||
container.appendChild( wrapper ); |
|||
/* Slider row */ |
|||
wrapper.appendChild( wix.el( 'div', { className: 'wix-ra-slider-row' }, [ |
|||
wix.el( 'span', { className: 'wix-ra-slider-label', textContent: 'Confidence level' } ), |
|||
slider, |
|||
sliderVal |
|||
] ) ); |
|||
/* Stats row */ |
|||
wrapper.appendChild( wix.el( 'div', { className: 'wix-ra-stats' }, [ |
|||
wix.el( 'div', { className: 'wix-ra-stat' }, [ |
|||
wix.el( 'div', { className: 'wix-ra-stat-label', textContent: 'Best estimate' } ), |
|||
elBE |
|||
] ), |
|||
wix.el( 'div', { className: 'wix-ra-stat' }, [ |
|||
wix.el( 'div', { className: 'wix-ra-stat-label', textContent: 'Covered up to' } ), |
|||
elPct |
|||
] ), |
|||
wix.el( 'div', { className: 'wix-ra-stat' }, [ |
|||
wix.el( 'div', { className: 'wix-ra-stat-label', textContent: 'Risk adjustment' } ), |
|||
elRA |
|||
] ) |
|||
] ) ); |
|||
/* Chart */ |
|||
wrapper.appendChild( wix.el( 'div', { className: 'wix-ra-chart' }, [ canvas ] ) ); |
|||
/* Legend */ |
|||
function swatch( color ) { |
|||
return wix.el( 'span', { className: 'wix-ra-legend-swatch', style: { background: color } } ); |
|||
} |
|||
wrapper.appendChild( wix.el( 'div', { className: 'wix-ra-legend' }, [ |
|||
wix.el( 'span', { className: 'wix-ra-legend-item' }, [ |
|||
swatch( colorAccent + '40' ), 'Claim distribution' |
|||
] ), |
|||
wix.el( 'span', { className: 'wix-ra-legend-item' }, [ |
|||
swatch( colorAccent ), 'Best estimate (mean)' |
|||
] ), |
|||
wix.el( 'span', { className: 'wix-ra-legend-item' }, [ |
|||
swatch( colorWarn ), 'Confidence threshold' |
|||
] ) |
|||
] ) ); |
|||
/* Explanation */ |
|||
wrapper.appendChild( explain ); |
|||
/* ── Drawing ─────────────────────────────────────────────── */ |
|||
function draw() { |
|||
var ctx = canvas.getContext( '2d' ); |
|||
var dpr = window.devicePixelRatio || 1; |
|||
var rect = canvas.getBoundingClientRect(); |
|||
if ( rect.width === 0 || rect.height === 0 ) return; |
|||
canvas.width = rect.width * dpr; |
|||
canvas.height = rect.height * dpr; |
|||
ctx.scale( dpr, dpr ); |
|||
var W = rect.width, H = rect.height; |
|||
var pad = { t: 16, r: 16, b: 36, l: 44 }; |
|||
var cw = W - pad.l - pad.r; |
|||
var ch = H - pad.t - pad.b; |
|||
ctx.clearRect( 0, 0, W, H ); |
|||
/* Compute PDF curve */ |
|||
var xs = [], ys = [], ymax = 0; |
|||
for ( var i = 0; i <= STEPS; i++ ) { |
|||
var x = XMIN + ( XMAX - XMIN ) * i / STEPS; |
|||
var y = lognormPDF( x ); |
|||
xs.push( x ); ys.push( y ); |
|||
if ( y > ymax ) ymax = y; |
|||
} |
|||
ymax *= 1.1; |
|||
function tx( x ) { return pad.l + ( x - XMIN ) / ( XMAX - XMIN ) * cw; } |
|||
function ty( y ) { return pad.t + ch - ( y / ymax ) * ch; } |
|||
var conf = parseInt( slider.value, 10 ); |
|||
var pctX = lognormCDFInv( conf / 100 ); |
|||
/* Axes */ |
|||
ctx.strokeStyle = colorGrid; |
|||
ctx.lineWidth = 0.5; |
|||
ctx.beginPath(); ctx.moveTo( pad.l, pad.t + ch ); ctx.lineTo( pad.l + cw, pad.t + ch ); ctx.stroke(); |
|||
ctx.beginPath(); ctx.moveTo( pad.l, pad.t ); ctx.lineTo( pad.l, pad.t + ch ); ctx.stroke(); |
|||
/* X-axis labels */ |
|||
ctx.font = '11px sans-serif'; |
|||
ctx.fillStyle = colorAxis; |
|||
ctx.textAlign = 'center'; |
|||
for ( var v = 2; v <= 16; v += 2 ) { |
|||
var xp = tx( v ); |
|||
ctx.fillText( '\u20AC' + v + 'm', xp, pad.t + ch + 20 ); |
|||
ctx.strokeStyle = colorGrid; |
|||
ctx.beginPath(); ctx.moveTo( xp, pad.t + ch ); ctx.lineTo( xp, pad.t + ch + 4 ); ctx.stroke(); |
|||
} |
|||
/* Shaded area under curve up to percentile */ |
|||
ctx.beginPath(); |
|||
ctx.moveTo( tx( xs[0] ), ty( 0 ) ); |
|||
for ( i = 0; i <= STEPS; i++ ) { |
|||
if ( xs[i] <= pctX ) ctx.lineTo( tx( xs[i] ), ty( ys[i] ) ); |
|||
} |
|||
var clipIdx = -1; |
|||
for ( i = 0; i < xs.length; i++ ) { |
|||
if ( xs[i] > pctX ) { clipIdx = i; break; } |
|||
} |
|||
if ( clipIdx > 0 ) { |
|||
var frac = ( pctX - xs[clipIdx-1] ) / ( xs[clipIdx] - xs[clipIdx-1] ); |
|||
var yClip = ys[clipIdx-1] + frac * ( ys[clipIdx] - ys[clipIdx-1] ); |
|||
ctx.lineTo( tx( pctX ), ty( yClip ) ); |
|||
} |
|||
ctx.lineTo( tx( pctX ), ty( 0 ) ); |
|||
ctx.closePath(); |
|||
ctx.fillStyle = colorAccent + '30'; |
|||
ctx.fill(); |
|||
/* Full PDF curve */ |
|||
ctx.beginPath(); |
|||
ctx.moveTo( tx( xs[0] ), ty( ys[0] ) ); |
|||
for ( i = 1; i <= STEPS; i++ ) ctx.lineTo( tx( xs[i] ), ty( ys[i] ) ); |
|||
ctx.strokeStyle = colorAccent; |
|||
ctx.lineWidth = 2; |
|||
ctx.stroke(); |
|||
/* Best-estimate (mean) dashed line */ |
|||
ctx.setLineDash( [ 5, 4 ] ); |
|||
ctx.lineWidth = 1.5; |
|||
ctx.strokeStyle = colorAccent; |
|||
ctx.beginPath(); |
|||
ctx.moveTo( tx( MEAN ), ty( 0 ) ); |
|||
ctx.lineTo( tx( MEAN ), ty( lognormPDF( MEAN ) ) ); |
|||
ctx.stroke(); |
|||
/* Percentile dashed line */ |
|||
ctx.strokeStyle = colorWarn; |
|||
ctx.beginPath(); |
|||
ctx.moveTo( tx( pctX ), ty( 0 ) ); |
|||
ctx.lineTo( tx( pctX ), ty( lognormPDF( pctX ) ) ); |
|||
ctx.stroke(); |
|||
ctx.setLineDash( [] ); |
|||
/* Risk-adjustment bracket */ |
|||
var raStart = tx( MEAN ), raEnd = tx( pctX ); |
|||
var arrowY = ty( 0 ) - 18; |
|||
if ( raEnd - raStart > 20 ) { |
|||
ctx.strokeStyle = colorWarn; |
|||
ctx.lineWidth = 1.5; |
|||
ctx.beginPath(); ctx.moveTo( raStart, arrowY ); ctx.lineTo( raEnd, arrowY ); ctx.stroke(); |
|||
ctx.beginPath(); ctx.moveTo( raStart, arrowY - 4 ); ctx.lineTo( raStart, arrowY + 4 ); ctx.stroke(); |
|||
ctx.beginPath(); ctx.moveTo( raEnd, arrowY - 4 ); ctx.lineTo( raEnd, arrowY + 4 ); ctx.stroke(); |
|||
ctx.fillStyle = colorWarn; |
|||
ctx.font = '500 12px sans-serif'; |
|||
ctx.textAlign = 'center'; |
|||
ctx.fillText( 'RA: \u20AC' + ( pctX - MEAN ).toFixed( 1 ) + 'm', |
|||
( raStart + raEnd ) / 2, arrowY - 6 ); |
|||
} |
|||
/* Update stat cards */ |
|||
var ra = pctX - MEAN; |
|||
sliderVal.textContent = conf + '%'; |
|||
elPct.textContent = '\u20AC' + pctX.toFixed( 1 ) + 'm'; |
|||
elRA.textContent = '\u20AC' + ra.toFixed( 1 ) + 'm'; |
|||
/* Update explanation */ |
|||
explain.textContent = conf === 50 |
|||
? 'At the 50% level the threshold equals the best estimate, so the risk adjustment is zero \u2014 the insurer holds no buffer for uncertainty at all.' |
|||
: 'At the ' + conf + '% confidence level, the insurer holds enough to cover outcomes up to \u20AC' + pctX.toFixed( 1 ) + 'm. The risk adjustment of \u20AC' + ra.toFixed( 1 ) + 'm is the gap between that threshold and the \u20AC6.0m best estimate \u2014 the price AXA pays for bearing the uncertainty on 3,000 Brittany homes.'; |
|||
} |
|||
/* ── Events ──────────────────────────────────────────────── */ |
|||
slider.addEventListener( 'input', draw ); |
|||
window.addEventListener( 'resize', draw ); |
|||
draw(); |
|||
} |
} |
||
Revision as of 22:48, 6 April 2026
/* ================================================================
WIX-INTERACTIVE.JS — Wiki Interactive Experience: Complex Widgets
================================================================
Depends on: ext.gadget.wix-core (window.wix must exist)
Dispatches by data-wix-module value. Each value maps to an
initializer function that builds the widget DOM and wires logic.
Supported modules:
- "pool-simulator" Risk pooling comparison (Bordeaux vs Normandy)
- "insurer-engines" Two-engine profit simulator (underwriting + investment)
- "risk-adjustment" Brittany storm confidence-level risk adjustment chart
================================================================ */
( function () {
'use strict';
/* ── Dispatcher ─────────────────────────────────────────────── */
var dispatchers = {
'pool-simulator': initPoolSimulator,
'insurer-engines': initInsurerEngines,
'premium-matching': initPremiumMatching,
'reserve-sensitivity': initReserveSensitivity,
'ifrs-timeline': initIfrsTimeline,
'liability-waterfall': initLiabilityWaterfall,
'prob-weighted': initProbWeighted,
'discount-rate': initDiscountRate,
'balance-sheet': initBalanceSheet,
'risk-adjustment': initRiskAdjustment
};
mw.hook( 'wikipage.content' ).add( function () {
Object.keys( dispatchers ).forEach( function ( moduleType ) {
wix.initModules( moduleType ).forEach( dispatchers[ moduleType ] );
} );
} );
/* ================================================================
POOL SIMULATOR
Compares self-insured ("alone") vs pooled risk over 25 years.
================================================================ */
function initPoolSimulator( container ) {
/* ── Constants ────────────────────────────────────────────── */
var YEARS = 25;
var PROB = 0.02;
var REPAIR = 15000;
var POOL_FEE = 200;
var START = 20000;
var ANNUAL_SAVE = 2000;
/* ── Chart colors (match WIX tokens) ─────────────────────── */
var styles = getComputedStyle( container );
var colorAlone = styles.getPropertyValue( '--wix-wrong' ).trim() || '#922b21';
var colorPool = styles.getPropertyValue( '--wix-correct' ).trim() || '#1e8449';
var colorGrid = styles.getPropertyValue( '--wix-border-subtle' ).trim() || '#eaecf0';
var colorAxis = styles.getPropertyValue( '--wix-text-muted' ).trim() || '#54595d';
/* ── State ────────────────────────────────────────────────── */
var year = 0;
var playing = false;
var timer = null;
var aloneSavings = START;
var poolSavings = START;
var aloneSpent = 0;
var poolSpent = 0;
var aloneData = [ START ];
var poolData = [ START ];
var hitYears = {};
/* ── Formatting ───────────────────────────────────────────── */
function fmt( n ) {
return '\u20AC' + wix.formatNumber( Math.round( n ) );
}
function speedMs() {
var speeds = [ 800, 500, 300, 150, 80 ];
return speeds[ parseInt( speedSlider.value, 10 ) - 1 ] || 300;
}
/* ── Build UI ─────────────────────────────────────────────── */
wix.empty( container );
// Dynamic elements (need references for updates)
var elAloneSavings = wix.el( 'div', { className: 'wix-sim-stat-val', textContent: fmt( START ) } );
var elAloneSpent = wix.el( 'div', { className: 'wix-sim-stat-val', textContent: fmt( 0 ) } );
var elPoolSavings = wix.el( 'div', { className: 'wix-sim-stat-val', textContent: fmt( START ) } );
var elPoolSpent = wix.el( 'div', { className: 'wix-sim-stat-val', textContent: fmt( 0 ) } );
var canvasAlone = wix.el( 'canvas' );
var canvasPool = wix.el( 'canvas' );
var elAloneLog = wix.el( 'div', { className: 'wix-sim-log wix-sim-log--safe', textContent: 'Ready to simulate' } );
var elPoolLog = wix.el( 'div', { className: 'wix-sim-log wix-sim-log--safe', textContent: 'Ready to simulate' } );
// Alone panel
var alonePanel = wix.el( 'div', { className: 'wix-sim-panel wix-sim-panel--alone' }, [
wix.el( 'div', { className: 'wix-sim-label wix-sim-label--alone', textContent: 'Going it alone' } ),
wix.el( 'div', { className: 'wix-sim-title', textContent: 'Bordeaux homeowner' } ),
wix.el( 'div', { className: 'wix-sim-stats' }, [
wix.el( 'div', { className: 'wix-sim-stat' }, [
wix.el( 'div', { className: 'wix-sim-stat-label', textContent: 'Savings' } ),
elAloneSavings
] ),
wix.el( 'div', { className: 'wix-sim-stat' }, [
wix.el( 'div', { className: 'wix-sim-stat-label', textContent: 'Total spent on hail' } ),
elAloneSpent
] )
] ),
wix.el( 'div', { className: 'wix-sim-chart' }, [ canvasAlone ] ),
elAloneLog
] );
// Pool panel
var poolPanel = wix.el( 'div', { className: 'wix-sim-panel wix-sim-panel--pool' }, [
wix.el( 'div', { className: 'wix-sim-label wix-sim-label--pool', textContent: 'In the Normandy pool' } ),
wix.el( 'div', { className: 'wix-sim-title', textContent: '1 of 1,000 homeowners' } ),
wix.el( 'div', { className: 'wix-sim-stats' }, [
wix.el( 'div', { className: 'wix-sim-stat' }, [
wix.el( 'div', { className: 'wix-sim-stat-label', textContent: 'Savings' } ),
elPoolSavings
] ),
wix.el( 'div', { className: 'wix-sim-stat' }, [
wix.el( 'div', { className: 'wix-sim-stat-label', textContent: 'Total paid to pool' } ),
elPoolSpent
] )
] ),
wix.el( 'div', { className: 'wix-sim-chart' }, [ canvasPool ] ),
elPoolLog
] );
// Outer wrapper card
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Panels grid
wrapper.appendChild( wix.el( 'div', { className: 'wix-sim-panels' }, [
alonePanel,
poolPanel
] ) );
// Controls
var btnStep = wix.el( 'button', { className: 'wix-btn wix-btn--outline', textContent: 'Step 1 year' } );
var btnPlay = wix.el( 'button', { className: 'wix-btn', textContent: 'Play', style: { minWidth: '4.5rem' } } );
var btnReset = wix.el( 'button', { className: 'wix-btn wix-btn--outline', textContent: 'Reset' } );
var elYear = wix.el( 'span', { className: 'wix-sim-year', textContent: 'Year 0 / ' + YEARS } );
var speedSlider = wix.el( 'input', { type: 'range', min: '1', max: '5', value: '3', step: '1' } );
var speedLabel = wix.el( 'label', { className: 'wix-sim-speed' }, [
'Speed ',
speedSlider
] );
wrapper.appendChild( wix.el( 'div', { className: 'wix-sim-controls' }, [
btnStep, btnPlay, btnReset, elYear, speedLabel
] ) );
// Summary (hidden until simulation ends)
var summaryEl = wix.el( 'div', { className: 'wix-sim-summary wix-hidden' } );
wrapper.appendChild( summaryEl );
/* ── Chart Drawing ────────────────────────────────────────── */
function drawChart( canvas, data, dots, lineColor ) {
var ctx = canvas.getContext( '2d' );
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
if ( rect.width === 0 || rect.height === 0 ) {
return;
}
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale( dpr, dpr );
var w = rect.width;
var h = rect.height;
var pad = { t: 8, b: 24, l: 50, r: 12 };
var cw = w - pad.l - pad.r;
var ch = h - pad.t - pad.b;
ctx.clearRect( 0, 0, w, h );
// Value range
var allVals = data.slice();
var maxVal = Math.max( START + ANNUAL_SAVE * YEARS, Math.max.apply( null, allVals ) ) * 1.05;
var minVal = Math.min( 0, Math.min.apply( null, allVals ) );
var range = maxVal - minVal || 1;
// Grid lines + Y-axis labels
ctx.strokeStyle = colorGrid;
ctx.lineWidth = 0.5;
ctx.fillStyle = colorAxis;
ctx.font = '11px system-ui, sans-serif';
ctx.textAlign = 'right';
var i, y, x, yr;
for ( i = 0; i <= 4; i++ ) {
y = pad.t + ch - ( ch * i / 4 );
ctx.beginPath();
ctx.moveTo( pad.l, y );
ctx.lineTo( pad.l + cw, y );
ctx.stroke();
ctx.fillText( fmt( minVal + range * i / 4 ), pad.l - 6, y + 4 );
}
// X-axis labels
ctx.textAlign = 'center';
ctx.fillStyle = colorAxis;
for ( yr = 0; yr <= YEARS; yr += 5 ) {
x = pad.l + ( cw * yr / YEARS );
ctx.fillText( String( yr ), x, h - 4 );
}
if ( data.length < 2 ) {
return;
}
// Line
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = lineColor;
for ( i = 0; i < data.length; i++ ) {
x = pad.l + ( cw * i / YEARS );
y = pad.t + ch - ( ch * ( data[ i ] - minVal ) / range );
if ( i === 0 ) {
ctx.moveTo( x, y );
} else {
ctx.lineTo( x, y );
}
}
ctx.stroke();
// Gradient fill
var lastIdx = data.length - 1;
var grad = ctx.createLinearGradient( 0, pad.t, 0, pad.t + ch );
grad.addColorStop( 0, lineColor + '18' );
grad.addColorStop( 1, lineColor + '02' );
ctx.beginPath();
for ( i = 0; i < data.length; i++ ) {
x = pad.l + ( cw * i / YEARS );
y = pad.t + ch - ( ch * ( data[ i ] - minVal ) / range );
if ( i === 0 ) {
ctx.moveTo( x, y );
} else {
ctx.lineTo( x, y );
}
}
ctx.lineTo( pad.l + ( cw * lastIdx / YEARS ), pad.t + ch );
ctx.lineTo( pad.l, pad.t + ch );
ctx.closePath();
ctx.fillStyle = grad;
ctx.fill();
// Hit year dots
Object.keys( dots ).forEach( function ( hy ) {
hy = parseInt( hy, 10 );
if ( hy >= data.length ) {
return;
}
x = pad.l + ( cw * hy / YEARS );
y = pad.t + ch - ( ch * ( data[ hy ] - minVal ) / range );
ctx.beginPath();
ctx.arc( x, y, 4, 0, Math.PI * 2 );
ctx.fillStyle = lineColor;
ctx.fill();
} );
// Current position dot (ring)
var cx = pad.l + ( cw * lastIdx / YEARS );
var cy = pad.t + ch - ( ch * ( data[ lastIdx ] - minVal ) / range );
ctx.beginPath();
ctx.arc( cx, cy, 5, 0, Math.PI * 2 );
ctx.fillStyle = lineColor;
ctx.fill();
ctx.beginPath();
ctx.arc( cx, cy, 3, 0, Math.PI * 2 );
ctx.fillStyle = '#fff';
ctx.fill();
}
/* ── Update UI ────────────────────────────────────────────── */
function updateUI() {
elAloneSavings.textContent = fmt( aloneSavings );
elAloneSpent.textContent = fmt( aloneSpent );
elPoolSavings.textContent = fmt( poolSavings );
elPoolSpent.textContent = fmt( poolSpent );
elYear.textContent = 'Year ' + year + ' / ' + YEARS;
drawChart( canvasAlone, aloneData, hitYears, colorAlone );
drawChart( canvasPool, poolData, {}, colorPool );
}
/* ── Simulation Logic ─────────────────────────────────────── */
function stepYear() {
if ( year >= YEARS ) {
stop();
return;
}
year++;
aloneSavings += ANNUAL_SAVE;
poolSavings += ANNUAL_SAVE;
// Hailstorm?
var hit = Math.random() < PROB;
if ( hit ) {
aloneSavings -= REPAIR;
aloneSpent += REPAIR;
hitYears[ year ] = true;
elAloneLog.textContent = 'Year ' + year + ': Hailstorm strikes! -\u20AC15,000 in repairs';
elAloneLog.className = 'wix-sim-log wix-sim-log--hit';
} else {
elAloneLog.textContent = 'Year ' + year + ': No hailstorm';
elAloneLog.className = 'wix-sim-log wix-sim-log--safe';
}
// Pool fee (always paid)
poolSavings -= POOL_FEE;
poolSpent += POOL_FEE;
if ( hit ) {
elPoolLog.textContent = 'Year ' + year + ': Hailstorm strikes \u2014 pool covers the repair. You paid \u20AC200';
elPoolLog.className = 'wix-sim-log wix-sim-log--safe';
} else {
elPoolLog.textContent = 'Year ' + year + ': No hailstorm. Your pool contribution: \u20AC200';
elPoolLog.className = 'wix-sim-log wix-sim-log--safe';
}
aloneData.push( aloneSavings );
poolData.push( poolSavings );
updateUI();
if ( year >= YEARS ) {
stop();
showSummary();
}
}
function stop() {
playing = false;
clearInterval( timer );
timer = null;
btnPlay.textContent = 'Play';
}
function reset() {
stop();
year = 0;
aloneSavings = START;
poolSavings = START;
aloneSpent = 0;
poolSpent = 0;
aloneData = [ START ];
poolData = [ START ];
hitYears = {};
elAloneLog.textContent = 'Ready to simulate';
elAloneLog.className = 'wix-sim-log wix-sim-log--safe';
elPoolLog.textContent = 'Ready to simulate';
elPoolLog.className = 'wix-sim-log wix-sim-log--safe';
summaryEl.classList.add( 'wix-hidden' );
wix.empty( summaryEl );
updateUI();
}
function showSummary() {
wix.empty( summaryEl );
summaryEl.classList.remove( 'wix-hidden' );
summaryEl.classList.add( 'wix-animate-in' );
var hitsCount = Object.keys( hitYears ).length;
summaryEl.appendChild( wix.el( 'div', { className: 'wix-sim-summary-title', textContent: 'After ' + YEARS + ' years' } ) );
var grid = wix.el( 'div', { className: 'wix-sim-summary-grid' }, [
wix.el( 'div', { className: 'wix-sim-summary-item' }, [
wix.el( 'div', { className: 'wix-sim-summary-heading wix-sim-summary-heading--alone', textContent: 'Going it alone' } ),
wix.el( 'div', {}, [ 'Hailstorms suffered: ' + hitsCount ] ),
wix.el( 'div', {}, [ 'Total repair cost: ' + fmt( aloneSpent ) ] ),
wix.el( 'div', {}, [ 'Final savings: ' + fmt( aloneSavings ) ] )
] ),
wix.el( 'div', { className: 'wix-sim-summary-item' }, [
wix.el( 'div', { className: 'wix-sim-summary-heading wix-sim-summary-heading--pool', textContent: 'In the pool' } ),
wix.el( 'div', {}, [ 'Hailstorms suffered: ' + hitsCount + ' (same weather)' ] ),
wix.el( 'div', {}, [ 'Total pool contributions: ' + fmt( poolSpent ) ] ),
wix.el( 'div', {}, [ 'Final savings: ' + fmt( poolSavings ) ] )
] )
] );
summaryEl.appendChild( grid );
}
/* ── Event Wiring ─────────────────────────────────────────── */
btnStep.addEventListener( 'click', function () {
stop();
stepYear();
} );
btnPlay.addEventListener( 'click', function () {
if ( year >= YEARS ) {
reset();
}
if ( playing ) {
stop();
return;
}
playing = true;
btnPlay.textContent = 'Pause';
timer = setInterval( stepYear, speedMs() );
} );
btnReset.addEventListener( 'click', reset );
speedSlider.addEventListener( 'input', function () {
if ( playing ) {
clearInterval( timer );
timer = setInterval( stepYear, speedMs() );
}
} );
/* ── Initial Render + Resize ──────────────────────────────── */
updateUI();
window.addEventListener( 'resize', function () {
drawChart( canvasAlone, aloneData, hitYears, colorAlone );
drawChart( canvasPool, poolData, {}, colorPool );
} );
}
/* ================================================================
INSURER ENGINES
Two-engine profit simulator: underwriting + investment income.
================================================================ */
function initInsurerEngines( container ) {
/* ── Constants ────────────────────────────────────────────── */
var PREMIUMS = 5000000;
var EXPENSES = 1200000;
/* ── Colors (match WIX tokens) ───────────────────────────── */
var styles = getComputedStyle( container );
var colorPos = styles.getPropertyValue( '--wix-correct' ).trim() || '#1e8449';
var colorNeg = styles.getPropertyValue( '--wix-wrong' ).trim() || '#922b21';
var colorInv = styles.getPropertyValue( '--wix-accent' ).trim() || '#1a5276';
/* ── Formatting ───────────────────────────────────────────── */
function fmt( v ) {
var sign = v < 0 ? '-' : '';
var abs = Math.abs( v );
if ( abs >= 1000000 ) {
return sign + '\u20AC' + ( abs / 1000000 ).toFixed( 2 ) + 'M';
}
return sign + '\u20AC' + wix.formatNumber( Math.round( abs ) );
}
/* ── Build UI ─────────────────────────────────────────────── */
wix.empty( container );
// Outer wrapper — single card containing the entire widget
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Fixed info row
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-fixed' }, [
wix.el( 'div', {}, [ 'Policies: ', wix.el( 'span', { textContent: '10,000' } ) ] ),
wix.el( 'div', {}, [ 'Avg premium: ', wix.el( 'span', { textContent: '\u20AC500' } ) ] ),
wix.el( 'div', {}, [ 'Total premiums: ', wix.el( 'span', { textContent: '\u20AC5,000,000' } ) ] ),
wix.el( 'div', {}, [ 'Expenses: ', wix.el( 'span', { textContent: '\u20AC1,200,000' } ) ] )
] ) );
// Scenario strip
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-section-label', textContent: 'Claims scenario' } ) );
var scenarios = [
{ label: 'Mild year', sub: '\u20AC2.8M', claims: 2800000 },
{ label: 'Expected', sub: '\u20AC3.2M', claims: 3200000 },
{ label: 'Harsh winter', sub: '\u20AC3.6M', claims: 3600000 }
];
var scenarioBtns = [];
var strip = wix.el( 'div', { className: 'wix-eng-scenario-strip' } );
scenarios.forEach( function ( s, idx ) {
var cls = 'wix-eng-scenario-btn';
if ( idx === 1 ) {
cls += ' wix-eng-scenario-btn--active';
}
var btn = wix.el( 'button', {
className: cls,
'data-claims': String( s.claims )
}, [
s.label,
wix.el( 'br' ),
s.sub
] );
scenarioBtns.push( btn );
strip.appendChild( btn );
} );
wrapper.appendChild( strip );
// Sliders
var claimsSlider = wix.el( 'input', {
type: 'range', min: '2400000', max: '4200000', step: '50000', value: '3200000'
} );
var claimsVal = wix.el( 'div', { className: 'wix-eng-slider-val', textContent: '\u20AC3.20M' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-slider-row' }, [
wix.el( 'div', { className: 'wix-eng-slider-label', textContent: 'Actual claims' } ),
claimsSlider,
claimsVal
] ) );
var investSlider = wix.el( 'input', {
type: 'range', min: '0', max: '8', step: '0.1', value: '3'
} );
var investVal = wix.el( 'div', { className: 'wix-eng-slider-val', textContent: '3.0%' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-slider-row' }, [
wix.el( 'div', { className: 'wix-eng-slider-label', textContent: 'Annual return on float' } ),
investSlider,
investVal
] ) );
// Divider
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-divider' } ) );
// Engine 1: Underwriting
var uwValEl = wix.el( 'div', { className: 'wix-eng-val', textContent: '\u20AC600,000' } );
var uwDetail = wix.el( 'div', { className: 'wix-eng-detail', textContent: '\u20AC5,000,000 premiums \u2212 \u20AC1,200,000 expenses \u2212 \u20AC3,200,000 claims' } );
var uwBar = wix.el( 'div', { className: 'wix-eng-bar' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-card' }, [
wix.el( 'div', { className: 'wix-eng-head' }, [
wix.el( 'div', { className: 'wix-eng-title', textContent: 'Engine 1: underwriting' } ),
uwValEl
] ),
uwDetail,
wix.el( 'div', { className: 'wix-eng-bar-track' }, [ uwBar ] )
] ) );
// Combined ratio
var crValEl = wix.el( 'div', { className: 'wix-eng-cr-val', textContent: '88.0%' } );
var crSub = wix.el( 'div', { className: 'wix-eng-cr-sub', textContent: 'Below 100% \u2014 underwriting profit' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-cr' }, [
wix.el( 'div', { className: 'wix-eng-cr-label', textContent: 'Combined ratio' } ),
crValEl,
crSub
] ) );
// Engine 2: Investment
var invValEl = wix.el( 'div', { className: 'wix-eng-val', textContent: '\u20AC75,000' } );
var invDetail = wix.el( 'div', { className: 'wix-eng-detail' } );
var invBar = wix.el( 'div', { className: 'wix-eng-bar' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-card' }, [
wix.el( 'div', { className: 'wix-eng-head' }, [
wix.el( 'div', { className: 'wix-eng-title', textContent: 'Engine 2: investment' } ),
invValEl
] ),
invDetail,
wix.el( 'div', { className: 'wix-eng-bar-track' }, [ invBar ] )
] ) );
// Divider
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-divider' } ) );
// Total profit
var totalValEl = wix.el( 'div', { className: 'wix-eng-total-val', textContent: '\u20AC675,000' } );
var totalSub = wix.el( 'div', { className: 'wix-eng-total-sub', textContent: 'Underwriting + investment, before tax' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-total' }, [
wix.el( 'div', { className: 'wix-eng-total-head' }, [
wix.el( 'div', { className: 'wix-eng-total-label', textContent: 'Total pre-tax profit' } ),
totalValEl
] ),
totalSub
] ) );
// Callout
var callout = wix.el( 'div', { className: 'wix-eng-callout wix-eng-callout--profit', textContent: 'Both engines are contributing to profit.' } );
wrapper.appendChild( callout );
/* ── Update Logic ─────────────────────────────────────────── */
function update() {
var claims = parseInt( claimsSlider.value, 10 );
var rate = parseFloat( investSlider.value ) / 100;
var uw = PREMIUMS - EXPENSES - claims;
var avgFloat = Math.round( PREMIUMS * 0.5 );
var inv = Math.round( avgFloat * rate );
var total = uw + inv;
var cr = ( claims + EXPENSES ) / PREMIUMS * 100;
// Slider displays
claimsVal.textContent = '\u20AC' + ( claims / 1000000 ).toFixed( 2 ) + 'M';
investVal.textContent = ( rate * 100 ).toFixed( 1 ) + '%';
// Engine 1: underwriting
var uwColor = uw >= 0 ? colorPos : colorNeg;
uwValEl.textContent = fmt( uw );
uwValEl.style.color = uwColor;
uwDetail.textContent = '\u20AC5,000,000 premiums \u2212 \u20AC1,200,000 expenses \u2212 \u20AC' + wix.formatNumber( claims ) + ' claims';
uwBar.style.width = Math.min( 100, Math.abs( uw ) / 1000000 * 100 ) + '%';
uwBar.style.background = uwColor;
// Combined ratio
crValEl.textContent = cr.toFixed( 1 ) + '%';
crValEl.style.color = cr > 100 ? colorNeg : colorPos;
if ( cr > 100 ) {
crSub.textContent = 'Above 100% \u2014 underwriting loss';
} else if ( cr === 100 ) {
crSub.textContent = 'Exactly 100% \u2014 break-even';
} else {
crSub.textContent = 'Below 100% \u2014 underwriting profit';
}
// Engine 2: investment
invValEl.textContent = fmt( inv );
invValEl.style.color = colorInv;
invDetail.textContent = 'The insurer collects \u20AC5M upfront and pays claims gradually. On average, \u20AC' +
wix.formatNumber( avgFloat ) + ' sits invested during the year, earning ' + ( rate * 100 ).toFixed( 1 ) + '%.';
invBar.style.width = Math.min( 100, inv / 200000 * 100 ) + '%';
invBar.style.background = colorInv;
// Total
totalValEl.textContent = fmt( total );
totalValEl.style.color = total >= 0 ? colorPos : colorNeg;
// Callout
if ( uw < 0 && total >= 0 ) {
callout.className = 'wix-eng-callout wix-eng-callout--profit';
callout.textContent = 'The combined ratio exceeds 100%, so underwriting alone is losing money. But investment income more than covers the gap \u2014 the insurer is still profitable overall.';
} else if ( uw < 0 && total < 0 ) {
callout.className = 'wix-eng-callout wix-eng-callout--loss';
callout.textContent = 'Investment income cannot offset the underwriting loss. The insurer is losing money overall.';
} else {
callout.className = 'wix-eng-callout wix-eng-callout--profit';
callout.textContent = 'Both engines are contributing to profit.';
}
// Sync scenario buttons
scenarioBtns.forEach( function ( btn ) {
if ( parseInt( btn.getAttribute( 'data-claims' ), 10 ) === claims ) {
btn.classList.add( 'wix-eng-scenario-btn--active' );
} else {
btn.classList.remove( 'wix-eng-scenario-btn--active' );
}
} );
}
/* ── Event Wiring ─────────────────────────────────────────── */
claimsSlider.addEventListener( 'input', update );
investSlider.addEventListener( 'input', update );
wix.on( strip, 'click', '.wix-eng-scenario-btn', function ( btn ) {
claimsSlider.value = btn.getAttribute( 'data-claims' );
update();
} );
// Initial render
update();
}
/* ================================================================
PREMIUM MATCHING
Demonstrates the matching principle: front-loaded vs spread
revenue recognition for a Madrid insurer.
================================================================ */
function initPremiumMatching( container ) {
/* ── Constants ────────────────────────────────────────────── */
var MONTHS = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
var PREMIUM = 1200;
var MONTHLY_EXPENSE = 80;
var EXPENSES = [];
var i;
for ( i = 0; i < 12; i++ ) {
EXPENSES.push( MONTHLY_EXPENSE );
}
/* ── Colors (WIX tokens) ─────────────────────────────────── */
var styles = getComputedStyle( container );
var colorRev = styles.getPropertyValue( '--wix-correct' ).trim() || '#1e8449';
var colorExp = styles.getPropertyValue( '--wix-wrong' ).trim() || '#922b21';
var colorProfit = styles.getPropertyValue( '--wix-accent' ).trim() || '#1a5276';
var colorGrid = styles.getPropertyValue( '--wix-border-subtle' ).trim() || '#eaecf0';
var colorAxis = styles.getPropertyValue( '--wix-text-muted' ).trim() || '#54595d';
/* ── State ───────────────────────────────────────────────── */
var mode = 'frontload';
var revenue = [ PREMIUM, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
/* ── Formatting ──────────────────────────────────────────── */
function fmt( n ) {
return '\u20AC' + wix.formatNumber( Math.round( n ) );
}
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
// Outer wrapper card
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Mode buttons
var btnFrontload = wix.el( 'button', {
className: 'wix-pm-mode-btn wix-pm-mode-btn--active',
textContent: 'All in January'
} );
var btnMatched = wix.el( 'button', {
className: 'wix-pm-mode-btn',
textContent: 'Spread evenly (matched)'
} );
wrapper.appendChild( wix.el( 'div', { className: 'wix-pm-modes' }, [
btnFrontload,
btnMatched
] ) );
// Stat cards
var elRevenue = wix.el( 'div', { className: 'wix-pm-stat-val', textContent: fmt( PREMIUM ) } );
var elExpense = wix.el( 'div', { className: 'wix-pm-stat-val', textContent: fmt( MONTHLY_EXPENSE * 12 ) } );
var elProfit = wix.el( 'div', { className: 'wix-pm-stat-val wix-pm-stat-val--profit', textContent: fmt( PREMIUM - MONTHLY_EXPENSE * 12 ) } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-pm-stats' }, [
wix.el( 'div', { className: 'wix-pm-stat' }, [
wix.el( 'div', { className: 'wix-pm-stat-label', textContent: 'Annual revenue' } ),
elRevenue
] ),
wix.el( 'div', { className: 'wix-pm-stat' }, [
wix.el( 'div', { className: 'wix-pm-stat-label', textContent: 'Annual expenses' } ),
elExpense
] ),
wix.el( 'div', { className: 'wix-pm-stat' }, [
wix.el( 'div', { className: 'wix-pm-stat-label', textContent: 'Annual profit' } ),
elProfit
] )
] ) );
// Chart canvas
var canvas = wix.el( 'canvas' );
wrapper.appendChild( wix.el( 'div', { className: 'wix-pm-chart' }, [ canvas ] ) );
// Legend
wrapper.appendChild( wix.el( 'div', { className: 'wix-pm-legend' }, [
wix.el( 'span', { className: 'wix-pm-legend-item' }, [
wix.el( 'span', { className: 'wix-pm-swatch wix-pm-swatch--rev' } ),
'Revenue'
] ),
wix.el( 'span', { className: 'wix-pm-legend-item' }, [
wix.el( 'span', { className: 'wix-pm-swatch wix-pm-swatch--exp' } ),
'Expenses'
] ),
wix.el( 'span', { className: 'wix-pm-legend-item' }, [
wix.el( 'span', { className: 'wix-pm-swatch wix-pm-swatch--profit' } ),
'Profit / loss'
] )
] ) );
// Insight callout
var insightEl = wix.el( 'div', { className: 'wix-eng-callout wix-eng-callout--profit' } );
wrapper.appendChild( insightEl );
/* ── Chart Drawing (vanilla canvas) ──────────────────────── */
function drawChart() {
var profit = [];
for ( var idx = 0; idx < 12; idx++ ) {
profit.push( revenue[ idx ] - EXPENSES[ idx ] );
}
var ctx = canvas.getContext( '2d' );
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
if ( rect.width === 0 || rect.height === 0 ) {
return;
}
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale( dpr, dpr );
var w = rect.width;
var h = rect.height;
var pad = { t: 16, b: 28, l: 48, r: 12 };
var cw = w - pad.l - pad.r;
var ch = h - pad.t - pad.b;
ctx.clearRect( 0, 0, w, h );
// Value range
var allVals = revenue.concat( EXPENSES ).concat( profit );
var yMax = Math.max.apply( null, allVals ) + 100;
var yMin = Math.min.apply( null, allVals ) - 60;
var range = yMax - yMin || 1;
// Y-axis helper
function yPos( v ) {
return pad.t + ch - ( ch * ( v - yMin ) / range );
}
// Grid lines + Y labels
ctx.strokeStyle = colorGrid;
ctx.lineWidth = 0.5;
ctx.fillStyle = colorAxis;
ctx.font = '11px system-ui, sans-serif';
ctx.textAlign = 'right';
var gridSteps = 5;
for ( i = 0; i <= gridSteps; i++ ) {
var gv = yMin + range * i / gridSteps;
var gy = yPos( gv );
ctx.beginPath();
ctx.moveTo( pad.l, gy );
ctx.lineTo( pad.l + cw, gy );
ctx.stroke();
ctx.fillText( '\u20AC' + Math.round( gv ), pad.l - 6, gy + 4 );
}
// Zero line (if visible)
if ( yMin < 0 && yMax > 0 ) {
var zy = yPos( 0 );
ctx.strokeStyle = colorAxis;
ctx.lineWidth = 0.8;
ctx.beginPath();
ctx.moveTo( pad.l, zy );
ctx.lineTo( pad.l + cw, zy );
ctx.stroke();
ctx.strokeStyle = colorGrid;
ctx.lineWidth = 0.5;
}
// Bar drawing
var groupWidth = cw / 12;
var barWidth = Math.max( 2, ( groupWidth - 8 ) / 3 );
var gap = 2;
for ( idx = 0; idx < 12; idx++ ) {
var gx = pad.l + groupWidth * idx + ( groupWidth - ( barWidth * 3 + gap * 2 ) ) / 2;
var zeroY = yPos( 0 );
// Revenue bar
var ry = yPos( revenue[ idx ] );
ctx.fillStyle = colorRev;
ctx.fillRect( gx, Math.min( ry, zeroY ), barWidth, Math.abs( ry - zeroY ) || 1 );
// Expense bar
var ey = yPos( EXPENSES[ idx ] );
ctx.fillStyle = colorExp;
ctx.fillRect( gx + barWidth + gap, Math.min( ey, zeroY ), barWidth, Math.abs( ey - zeroY ) || 1 );
// Profit / loss bar (single color to avoid confusion with expenses)
var pv = profit[ idx ];
var py = yPos( pv );
ctx.fillStyle = colorProfit;
ctx.fillRect( gx + ( barWidth + gap ) * 2, Math.min( py, zeroY ), barWidth, Math.abs( py - zeroY ) || 1 );
// X label
ctx.fillStyle = colorAxis;
ctx.textAlign = 'center';
ctx.font = '11px system-ui, sans-serif';
ctx.fillText( MONTHS[ idx ], pad.l + groupWidth * idx + groupWidth / 2, h - 6 );
}
}
/* ── Update ──────────────────────────────────────────────── */
function update() {
drawChart();
if ( mode === 'frontload' ) {
insightEl.className = 'wix-eng-callout wix-eng-callout--loss';
insightEl.textContent = 'January shows \u20AC1,120 profit while every other month shows an \u20AC80 loss \u2014 yet the underlying economics are perfectly steady. The annual profit is still \u20AC240 either way, but the monthly picture is wildly misleading. This is the distortion the matching principle prevents.';
} else {
insightEl.className = 'wix-eng-callout wix-eng-callout--profit';
insightEl.textContent = 'Revenue and expenses are aligned in every month: \u20AC100 of revenue minus \u20AC80 of expenses gives a steady \u20AC20 profit. The monthly picture now faithfully reflects the economics of the contract. This is proper matching.';
}
}
function setMode( m ) {
mode = m;
if ( m === 'frontload' ) {
revenue = [ PREMIUM, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
btnFrontload.classList.add( 'wix-pm-mode-btn--active' );
btnMatched.classList.remove( 'wix-pm-mode-btn--active' );
} else {
revenue = [];
for ( var j = 0; j < 12; j++ ) {
revenue.push( 100 );
}
btnMatched.classList.add( 'wix-pm-mode-btn--active' );
btnFrontload.classList.remove( 'wix-pm-mode-btn--active' );
}
update();
}
/* ── Event Wiring ────────────────────────────────────────── */
btnFrontload.addEventListener( 'click', function () {
setMode( 'frontload' );
} );
btnMatched.addEventListener( 'click', function () {
setMode( 'matched' );
} );
window.addEventListener( 'resize', drawChart );
/* ── Initial Render ──────────────────────────────────────── */
setMode( 'frontload' );
}
/* ================================================================
RESERVE SENSITIVITY
Slider showing how reserve reassessments flow through to P&L
and shareholders' equity.
================================================================ */
function initReserveSensitivity( container ) {
/* ── Constants ────────────────────────────────────────────── */
var RESERVES = 90; // €bn
var EQUITY = 50; // €bn
var TAX = 0.25;
var MAX_PCT = 10; // bar scale cap
/* ── Colors (WIX tokens) ─────────────────────────────────── */
var styles = getComputedStyle( container );
var colorPos = styles.getPropertyValue( '--wix-correct' ).trim() || '#1e8449';
var colorNeg = styles.getPropertyValue( '--wix-wrong' ).trim() || '#922b21';
/* ── Formatting ──────────────────────────────────────────── */
function fmtBn( v ) {
var abs = Math.abs( v );
return abs >= 1 ? abs.toFixed( 1 ) : abs.toFixed( 2 );
}
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Slider row
var slider = wix.el( 'input', {
type: 'range', min: '-5', max: '5', value: '2', step: '0.5'
} );
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-section-label', textContent: 'Reserve reassessment' } ) );
wrapper.appendChild( wix.el( 'div', { className: 'wix-rs-slider-row' }, [
wix.el( 'span', { className: 'wix-rs-bound', textContent: '-5%' } ),
slider,
wix.el( 'span', { className: 'wix-rs-bound', textContent: '+5%' } )
] ) );
// Stat cards — row 1: P&L impact
var elPretax = wix.el( 'div', { className: 'wix-rs-card-num' } );
var elAftertax = wix.el( 'div', { className: 'wix-rs-card-num' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-rs-cards' }, [
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'Pre-tax P&L impact' } ),
elPretax
] ),
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'After-tax (25%) P&L impact' } ),
elAftertax
] )
] ) );
// Stat cards — row 2: reserves
var elNewRes = wix.el( 'div', { className: 'wix-rs-card-num' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-rs-cards' }, [
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'Reserves (original)' } ),
wix.el( 'div', { className: 'wix-rs-card-num wix-rs-card-num--muted', textContent: '\u20AC90.0bn' } )
] ),
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'Reserves (revised)' } ),
elNewRes
] )
] ) );
// Bar charts
var bar1Fill = wix.el( 'div', { className: 'wix-rs-bar-fill' } );
var bar2Fill = wix.el( 'div', { className: 'wix-rs-bar-fill' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-rs-bars' }, [
wix.el( 'div', {}, [
wix.el( 'div', { className: 'wix-rs-bar-label', textContent: 'Reserve reassessment' } ),
wix.el( 'div', { className: 'wix-rs-bar-track' }, [
wix.el( 'div', { className: 'wix-rs-bar-center' } ),
bar1Fill
] )
] ),
wix.el( 'div', {}, [
wix.el( 'div', { className: 'wix-rs-bar-label', textContent: 'Impact on shareholders\u2019 equity (\u20AC50bn)' } ),
wix.el( 'div', { className: 'wix-rs-bar-track' }, [
wix.el( 'div', { className: 'wix-rs-bar-center' } ),
bar2Fill
] )
] )
] ) );
/* ── Bar Helper ──────────────────────────────────────────── */
function setBar( el, pct, color ) {
var clamped = wix.clamp( pct, -MAX_PCT, MAX_PCT );
var w = Math.abs( clamped ) / MAX_PCT * 50;
if ( clamped >= 0 ) {
el.style.left = '50%';
el.style.width = w + '%';
el.style.borderRadius = '0 var(--wix-radius) var(--wix-radius) 0';
} else {
el.style.left = ( 50 - w ) + '%';
el.style.width = w + '%';
el.style.borderRadius = 'var(--wix-radius) 0 0 var(--wix-radius)';
}
el.style.background = color;
var label = ( pct >= 0 ? '+' : '' ) + pct.toFixed( 1 ) + '%';
el.textContent = Math.abs( pct ) >= 0.5 ? label : '';
}
/* ── Update Logic ────────────────────────────────────────── */
function update() {
var p = parseFloat( slider.value );
var delta = RESERVES * p / 100;
var pretax = -delta;
var aftertax = pretax * ( 1 - TAX );
// P&L cards
var sign;
sign = pretax >= 0 ? '+' : '-';
elPretax.textContent = sign + '\u20AC' + fmtBn( pretax ) + 'bn';
elPretax.className = 'wix-rs-card-num' + ( pretax > 0 ? ' wix-rs-card-num--pos' : pretax < 0 ? ' wix-rs-card-num--neg' : '' );
sign = aftertax >= 0 ? '+' : '-';
elAftertax.textContent = sign + '\u20AC' + fmtBn( aftertax ) + 'bn';
elAftertax.className = 'wix-rs-card-num' + ( aftertax > 0 ? ' wix-rs-card-num--pos' : aftertax < 0 ? ' wix-rs-card-num--neg' : '' );
// Revised reserves
elNewRes.textContent = '\u20AC' + ( RESERVES + delta ).toFixed( 1 ) + 'bn';
// Bars
var resPct = p;
var eqPct = aftertax / EQUITY * 100;
setBar( bar1Fill, resPct, p <= 0 ? colorPos : colorNeg );
setBar( bar2Fill, eqPct, aftertax >= 0 ? colorPos : colorNeg );
}
/* ── Event Wiring ────────────────────────────────────────── */
slider.addEventListener( 'input', update );
// Initial render
update();
}
/* ================================================================
IFRS TIMELINE
Interactive horizontal timeline of IFRS / insurance-accounting
milestones. Events are read from a data-wix-events JSON attribute.
Each event: { year, cat ("ifrs"|"ins"), title, body }
================================================================ */
function initIfrsTimeline( container ) {
/* ── Read events from data attribute ─────────────────────── */
var raw = wix.data( container, 'wix-events', '[]' );
var events;
try {
events = JSON.parse( raw );
} catch ( e ) {
return;
}
if ( !events.length ) {
return;
}
/* ── Colors (WIX tokens + category overrides) ────────────── */
var styles = getComputedStyle( container );
var colorIfrs = styles.getPropertyValue( '--wix-accent' ).trim() || '#1a5276';
var colorIns = styles.getPropertyValue( '--wix-wrong' ).trim() || '#922b21';
/* ── State ───────────────────────────────────────────────── */
var active = 0;
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Timeline track
var line = wix.el( 'div', { className: 'wix-tl-line' } );
var dotsEl = wix.el( 'div', { className: 'wix-tl-dots' } );
var track = wix.el( 'div', { className: 'wix-tl-track' }, [ line, dotsEl ] );
wrapper.appendChild( track );
// Detail card
var cardTitle = wix.el( 'div', { className: 'wix-tl-card-title' } );
var cardBody = wix.el( 'div', { className: 'wix-tl-card-body' } );
var card = wix.el( 'div', { className: 'wix-tl-card' }, [ cardTitle, cardBody ] );
wrapper.appendChild( card );
// Legend
wrapper.appendChild( wix.el( 'div', { className: 'wix-tl-legend' }, [
wix.el( 'span', { className: 'wix-tl-legend-item' }, [
wix.el( 'span', { className: 'wix-tl-legend-dot wix-tl-legend-dot--ifrs' } ),
'IFRS programme'
] ),
wix.el( 'span', { className: 'wix-tl-legend-item' }, [
wix.el( 'span', { className: 'wix-tl-legend-dot wix-tl-legend-dot--ins' } ),
'Insurance-specific'
] )
] ) );
// Navigation buttons
var btnPrev = wix.el( 'button', { className: 'wix-btn wix-btn--outline', textContent: 'Previous' } );
var btnNext = wix.el( 'button', { className: 'wix-btn wix-btn--outline', textContent: 'Next' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-tl-nav' }, [ btnPrev, btnNext ] ) );
/* ── Render ──────────────────────────────────────────────── */
function render() {
wix.empty( dotsEl );
events.forEach( function ( ev, i ) {
var dot = wix.el( 'div', { className: 'wix-tl-dot' } );
var year = wix.el( 'span', { className: 'wix-tl-year', textContent: ev.year } );
var cls = 'wix-tl-dot-wrap';
if ( ev.cat === 'ins' ) {
cls += ' wix-tl-dot-wrap--ins';
}
if ( i === active ) {
cls += ' wix-tl-dot-wrap--active';
}
var wrap = wix.el( 'div', { className: cls }, [ dot, year ] );
wrap.setAttribute( 'data-idx', String( i ) );
dotsEl.appendChild( wrap );
} );
// Card content
var ev = events[ active ];
cardTitle.textContent = ev.title;
cardBody.textContent = ev.body;
// Card accent border color
card.style.borderLeftColor = ev.cat === 'ins' ? colorIns : colorIfrs;
// Button states
btnPrev.disabled = active === 0;
btnNext.disabled = active === events.length - 1;
}
/* ── Event Wiring ────────────────────────────────────────── */
wix.on( dotsEl, 'click', '.wix-tl-dot-wrap', function ( target ) {
var idx = parseInt( target.getAttribute( 'data-idx' ), 10 );
if ( !isNaN( idx ) ) {
active = idx;
render();
}
} );
btnPrev.addEventListener( 'click', function () {
if ( active > 0 ) {
active--;
render();
}
} );
btnNext.addEventListener( 'click', function () {
if ( active < events.length - 1 ) {
active++;
render();
}
} );
// 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();
}
/* ================================================================
PROB-WEIGHTED
Probability-weighted estimate calculator.
Two scenarios (quiet year / major flood) with an adjustable
flood probability slider.
================================================================ */
function initProbWeighted( container ) {
/* ── Constants ────────────────────────────────────────────── */
var QUIET = 8; // €m
var FLOOD = 40; // €m
/* ── Colors ──────────────────────────────────────────────── */
var COLOR_QUIET_BG = '#d4e6f1';
var COLOR_QUIET_FG = '#0C447C';
var COLOR_FLOOD_BG = '#fadbd8';
var COLOR_FLOOD_FG = '#791F1F';
var COLOR_RESULT_BG = '#d4e6f1';
var COLOR_RESULT_FG = '#1a5276';
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Slider row
var slider = wix.el( 'input', {
type: 'range', min: '0', max: '50', value: '10', step: '1'
} );
var sliderVal = wix.el( 'span', { className: 'wix-eng-slider-val', textContent: '10%' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-slider-row' }, [
wix.el( 'div', { className: 'wix-eng-slider-label', textContent: 'Flood probability' } ),
slider,
sliderVal
] ) );
// Scenario cards row
var elQuietVal = wix.el( 'div', { className: 'wix-pw-scene-val', style: { color: COLOR_QUIET_FG }, textContent: '\u20AC8m' } );
var elFloodVal = wix.el( 'div', { className: 'wix-pw-scene-val', style: { color: COLOR_FLOOD_FG }, textContent: '\u20AC40m' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-pw-calc' }, [
wix.el( 'div', { className: 'wix-pw-scene', style: { background: COLOR_QUIET_BG } }, [
wix.el( 'div', { className: 'wix-pw-scene-title', style: { color: COLOR_QUIET_FG }, textContent: 'Quiet year' } ),
elQuietVal
] ),
wix.el( 'div', { className: 'wix-pw-op', textContent: '+' } ),
wix.el( 'div', { className: 'wix-pw-scene', style: { background: COLOR_FLOOD_BG } }, [
wix.el( 'div', { className: 'wix-pw-scene-title', style: { color: COLOR_FLOOD_FG }, textContent: 'Major flood' } ),
elFloodVal
] )
] ) );
// Probability row (x multipliers)
var elQuietPct = wix.el( 'div', { className: 'wix-pw-prob-num', style: { color: COLOR_QUIET_FG }, textContent: '90%' } );
var elFloodPct = wix.el( 'div', { className: 'wix-pw-prob-num', style: { color: COLOR_FLOOD_FG }, textContent: '10%' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-pw-prob-row' }, [
wix.el( 'div', { style: { textAlign: 'center' } }, [
wix.el( 'div', { className: 'wix-pw-mult-x', textContent: '\u00D7' } ),
wix.el( 'div', { className: 'wix-pw-prob', style: { background: COLOR_QUIET_BG } }, [ elQuietPct ] )
] ),
wix.el( 'div' ),
wix.el( 'div', { style: { textAlign: 'center' } }, [
wix.el( 'div', { className: 'wix-pw-mult-x', textContent: '\u00D7' } ),
wix.el( 'div', { className: 'wix-pw-prob', style: { background: COLOR_FLOOD_BG } }, [ elFloodPct ] )
] )
] ) );
// Equals sign
wrapper.appendChild( wix.el( 'div', { className: 'wix-pw-eq' }, [
wix.el( 'span', { className: 'wix-pw-eq-sign', textContent: '=' } )
] ) );
// Result card
var elWeighted = wix.el( 'div', { className: 'wix-pw-result-num' } );
var elVs = wix.el( 'div', { className: 'wix-pw-result-vs' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-pw-result' }, [
wix.el( 'div', { className: 'wix-pw-result-label', textContent: 'Probability-weighted estimate' } ),
elWeighted,
elVs
] ) );
/* ── Update Logic ────────────────────────────────────────── */
function update() {
var fp = parseInt( slider.value, 10 );
var qp = 100 - fp;
sliderVal.textContent = fp + '%';
elQuietPct.textContent = qp + '%';
elFloodPct.textContent = fp + '%';
var w = ( qp / 100 ) * QUIET + ( fp / 100 ) * FLOOD;
var wR = Math.round( w * 10 ) / 10;
elWeighted.textContent = '\u20AC' + wR.toFixed( 1 ) + 'm';
var mostLikely = qp >= fp ? QUIET : FLOOD;
elVs.textContent = 'vs. \u20AC' + mostLikely + 'm most likely outcome';
}
/* ── Event Wiring ────────────────────────────────────────── */
slider.addEventListener( 'input', update );
// Initial render
update();
}
/* ================================================================
DISCOUNT RATE
Discount-rate dashboard: shows how discounting reduces a
future claim to present value, with year-by-year unwinding.
================================================================ */
function initDiscountRate( container ) {
/* ── Constants ────────────────────────────────────────────── */
var CLAIM = 100000;
var YEARS = 5;
/* ── Colors (WIX tokens) ─────────────────────────────────── */
var styles = getComputedStyle( container );
var colorPV = styles.getPropertyValue( '--wix-accent' ).trim() || '#1a5276';
var colorAccr = styles.getPropertyValue( '--wix-correct' ).trim() || '#1e8449';
var colorGrid = styles.getPropertyValue( '--wix-border-subtle' ).trim() || '#eaecf0';
var colorAxis = styles.getPropertyValue( '--wix-text-muted' ).trim() || '#54595d';
var colorNom = styles.getPropertyValue( '--wix-border' ).trim() || '#c8ccd1';
/* ── Helpers ─────────────────────────────────────────────── */
function pv( fv, r, t ) {
return fv / Math.pow( 1 + r, t );
}
function fmt( v ) {
return '\u20AC' + wix.formatNumber( Math.round( v ) );
}
function fmtK( v ) {
return '\u20AC' + Math.round( v / 1000 ) + 'k';
}
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Slider row (reuse insurer-engines pattern)
var slider = wix.el( 'input', {
type: 'range', min: '0', max: '6', step: '0.1', value: '3'
} );
var sliderVal = wix.el( 'div', { className: 'wix-eng-slider-val', textContent: '3.0%' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-slider-row' }, [
wix.el( 'div', { className: 'wix-eng-slider-label', textContent: 'Discount rate' } ),
slider,
sliderVal
] ) );
// Stat cards (3-column grid, reuse reserve-sensitivity pattern)
var elPV = wix.el( 'div', { className: 'wix-rs-card-num' } );
var elOver = wix.el( 'div', { className: 'wix-rs-card-num' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-dr-cards' }, [
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'Claim (nominal)' } ),
wix.el( 'div', { className: 'wix-rs-card-num wix-rs-card-num--muted', textContent: '\u20AC100,000' } )
] ),
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'Present value today' } ),
elPV
] ),
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'Overstatement if no discount' } ),
elOver
] )
] ) );
// Chart + table box (same style as stat cards)
var canvas = wix.el( 'canvas' );
var tableEl = wix.el( 'div', { className: 'wix-dr-table' } );
var chartBox = wix.el( 'div', { className: 'wix-rs-card wix-dr-box' }, [
wix.el( 'div', { className: 'wix-eng-section-label', textContent: 'Liability unwinding \u2014 year by year' } ),
wix.el( 'div', { className: 'wix-dr-chart' }, [ canvas ] ),
wix.el( 'div', { className: 'wix-pm-legend' }, [
wix.el( 'span', { className: 'wix-pm-legend-item' }, [
wix.el( 'span', { className: 'wix-pm-swatch', style: { background: colorPV } } ),
'Present value'
] ),
wix.el( 'span', { className: 'wix-pm-legend-item' }, [
wix.el( 'span', { className: 'wix-pm-swatch', style: { background: colorAccr } } ),
'Annual accretion'
] ),
wix.el( 'span', { className: 'wix-pm-legend-item' }, [
wix.el( 'span', { className: 'wix-dr-nom-swatch' } ),
'Nominal (\u20AC100,000)'
] )
] ),
tableEl
] );
wrapper.appendChild( chartBox );
/* ── Chart Drawing ───────────────────────────────────────── */
function drawChart( pvs, accretions ) {
var ctx = canvas.getContext( '2d' );
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
if ( rect.width === 0 || rect.height === 0 ) {
return;
}
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale( dpr, dpr );
var w = rect.width;
var h = rect.height;
var pad = { t: 12, b: 28, l: 50, r: 12 };
var cw = w - pad.l - pad.r;
var ch = h - pad.t - pad.b;
var yMax = 105000;
var n = YEARS + 1;
ctx.clearRect( 0, 0, w, h );
// Y helper
function yPos( v ) {
return pad.t + ch - ( ch * v / yMax );
}
function xPos( i ) {
return pad.l + ( cw * i / ( n - 1 ) );
}
// Grid + Y labels
ctx.strokeStyle = colorGrid;
ctx.lineWidth = 0.5;
ctx.fillStyle = colorAxis;
ctx.font = '11px system-ui, sans-serif';
ctx.textAlign = 'right';
var gridSteps = 5;
var i, y;
for ( i = 0; i <= gridSteps; i++ ) {
var gv = yMax * i / gridSteps;
y = yPos( gv );
ctx.beginPath();
ctx.moveTo( pad.l, y );
ctx.lineTo( pad.l + cw, y );
ctx.stroke();
ctx.fillText( fmtK( gv ), pad.l - 6, y + 4 );
}
// X labels
ctx.textAlign = 'center';
for ( i = 0; i < n; i++ ) {
ctx.fillText( 'Year ' + i, xPos( i ), h - 6 );
}
// Nominal dashed line
ctx.strokeStyle = colorNom;
ctx.lineWidth = 1;
ctx.setLineDash( [ 4, 4 ] );
ctx.beginPath();
ctx.moveTo( pad.l, yPos( CLAIM ) );
ctx.lineTo( pad.l + cw, yPos( CLAIM ) );
ctx.stroke();
ctx.setLineDash( [] );
// Accretion bars
var barWidth = Math.min( 30, cw / n * 0.45 );
for ( i = 0; i < n; i++ ) {
if ( accretions[ i ] <= 0 ) {
continue;
}
var bx = xPos( i ) - barWidth / 2;
var by = yPos( accretions[ i ] );
var bh = yPos( 0 ) - by;
ctx.fillStyle = colorAccr + '66';
ctx.fillRect( bx, by, barWidth, bh );
ctx.strokeStyle = colorAccr;
ctx.lineWidth = 1;
ctx.strokeRect( bx, by, barWidth, bh );
}
// PV line
ctx.beginPath();
ctx.strokeStyle = colorPV;
ctx.lineWidth = 2;
for ( i = 0; i < n; i++ ) {
var px = xPos( i );
var py = yPos( pvs[ i ] );
if ( i === 0 ) {
ctx.moveTo( px, py );
} else {
ctx.lineTo( px, py );
}
}
ctx.stroke();
// PV dots
for ( i = 0; i < n; i++ ) {
ctx.beginPath();
ctx.arc( xPos( i ), yPos( pvs[ i ] ), 4, 0, Math.PI * 2 );
ctx.fillStyle = colorPV;
ctx.fill();
}
}
/* ── Table ───────────────────────────────────────────────── */
function buildTable( pvs, accretions ) {
wix.empty( tableEl );
var thead = wix.el( 'tr', {}, [
wix.el( 'th', { textContent: 'Year' } ),
wix.el( 'th', { textContent: 'Remaining' } ),
wix.el( 'th', { textContent: 'Present value' } ),
wix.el( 'th', { textContent: 'Accretion' } )
] );
var table = wix.el( 'table', { className: 'wix-dr-tbl' }, [ thead ] );
for ( var yr = 0; yr <= YEARS; yr++ ) {
table.appendChild( wix.el( 'tr', {}, [
wix.el( 'td', { textContent: 'Year ' + yr } ),
wix.el( 'td', { textContent: ( YEARS - yr ) + ' yr' } ),
wix.el( 'td', { textContent: fmt( pvs[ yr ] ) } ),
wix.el( 'td', { textContent: yr === 0 ? '\u2014' : fmt( accretions[ yr ] ) } )
] ) );
}
tableEl.appendChild( table );
}
/* ── Update ──────────────────────────────────────────────── */
function update() {
var r = parseFloat( slider.value ) / 100;
sliderVal.textContent = slider.value + '%';
var todayPV = pv( CLAIM, r, YEARS );
elPV.textContent = fmt( todayPV );
elOver.textContent = fmt( CLAIM - todayPV );
var pvs = [];
var accretions = [];
for ( var yr = 0; yr <= YEARS; yr++ ) {
var remaining = YEARS - yr;
var val = pv( CLAIM, r, remaining );
pvs.push( Math.round( val ) );
accretions.push( yr === 0 ? 0 : Math.round( val - pv( CLAIM, r, remaining + 1 ) ) );
}
drawChart( pvs, accretions );
buildTable( pvs, accretions );
}
/* ── Event Wiring ────────────────────────────────────────── */
slider.addEventListener( 'input', update );
window.addEventListener( 'resize', function () {
update();
} );
// Initial render
update();
}
/* ================================================================
BALANCE SHEET
IFRS 17 balance sheet: two-column stacked bar (assets vs
liabilities) with click-to-explore detail card and brackets.
Blocks read from data-wix-blocks JSON attribute.
================================================================ */
function initBalanceSheet( 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;
}
var TOTAL = blocks[ 0 ].val;
/* ── Colors (WIX tokens for brackets/axis) ───────────────── */
var styles = getComputedStyle( container );
var colorAxis = styles.getPropertyValue( '--wix-text-muted' ).trim() || '#54595d';
var colorLine = styles.getPropertyValue( '--wix-border' ).trim() || '#c8ccd1';
/* ── State ───────────────────────────────────────────────── */
var selected = 0;
var hitAreas = [];
var W, H;
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Canvas
var canvas = wix.el( 'canvas', { style: { cursor: 'pointer' } } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-bs-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 = 400;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.getContext( '2d' ).setTransform( dpr, 0, 0, dpr, 0, 0 );
}
/* roundRect polyfill */
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();
}
/* Bracket with 1 or 2 line label */
function drawBracket( ctx2, x, y1, y2, label1, label2 ) {
var mid = ( y1 + y2 ) / 2;
ctx2.strokeStyle = colorLine;
ctx2.lineWidth = 1;
ctx2.beginPath();
ctx2.moveTo( x, y1 );
ctx2.lineTo( x + 6, y1 );
ctx2.lineTo( x + 6, y2 );
ctx2.lineTo( x, y2 );
ctx2.stroke();
ctx2.fillStyle = colorAxis;
ctx2.font = '400 11px system-ui, sans-serif';
ctx2.textAlign = 'left';
ctx2.textBaseline = 'middle';
if ( label2 ) {
ctx2.fillText( label1, x + 12, mid - 7 );
ctx2.fillText( label2, x + 12, mid + 7 );
} else {
ctx2.fillText( label1, x + 12, mid );
}
}
/* ── Draw ────────────────────────────────────────────────── */
function draw() {
var ctx2 = canvas.getContext( '2d' );
ctx2.clearRect( 0, 0, W, H );
hitAreas = [];
var padTop = 30;
var padBot = 10;
var bracketZone = 150;
var colGap = 12;
var colW = Math.min( 180, ( W - bracketZone - colGap ) / 2 );
var chartH = H - padTop - padBot;
var totalW = colW * 2 + colGap + bracketZone;
var padL = Math.max( 10, ( W - totalW ) / 2 );
var xA = padL;
var xL = padL + colW + colGap;
// Column headers
ctx2.font = '400 13px system-ui, sans-serif';
ctx2.textAlign = 'center';
ctx2.fillStyle = colorAxis;
ctx2.fillText( 'Assets', xA + colW / 2, padTop - 10 );
ctx2.fillText( 'Liabilities', xL + colW / 2, padTop - 10 );
// ── Assets column (single block) ──
var aBlock = blocks[ 0 ];
var isSA = selected === 0;
ctx2.fillStyle = isSA ? aBlock.hColor : aBlock.color;
roundRect( ctx2, xA, padTop, colW, chartH, 4 );
ctx2.fill();
ctx2.fillStyle = aBlock.textColor;
ctx2.font = '500 15px system-ui, sans-serif';
ctx2.textAlign = 'center';
ctx2.textBaseline = 'middle';
ctx2.fillText( aBlock.label, xA + colW / 2, padTop + chartH / 2 - 14 );
ctx2.fillStyle = aBlock.subColor;
ctx2.font = '400 12px system-ui, sans-serif';
ctx2.fillText( aBlock.sub, xA + colW / 2, padTop + chartH / 2 + 4 );
ctx2.fillText( '\u20AC' + aBlock.val + 'm', xA + colW / 2, padTop + chartH / 2 + 22 );
hitAreas.push( { x: xA, y: padTop, w: colW, h: chartH, idx: 0 } );
// ── Liabilities column (stacked segments) ──
var liab = blocks.slice( 1 );
var segGap = 3;
var totalGap = ( liab.length - 1 ) * segGap;
var availH = chartH - totalGap;
var y = padTop;
var yPositions = [];
var i, b, bH, isSel, midY;
for ( i = 0; i < liab.length; i++ ) {
b = liab[ i ];
bH = ( b.val / TOTAL ) * availH;
isSel = selected === i + 1;
ctx2.fillStyle = isSel ? b.hColor : b.color;
roundRect( ctx2, xL, y, colW, bH, 4 );
ctx2.fill();
midY = y + bH / 2;
ctx2.textAlign = 'center';
ctx2.textBaseline = 'middle';
if ( bH > 60 ) {
ctx2.fillStyle = b.textColor;
ctx2.font = '500 14px system-ui, sans-serif';
ctx2.fillText( b.label, xL + colW / 2, midY - 10 );
ctx2.fillStyle = b.subColor;
ctx2.font = '400 11px system-ui, sans-serif';
ctx2.fillText( b.sub, xL + colW / 2, midY + 6 );
ctx2.fillText( '\u20AC' + b.val + 'm', xL + colW / 2, midY + 22 );
} else if ( bH > 35 ) {
ctx2.fillStyle = b.textColor;
ctx2.font = '500 14px system-ui, sans-serif';
ctx2.fillText( b.label, xL + colW / 2, midY - 6 );
ctx2.fillStyle = b.subColor;
ctx2.font = '400 11px system-ui, sans-serif';
ctx2.fillText( '\u20AC' + b.val + 'm', xL + colW / 2, midY + 10 );
} else {
ctx2.fillStyle = b.textColor;
ctx2.font = '500 14px system-ui, sans-serif';
ctx2.fillText( b.label + ' \u20AC' + b.val + 'm', xL + colW / 2, midY );
}
hitAreas.push( { x: xL, y: y, w: colW, h: bH, idx: i + 1 } );
yPositions.push( { top: y, bot: y + bH } );
y += bH + segGap;
}
// ── Brackets ──
var bx1 = xL + colW + 10;
if ( yPositions.length >= 1 ) {
drawBracket( ctx2, bx1, yPositions[ 0 ].top + 2, yPositions[ 0 ].bot - 2, 'Firm\u2019s funds' );
}
if ( yPositions.length >= 4 ) {
drawBracket( ctx2, bx1, yPositions[ 1 ].top + 2, yPositions[ 3 ].bot - 2, 'Technical', 'provisions' );
var bx2 = bx1 + 72;
drawBracket( ctx2, bx2, yPositions[ 2 ].top + 2, yPositions[ 3 ].bot - 2, 'Fulfilment', 'cash flows' );
}
}
/* ── Detail Card ─────────────────────────────────────────── */
function renderDetail() {
var b = blocks[ selected ];
detailTitle.textContent = b.question;
detailTitle.style.color = b.textColor;
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;
for ( var i = hitAreas.length - 1; i >= 0; i-- ) {
var 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;
}
/* ── Event Wiring ────────────────────────────────────────── */
canvas.addEventListener( 'mousemove', function ( e ) {
canvas.style.cursor = hitTest( e ) >= 0 ? 'pointer' : 'default';
} );
canvas.addEventListener( 'click', function ( e ) {
var h = hitTest( e );
if ( h >= 0 ) {
selected = h;
draw();
renderDetail();
}
} );
window.addEventListener( 'resize', function () {
resize();
draw();
} );
// Initial render
resize();
draw();
renderDetail();
}
/* ================================================================
RISK ADJUSTMENT — data-wix-module="risk-adjustment"
Brittany storm: lognormal claim distribution with interactive
confidence slider. Shows best estimate, percentile threshold,
and the risk adjustment gap on a PDF chart.
================================================================ */
function initRiskAdjustment( container ) {
/* ── Constants ────────────────────────────────────────────── */
var MU = 1.7118;
var SIG = 0.40;
var MEAN = 6.0;
var XMIN = 1, XMAX = 18, STEPS = 400;
/* ── Math helpers ────────────────────────────────────────── */
function lognormPDF( x ) {
if ( x <= 0 ) return 0;
var lx = Math.log( x );
return Math.exp( -0.5 * Math.pow( ( lx - MU ) / SIG, 2 ) ) /
( x * SIG * Math.sqrt( 2 * Math.PI ) );
}
function ratApprox( t ) {
var c = [ 2.515517, 0.802853, 0.010328 ];
var d = [ 1.432788, 0.189269, 0.001308 ];
return t - ( c[0] + c[1]*t + c[2]*t*t ) /
( 1 + d[0]*t + d[1]*t*t + d[2]*t*t*t );
}
function normInv( p ) {
if ( p <= 0 ) return -Infinity;
if ( p >= 1 ) return Infinity;
if ( p < 0.5 ) return -ratApprox( Math.sqrt( -2 * Math.log( p ) ) );
if ( p > 0.5 ) return ratApprox( Math.sqrt( -2 * Math.log( 1 - p ) ) );
return 0;
}
function lognormCDFInv( p ) {
return Math.exp( MU + SIG * normInv( p ) );
}
/* ── Chart colors (from WIX tokens) ──────────────────────── */
var styles = getComputedStyle( container );
var colorAccent = styles.getPropertyValue( '--wix-accent' ).trim() || '#1a5276';
var colorWarn = styles.getPropertyValue( '--wix-wrong' ).trim() || '#922b21';
var colorGrid = styles.getPropertyValue( '--wix-border-subtle' ).trim() || '#eaecf0';
var colorAxis = styles.getPropertyValue( '--wix-text-muted' ).trim() || '#54595d';
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var slider = wix.el( 'input', { type: 'range', min: '50', max: '95', value: '75', step: '1' } );
var sliderVal = wix.el( 'span', { className: 'wix-ra-slider-val', textContent: '75%' } );
var elBE = wix.el( 'div', { className: 'wix-ra-stat-val', textContent: '\u20AC6.0m' } );
var elPct = wix.el( 'div', { className: 'wix-ra-stat-val', textContent: '\u20AC7.2m' } );
var elRA = wix.el( 'div', { className: 'wix-ra-stat-val wix-ra-stat-val--accent', textContent: '\u20AC1.2m' } );
var canvas = wix.el( 'canvas' );
var explain = wix.el( 'p', { className: 'wix-ra-explain' } );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
/* Slider row */
wrapper.appendChild( wix.el( 'div', { className: 'wix-ra-slider-row' }, [
wix.el( 'span', { className: 'wix-ra-slider-label', textContent: 'Confidence level' } ),
slider,
sliderVal
] ) );
/* Stats row */
wrapper.appendChild( wix.el( 'div', { className: 'wix-ra-stats' }, [
wix.el( 'div', { className: 'wix-ra-stat' }, [
wix.el( 'div', { className: 'wix-ra-stat-label', textContent: 'Best estimate' } ),
elBE
] ),
wix.el( 'div', { className: 'wix-ra-stat' }, [
wix.el( 'div', { className: 'wix-ra-stat-label', textContent: 'Covered up to' } ),
elPct
] ),
wix.el( 'div', { className: 'wix-ra-stat' }, [
wix.el( 'div', { className: 'wix-ra-stat-label', textContent: 'Risk adjustment' } ),
elRA
] )
] ) );
/* Chart */
wrapper.appendChild( wix.el( 'div', { className: 'wix-ra-chart' }, [ canvas ] ) );
/* Legend */
function swatch( color ) {
return wix.el( 'span', { className: 'wix-ra-legend-swatch', style: { background: color } } );
}
wrapper.appendChild( wix.el( 'div', { className: 'wix-ra-legend' }, [
wix.el( 'span', { className: 'wix-ra-legend-item' }, [
swatch( colorAccent + '40' ), 'Claim distribution'
] ),
wix.el( 'span', { className: 'wix-ra-legend-item' }, [
swatch( colorAccent ), 'Best estimate (mean)'
] ),
wix.el( 'span', { className: 'wix-ra-legend-item' }, [
swatch( colorWarn ), 'Confidence threshold'
] )
] ) );
/* Explanation */
wrapper.appendChild( explain );
/* ── Drawing ─────────────────────────────────────────────── */
function draw() {
var ctx = canvas.getContext( '2d' );
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
if ( rect.width === 0 || rect.height === 0 ) return;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale( dpr, dpr );
var W = rect.width, H = rect.height;
var pad = { t: 16, r: 16, b: 36, l: 44 };
var cw = W - pad.l - pad.r;
var ch = H - pad.t - pad.b;
ctx.clearRect( 0, 0, W, H );
/* Compute PDF curve */
var xs = [], ys = [], ymax = 0;
for ( var i = 0; i <= STEPS; i++ ) {
var x = XMIN + ( XMAX - XMIN ) * i / STEPS;
var y = lognormPDF( x );
xs.push( x ); ys.push( y );
if ( y > ymax ) ymax = y;
}
ymax *= 1.1;
function tx( x ) { return pad.l + ( x - XMIN ) / ( XMAX - XMIN ) * cw; }
function ty( y ) { return pad.t + ch - ( y / ymax ) * ch; }
var conf = parseInt( slider.value, 10 );
var pctX = lognormCDFInv( conf / 100 );
/* Axes */
ctx.strokeStyle = colorGrid;
ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.moveTo( pad.l, pad.t + ch ); ctx.lineTo( pad.l + cw, pad.t + ch ); ctx.stroke();
ctx.beginPath(); ctx.moveTo( pad.l, pad.t ); ctx.lineTo( pad.l, pad.t + ch ); ctx.stroke();
/* X-axis labels */
ctx.font = '11px sans-serif';
ctx.fillStyle = colorAxis;
ctx.textAlign = 'center';
for ( var v = 2; v <= 16; v += 2 ) {
var xp = tx( v );
ctx.fillText( '\u20AC' + v + 'm', xp, pad.t + ch + 20 );
ctx.strokeStyle = colorGrid;
ctx.beginPath(); ctx.moveTo( xp, pad.t + ch ); ctx.lineTo( xp, pad.t + ch + 4 ); ctx.stroke();
}
/* Shaded area under curve up to percentile */
ctx.beginPath();
ctx.moveTo( tx( xs[0] ), ty( 0 ) );
for ( i = 0; i <= STEPS; i++ ) {
if ( xs[i] <= pctX ) ctx.lineTo( tx( xs[i] ), ty( ys[i] ) );
}
var clipIdx = -1;
for ( i = 0; i < xs.length; i++ ) {
if ( xs[i] > pctX ) { clipIdx = i; break; }
}
if ( clipIdx > 0 ) {
var frac = ( pctX - xs[clipIdx-1] ) / ( xs[clipIdx] - xs[clipIdx-1] );
var yClip = ys[clipIdx-1] + frac * ( ys[clipIdx] - ys[clipIdx-1] );
ctx.lineTo( tx( pctX ), ty( yClip ) );
}
ctx.lineTo( tx( pctX ), ty( 0 ) );
ctx.closePath();
ctx.fillStyle = colorAccent + '30';
ctx.fill();
/* Full PDF curve */
ctx.beginPath();
ctx.moveTo( tx( xs[0] ), ty( ys[0] ) );
for ( i = 1; i <= STEPS; i++ ) ctx.lineTo( tx( xs[i] ), ty( ys[i] ) );
ctx.strokeStyle = colorAccent;
ctx.lineWidth = 2;
ctx.stroke();
/* Best-estimate (mean) dashed line */
ctx.setLineDash( [ 5, 4 ] );
ctx.lineWidth = 1.5;
ctx.strokeStyle = colorAccent;
ctx.beginPath();
ctx.moveTo( tx( MEAN ), ty( 0 ) );
ctx.lineTo( tx( MEAN ), ty( lognormPDF( MEAN ) ) );
ctx.stroke();
/* Percentile dashed line */
ctx.strokeStyle = colorWarn;
ctx.beginPath();
ctx.moveTo( tx( pctX ), ty( 0 ) );
ctx.lineTo( tx( pctX ), ty( lognormPDF( pctX ) ) );
ctx.stroke();
ctx.setLineDash( [] );
/* Risk-adjustment bracket */
var raStart = tx( MEAN ), raEnd = tx( pctX );
var arrowY = ty( 0 ) - 18;
if ( raEnd - raStart > 20 ) {
ctx.strokeStyle = colorWarn;
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo( raStart, arrowY ); ctx.lineTo( raEnd, arrowY ); ctx.stroke();
ctx.beginPath(); ctx.moveTo( raStart, arrowY - 4 ); ctx.lineTo( raStart, arrowY + 4 ); ctx.stroke();
ctx.beginPath(); ctx.moveTo( raEnd, arrowY - 4 ); ctx.lineTo( raEnd, arrowY + 4 ); ctx.stroke();
ctx.fillStyle = colorWarn;
ctx.font = '500 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText( 'RA: \u20AC' + ( pctX - MEAN ).toFixed( 1 ) + 'm',
( raStart + raEnd ) / 2, arrowY - 6 );
}
/* Update stat cards */
var ra = pctX - MEAN;
sliderVal.textContent = conf + '%';
elPct.textContent = '\u20AC' + pctX.toFixed( 1 ) + 'm';
elRA.textContent = '\u20AC' + ra.toFixed( 1 ) + 'm';
/* Update explanation */
explain.textContent = conf === 50
? 'At the 50% level the threshold equals the best estimate, so the risk adjustment is zero \u2014 the insurer holds no buffer for uncertainty at all.'
: 'At the ' + conf + '% confidence level, the insurer holds enough to cover outcomes up to \u20AC' + pctX.toFixed( 1 ) + 'm. The risk adjustment of \u20AC' + ra.toFixed( 1 ) + 'm is the gap between that threshold and the \u20AC6.0m best estimate \u2014 the price AXA pays for bearing the uncertainty on 3,000 Brittany homes.';
}
/* ── Events ──────────────────────────────────────────────── */
slider.addEventListener( 'input', draw );
window.addEventListener( 'resize', draw );
draw();
}
}() );