MediaWiki:Gadget-wix-interactive.js: Difference between revisions
Appearance
Content deleted Content added
Created page with "/* ================================================================ 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 (Borde..." |
No edit summary |
||
| Line 8: | Line 8: | ||
Supported modules: |
Supported modules: |
||
- "pool-simulator" Risk pooling comparison (Bordeaux vs Normandy) |
- "pool-simulator" Risk pooling comparison (Bordeaux vs Normandy) |
||
- "insurer-engines" Two-engine profit simulator (underwriting + investment) |
|||
================================================================ */ |
================================================================ */ |
||
| Line 17: | Line 18: | ||
var dispatchers = { |
var dispatchers = { |
||
'pool-simulator': initPoolSimulator |
'pool-simulator': initPoolSimulator, |
||
'insurer-engines': initInsurerEngines |
|||
}; |
}; |
||
| Line 433: | Line 435: | ||
drawChart( canvasPool, poolData, {}, colorPool ); |
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 ); |
|||
// Fixed info row |
|||
container.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 |
|||
container.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 ); |
|||
} ); |
|||
container.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' } ); |
|||
container.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%' } ); |
|||
container.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 |
|||
container.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' } ); |
|||
container.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' } ); |
|||
container.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' } ); |
|||
container.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 |
|||
container.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' } ); |
|||
container.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.' } ); |
|||
container.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(); |
|||
} |
} |
||
Revision as of 01:36, 1 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)
================================================================ */
( function () {
'use strict';
/* ── Dispatcher ─────────────────────────────────────────────── */
var dispatchers = {
'pool-simulator': initPoolSimulator,
'insurer-engines': initInsurerEngines
};
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
] );
// Panels grid
container.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
] );
container.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' } );
container.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 );
// Fixed info row
container.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
container.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 );
} );
container.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' } );
container.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%' } );
container.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
container.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' } );
container.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' } );
container.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' } );
container.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
container.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' } );
container.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.' } );
container.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();
}
}() );