Spaces:
Running
Running
| <div class="d3-correlation-matrix"></div> | |
| <style> | |
| .d3-correlation-matrix { | |
| position: relative; | |
| overflow: visible; | |
| } | |
| .d3-correlation-matrix .chart-card { | |
| background: var(--surface-bg); | |
| border: none; | |
| border-radius: 10px; | |
| padding: 0; | |
| overflow: visible; | |
| } | |
| .d3-correlation-matrix .chart-card svg { | |
| overflow: visible; | |
| } | |
| .d3-correlation-matrix .axis-label-x { | |
| fill: var(--text-color); | |
| font-size: 11px; | |
| font-weight: 700; | |
| } | |
| .d3-correlation-matrix .axis-label-x text { | |
| word-break: break-word; | |
| white-space: normal; | |
| } | |
| .d3-correlation-matrix .axis-label { | |
| fill: var(--text-color); | |
| font-size: 11px; | |
| font-weight: 700; | |
| } | |
| .d3-correlation-matrix .cell-text { | |
| fill: var(--muted-color); | |
| font-size: 11px; | |
| pointer-events: none; | |
| } | |
| .d3-correlation-matrix .colorbar-label { | |
| fill: var(--text-color); | |
| font-size: 10px; | |
| font-weight: 600; | |
| } | |
| </style> | |
| <script> | |
| (() => { | |
| // Load D3 from CDN once | |
| 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-correlation-matrix'))){ | |
| const cs = Array.from(document.querySelectorAll('.d3-correlation-matrix')).filter(el => !(el.dataset && el.dataset.mounted === 'true')); | |
| container = cs[cs.length - 1] || null; | |
| } | |
| if (!container) return; | |
| if (container.dataset) { | |
| if (container.dataset.mounted === 'true') return; | |
| container.dataset.mounted = 'true'; | |
| } | |
| // Tooltip (HTML, single instance inside container) | |
| container.style.position = container.style.position || 'relative'; | |
| let tip = container.querySelector('.d3-tooltip'); | |
| let tipInner; | |
| if (!tip) { | |
| tip = document.createElement('div'); | |
| tip.className = 'd3-tooltip'; | |
| Object.assign(tip.style, { | |
| position: 'absolute', | |
| top: '0px', | |
| left: '0px', | |
| transform: 'translate(-9999px, -9999px)', | |
| pointerEvents: 'none', | |
| padding: '8px 10px', | |
| borderRadius: '8px', | |
| fontSize: '12px', | |
| lineHeight: '1.35', | |
| border: '1px solid var(--border-color)', | |
| background: 'var(--surface-bg)', | |
| color: 'var(--text-color)', | |
| boxShadow: '0 4px 24px rgba(0,0,0,.18)', | |
| opacity: '0', | |
| transition: 'opacity .12s ease' | |
| }); | |
| tipInner = document.createElement('div'); | |
| tipInner.className = 'd3-tooltip__inner'; | |
| tipInner.style.textAlign = 'left'; | |
| tip.appendChild(tipInner); | |
| container.appendChild(tip); | |
| } else { | |
| tipInner = tip.querySelector('.d3-tooltip__inner') || tip; | |
| } | |
| // SVG scaffolding | |
| const card = document.createElement('div'); | |
| card.className = 'chart-card'; | |
| container.appendChild(card); | |
| const svg = d3.select(card).append('svg').attr('width', '100%').style('display', 'block'); | |
| const gRoot = svg.append('g'); | |
| const gCells = gRoot.append('g'); | |
| const gAxes = gRoot.append('g'); | |
| const gColorbar = gRoot.append('g'); | |
| const gColorbarRects = gColorbar.append('g'); // Separate group for colorbar rectangles | |
| const gColorbarLabels = gColorbar.append('g'); // Separate group for colorbar labels | |
| // Data: 6x6 correlation matrix | |
| const metrics = [ | |
| 'LLM score\nconcept', | |
| 'LLM score\ninstruction', | |
| 'LLM score\nfluency', | |
| 'Explicit\nconcept\ninclusion', | |
| 'Surprise', | |
| '3-gram\nrepetition' | |
| ]; | |
| // Correlation matrix (exact values from the image) | |
| const correlationMatrix = [ | |
| [1.00, -0.28, -0.37, 0.45, 0.57, 0.00], | |
| [-0.28, 1.00, 0.80, 0.068, -0.45, -0.90], | |
| [-0.37, 0.80, 1.00, 0.015, -0.67, -0.90], | |
| [0.45, 0.068, 0.015, 1.00, 0.10, -0.093], | |
| [0.57, -0.45, -0.67, 0.10, 1.00, 0.53], | |
| [0.00, -0.90, -0.90, -0.093, 0.53, 1.00] | |
| ]; | |
| // Colors: diverging palette via window.ColorPalettes | |
| const getDivergingColors = (count) => { | |
| try { | |
| if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') { | |
| const palette = window.ColorPalettes.getColors('diverging', count); | |
| // Invert the palette: reverse the array | |
| return palette.slice().reverse(); | |
| } | |
| } catch (_) {} | |
| // Fallback: generate diverging scale (blue for negative, red for positive) | |
| const steps = Math.max(3, count|0); | |
| const arr = []; | |
| for (let i = 0; i < steps; i++) { | |
| const t = i / (steps - 1); | |
| const pct = Math.round(t * 100); | |
| // Blue (negative) to Red (positive) via white | |
| if (t < 0.5) { | |
| const bluePct = Math.round((0.5 - t) * 200); | |
| arr.push(`color-mix(in srgb, #3A7BD5 ${bluePct}%, #ffffff ${100-bluePct}%)`); | |
| } else { | |
| const redPct = Math.round((t - 0.5) * 200); | |
| arr.push(`color-mix(in srgb, #ffffff ${100-redPct}%, #D64545 ${redPct}%)`); | |
| } | |
| } | |
| return arr; | |
| }; | |
| const divergingPalette = getDivergingColors(21); | |
| let width = 800; | |
| let height = 600; | |
| const margin = { top: 40, right: 0, bottom: 0, left: 70 }; | |
| const xLabelHeight = 20; // Height reserved for X-axis labels | |
| function updateSize() { | |
| const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; | |
| width = container.clientWidth || 800; | |
| // Use more of the available width and calculate height based on content | |
| const availableWidth = width; | |
| const minGridSize = 400; | |
| // Reserve space for colorbar (80px) and calculate optimal grid size | |
| const maxGridSize = Math.min(availableWidth - margin.left - margin.right - 80, 800); | |
| const gridSize = Math.max(minGridSize, maxGridSize); | |
| // Height must include: top margin + grid + x labels height + extra padding to ensure visibility | |
| height = margin.top + gridSize + xLabelHeight + 20; | |
| // Responsive SVG: width 100%, height auto, preserve aspect via viewBox | |
| svg | |
| .attr('viewBox', `0 0 ${width} ${height}`) | |
| .attr('preserveAspectRatio', 'xMidYMid meet') | |
| .style('width', '100%') | |
| .style('height', 'auto'); | |
| gRoot.attr('transform', `translate(${margin.left},${margin.top})`); | |
| const innerWidth = width - margin.left - margin.right; | |
| const innerHeight = height - margin.top - margin.bottom; | |
| return { innerWidth, innerHeight, isDark, gridSize }; | |
| } | |
| // Compute a fixed readable text color from a CSS rgb()/rgba() string | |
| function chooseFixedReadableTextOnBg(bgCss){ | |
| try { | |
| const m = String(bgCss||'').match(/rgba?\(([^)]+)\)/); | |
| if (!m) return '#0e1116'; | |
| const parts = m[1].split(',').map(s => parseFloat(s.trim())); | |
| const [r, g, b] = parts; | |
| // sRGB → relative luminance | |
| const srgb = [r, g, b].map(v => Math.max(0, Math.min(255, v)) / 255); | |
| const linear = srgb.map(c => (c <= 0.03928 ? c/12.92 : Math.pow((c + 0.055)/1.055, 2.4))); | |
| const L = 0.2126*linear[0] + 0.7152*linear[1] + 0.0722*linear[2]; | |
| // Threshold ~ 0.5 for readability; darker BG → white text, else near-black | |
| return L < 0.5 ? '#ffffff' : '#0e1116'; | |
| } catch(_) { return '#0e1116'; } | |
| } | |
| function render() { | |
| const { innerWidth, innerHeight, gridSize } = updateSize(); | |
| const n = metrics.length; | |
| const cellSize = gridSize / n; | |
| // Ensure xLabelHeight is accessible | |
| const labelHeight = xLabelHeight; | |
| const x = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0); | |
| const y = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0); | |
| // Flatten correlation data | |
| const flatData = []; | |
| for (let r = 0; r < n; r++) { | |
| for (let c = 0; c < n; c++) { | |
| flatData.push({ r, c, value: correlationMatrix[r][c] }); | |
| } | |
| } | |
| // Color scale: diverging from -1 to 1 | |
| const colorScale = d3.scaleQuantize() | |
| .domain([-1, 1]) | |
| .range(divergingPalette); | |
| const cells = gCells.selectAll('g.cell') | |
| .data(flatData, d => `${d.r}-${d.c}`); | |
| const cellsEnter = cells.enter() | |
| .append('g') | |
| .attr('class', 'cell'); | |
| cellsEnter.append('rect') | |
| .attr('rx', 0) | |
| .attr('ry', 0) | |
| .on('mousemove', (event, d) => { | |
| const [px, py] = d3.pointer(event, container); | |
| tipInner.innerHTML = `<strong>${metrics[d.r]}</strong> × <strong>${metrics[d.c]}</strong><br/>Correlation: ${d.value.toFixed(2)}`; | |
| tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`; | |
| tip.style.opacity = '1'; | |
| }) | |
| .on('mouseleave', () => { | |
| tip.style.opacity = '0'; | |
| }); | |
| cellsEnter.append('text') | |
| .attr('class', 'cell-text') | |
| .attr('text-anchor', 'middle') | |
| .attr('dominant-baseline', 'middle'); | |
| const cellsMerged = cellsEnter.merge(cells); | |
| cellsMerged.select('rect') | |
| .attr('x', d => x(d.c)) | |
| .attr('y', d => y(d.r)) | |
| .attr('width', Math.max(1, x.bandwidth())) | |
| .attr('height', Math.max(1, y.bandwidth())) | |
| .attr('fill', d => colorScale(d.value)); | |
| cellsMerged.select('text') | |
| .attr('x', d => x(d.c) + x.bandwidth() / 2) | |
| .attr('y', d => y(d.r) + y.bandwidth() / 2) | |
| .text(d => { | |
| if (d.value === 1.00) return '1'; | |
| const absVal = Math.abs(d.value); | |
| if (absVal < 0.01) return '0'; | |
| return d.value.toFixed(2); | |
| }) | |
| .style('fill', function(d){ | |
| try { | |
| const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null; | |
| const bg = rect ? getComputedStyle(rect).fill : colorScale(d.value); | |
| return chooseFixedReadableTextOnBg(bg); | |
| } catch (_) { | |
| return '#0e1116'; | |
| } | |
| }); | |
| cells.exit().remove(); | |
| // Create clipPath for matrix cells to preserve rounded corners | |
| const matrixClipId = `matrix-clip-${Math.random().toString(36).slice(2)}`; | |
| let defsMatrix = svg.select('defs'); | |
| if (defsMatrix.empty()) { | |
| defsMatrix = svg.append('defs'); | |
| } | |
| const matrixClipPath = defsMatrix.append('clipPath').attr('id', matrixClipId); | |
| matrixClipPath.append('rect') | |
| .attr('x', 0) | |
| .attr('y', 0) | |
| .attr('width', gridSize) | |
| .attr('height', gridSize) | |
| .attr('rx', 8) | |
| .attr('ry', 8); | |
| // Apply clipPath to cells group | |
| gCells.attr('clip-path', `url(#${matrixClipId})`); | |
| // Draw outer border with rounded corners (in a separate group above cells) | |
| const gBorder = gRoot.append('g').attr('class', 'matrix-border'); | |
| gBorder.selectAll('rect.cell-bg') | |
| .data([0]) | |
| .join('rect') | |
| .attr('class', 'cell-bg') | |
| .attr('x', 0) | |
| .attr('y', 0) | |
| .attr('width', gridSize) | |
| .attr('height', gridSize) | |
| .attr('rx', 8) | |
| .attr('ry', 8) | |
| .attr('fill', 'none') | |
| .attr('stroke', 'var(--border-color)') | |
| .attr('stroke-width', 1); | |
| // Axes labels | |
| gAxes.selectAll('*').remove(); | |
| // X-axis labels (bottom) - using SVG text with manual line breaks | |
| const xLabelsGroup = gAxes.append('g').attr('class', 'x-labels'); | |
| const xLabels = xLabelsGroup.selectAll('text') | |
| .data(metrics) | |
| .join('text') | |
| .attr('class', 'axis-label') | |
| .attr('text-anchor', 'middle') | |
| .attr('x', (_, i) => x(i) + x.bandwidth() / 2) | |
| .attr('y', gridSize + 16) | |
| .style('font-size', '11px') | |
| .style('font-weight', '700') | |
| .style('fill', 'var(--text-color)') | |
| .each(function(d) { | |
| const text = d3.select(this); | |
| const lines = d.split('\n'); | |
| lines.forEach((line, i) => { | |
| text.append('tspan') | |
| .attr('x', text.attr('x')) | |
| .attr('dy', i === 0 ? '0' : '1.2em') | |
| .text(line); | |
| }); | |
| }); | |
| // Y-axis labels (left) - using SVG text with manual line breaks | |
| const yLabelsGroup = gAxes.append('g').attr('class', 'y-labels'); | |
| const yLabels = yLabelsGroup.selectAll('text') | |
| .data(metrics) | |
| .join('text') | |
| .attr('class', 'axis-label') | |
| .attr('text-anchor', 'end') | |
| .attr('x', -12) | |
| .attr('y', (_, i) => y(i) + y.bandwidth() / 2) | |
| .style('font-size', '11px') | |
| .style('font-weight', '700') | |
| .style('fill', 'var(--text-color)') | |
| .each(function(d) { | |
| const text = d3.select(this); | |
| const lines = d.split('\n'); | |
| // Center the text vertically around the y position | |
| const lineHeight = 1.2; | |
| const totalHeight = (lines.length - 1) * lineHeight; | |
| const startY = -totalHeight / 2; | |
| lines.forEach((line, i) => { | |
| text.append('tspan') | |
| .attr('x', text.attr('x')) | |
| .attr('dy', i === 0 ? startY + 'em' : lineHeight + 'em') | |
| .attr('text-anchor', 'end') | |
| .text(line); | |
| }); | |
| }); | |
| // Title | |
| gAxes.append('text') | |
| .attr('class', 'axis-label') | |
| .attr('text-anchor', 'middle') | |
| .attr('x', gridSize / 2) | |
| .attr('y', -20) | |
| .style('font-size', '14px') | |
| .text('Correlation Matrix'); | |
| // Colorbar | |
| const colorbarWidth = 20; | |
| const colorbarHeight = gridSize; | |
| const colorbarX = gridSize + 20; | |
| const colorbarY = 0; | |
| const colorbarSteps = divergingPalette.length; | |
| const colorbarRadius = 8; | |
| // Create clipPath for rounded corners (top and bottom only) | |
| const clipId = `colorbar-clip-${Math.random().toString(36).slice(2)}`; | |
| let defsColorbar = svg.select('defs'); | |
| if (defsColorbar.empty()) { | |
| defsColorbar = svg.append('defs'); | |
| } | |
| const clipPath = defsColorbar.append('clipPath').attr('id', clipId); | |
| clipPath.append('rect') | |
| .attr('x', colorbarX) | |
| .attr('y', colorbarY) | |
| .attr('width', colorbarWidth) | |
| .attr('height', colorbarHeight) | |
| .attr('rx', colorbarRadius) | |
| .attr('ry', colorbarRadius); | |
| // Apply clipPath only to colorbar rectangles group, not labels | |
| gColorbarRects.attr('clip-path', `url(#${clipId})`); | |
| gColorbarRects.selectAll('rect.colorbar-rect') | |
| .data(d3.range(colorbarSteps)) | |
| .join('rect') | |
| .attr('class', 'colorbar-rect') | |
| .attr('x', colorbarX) | |
| .attr('y', (_, i) => colorbarY + (colorbarHeight / colorbarSteps) * i) | |
| .attr('width', colorbarWidth) | |
| .attr('height', colorbarHeight / colorbarSteps) | |
| .attr('fill', (_, i) => divergingPalette[colorbarSteps - 1 - i]) | |
| .attr('stroke', 'none'); | |
| // Colorbar border with rounded corners (top and bottom) | |
| gColorbarRects.append('rect') | |
| .attr('x', colorbarX) | |
| .attr('y', colorbarY) | |
| .attr('width', colorbarWidth) | |
| .attr('height', colorbarHeight) | |
| .attr('rx', colorbarRadius) | |
| .attr('ry', colorbarRadius) | |
| .attr('fill', 'none') | |
| .attr('stroke', 'var(--border-color)') | |
| .attr('stroke-width', 1); | |
| // Colorbar labels (outside clipPath so they're visible) | |
| const colorbarTicks = [-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1]; | |
| gColorbarLabels.selectAll('text.colorbar-tick') | |
| .data(colorbarTicks) | |
| .join('text') | |
| .attr('class', 'colorbar-label') | |
| .attr('text-anchor', 'start') | |
| .attr('x', colorbarX + colorbarWidth + 6) | |
| .attr('y', d => colorbarY + (1 - d) / 2 * colorbarHeight) | |
| .attr('dominant-baseline', 'middle') | |
| .text(d => d.toFixed(2)); | |
| } | |
| // Initial render + resize handling | |
| const rerender = () => render(); | |
| if (window.ResizeObserver) { | |
| const ro = new ResizeObserver(() => rerender()); | |
| ro.observe(container); | |
| } else { | |
| window.addEventListener('resize', rerender); | |
| } | |
| }; | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); | |
| } else { | |
| ensureD3(bootstrap); | |
| } | |
| })(); | |
| </script> | |