Council Cube [BETA]

This graph illustrates Council book categories and relations.

The Council 3D Book Evaluator body { font-family: Inter, system-ui, -apple-system, “Segoe UI”, Roboto, Arial; margin: 0; padding: 0; background:#f6f8fb; color:#111; } header { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:12px 16px; background:#fff; border-bottom:1px solid #e6e9ee; position:sticky; top:0; z-index:20; } h1 { margin:0; font-size:18px; } .wrap { display:grid; grid-template-columns: 380px 1fr; gap:12px; padding:12px; } @media(max-width:880px){ .wrap{ grid-template-columns:1fr; } #graph,#threeContainer{height:520px;} textarea{min-height:160px;} .controls{padding:8px;} .pill{padding:10px 12px;font-size:15px;} } textarea { width:100%; min-height:220px; font-family: monospace; padding:8px; box-sizing:border-box; border-radius:8px; border:1px solid #dfe6ee; background:#fff; white-space:pre; } .controls { display:flex; flex-direction:column; gap:10px; margin-top:10px; padding:12px; border-radius:8px; background:#fff; box-shadow:0 2px 6px rgba(20,30,40,0.04); } .pill { padding:8px 12px; border-radius:6px; border:1px solid #d0d7de; background:#fff; cursor:pointer; font-size:14px; transition:all .12s ease; } .pill:active{ transform:scale(.985); } label { font-size:13px; color:#333; display:inline-flex; align-items:center; gap:8px; } #graph,#threeContainer { width:100%; height:640px; background:linear-gradient(180deg,#ffffff,#fbfdff); border-radius:10px; box-shadow:0 6px 20px rgba(16,24,40,0.04); overflow:hidden; } #threeContainer{ display:none; position:relative; } .control-grid{ display:grid; grid-template-columns:1fr 1fr; gap:10px; } select,input[type=range]{ width:100%; padding:6px; border-radius:6px; border:1px solid #dfe6ee; font-size:14px; } .button-row{ display:flex; gap:8px; flex-wrap:wrap; } .tiny{ font-size:12px; color:#666; margin-top:6px; } https://cdn.plot.ly/plotly-latest.min.js https://unpkg.com/three@0.160.0/build/three.min.js https://unpkg.com/three@0.160.0/examples/js/controls/OrbitControls.js https://unpkg.com/three@0.160.0/examples/js/renderers/CSS2DRenderer.js https://unpkg.com/three@0.160.0/examples/js/geometries/ConvexGeometry.js https://unpkg.com/three@0.160.0/examples/js/objects/MarchingCubes.js

The Council 3D Book Evaluator

X = Hard Sci Fi | Y = Comfort Read | Z = Hidden Gem
Neuromancer 85 40 10 Solaris 85 38 20 Left Hand of Darkness 30 50 15 Roadside Picnic 75 19 40 Handmaid’s Tale 10 10 5 Flow my Tears the Policeman Said 55 20 50 Parable of the Sower 25 15 35 Dune 40 40 1 Snow Crash 50 60 10 Foundation 80 55 5 Red Mars 99 50 10 To Say Nothing of the Dog 10 90 65 Blindsight 96 10 50 Best Short Stories 50 23 35 Trouble on Triton 65 45 70 The Dispossessed 55 55 40 Herland 20 80 75 Brave New World 30 24 5 Feersum Endjinn 75 25 50 The Three-Body Problem 90 71 5 Lightless 85 20 80 A Connecticut Yankee in King Arthur’s Court 5 81 10 Cat’s Cradle 35 50 10 The Fifth Season 40 25 15 Shikasta 20 40 50 The Forever War 90 17 30 Woman on the Edge of Time 30 40 55 A Distant Soil 10 85 85 The Stars my Destination 70 35 20 Never Let Me Go 25 10 10 Last and First Men 85 25 60 Ancillary Justice 60 55 30 The Rise of the Meritocracy 10 10 85 Pattern Recognition 5 60 45 On a Red Station, Drifting 50 40 65 Hyperion 65 45 15 The Female Man 40 74 40 The Context 17 85 95 Exhalation 80 60 30 Children of Time 89 55 33 The Diamond Age 70 80 60 Dream Snake 35 42 55 Dhalgren 15 10 30 The Inverted World 85 45 35 The Blazing World 10 99 99 Deathbird Stories 30 19 25 Cloud Atlas 25 50 7 Aurora 95 50 25 The Player of Games 60 40 24 More than Human 50 35 50 Blood Music 90 34 60 RUR 50 25 90 The Terminator (Screenplay) 30 22 70 Her Smoke Rose Up Forever 70 13 40 The Fifth Head of Cerberus 60 18 55 Terminal Boredom 30 4 70 Diaspora 98 65 40 Flatland 60 85 85 Sphereland 50 75 90 Grass 45 25 60 The World Without Us 0 10 20 City 40 60 66 The Quantum Thief 65 60 17 This Is How You Lose the Time War 40 45 10 Five Ways to Forgiveness 8 11 80 Children of Ruin 85 60 15 Stations of the Tide 20 70 60
Plot Plotly Three.js
Three.js Render Billboards Volumetric Fog Metaballs Convex Hull
Marker Size
Color by None Hard Sci Fi (X) Comfort Read (Y) Hidden Gem (Z)
View Mode (Plotly only) Scatter Connected Lines 3D Blob Bounding Box
Toggle Titles Rotate X Rotate Y Rotate Z Stop Fit View
Tip: use Metaballs or Fog for density; use Hull to see envelope.
/* ========================= State ========================= */ let graphData = { labels: [], x: [], y: [], z: [] }; let showLabels = true; let engine = ‘plotly’; let rotationTimer = null; /* Plotly layout */ const plotlyLayout = { margin: { l:0, r:0, b:0, t:0 }, scene: { xaxis: { title: ‘Hard Sci Fi’ }, yaxis: { title: ‘Comfort Read’ }, zaxis: { title: ‘Hidden Gem’ }, aspectmode: ‘cube’, camera: { eye: { x:1.5, y:1.5, z:1.5 } } } }; /* ========================= Parsing + Plotting ========================= */ function plotData(){ const raw = document.getElementById(‘input’).value.trim(); const rows = raw.split(‘\n’).map(r=>r.trim()).filter(r=>r.length>0); const labels = [], x = [], y = [], z = []; for (let r of rows){ let cols = r.includes(‘\t’) ? r.split(‘\t’) : r.split(/\s+/); cols = cols.filter(c => c.trim().length>0); if (cols.length Number.isNaN(v))) continue; const title = cols.slice(0, cols.length-3).join(‘ ‘); labels.push(title); x.push(xTry); y.push(yTry); z.push(zTry); } graphData = { labels, x, y, z }; if (engine === ‘plotly’) updateMode(); else renderThree(); fitView(); } /* Plotly traces */ function makeScatter(){ const size = parseInt(document.getElementById(‘sizeSlider’).value); return { x: graphData.x, y: graphData.y, z: graphData.z, mode: showLabels ? ‘markers+text’ : ‘markers’, type: ‘scatter3d’, text: graphData.labels, textposition: ‘top center’, marker: { size: size, color: ‘#1e88e5’ }, hovertemplate: “%{text}
Hard Sci Fi: %{x}
Comfort Read: %{y}
Hidden Gem: %{z}” }; } function makeLines(){ return { x: graphData.x, y: graphData.y, z: graphData.z, mode:’lines+markers’, type:’scatter3d’, marker:{size:4, color:’#d32f2f’}, text:graphData.labels }; } function makeMesh(){ return { x:graphData.x, y:graphData.y, z:graphData.z, type:’mesh3d’, opacity:0.35, color:’#8ecae6′ }; } function makeBox(){ if (!graphData.x.length) return {}; const xmin = Math.min(…graphData.x), xmax = Math.max(…graphData.x); const ymin = Math.min(…graphData.y), ymax = Math.max(…graphData.y); const zmin = Math.min(…graphData.z), zmax = Math.max(…graphData.z); return { type:’mesh3d’, x:[xmin,xmax,xmax,xmin,xmin,xmax,xmax,xmin], y:[ymin,ymin,ymax,ymax,ymin,ymin,ymax,ymax], z:[zmin,zmin,zmin,zmin,zmax,zmax,zmax,zmax], i:[0,0,0,4,4,4,1,1,2,2,3,5], j:[1,2,3,5,6,7,5,6,6,7,7,6], k:[2,3,1,6,7,5,2,5,3,6,0,2], opacity:0.08, color:’#999999′ }; } function updateMode(){ const mode = document.getElementById(‘modeSelect’).value; const traces = [makeScatter()]; if (mode === ‘lines’) traces.push(makeLines()); else if (mode === ‘mesh’) traces.push(makeMesh(), makeScatter()); else if (mode === ‘box’) traces.push(makeBox(), makeScatter()); Plotly.react(‘graph’, traces, plotlyLayout, {displaylogo:false}); applyPlotlyColor(); } /* color mapping */ function applyPlotlyColor(){ const opt = document.getElementById(‘colorSelect’).value; if (opt === ‘none’) { Plotly.restyle(‘graph’, {‘marker.color’: [‘#1e88e5’]}); return; } let arr = (opt === ‘x’ ? graphData.x : (opt === ‘y’ ? graphData.y : graphData.z)); if (!arr || !arr.length) { Plotly.restyle(‘graph’, {‘marker.color’: [‘#1e88e5’]}); return; } Plotly.restyle(‘graph’, {‘marker.color’: [arr]}); } /* ========================= Engine / UI handlers ========================= */ function switchEngine(which){ engine = which; document.getElementById(‘graph’).style.display = (which === ‘plotly’) ? ‘block’ : ‘none’; document.getElementById(‘threeContainer’).style.display = (which === ‘three’) ? ‘block’ : ‘none’; if (which === ‘plotly’) updateMode(); else renderThree(); } function toggleTitles(){ showLabels = !showLabels; if (engine===’plotly’) updateMode(); else renderThreeLabels(); } function onSizeChange(v){ if (engine===’plotly’) updateMode(); else renderThree(); } function onColorChange(){ if (engine===’plotly’) applyPlotlyColor(); else renderThree(); } function fitView(){ if (engine === ‘plotly’){ if (!graphData.x.length) return; const eye = {x:1.5,y:1.5,z:1.5}; plotlyLayout.scene.camera.eye = eye; Plotly.relayout(‘graph’, {‘scene.camera.eye’: eye}); } else { if (!threeInit) return; centerThreeOnData(); } } /* ========================= Rotation ========================= */ function startRotation(axis){ stopRotation(); if (engine === ‘plotly’){ rotationTimer = setInterval(()=>{ const eye = {…plotlyLayout.scene.camera.eye}; const a = 0.02; if (axis===’x’){ const y=eye.y, z=eye.z; eye.y = y*Math.cos(a)-z*Math.sin(a); eye.z = y*Math.sin(a)+z*Math.cos(a); } if (axis===’y’){ const x=eye.x, z=eye.z; eye.x = x*Math.cos(a)-z*Math.sin(a); eye.z = x*Math.sin(a)+z*Math.cos(a); } if (axis===’z’){ const x=eye.x, y=eye.y; eye.x = x*Math.cos(a)-y*Math.sin(a); eye.y = x*Math.sin(a)+y*Math.cos(a); } plotlyLayout.scene.camera.eye = eye; Plotly.relayout(‘graph’, {‘scene.camera.eye’: eye}); }, 100); } else { rotationTimer = setInterval(()=>{ if (!threeInit) return; const a = 0.01; if (axis===’x’) threeRoot.rotation.x += a; if (axis===’y’) threeRoot.rotation.y += a; if (axis===’z’) threeRoot.rotation.z += a; }, 16); } } function stopRotation(){ if (rotationTimer){ clearInterval(rotationTimer); rotationTimer = null; } } /* ========================= THREE.JS implementation ========================= */ let threeInit = false; let threeRoot = null; let threeScene, threeCamera, threeRenderer, threeControls, labelRenderer; let threeGradient = null; let threeSprites = [], threeLabels = [], threeMetaballs = null, threeHull = null, threeFogGroup = null; function initThree(){ if (threeInit) return; threeInit = true; const THREE = window.THREE; const container = document.getElementById(‘threeContainer’); container.innerHTML = ”; threeScene = new THREE.Scene(); threeScene.background = new THREE.Color(0xf7fbff); const w = container.clientWidth, h = container.clientHeight; threeCamera = new THREE.PerspectiveCamera(50, w/h, 0.01, 1000); threeCamera.position.set(2.2, 2.0, 2.4); threeRenderer = new THREE.WebGLRenderer({ antialias:true, alpha:false }); threeRenderer.setSize(w,h); threeRenderer.setPixelRatio(Math.min(window.devicePixelRatio,2)); container.appendChild(threeRenderer.domElement); labelRenderer = new THREE.CSS2DRenderer(); labelRenderer.setSize(w,h); labelRenderer.domElement.style.position = ‘absolute’; labelRenderer.domElement.style.top = ‘0’; labelRenderer.domElement.style.pointerEvents = ‘none’; container.appendChild(labelRenderer.domElement); threeControls = new THREE.OrbitControls(threeCamera, labelRenderer.domElement); threeControls.enableDamping = true; threeControls.dampingFactor = 0.08; threeControls.zoomSpeed = 1.2; threeScene.add(new THREE.AmbientLight(0xffffff, 0.9)); const dir = new THREE.DirectionalLight(0xffffff, 0.6); dir.position.set(3,4,5); threeScene.add(dir); threeRoot = new THREE.Group(); threeScene.add(threeRoot); const box = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(2,2,2)), new THREE.LineBasicMaterial({color:0x999999,linewidth:1,opacity:0.25,transparent:true})); threeRoot.add(box); threeGradient = makeRadialTexture(); window.addEventListener(‘resize’, onThreeResize); animateThree(); } function onThreeResize(){ const container = document.getElementById(‘threeContainer’); if (!threeCamera) return; threeCamera.aspect = container.clientWidth / container.clientHeight; threeCamera.updateProjectionMatrix(); threeRenderer.setSize(container.clientWidth, container.clientHeight); labelRenderer.setSize(container.clientWidth, container.clientHeight); } function animateThree(){ requestAnimationFrame(animateThree); if (!threeRenderer) return; threeControls.update(); threeRenderer.render(threeScene, threeCamera); labelRenderer.render(threeScene, threeCamera); } function makeRadialTexture(){ const size = 256; const canvas = document.createElement(‘canvas’); canvas.width = size; canvas.height = size; const ctx = canvas.getContext(‘2d’); const g = ctx.createRadialGradient(size/2, size/2, 1, size/2, size/2, size/2); g.addColorStop(0, ‘rgba(255,255,255,1)’); g.addColorStop(0.2, ‘rgba(255,255,255,0.9)’); g.addColorStop(0.5, ‘rgba(255,255,255,0.35)’); g.addColorStop(1, ‘rgba(255,255,255,0)’); ctx.fillStyle = g; ctx.fillRect(0,0,size,size); const tex = new THREE.Texture(canvas); tex.needsUpdate = true; return tex; } function normalize(arr){ if (!arr || arr.length===0) return []; const mn = Math.min(…arr), mx = Math.max(…arr); if (mx === mn) return arr.map(_=>0); return arr.map(v => ((v – mn) / (mx – mn)) * 2 – 1); } function colorForIndex(mode, i){ if (mode === ‘none’) return ‘#1976d2′; const vals = (mode===’x’ ? graphData.x : (mode===’y’ ? graphData.y : graphData.z)); const vmin = Math.min(…vals), vmax = Math.max(…vals); const t = (vals[i]-vmin)/(vmax-vmin||1); const r = Math.round(40 + (220-40)*t); const g = Math.round(80 + (60-80)*t); const b = Math.round(220 + (50-220)*t); return `rgb(${r},${g},${b})`; } function centerThreeOnData(){ if (!graphData.x.length) return; threeRoot.position.set(0,0,0); threeRoot.scale.set(1,1,1); threeRoot.rotation.set(0,0,0); } /* safe clear (keeps box at index 0) */ function clearThreeRoot(){ if (!threeRoot) return; while (threeRoot.children.length > 1) { const c = threeRoot.children[1]; if (c.geometry) { try{ c.geometry.dispose(); }catch(e){} } if (c.material) { try{ if (Array.isArray(c.material)) c.material.forEach(m=>m.dispose && m.dispose()); else c.material.dispose && c.material.dispose(); }catch(e){} } threeRoot.remove(c); } threeSprites = []; threeLabels = []; threeMetaballs = null; threeHull = null; threeFogGroup = null; } /* full renderThree builder */ function renderThree(){ initThree(); clearThreeRoot(); if (!graphData.x.length) return; const THREE = window.THREE; const mode = document.getElementById(‘threeRenderMode’).value; const size = parseInt(document.getElementById(‘sizeSlider’).value); const colorMode = document.getElementById(‘colorSelect’).value; const nx = normalize(graphData.x), ny = normalize(graphData.y), nz = normalize(graphData.z); if (mode === ‘billboards’) { for (let i=0;i<nx.length;i++){ const col = new THREE.Color(colorForIndex(colorMode, i)); const mat = new THREE.SpriteMaterial({ map: threeGradient, transparent:true, blending: THREE.AdditiveBlending, depthWrite:false, color:col }); const s = new THREE.Sprite(mat); s.scale.setScalar(0.28 * (size/6)); s.position.set(nx[i], ny[i], nz[i]); threeRoot.add(s); threeSprites.push(s); } } else if (mode === 'fog'){ const layers = 6; threeFogGroup = new THREE.Group(); for (let i=0;i<nx.length;i++){ const base = new THREE.Vector3(nx[i], ny[i], nz[i]); const baseColor = new THREE.Color(colorForIndex(colorMode,i)); for (let l=0;l<layers;l++){ const mat = new THREE.SpriteMaterial({ map: threeGradient, transparent:true, blending: THREE.AdditiveBlending, depthWrite:false, color: baseColor.clone() }); const s = new THREE.Sprite(mat); const spread = 0.02 * (l+1) * (size/6); s.position.copy(base).add(new THREE.Vector3(randn()*spread, randn()*spread, randn()*spread)); s.scale.setScalar(0.18 * (size/6) * (1 + l*0.4)); s.material.opacity = 0.16 * (1 – l/(layers+1)); threeFogGroup.add(s); } } threeRoot.add(threeFogGroup); } else if (mode === 'metaballs'){ const resolution = 36; const material = new THREE.MeshStandardMaterial({ color:0x4f83cc, roughness:0.45, metalness:0.05, transparent:true, opacity:0.6 }); const effect = new THREE.MarchingCubes(resolution, material, true, true); effect.position.set(-1,-1,-1); effect.scale.set(2,2,2); effect.isolation = 20; effect.reset(); const strength = 0.55; for (let i=0;i<nx.length;i++){ const fx = (nx[i]+1)/2, fy=(ny[i]+1)/2, fz=(nz[i]+1)/2; effect.addBall(fx,fy,fz,strength,0); } threeRoot.add(effect); threeMetaballs = effect; const pts = new THREE.BufferGeometry(); const arr = new Float32Array(nx.length*3); for (let i=0;i<nx.length;i++){ arr[i*3]=nx[i]; arr[i*3+1]=ny[i]; arr[i*3+2]=nz[i]; } pts.setAttribute('position', new THREE.BufferAttribute(arr,3)); const pmat = new THREE.PointsMaterial({ size:0.02*(size/6), color:0x003f7f }); const points = new THREE.Points(pts, pmat); threeRoot.add(points); } else if (mode === 'hull'){ const pts = []; for (let i=0;i<nx.length;i++) pts.push(new THREE.Vector3(nx[i], ny[i], nz[i])); try { const geom = new THREE.ConvexGeometry(pts); geom.computeVertexNormals(); const mat = new THREE.MeshPhysicalMaterial({ color:0x88bfe6, roughness:0.55, metalness:0.02, transparent:true, opacity:0.45, side: THREE.DoubleSide }); const mesh = new THREE.Mesh(geom, mat); threeRoot.add(mesh); threeHull = mesh; const ptsGeom = new THREE.BufferGeometry(); const arr = new Float32Array(nx.length*3); for (let i=0;i { if (lbl.parent) lbl.parent.remove(lbl); }); threeLabels = []; if (!showLabels) return; const THREE = window.THREE; for (let i=0;i{ try{ plotData(); }catch(e){ console.error(‘plotData error’, e); } }, 120); }catch(e){ console.error(‘init error’, e); } } function waitForLibsAndDOM(){ const check = () => { if (window.Plotly && window.THREE) { if (document.readyState === ‘loading’) { document.addEventListener(‘DOMContentLoaded’, readyToInit); } else { readyToInit(); } } else { setTimeout(check, 60); } }; check(); } waitForLibsAndDOM();