Spaces:
Running
Running
| <div class="d3-complexity-ratio"></div> | |
| <style> | |
| .d3-complexity-ratio { | |
| width: 100%; | |
| margin: 10px 0; | |
| position: relative; | |
| font-family: system-ui, -apple-system, sans-serif; | |
| } | |
| .d3-complexity-ratio svg { | |
| display: block; | |
| width: 100%; | |
| height: auto; | |
| } | |
| .d3-complexity-ratio .axes path, | |
| .d3-complexity-ratio .axes line { | |
| stroke: var(--axis-color, var(--text-color)); | |
| } | |
| .d3-complexity-ratio .axes text { | |
| fill: var(--tick-color, var(--muted-color)); | |
| font-size: 11px; | |
| } | |
| .d3-complexity-ratio .grid line { | |
| stroke: var(--grid-color, rgba(0,0,0,.08)); | |
| } | |
| .d3-complexity-ratio .axes text.axis-label { | |
| font-size: 14px; | |
| font-weight: 500; | |
| fill: var(--text-color); | |
| } | |
| .d3-complexity-ratio .reference-line { | |
| stroke: var(--muted-color); | |
| stroke-dasharray: 5, 5; | |
| stroke-width: 1.5; | |
| } | |
| .d3-complexity-ratio .whisker-line { | |
| stroke-width: 1.5; | |
| } | |
| .d3-complexity-ratio .whisker-cap { | |
| stroke-width: 1.5; | |
| } | |
| .d3-complexity-ratio .model-point { | |
| stroke-width: 2; | |
| cursor: pointer; | |
| } | |
| .d3-complexity-ratio .model-point:hover { | |
| stroke-width: 3; | |
| } | |
| .d3-complexity-ratio .ratio-label { | |
| font-size: 11px; | |
| fill: var(--muted-color); | |
| } | |
| .d3-complexity-ratio .legend-item { | |
| cursor: default; | |
| } | |
| .d3-complexity-ratio .legend-text { | |
| font-size: 11px; | |
| fill: var(--text-color); | |
| } | |
| .d3-complexity-ratio .subtitle { | |
| font-size: 11px; | |
| fill: var(--muted-color); | |
| } | |
| .d3-complexity-ratio .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.4; | |
| 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; | |
| } | |
| .d3-complexity-ratio .d3-tooltip .model-name { | |
| font-weight: 600; | |
| margin-bottom: 4px; | |
| } | |
| .d3-complexity-ratio .d3-tooltip .metric { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 16px; | |
| } | |
| .d3-complexity-ratio .d3-tooltip .metric-label { | |
| color: var(--muted-color); | |
| } | |
| .d3-complexity-ratio .d3-tooltip .metric-value { | |
| font-weight: 500; | |
| } | |
| </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-ratio'))) { | |
| const candidates = Array.from(document.querySelectorAll('.d3-complexity-ratio')) | |
| .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 gGrid = gRoot.append('g').attr('class', 'grid'); | |
| const gReference = gRoot.append('g').attr('class', 'reference'); | |
| const gAxes = gRoot.append('g').attr('class', 'axes'); | |
| const gWhiskers = gRoot.append('g').attr('class', 'whiskers'); | |
| const gPoints = gRoot.append('g').attr('class', 'points'); | |
| const gLabels = gRoot.append('g').attr('class', 'labels'); | |
| const gLegend = gRoot.append('g').attr('class', 'legend'); | |
| // State | |
| let data = null; | |
| let width = 800; | |
| let height = 500; | |
| const margin = { top: 30, right: 100, bottom: 60, left: 180 }; | |
| // Scales | |
| const xScale = d3.scaleLinear(); | |
| const yScale = d3.scaleBand(); | |
| // Data loading | |
| const DATA_URL = '/data/complexity_ratio.json'; | |
| function showTooltip(event, model) { | |
| const rect = container.getBoundingClientRect(); | |
| const x = event.clientX - rect.left; | |
| const y = event.clientY - rect.top; | |
| const interpretation = model.median_ratio > 1.05 | |
| ? 'Tends to overcomplicate' | |
| : model.median_ratio < 0.95 | |
| ? 'Tends to oversimplify' | |
| : 'Matches complexity well'; | |
| tip.innerHTML = ` | |
| <div class="model-name" style="color: ${model.color}">${model.name}</div> | |
| <div class="metric"> | |
| <span class="metric-label">Median ratio:</span> | |
| <span class="metric-value">${model.median_ratio.toFixed(2)}</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">IQR:</span> | |
| <span class="metric-value">${model.q25.toFixed(2)} – ${model.q75.toFixed(2)}</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">Samples:</span> | |
| <span class="metric-value">n=${model.count}</span> | |
| </div> | |
| <div class="metric" style="margin-top: 4px;"> | |
| <span class="metric-label">Interpretation:</span> | |
| <span class="metric-value">${interpretation}</span> | |
| </div> | |
| `; | |
| const tipWidth = tip.offsetWidth || 180; | |
| 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 updateSize() { | |
| width = container.clientWidth || 800; | |
| height = Math.max(420, Math.round(width * 0.55)); | |
| 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 render() { | |
| if (!data) return; | |
| const { innerWidth, innerHeight } = updateSize(); | |
| // Sort models by median ratio (ascending - lowest at top) | |
| const models = [...data.models].sort((a, b) => a.median_ratio - b.median_ratio); | |
| // X scale: ratio values with padding | |
| const xMin = d3.min(models, m => m.q25); | |
| const xMax = d3.max(models, m => m.q75); | |
| const xPadding = (xMax - xMin) * 0.1; | |
| xScale | |
| .domain([Math.min(0.6, xMin - xPadding), Math.max(2.4, xMax + xPadding)]) | |
| .range([0, innerWidth]); | |
| // Y scale: categorical (model names) | |
| yScale | |
| .domain(models.map(m => m.name)) | |
| .range([0, innerHeight]) | |
| .padding(0.4); | |
| // Grid lines (vertical) | |
| const xTicks = xScale.ticks(8); | |
| gGrid.selectAll('.grid-x') | |
| .data(xTicks) | |
| .join('line') | |
| .attr('class', 'grid-x') | |
| .attr('x1', d => xScale(d)) | |
| .attr('x2', d => xScale(d)) | |
| .attr('y1', 0) | |
| .attr('y2', innerHeight); | |
| // Reference line at x=1 | |
| gReference.selectAll('.reference-line') | |
| .data([1]) | |
| .join('line') | |
| .attr('class', 'reference-line') | |
| .attr('x1', d => xScale(d)) | |
| .attr('x2', d => xScale(d)) | |
| .attr('y1', 0) | |
| .attr('y2', innerHeight); | |
| // Axes | |
| const tickSize = 6; | |
| gAxes.selectAll('.x-axis') | |
| .data([0]) | |
| .join('g') | |
| .attr('class', 'x-axis') | |
| .attr('transform', `translate(0,${innerHeight})`) | |
| .call(d3.axisBottom(xScale) | |
| .ticks(8) | |
| .tickFormat(d3.format('.2f')) | |
| .tickSizeInner(-tickSize) | |
| .tickSizeOuter(0)); | |
| gAxes.selectAll('.y-axis') | |
| .data([0]) | |
| .join('g') | |
| .attr('class', 'y-axis') | |
| .call(d3.axisLeft(yScale) | |
| .tickSizeInner(-tickSize) | |
| .tickSizeOuter(0)); | |
| // X-axis label | |
| gAxes.selectAll('.x-label') | |
| .data([0]) | |
| .join('text') | |
| .attr('class', 'x-label axis-label') | |
| .attr('x', innerWidth / 2) | |
| .attr('y', innerHeight + 40) | |
| .attr('text-anchor', 'middle') | |
| .text('Complexity Ratio (Tentative / Actual)'); | |
| // Subtitle | |
| gAxes.selectAll('.subtitle') | |
| .data([0]) | |
| .join('text') | |
| .attr('class', 'subtitle') | |
| .attr('x', innerWidth / 2) | |
| .attr('y', innerHeight + 54) | |
| .attr('text-anchor', 'middle') | |
| .text('>1: Overcomplicates | <1: Oversimplifies | =1: Matches complexity'); | |
| const bandHeight = yScale.bandwidth(); | |
| const capHeight = bandHeight * 0.4; | |
| const pointSize = Math.min(8, bandHeight * 0.35); | |
| // Whiskers (IQR lines) | |
| gWhiskers.selectAll('.whisker-line') | |
| .data(models, d => d.name) | |
| .join('line') | |
| .attr('class', 'whisker-line') | |
| .attr('x1', d => xScale(d.q25)) | |
| .attr('x2', d => xScale(d.q75)) | |
| .attr('y1', d => yScale(d.name) + bandHeight / 2) | |
| .attr('y2', d => yScale(d.name) + bandHeight / 2) | |
| .attr('stroke', d => d.color); | |
| // Left whisker caps | |
| gWhiskers.selectAll('.whisker-cap-left') | |
| .data(models, d => d.name) | |
| .join('line') | |
| .attr('class', 'whisker-cap whisker-cap-left') | |
| .attr('x1', d => xScale(d.q25)) | |
| .attr('x2', d => xScale(d.q25)) | |
| .attr('y1', d => yScale(d.name) + bandHeight / 2 - capHeight / 2) | |
| .attr('y2', d => yScale(d.name) + bandHeight / 2 + capHeight / 2) | |
| .attr('stroke', d => d.color); | |
| // Right whisker caps | |
| gWhiskers.selectAll('.whisker-cap-right') | |
| .data(models, d => d.name) | |
| .join('line') | |
| .attr('class', 'whisker-cap whisker-cap-right') | |
| .attr('x1', d => xScale(d.q75)) | |
| .attr('x2', d => xScale(d.q75)) | |
| .attr('y1', d => yScale(d.name) + bandHeight / 2 - capHeight / 2) | |
| .attr('y2', d => yScale(d.name) + bandHeight / 2 + capHeight / 2) | |
| .attr('stroke', d => d.color); | |
| // Model points - circles for closed, squares for open | |
| const closedModels = models.filter(m => !m.is_open); | |
| const openModels = models.filter(m => m.is_open); | |
| // Closed models: circles | |
| gPoints.selectAll('.model-point-circle') | |
| .data(closedModels, d => d.name) | |
| .join('circle') | |
| .attr('class', 'model-point model-point-circle') | |
| .attr('cx', d => xScale(d.median_ratio)) | |
| .attr('cy', d => yScale(d.name) + bandHeight / 2) | |
| .attr('r', pointSize) | |
| .attr('fill', d => d.color) | |
| .attr('stroke', d => d.color) | |
| .on('mouseenter', (event, d) => showTooltip(event, d)) | |
| .on('mousemove', (event, d) => showTooltip(event, d)) | |
| .on('mouseleave', hideTooltip); | |
| // Open models: squares | |
| gPoints.selectAll('.model-point-square') | |
| .data(openModels, d => d.name) | |
| .join('rect') | |
| .attr('class', 'model-point model-point-square') | |
| .attr('x', d => xScale(d.median_ratio) - pointSize) | |
| .attr('y', d => yScale(d.name) + bandHeight / 2 - pointSize) | |
| .attr('width', pointSize * 2) | |
| .attr('height', pointSize * 2) | |
| .attr('fill', 'none') | |
| .attr('stroke', d => d.color) | |
| .attr('stroke-width', 2) | |
| .on('mouseenter', (event, d) => showTooltip(event, d)) | |
| .on('mousemove', (event, d) => showTooltip(event, d)) | |
| .on('mouseleave', hideTooltip); | |
| // Ratio labels on the right | |
| gLabels.selectAll('.ratio-label') | |
| .data(models, d => d.name) | |
| .join('text') | |
| .attr('class', 'ratio-label') | |
| .attr('x', innerWidth + 8) | |
| .attr('y', d => yScale(d.name) + bandHeight / 2) | |
| .attr('dy', '0.35em') | |
| .text(d => `${d.median_ratio.toFixed(2)} (n=${d.count})`); | |
| // Legend | |
| const legendY = -15; | |
| const legendItems = [ | |
| { label: 'Closed model', shape: 'circle' }, | |
| { label: 'Open model', shape: 'square' } | |
| ]; | |
| const legendGroup = gLegend.selectAll('.legend-item') | |
| .data(legendItems) | |
| .join('g') | |
| .attr('class', 'legend-item') | |
| .attr('transform', (d, i) => `translate(${innerWidth - 80 - i * 100}, ${legendY})`); | |
| legendGroup.selectAll('.legend-shape-circle') | |
| .data(d => d.shape === 'circle' ? [d] : []) | |
| .join('circle') | |
| .attr('class', 'legend-shape-circle') | |
| .attr('cx', 0) | |
| .attr('cy', 0) | |
| .attr('r', 5) | |
| .attr('fill', 'var(--muted-color)'); | |
| legendGroup.selectAll('.legend-shape-square') | |
| .data(d => d.shape === 'square' ? [d] : []) | |
| .join('rect') | |
| .attr('class', 'legend-shape-square') | |
| .attr('x', -5) | |
| .attr('y', -5) | |
| .attr('width', 10) | |
| .attr('height', 10) | |
| .attr('fill', 'none') | |
| .attr('stroke', 'var(--muted-color)') | |
| .attr('stroke-width', 2); | |
| legendGroup.selectAll('.legend-text') | |
| .data(d => [d]) | |
| .join('text') | |
| .attr('class', 'legend-text') | |
| .attr('x', 10) | |
| .attr('y', 0) | |
| .attr('dy', '0.35em') | |
| .text(d => d.label); | |
| } | |
| // 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> | |