Spaces:
Running
Running
| <div class="d3-complexity-analysis"></div> | |
| <style> | |
| .d3-complexity-analysis { | |
| width: 100%; | |
| margin: 10px 0; | |
| position: relative; | |
| font-family: system-ui, -apple-system, sans-serif; | |
| } | |
| .d3-complexity-analysis svg { | |
| display: block; | |
| width: 100%; | |
| height: auto; | |
| } | |
| .d3-complexity-analysis .axes path, | |
| .d3-complexity-analysis .axes line { | |
| stroke: var(--axis-color, var(--text-color)); | |
| } | |
| .d3-complexity-analysis .axes text { | |
| fill: var(--tick-color, var(--muted-color)); | |
| font-size: 11px; | |
| } | |
| .d3-complexity-analysis .axes text.axis-label { | |
| font-size: 14px; | |
| font-weight: 500; | |
| fill: var(--text-color); | |
| } | |
| .d3-complexity-analysis .axes text.chart-title { | |
| font-size: 16px; | |
| font-weight: 600; | |
| fill: var(--text-color); | |
| } | |
| .d3-complexity-analysis .cell { | |
| stroke: var(--surface-bg, #fff); | |
| stroke-width: 2; | |
| cursor: pointer; | |
| transition: opacity 0.1s ease; | |
| } | |
| .d3-complexity-analysis .cell:hover { | |
| opacity: 0.85; | |
| } | |
| .d3-complexity-analysis .cell-text { | |
| font-size: 13px; | |
| font-weight: 600; | |
| pointer-events: none; | |
| } | |
| .d3-complexity-analysis .model-label { | |
| font-size: 12px; | |
| fill: var(--text-color); | |
| } | |
| .d3-complexity-analysis .quartile-label { | |
| font-size: 12px; | |
| fill: var(--text-color); | |
| } | |
| .d3-complexity-analysis .legend-title { | |
| font-size: 11px; | |
| fill: var(--muted-color); | |
| } | |
| .d3-complexity-analysis .legend-tick { | |
| font-size: 10px; | |
| fill: var(--muted-color); | |
| } | |
| .d3-complexity-analysis .d3-tooltip { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| transform: translate(-9999px, -9999px); | |
| pointer-events: none; | |
| padding: 10px 12px; | |
| border-radius: 8px; | |
| font-size: 12px; | |
| line-height: 1.5; | |
| border: 1px solid var(--border-color); | |
| background: var(--surface-bg); | |
| color: var(--text-color); | |
| box-shadow: 0 4px 24px rgba(0,0,0,.18); | |
| opacity: 0; | |
| transition: opacity 0.12s ease; | |
| z-index: 10; | |
| max-width: 280px; | |
| } | |
| .d3-complexity-analysis .d3-tooltip .model-name { | |
| font-weight: 600; | |
| margin-bottom: 4px; | |
| } | |
| .d3-complexity-analysis .d3-tooltip .metric { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 16px; | |
| } | |
| .d3-complexity-analysis .d3-tooltip .metric-label { | |
| color: var(--muted-color); | |
| } | |
| .d3-complexity-analysis .d3-tooltip .metric-value { | |
| font-weight: 500; | |
| } | |
| .d3-complexity-analysis .d3-tooltip .interpretation { | |
| margin-top: 6px; | |
| font-size: 11px; | |
| color: var(--muted-color); | |
| font-style: italic; | |
| } | |
| </style> | |
| <script> | |
| (() => { | |
| const ensureD3 = (cb) => { | |
| if (window.d3 && typeof window.d3.select === 'function') return cb(); | |
| let s = document.getElementById('d3-cdn-script'); | |
| if (!s) { | |
| s = document.createElement('script'); | |
| s.id = 'd3-cdn-script'; | |
| s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; | |
| document.head.appendChild(s); | |
| } | |
| const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); }; | |
| s.addEventListener('load', onReady, { once: true }); | |
| if (window.d3) onReady(); | |
| }; | |
| const bootstrap = () => { | |
| const scriptEl = document.currentScript; | |
| let container = scriptEl ? scriptEl.previousElementSibling : null; | |
| if (!(container && container.classList && container.classList.contains('d3-complexity-analysis'))) { | |
| const candidates = Array.from(document.querySelectorAll('.d3-complexity-analysis')) | |
| .filter((el) => !(el.dataset && el.dataset.mounted === 'true')); | |
| container = candidates[candidates.length - 1] || null; | |
| } | |
| if (!container) return; | |
| if (container.dataset) { | |
| if (container.dataset.mounted === 'true') return; | |
| container.dataset.mounted = 'true'; | |
| } | |
| // Tooltip setup | |
| container.style.position = container.style.position || 'relative'; | |
| const tip = document.createElement('div'); | |
| tip.className = 'd3-tooltip'; | |
| container.appendChild(tip); | |
| // SVG setup | |
| const svg = d3.select(container).append('svg'); | |
| const gRoot = svg.append('g'); | |
| // Chart groups | |
| const gAxes = gRoot.append('g').attr('class', 'axes'); | |
| const gCells = gRoot.append('g').attr('class', 'cells'); | |
| const gLegend = gRoot.append('g').attr('class', 'legend'); | |
| // State | |
| let data = null; | |
| let width = 700; | |
| let height = 450; | |
| const margin = { top: 60, right: 100, bottom: 60, left: 160 }; | |
| // Scales | |
| const xScale = d3.scaleBand(); | |
| const yScale = d3.scaleBand(); | |
| // Linear color scale: red (0%) -> green (100%+) | |
| const colorScale = d3.scaleLinear() | |
| .interpolate(() => d3.interpolateRdYlGn); | |
| const DATA_URL = '/data/complexity_analysis.json'; | |
| function updateSize() { | |
| width = Math.min(container.clientWidth || 700, 800); | |
| const numModels = data ? data.models.length : 10; | |
| const cellHeight = 36; | |
| height = margin.top + margin.bottom + numModels * cellHeight; | |
| svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`); | |
| gRoot.attr('transform', `translate(${margin.left},${margin.top})`); | |
| return { | |
| innerWidth: width - margin.left - margin.right, | |
| innerHeight: height - margin.top - margin.bottom | |
| }; | |
| } | |
| function getContrastColor(hexColor) { | |
| const hex = hexColor.replace('#', ''); | |
| const r = parseInt(hex.substr(0, 2), 16) / 255; | |
| const g = parseInt(hex.substr(2, 2), 16) / 255; | |
| const b = parseInt(hex.substr(4, 2), 16) / 255; | |
| const luminance = 0.299 * r + 0.587 * g + 0.114 * b; | |
| return luminance > 0.5 ? '#000000' : '#ffffff'; | |
| } | |
| function rgbToHex(rgb) { | |
| // Convert rgb(r, g, b) string to #rrggbb | |
| const match = rgb.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); | |
| if (!match) return rgb; | |
| const r = parseInt(match[1]).toString(16).padStart(2, '0'); | |
| const g = parseInt(match[2]).toString(16).padStart(2, '0'); | |
| const b = parseInt(match[3]).toString(16).padStart(2, '0'); | |
| return `#${r}${g}${b}`; | |
| } | |
| function showTooltip(event, d) { | |
| const rect = container.getBoundingClientRect(); | |
| const x = event.clientX - rect.left; | |
| const y = event.clientY - rect.top; | |
| const pct = d.score * 100; | |
| const interpretation = pct > 100 | |
| ? `Performs ${(pct - 100).toFixed(0)}% above average on ${d.quartile} rules` | |
| : pct < 100 | |
| ? `Performs ${(100 - pct).toFixed(0)}% below average on ${d.quartile} rules` | |
| : 'Performs at average on these rules'; | |
| const quartileDesc = { | |
| 'Q1': 'Easiest (lowest complexity)', | |
| 'Q2': 'Easy-Medium', | |
| 'Q3': 'Medium-Hard', | |
| 'Q4': 'Hardest (highest complexity)' | |
| }; | |
| tip.innerHTML = ` | |
| <div class="model-name">${d.model}</div> | |
| <div class="metric"> | |
| <span class="metric-label">Quartile:</span> | |
| <span class="metric-value">${d.quartile}</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">Difficulty:</span> | |
| <span class="metric-value">${quartileDesc[d.quartile]}</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">Relative Score:</span> | |
| <span class="metric-value">${pct.toFixed(0)}%</span> | |
| </div> | |
| <div class="interpretation">${interpretation}</div> | |
| `; | |
| const tipWidth = tip.offsetWidth || 200; | |
| const tipHeight = tip.offsetHeight || 120; | |
| let tipX = x + 12; | |
| let tipY = y - tipHeight / 2; | |
| if (tipX + tipWidth > width) tipX = x - tipWidth - 12; | |
| if (tipY < 0) tipY = 8; | |
| if (tipY + tipHeight > height) tipY = height - tipHeight - 8; | |
| tip.style.transform = `translate(${tipX}px, ${tipY}px)`; | |
| tip.style.opacity = '1'; | |
| } | |
| function hideTooltip() { | |
| tip.style.opacity = '0'; | |
| tip.style.transform = 'translate(-9999px, -9999px)'; | |
| } | |
| function render() { | |
| if (!data) return; | |
| const { innerWidth, innerHeight } = updateSize(); | |
| const quartiles = data.quartiles; | |
| const models = data.models; | |
| // Update scales | |
| xScale | |
| .domain(quartiles) | |
| .range([0, innerWidth]) | |
| .padding(0.08); | |
| yScale | |
| .domain(models.map(m => m.name)) | |
| .range([0, innerHeight]) | |
| .padding(0.08); | |
| // Find score extent for color scale (in percentage: 0-100%+) | |
| const allScores = []; | |
| models.forEach(m => { | |
| quartiles.forEach(q => { | |
| allScores.push(m.quartile_scores[q] * 100); | |
| }); | |
| }); | |
| const minPct = Math.min(...allScores); | |
| const maxPct = Math.max(...allScores); | |
| // Linear scale from 0% (red) to 100%+ (green) | |
| colorScale.domain([0, maxPct]); | |
| // Build cell data (with percentage values) | |
| const cellData = []; | |
| models.forEach(m => { | |
| quartiles.forEach(q => { | |
| cellData.push({ | |
| model: m.name, | |
| quartile: q, | |
| score: m.quartile_scores[q], | |
| pct: m.quartile_scores[q] * 100 | |
| }); | |
| }); | |
| }); | |
| // Draw cells | |
| gCells.selectAll('.cell') | |
| .data(cellData, d => `${d.model}-${d.quartile}`) | |
| .join('rect') | |
| .attr('class', 'cell') | |
| .attr('x', d => xScale(d.quartile)) | |
| .attr('y', d => yScale(d.model)) | |
| .attr('width', xScale.bandwidth()) | |
| .attr('height', yScale.bandwidth()) | |
| .attr('fill', d => colorScale(d.pct)) | |
| .attr('rx', 4) | |
| .on('mouseenter', showTooltip) | |
| .on('mousemove', showTooltip) | |
| .on('mouseleave', hideTooltip); | |
| // Draw cell text | |
| gCells.selectAll('.cell-text') | |
| .data(cellData, d => `${d.model}-${d.quartile}`) | |
| .join('text') | |
| .attr('class', 'cell-text') | |
| .attr('x', d => xScale(d.quartile) + xScale.bandwidth() / 2) | |
| .attr('y', d => yScale(d.model) + yScale.bandwidth() / 2) | |
| .attr('text-anchor', 'middle') | |
| .attr('dominant-baseline', 'central') | |
| .style('fill', d => { | |
| const bgColor = colorScale(d.pct); | |
| const hex = bgColor.startsWith('rgb') ? rgbToHex(bgColor) : bgColor; | |
| return getContrastColor(hex); | |
| }) | |
| .text(d => `${d.pct.toFixed(0)}%`); | |
| // Model labels (Y-axis) | |
| gAxes.selectAll('.model-label') | |
| .data(models, d => d.name) | |
| .join('text') | |
| .attr('class', 'model-label') | |
| .attr('x', -10) | |
| .attr('y', d => yScale(d.name) + yScale.bandwidth() / 2) | |
| .attr('text-anchor', 'end') | |
| .attr('dominant-baseline', 'central') | |
| .text(d => d.name); | |
| // Quartile labels (X-axis) | |
| gAxes.selectAll('.quartile-label') | |
| .data(quartiles) | |
| .join('text') | |
| .attr('class', 'quartile-label') | |
| .attr('x', d => xScale(d) + xScale.bandwidth() / 2) | |
| .attr('y', -10) | |
| .attr('text-anchor', 'middle') | |
| .text(d => d); | |
| // X-axis title | |
| gAxes.selectAll('.x-title') | |
| .data([0]) | |
| .join('text') | |
| .attr('class', 'x-title axis-label') | |
| .attr('x', innerWidth / 2) | |
| .attr('y', innerHeight + 40) | |
| .attr('text-anchor', 'middle') | |
| .text('Complexity Quartile (Q1 = easiest)'); | |
| // Chart title | |
| gAxes.selectAll('.chart-title') | |
| .data([0]) | |
| .join('text') | |
| .attr('class', 'chart-title') | |
| .attr('x', innerWidth / 2) | |
| .attr('y', -35) | |
| .attr('text-anchor', 'middle') | |
| .text('Model Performance by Rule Complexity'); | |
| // Legend | |
| const legendWidth = 20; | |
| const legendHeight = innerHeight * 0.6; | |
| const legendX = innerWidth + 30; | |
| const legendY = (innerHeight - legendHeight) / 2; | |
| // Create gradient | |
| const gradientId = 'complexity-legend-gradient'; | |
| let defs = svg.select('defs'); | |
| if (defs.empty()) { | |
| defs = svg.append('defs'); | |
| } | |
| defs.selectAll(`#${gradientId}`).remove(); | |
| const gradient = defs.append('linearGradient') | |
| .attr('id', gradientId) | |
| .attr('x1', '0%') | |
| .attr('x2', '0%') | |
| .attr('y1', '100%') | |
| .attr('y2', '0%'); | |
| const numStops = 11; | |
| for (let i = 0; i <= numStops; i++) { | |
| const t = i / numStops; | |
| const value = t * maxPct; | |
| gradient.append('stop') | |
| .attr('offset', `${t * 100}%`) | |
| .attr('stop-color', colorScale(value)); | |
| } | |
| // Legend rectangle | |
| gLegend.selectAll('.legend-rect') | |
| .data([0]) | |
| .join('rect') | |
| .attr('class', 'legend-rect') | |
| .attr('x', legendX) | |
| .attr('y', legendY) | |
| .attr('width', legendWidth) | |
| .attr('height', legendHeight) | |
| .attr('fill', `url(#${gradientId})`) | |
| .attr('rx', 2) | |
| .attr('stroke', 'var(--border-color)') | |
| .attr('stroke-width', 0.5); | |
| // Legend ticks (in percentage) | |
| const legendScale = d3.scaleLinear() | |
| .domain([0, maxPct]) | |
| .range([legendY + legendHeight, legendY]); | |
| // Generate nice tick values for percentage scale | |
| const tickValues = [0, 50, 100]; | |
| if (maxPct > 100) tickValues.push(Math.round(maxPct / 10) * 10); | |
| gLegend.selectAll('.legend-tick') | |
| .data(tickValues.filter(v => v <= maxPct)) | |
| .join('text') | |
| .attr('class', 'legend-tick') | |
| .attr('x', legendX + legendWidth + 6) | |
| .attr('y', d => legendScale(d)) | |
| .attr('dominant-baseline', 'middle') | |
| .text(d => `${d}%`); | |
| // Legend title | |
| gLegend.selectAll('.legend-title') | |
| .data([0]) | |
| .join('text') | |
| .attr('class', 'legend-title') | |
| .attr('x', legendX + legendWidth / 2) | |
| .attr('y', legendY - 12) | |
| .attr('text-anchor', 'middle') | |
| .text('Relative Score'); | |
| } | |
| // Initialize | |
| fetch(DATA_URL, { cache: 'no-cache' }) | |
| .then(r => r.json()) | |
| .then(json => { | |
| data = json; | |
| render(); | |
| }) | |
| .catch(err => { | |
| const pre = document.createElement('pre'); | |
| pre.style.color = 'red'; | |
| pre.style.padding = '16px'; | |
| pre.textContent = `Error loading data: ${err.message}`; | |
| container.appendChild(pre); | |
| }); | |
| // Resize handling | |
| if (window.ResizeObserver) { | |
| new ResizeObserver(() => render()).observe(container); | |
| } else { | |
| window.addEventListener('resize', render); | |
| } | |
| // Theme change handling | |
| const observer = new MutationObserver(() => render()); | |
| observer.observe(document.documentElement, { | |
| attributes: true, | |
| attributeFilter: ['data-theme'] | |
| }); | |
| }; | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); | |
| } else { | |
| ensureD3(bootstrap); | |
| } | |
| })(); | |
| </script> | |