MediaWiki:Gadget-wix-interactive.js: Difference between revisions
Content deleted Content added
No edit summary |
No edit summary |
||
Line 18:
var dispatchers = {
'pool-simulator': initPoolSimulator,
'insurer-engines': initInsurerEngines,
'premium-matching': initPremiumMatching
};
Line 128 ⟶ 129:
elPoolLog
] );
// Outer wrapper card
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Panels grid
alonePanel,
poolPanel
Line 146 ⟶ 151:
] );
btnStep, btnPlay, btnReset, elYear, speedLabel
] ) );
Line 152 ⟶ 157:
// Summary (hidden until simulation ends)
var summaryEl = wix.el( 'div', { className: 'wix-sim-summary wix-hidden' } );
Line 681 ⟶ 686:
// 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' );
}
| |||