Jump to content

MediaWiki:Gadget-wix-interactive.js

From Insurer Brain
Revision as of 01:07, 1 April 2026 by Wikilah admin (talk | contribs) (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...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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 );
    } );
  }

}() );