144144 <button :class =" { active: subTab === 'ecosystem' }" @click =" subTab = 'ecosystem'" >{{ $t('results.ecosystem') }}</button >
145145 <button v-if =" population.length > 1" :class =" { active: subTab === 'stability' }" @click =" subTab = 'stability'" >{{ $t('results.stability') }}</button >
146146 <button v-if =" pheromoneData && pheromoneData.length > 0" :class =" { active: subTab === 'pheromone' }" @click =" subTab = 'pheromone'" >Pheromone</button >
147+ <button v-if =" hasExplorationData" :class =" { active: subTab === 'exploration' }" @click =" subTab = 'exploration'" >Exploration</button >
147148 <button :class =" { active: subTab === 'basket' }" @click =" subTab = 'basket'" >{{ $t('results.basket') }}<span v-if =" basketCount > 0" class =" basket-badge" >{{ basketCount }}</span ></button >
148149 <button :class =" { active: subTab === 'console' }" @click =" subTab = 'console'; loadConsoleLog()" >{{ $t('results.console') }}</button >
149150 <div class =" export-dropdown-wrap" v-if =" detail" >
998999 />
9991000 </div >
10001001
1002+ <!-- ============================================================ -->
1003+ <!-- EXPLORATION SUB-TAB -->
1004+ <!-- ============================================================ -->
1005+ <div v-if =" detail && subTab === 'exploration' && hasExplorationData" class =" sub-content" >
1006+ <section class =" section" >
1007+ <div class =" section-title" >Search Space Exploration — k × fit density</div >
1008+ <p class =" info-text" style =" margin-bottom : 12px ;" >
1009+ Each dot represents a model explored by the algorithm. Density shows where the search concentrates over iterations.
1010+ Use the slider to animate through generations or view all at once.
1011+ </p >
1012+ <div class =" exploration-controls" style =" display :flex ; gap :16px ; align-items :center ; margin-bottom :12px ;" >
1013+ <label >Generation:
1014+ <input type =" range" v-model.number =" explorationGen" :min =" -1" :max =" generationTracking.length - 1" style =" width :200px " />
1015+ <span style =" min-width :60px ; display :inline-block " >{{ explorationGen === -1 ? 'All' : explorationGen }}</span >
1016+ </label >
1017+ <button class =" btn-sm btn-outline" @click =" animateExploration" >▶ Animate</button >
1018+ <div style =" flex :1 " ></div >
1019+ <div class =" exploration-stats" v-if =" explorationStats" >
1020+ <span ><strong >Total models:</strong > {{ explorationStats.totalModels.toLocaleString() }}</span >
1021+ <span style =" margin-left :16px " ><strong >Unique (sampled):</strong > {{ explorationStats.uniqueSampled.toLocaleString() }}</span >
1022+ <span style =" margin-left :16px " ><strong >Generations:</strong > {{ explorationStats.nGens }}</span >
1023+ </div >
1024+ </div >
1025+ <div ref =" explorationChartEl" class =" plotly-chart" style =" min-height : 500px ;" ></div >
1026+ </section >
1027+ </div >
1028+
10011029 <!-- ============================================================ -->
10021030 <!-- PHEROMONE SUB-TAB (ACO only) -->
10031031 <!-- ============================================================ -->
@@ -1258,6 +1286,11 @@ const importanceData = ref(null)
12581286const pheromoneData = ref (null )
12591287const pheromoneTopN = ref (30 )
12601288const pheromoneChartEl = ref (null )
1289+ const explorationChartEl = ref (null )
1290+ const explorationGen = ref (- 1 ) // -1 = all generations
1291+ const hasExplorationData = computed (() => {
1292+ return generationTracking .value .some (g => g .k_fit_sample && g .k_fit_sample .length > 0 )
1293+ })
12611294
12621295// Population filter state (P2.2 + P2.3)
12631296const selectedLanguages = ref ([])
@@ -4059,6 +4092,118 @@ function renderBasketCharts() {
40594092 renderBasketHeatmap()
40604093}
40614094
4095+ const explorationStats = computed(() => {
4096+ if (!generationTracking.value.length) return null
4097+ const totalModels = generationTracking.value.reduce((s, g) => s + (g.population_size || 0), 0)
4098+ const allSamples = generationTracking.value.flatMap(g => g.k_fit_sample || [])
4099+ const uniqueSet = new Set(allSamples.map(([k, f]) => ` ${k }_ ${f }` ))
4100+ return { totalModels, uniqueSampled: uniqueSet.size, nGens: generationTracking.value.length }
4101+ })
4102+
4103+ function renderExplorationChart() {
4104+ if (!explorationChartEl.value || !generationTracking.value.length) return
4105+ const Plotly = window.Plotly
4106+ if (!Plotly) return
4107+
4108+ const isDark = document.documentElement.classList.contains('dark')
4109+ const textColor = isDark ? '#ccc' : '#333'
4110+ const bgColor = 'rgba(0,0,0,0)'
4111+
4112+ // Collect data points
4113+ let ks = [], fits = [], colors = []
4114+ const nGens = generationTracking.value.length
4115+
4116+ if (explorationGen.value === -1) {
4117+ // All generations — color by generation
4118+ for (let g = 0; g < nGens; g++) {
4119+ const samples = generationTracking.value[g].k_fit_sample || []
4120+ for (const [k, f] of samples) {
4121+ ks.push(k)
4122+ fits.push(f)
4123+ colors.push(g)
4124+ }
4125+ }
4126+ } else {
4127+ // Single generation
4128+ const samples = generationTracking.value[explorationGen.value]?.k_fit_sample || []
4129+ for (const [k, f] of samples) {
4130+ ks.push(k)
4131+ fits.push(f)
4132+ colors.push(explorationGen.value)
4133+ }
4134+ }
4135+
4136+ if (ks.length === 0) return
4137+
4138+ const traces = [{
4139+ type: 'histogram2d',
4140+ x: ks,
4141+ y: fits,
4142+ colorscale: isDark ? 'Hot' : 'YlOrRd',
4143+ reversescale: true,
4144+ nbinsx: 40,
4145+ nbinsy: 40,
4146+ colorbar: { title: { text: 'Density', font: { color: textColor } }, tickfont: { color: textColor } },
4147+ }, {
4148+ type: 'scatter',
4149+ mode: 'markers',
4150+ x: ks,
4151+ y: fits,
4152+ marker: {
4153+ size: 3,
4154+ color: colors,
4155+ colorscale: 'Viridis',
4156+ opacity: 0.3,
4157+ },
4158+ hovertemplate: 'k=%{x}, fit=%{y:.4f}<extra></extra>',
4159+ showlegend: false,
4160+ }]
4161+
4162+ // Add best trajectory
4163+ const bestKs = generationTracking.value.map(g => g.best_k)
4164+ const bestFits = generationTracking.value.map(g => g.best_fit || g.best_auc)
4165+ traces.push({
4166+ type: 'scatter',
4167+ mode: 'lines+markers',
4168+ x: bestKs,
4169+ y: bestFits,
4170+ name: 'Best model trajectory',
4171+ marker: { size: 6, color: '#00ff88', symbol: 'star' },
4172+ line: { color: '#00ff88', width: 2 },
4173+ hovertemplate: 'Gen %{customdata}: k=%{x}, fit=%{y:.4f}<extra>Best</extra>',
4174+ customdata: generationTracking.value.map((_, i) => i),
4175+ })
4176+
4177+ const genLabel = explorationGen.value === -1 ? 'All generations' : ` Generation ${explorationGen .value }`
4178+ const layout = {
4179+ title: { text: ` Search Space Exploration — ${genLabel } (${ks .length } models )` , font: { color: textColor, size: 14 } },
4180+ xaxis: { title: { text: 'k (model complexity)', font: { color: textColor } }, tickfont: { color: textColor } },
4181+ yaxis: { title: { text: 'Penalized fitness', font: { color: textColor } }, tickfont: { color: textColor } },
4182+ height: 500,
4183+ margin: { l: 60, r: 30, t: 40, b: 50 },
4184+ paper_bgcolor: bgColor,
4185+ plot_bgcolor: bgColor,
4186+ legend: { font: { color: textColor }, x: 0.01, y: 0.99 },
4187+ }
4188+
4189+ Plotly.react(explorationChartEl.value, traces, layout, { responsive: true })
4190+ }
4191+
4192+ let _animTimer = null
4193+ function animateExploration() {
4194+ if (_animTimer) { clearInterval(_animTimer); _animTimer = null; return }
4195+ explorationGen.value = 0
4196+ _animTimer = setInterval(() => {
4197+ if (explorationGen.value >= generationTracking.value.length - 1) {
4198+ clearInterval(_animTimer)
4199+ _animTimer = null
4200+ explorationGen.value = -1 // show all at end
4201+ return
4202+ }
4203+ explorationGen.value++
4204+ }, 200)
4205+ }
4206+
40624207function renderPheromoneChart() {
40634208 if (!pheromoneData.value || !pheromoneChartEl.value) return
40644209 const Plotly = window.Plotly
@@ -4130,6 +4275,7 @@ async function renderActiveTab() {
41304275 else if (subTab.value === 'jury') renderJuryCharts()
41314276 else if (subTab.value === 'comparative') renderComparativeCharts()
41324277 else if (subTab.value === 'copresence') renderCoPresenceCharts()
4278+ else if (subTab.value === 'exploration') renderExplorationChart()
41334279 else if (subTab.value === 'pheromone') renderPheromoneChart()
41344280 else if (subTab.value === 'basket') renderBasketCharts()
41354281}
@@ -4156,6 +4302,11 @@ watch(basketItems, () => {
41564302 if (subTab.value === 'basket') nextTick(() => renderBasketCharts())
41574303}, { deep: true })
41584304
4305+ // Re-render exploration chart when generation slider changes
4306+ watch(explorationGen, () => {
4307+ if (subTab.value === 'exploration') nextTick(() => renderExplorationChart())
4308+ })
4309+
41594310// Re-render pheromone chart when topN changes
41604311watch(pheromoneTopN, () => {
41614312 if (subTab.value === 'pheromone') nextTick(() => renderPheromoneChart())
0 commit comments