OER Impact Calculator

Copy the code from the box below to add this widget to your LibGuide.

</> Embed Code
<!-- START FIXED OER WIDGET -->
<div id="oer-a11y-widget">

    <!-- 1. Load Chart.js -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

    <!-- 2. CSS Styles -->
    <style>
        #oer-a11y-widget {
            font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
            max-width: 100%;
            margin: 0 auto;
            background: #ffffff;
            border: 1px solid #767676;
            border-radius: 8px;
            display: flex;
            flex-wrap: wrap;
            color: #222;
        }

        /* SCREEN READER ONLY CLASS */
        .sr-only {
            position: absolute;
            width: 1px;
            height: 1px;
            padding: 0;
            margin: -1px;
            overflow: hidden;
            clip: rect(0, 0, 0, 0);
            white-space: nowrap;
            border: 0;
        }

        /* LEFT PANEL */
        .oer-panel-left {
            flex: 1;
            min-width: 300px;
            padding: 20px;
            background-color: #f4f4f4;
            border-right: 1px solid #ccc;
        }

        #oer-a11y-widget h2 {
            margin: 0 0 10px 0;
            color: #1b5e20;
            font-size: 1.4rem;
        }

        .oer-desc {
            font-size: 0.95rem;
            color: #444;
            margin-bottom: 20px;
            border-bottom: 2px solid #ccc;
            padding-bottom: 15px;
        }

        /* FORM CONTROLS */
        .input-group { margin-bottom: 15px; }
        
        #oer-a11y-widget label {
            display: block;
            font-weight: 700;
            margin-bottom: 5px;
            color: #222;
        }

        #oer-a11y-widget input[type="number"] {
            width: 100%;
            padding: 10px;
            border: 2px solid #555;
            border-radius: 4px;
            font-size: 1rem;
            background: #fff;
            color: #000;
            box-sizing: border-box; 
        }

        #oer-a11y-widget input[type="number"]:focus {
            outline: 3px solid #005fcc;
            border-color: #005fcc;
        }

        /* ACCESSIBLE RADIO BUTTONS */
        #oer-a11y-widget fieldset {
            border: 0;
            padding: 0;
            margin: 20px 0 0 0;
            border-top: 1px solid #ccc;
            padding-top: 15px;
        }

        #oer-a11y-widget legend {
            font-weight: 700;
            margin-bottom: 10px;
            display: block;
            width: 100%;
        }

        .radio-flex { display: flex; gap: 10px; }

        .custom-radio {
            position: relative;
            flex: 1;
        }

        .custom-radio input {
            opacity: 0;
            position: absolute;
            width: 100%;
            height: 100%;
            cursor: pointer;
            z-index: 1;
        }

        .radio-tile {
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 10px;
            background: #fff;
            border: 2px solid #555;
            border-radius: 4px;
            font-weight: 600;
            transition: all 0.2s;
        }

        .custom-radio input:focus + .radio-tile {
            outline: 3px solid #005fcc;
            border-color: #005fcc;
        }

        .custom-radio input:checked + .radio-tile {
            background-color: #1b5e20;
            color: #fff;
            border-color: #1b5e20;
        }

        /* RIGHT PANEL */
        .oer-panel-right {
            flex: 1.5;
            min-width: 320px;
            padding: 20px;
            background: #fff;
            display: flex;
            flex-direction: column;
        }

        .viz-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 10px;
        }

        .viz-title {
            font-weight: 800;
            color: #1b5e20;
            font-size: 1.1rem;
        }

        /* TOGGLE SWITCH */
        .toggle-wrapper {
            position: relative;
            display: inline-flex;
            align-items: center;
            font-weight: 600;
            font-size: 0.9rem;
        }
        
        .toggle-wrapper input {
            opacity: 0;
            width: 100%;
            height: 100%;
            position: absolute;
            z-index: 2;
            cursor: pointer;
        }

        .toggle-bg {
            width: 40px; height: 22px;
            background: #555;
            border-radius: 20px;
            margin: 0 8px;
            position: relative;
        }
        
        .toggle-dot {
            width: 16px; height: 16px;
            background: #fff;
            border-radius: 50%;
            position: absolute;
            top: 3px; left: 3px;
            transition: transform 0.2s;
        }

        .toggle-wrapper input:checked ~ .toggle-bg { background: #1b5e20; }
        .toggle-wrapper input:checked ~ .toggle-bg .toggle-dot { transform: translateX(18px); }
        .toggle-wrapper input:focus ~ .toggle-bg { outline: 3px solid #005fcc; }

        .chart-box {
            position: relative;
            flex-grow: 1;
            min-height: 300px;
            width: 100%;
            /* Extra padding to ensure labels don't clip */
            padding-right: 5px; 
            padding-left: 5px;
        }

        /* RESULTS CARDS */
        .results-section { margin-top: 20px; }
        
        .hero-card {
            background: #e8f5e9;
            border: 2px solid #1b5e20;
            padding: 15px;
            border-radius: 6px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            color: #000;
            margin-bottom: 10px;
        }
        
        .hero-text { font-weight: 700; text-transform: uppercase; font-size: 0.9rem; }
        .hero-num { font-size: 1.8rem; font-weight: 800; color: #1b5e20; }

        .sub-card {
            background: #f0f0f0;
            border: 1px solid #999;
            padding: 12px;
            border-radius: 6px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            color: #222;
            margin-bottom: 10px;
        }
        .sub-num { font-weight: 800; font-size: 1.1rem; }

    </style>

    <!-- 3. HTML Structure -->
    <div class="oer-panel-left">
        <h2>OER Impact Calculator</h2>
        <div class="oer-desc">
            Calculate the cumulative student savings or costs by adopting Open Educational Resources (OER).
        </div>
        
        <div class="input-group">
            <label for="inp-curr">Current Textbook Price ($)</label>
            <input type="number" id="inp-curr" value="185" min="0">
        </div>

        <div class="input-group">
            <label for="inp-alt">Alternative Low-Cost Price ($)</label>
            <input type="number" id="inp-alt" value="75" min="0">
        </div>

        <div class="input-group">
            <label for="inp-stu">Students per Semester</label>
            <input type="number" id="inp-stu" value="30" min="1">
        </div>

        <div class="input-group">
            <label for="inp-sem">Duration (Semesters)</label>
            <input type="number" id="inp-sem" value="3" min="1" max="15">
        </div>

        <fieldset>
            <legend>Visualization Perspective</legend>
            <div class="radio-flex">
                <div class="custom-radio">
                    <input type="radio" name="viewMode" id="mode-save" value="savings" checked>
                    <div class="radio-tile">Savings</div>
                </div>
                <div class="custom-radio">
                    <input type="radio" name="viewMode" id="mode-cost" value="costs">
                    <div class="radio-tile">Costs</div>
                </div>
            </div>
        </fieldset>
    </div>

    <div class="oer-panel-right">
        <div class="viz-header">
            <div class="viz-title" id="viz-heading" role="heading" aria-level="3">Cumulative Savings</div>
            
            <label class="toggle-wrapper">
                <span>Line</span>
                <input type="checkbox" id="type-toggle">
                <div class="toggle-bg"><div class="toggle-dot"></div></div>
                <span>Bar</span>
            </label>
        </div>

        <div class="chart-box">
            <canvas id="a11yCanvas" aria-label="Visualization graph. Please refer to the table below for data." role="img"></canvas>
        </div>

        <!-- Screen Reader Data Table (Visually Hidden) -->
        <div class="sr-only" aria-live="polite">
            <table id="sr-data-table">
                <caption>Graph Data Values</caption>
                <thead>
                    <tr>
                        <th scope="col">Semester</th>
                        <th scope="col" id="th-oer">OER</th>
                        <th scope="col" id="th-alt">Alternative</th>
                        <th scope="col" id="th-curr">Current</th>
                    </tr>
                </thead>
                <tbody id="sr-table-body"></tbody>
            </table>
        </div>

        <!-- Live Region for Results -->
        <div class="results-section" id="live-results" aria-live="polite" aria-atomic="true">
            <!-- Injected via JS -->
        </div>
    </div>

    <!-- 4. Logic -->
    <script>
        (function() {
            // Check for Reduced Motion Preference
            const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

            const ctx = document.getElementById('a11yCanvas').getContext('2d');
            
            const ui = {
                curr: document.getElementById('inp-curr'),
                alt: document.getElementById('inp-alt'),
                stu: document.getElementById('inp-stu'),
                sem: document.getElementById('inp-sem'),
                toggle: document.getElementById('type-toggle'),
                heading: document.getElementById('viz-heading'),
                results: document.getElementById('live-results'),
                tbody: document.getElementById('sr-table-body')
            };

            const radios = document.querySelectorAll('input[name="viewMode"]');

            let myChart = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: [],
                    datasets: [
                        {
                            label: 'OER (Open Ed)',
                            data: [],
                            borderColor: '#1b5e20', // Green
                            backgroundColor: 'rgba(27, 94, 32, 0.7)',
                            borderWidth: 3,
                            pointStyle: 'rectRot',
                            pointRadius: 6,
                            tension: 0.2
                        },
                        {
                            label: 'Alternative Option',
                            data: [],
                            borderColor: '#e65100', // Orange
                            backgroundColor: 'rgba(230, 81, 0, 0.7)',
                            borderWidth: 3,
                            borderDash: [5, 5],
                            pointStyle: 'triangle',
                            pointRadius: 6,
                            tension: 0.2
                        },
                        {
                            label: 'Current Book',
                            data: [],
                            borderColor: '#b71c1c', // Red
                            backgroundColor: 'rgba(183, 28, 28, 0.7)',
                            borderWidth: 3,
                            pointStyle: 'circle',
                            pointRadius: 6,
                            tension: 0.2,
                            hidden: true
                        }
                    ]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    animation: {
                        duration: prefersReducedMotion ? 0 : 750
                    },
                    layout: {
                        padding: {
                            left: 0,
                            right: 10, /* Added padding specifically to help prevent edge clipping */
                            top: 0,
                            bottom: 0
                        }
                    },
                    scales: {
                        y: {
                            beginAtZero: true,
                            grace: '5%',
                            ticks: { callback: (v) => '$' + v.toLocaleString() },
                            title: { display: true, text: 'US Dollars ($)' }
                        },
                        x: {
                            title: { display: true, text: 'Timeline (Semesters)' }
                        }
                    },
                    plugins: {
                        legend: { 
                            labels: { usePointStyle: true, color: '#000', font: { weight: 'bold' } } 
                        },
                        tooltip: {
                            titleColor: '#fff',
                            bodyColor: '#fff',
                            callbacks: { label: (c) => c.dataset.label + ': $' + c.raw.toLocaleString() }
                        }
                    }
                }
            });

            function update() {
                const pCurr = parseFloat(ui.curr.value) || 0;
                const pAlt = parseFloat(ui.alt.value) || 0;
                const nStu = parseInt(ui.stu.value) || 0;
                const nSem = parseInt(ui.sem.value) || 1;

                let mode = 'savings';
                radios.forEach(r => { if(r.checked) mode = r.value; });

                const labels = [];
                const dOER = [], dAlt = [], dCurr = [];
                let runOER = 0, runAlt = 0, runCurr = 0;
                let tableHTML = "";

                // Per Semester Calculations
                // FIX: Added Math.max(0, ...) to ensure savings don't display as negative
                const savingSemOER = Math.max(0, (pCurr - 0) * nStu);
                const savingSemAlt = Math.max(0, (pCurr - pAlt) * nStu);
                
                const costSemOER = 0;
                const costSemAlt = pAlt * nStu;
                const costSemCurr = pCurr * nStu;

                for(let i=1; i<=nSem; i++) {
                    labels.push('Sem ' + i);
                    
                    if(mode === 'savings') {
                        runOER += savingSemOER;
                        runAlt += savingSemAlt;
                    } else {
                        runOER += costSemOER;
                        runAlt += costSemAlt;
                        runCurr += costSemCurr;
                    }

                    dOER.push(runOER);
                    dAlt.push(runAlt);
                    dCurr.push(runCurr);

                    tableHTML += `
                        <tr>
                            <td>Semester ${i}</td>
                            <td>$${runOER.toLocaleString()}</td>
                            <td>$${runAlt.toLocaleString()}</td>
                            <td>$${(mode==='costs' ? runCurr : 0).toLocaleString()}</td>
                        </tr>
                    `;
                }

                ui.tbody.innerHTML = tableHTML;
                myChart.data.labels = labels;

                if(mode === 'savings') {
                    // --- SAVINGS VIEW ---
                    ui.heading.textContent = "Cumulative Savings Potential";
                    
                    myChart.data.datasets[0].label = "Savings with OER";
                    myChart.data.datasets[0].data = dOER;
                    myChart.data.datasets[0].hidden = false;

                    myChart.data.datasets[1].label = "Savings with Alternative";
                    myChart.data.datasets[1].data = dAlt;
                    myChart.data.datasets[1].hidden = false;

                    myChart.data.datasets[2].hidden = true;

                    ui.results.innerHTML = `
                        <div class="hero-card">
                            <span class="hero-text">Total OER Savings</span>
                            <span class="hero-num">$${runOER.toLocaleString()}</span>
                        </div>
                        <div class="sub-card">
                            <span>Total Savings with Alternative</span>
                            <span class="sub-num">$${runAlt.toLocaleString()}</span>
                        </div>
                    `;
                } else {
                    // --- COSTS VIEW ---
                    ui.heading.textContent = "Cumulative Student Costs";
                    
                    myChart.data.datasets[0].label = "Cost of OER ($0)";
                    myChart.data.datasets[0].data = dOER;
                    myChart.data.datasets[0].hidden = false;

                    myChart.data.datasets[1].label = "Cost of Alternative";
                    myChart.data.datasets[1].data = dAlt;
                    myChart.data.datasets[1].hidden = false;

                    myChart.data.datasets[2].label = "Cost of Current Book";
                    myChart.data.datasets[2].data = dCurr;
                    myChart.data.datasets[2].hidden = false;

                    ui.results.innerHTML = `
                        <div class="hero-card">
                            <span class="hero-text">Cost of OER (Free)</span>
                            <span class="hero-num">$0</span>
                        </div>
                        <div class="sub-card" style="border-left: 5px solid #e65100;">
                            <span>Cost of Alternative Option</span>
                            <span class="sub-num">$${runAlt.toLocaleString()}</span>
                        </div>
                        <div class="sub-card" style="border-left: 5px solid #b71c1c;">
                            <span>Cost of Current Book</span>
                            <span class="sub-num">$${runCurr.toLocaleString()}</span>
                        </div>
                    `;
                }

                const isBar = ui.toggle.checked;
                myChart.config.type = isBar ? 'bar' : 'line';
                
                myChart.options.scales.x.offset = isBar;
                myChart.data.datasets.forEach(ds => ds.fill = isBar);
                
                myChart.update();
            }

            [ui.curr, ui.alt, ui.stu, ui.sem, ui.toggle].forEach(el => {
                el.addEventListener('input', update);
                el.addEventListener('change', update);
            });
            radios.forEach(r => r.addEventListener('change', update));

            update();
        })();
    </script>
</div>
<!-- END WIDGET CODE -->
    This widget (a <a href="https://e3la.github.io/oer-savings-widget">copy of the code is here for your convenience</a>) was created with Google Gemini by Helena Marvin and you can see <a href="https://aistudio.google.com/app/prompts?state=%7B%22ids%22:%5B%221ZUFwqIOCJByHFj0w-RX45LuzWU-5YQAh%22%5D,%22action%22:%22open%22,%22userId%22:%22106905111806513074045%22,%22resourceKeys%22:%7B%7D%7D&usp=sharing">the prompt here.</a> Much thanks to the chart.js library under the the MIT License.
👁 Widget Preview

OER Impact Calculator

Calculate the cumulative student savings or costs by adopting Open Educational Resources (OER).
Visualization Perspective
Savings
Costs
Cumulative Savings
Graph Data Values
Semester OER Alternative Current

This widget was created with Google Gemini by Helena Marvin and you can see the prompt here. Much thanks to the chart.js library under the the MIT License.

This CC0 page was crafted with Gemini by Helena Marvin and the prompt can be found here.