MediaWiki:Gadget-wix-interactive.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* ================================================================
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)
================================================================ */
( function () {
'use strict';
/* ── Dispatcher ─────────────────────────────────────────────── */
var dispatchers = {
'pool-simulator': initPoolSimulator
};
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 );
} );
}
}() );