Spaces:
Configuration error
Configuration error
| /** | |
| * Copyright (c) 2023 Amphion. | |
| * | |
| * This source code is licensed under the MIT license found in the | |
| * LICENSE file in the root directory of this source tree. | |
| */ | |
| // Copyright 2021, Observable Inc. | |
| // Released under the ISC license. | |
| // https://observablehq.com/@d3/color-legend | |
| function Swatches(color, { | |
| columns = null, | |
| format, | |
| unknown: formatUnknown, | |
| swatchSize = 15, | |
| swatchWidth = swatchSize, | |
| swatchHeight = swatchSize, | |
| marginLeft = 0 | |
| } = {}) { | |
| const id = `-swatches-${Math.random().toString(16).slice(2)}`; | |
| const unknown = formatUnknown == null ? undefined : color.unknown(); | |
| const unknowns = unknown == null || unknown === d3.scaleImplicit ? [] : [unknown]; | |
| const domain = color.domain().concat(unknowns); | |
| if (format === undefined) format = x => x === unknown ? formatUnknown : x; | |
| function entity(character) { | |
| return `&#${character.charCodeAt(0).toString()};`; | |
| } | |
| if (columns !== null) return htl.html`<div style="display: flex; align-items: center; margin-left: ${+marginLeft}px; min-height: 33px; font: 10px sans-serif;"> | |
| <style> | |
| .${id}-item { | |
| break-inside: avoid; | |
| display: flex; | |
| align-items: center; | |
| padding-bottom: 1px; | |
| } | |
| .${id}-label { | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| max-width: calc(100% - ${+swatchWidth}px - 0.5em); | |
| } | |
| .${id}-swatch { | |
| width: ${+swatchWidth}px; | |
| height: ${+swatchHeight}px; | |
| border-radius: ${+swatchHeight}px; | |
| margin: 0 0.5em 0 0; | |
| } | |
| </style> | |
| <div style=${{ width: "100%", columns }}>${domain.map(value => { | |
| const label = `${format(value)}`; | |
| return htl.html`<div class=${id}-item> | |
| <div class=${id}-swatch style=${{ background: color(value) }}></div> | |
| <div class=${id}-label title=${label}>${label}</div> | |
| </div>`; | |
| })} | |
| </div> | |
| </div>`; | |
| return htl.html`<div style="display: flex; align-items: center; min-height: 33px; margin-left: ${+marginLeft}px; font: 10px sans-serif;"> | |
| <style> | |
| .${id} { | |
| display: inline-flex; | |
| align-items: center; | |
| margin-right: 1em; | |
| } | |
| .${id}::before { | |
| content: ""; | |
| width: ${+swatchWidth}px; | |
| height: ${+swatchHeight}px; | |
| margin-right: 0.5em; | |
| background: var(--color); | |
| } | |
| </style> | |
| <div>${domain.map(value => htl.html`<span class="${id}" style="--color: ${color(value)}">${format(value)}</span>`)}</div>`; | |
| } | |
| function ramp(color, n = 256) { | |
| const canvas = document.createElement("canvas"); | |
| canvas.width = 1; | |
| canvas.height = n; | |
| const context = canvas.getContext("2d"); | |
| for (let i = 0; i < n; ++i) { | |
| context.fillStyle = color(i / (n - 1)); | |
| context.fillRect(0, n - i, 1, 1); | |
| } | |
| return canvas; | |
| } | |
| function legend({ | |
| color, | |
| title, | |
| tickSize = 6, | |
| width = 36 + tickSize, | |
| height = 320, | |
| marginTop = 10, | |
| marginRight = 10 + tickSize, | |
| marginBottom = 20, | |
| marginLeft = 5, | |
| ticks = height / 64, | |
| tickFormat, | |
| tickValues | |
| } = {}) { | |
| const svg = d3.create("svg") | |
| .attr("width", width) | |
| .attr("height", height) | |
| .attr("viewBox", [0, 0, width, height]) | |
| .style("overflow", "visible") | |
| .style("display", "block"); | |
| let tickAdjust = g => g.selectAll(".tick line").attr("x1", marginLeft - width + marginRight); | |
| let x; | |
| if (color.interpolator) { | |
| x = Object.assign(color.copy() | |
| .interpolator(d3.interpolateRound(height - marginBottom, marginTop)), | |
| { range() { return [height - marginBottom, marginTop]; } }); | |
| svg.append("image") | |
| .attr("x", marginLeft) | |
| .attr("y", marginTop) | |
| .attr("width", width - marginLeft - marginRight) | |
| .attr("height", height - marginTop - marginBottom) | |
| .attr("preserveAspectRatio", "none") | |
| .attr("xlink:href", ramp(color.interpolator()).toDataURL()); | |
| // scaleSequentialQuantile doesn’t implement ticks or tickFormat. | |
| if (!x.ticks) { | |
| if (tickValues === undefined) { | |
| const n = Math.round(ticks + 1); | |
| tickValues = d3.range(n).map(i => d3.quantile(color.domain(), i / (n - 1))); | |
| } | |
| if (typeof tickFormat !== "function") { | |
| tickFormat = d3.format(tickFormat === undefined ? ",f" : tickFormat); | |
| } | |
| } | |
| } | |
| svg.append("g") | |
| .attr("transform", `translate(${width - marginRight},0)`) | |
| .call(d3.axisRight(x) | |
| .ticks(ticks, typeof tickFormat === "string" ? tickFormat : undefined) | |
| .tickFormat(typeof tickFormat === "function" ? tickFormat : undefined) | |
| .tickSize(tickSize) | |
| .tickValues(tickValues)) | |
| .call(tickAdjust) | |
| .call(g => g.select(".domain").remove()) | |
| .call(g => g.append("text") | |
| .attr("x", marginLeft - width + marginRight) | |
| .attr("y", height - 2) | |
| .attr("fill", "currentColor") | |
| .attr("text-anchor", "start") | |
| // .attr("font-weight", "bold") | |
| .attr("class", "title") | |
| .text(title)); | |
| return svg.node(); | |
| } | |
| // === init info functions === | |
| const autoMapFunc = (id) => config.mapToName[id] ?? config.mapToSong[id] ?? id; | |
| const mapToNameFunc = (id) => config.mapToName[id] ?? id; | |
| const mapToSongFunc = (id) => config.mapToSong[id] ?? id; | |
| const mapToSpaceFunc = (id) => config.mapToSpace[id] ?? id; | |
| const isConfirmed = () => currentSong.length > 0 && currentSinger.length > 0 && currentTargetSinger.length > 0 | |
| const isSupportMultiMode = (id = 'all') => { | |
| const multiConfig = config.pathData[currentMode].multi; | |
| if (multiConfig === true) return true; | |
| if (multiConfig === false) return false; | |
| return multiConfig.includes(id); | |
| } | |
| const isMultiMode = () => currentSong.length >= 2 || currentSinger.length >= 2 || currentTargetSinger.length >= 2 | |
| const isSelectable = () => { | |
| return isMultiMode() || currentMode === "Metric Comparison" | |
| } | |
| const getSrcPerfix = (i = currentSinger[0], t = currentTargetSinger[0], s = currentSong[0]) => { | |
| let basePath = config.pathData[currentMode].data.find((d) => Object.keys(d.pathMap).includes(i)).basePath; | |
| return `${baseLink}/${basePath}/to_${t}/${i}_${s}_pred_autoshift` | |
| } | |
| const getCsvSrc = (v = 999, i = currentSinger[0], t = currentTargetSinger[0], s = currentSong[0]) => { | |
| return getSrcPerfix(i, t, s) + `_step_${v}.csv` | |
| } | |
| const getReferenceCsvSrc = (i = currentSinger[0], s = currentSong[0]) => `${baseLink}/data/rf_all/${i}_${s}.csv` | |
| const getTargetReferenceCsvSrc = (i = currentSinger[0], s = currentSong[0]) => { | |
| const referenceMap = config.pathData[currentMode].referenceMap; | |
| let index = getSongs(currentSinger[currentSinger.length - 1]).indexOf(s); | |
| if (index > referenceMap[i].length) { | |
| index = referenceMap[i].length - 1; | |
| } | |
| const path = referenceMap[i][index]; | |
| return `${baseLink}/data/rf_all/${path}.csv` | |
| } | |
| const getStepSrc = (s = currentSinger[0], t = currentTargetSinger[0], o = currentSong[0], p = currentShowingPic) => { | |
| return getSrcPerfix(s, t, o) + `_${p}_all_steps.csv` | |
| } | |
| const getMetricsSrc = (s = currentSinger[0], t = currentTargetSinger[0], o = currentSong[0]) => { | |
| return getSrcPerfix(s, t, o) + `_metrics.csv` | |
| } | |
| const getSongs = (singer) => { | |
| return config.pathData[currentMode].data.find((d) => Object.keys(d.pathMap).includes(singer))?.pathMap[singer].songs ?? []; | |
| } | |
| const findCorrespondingSong = (targetSinger, song = currentSong[0]) => { | |
| const singer = currentSinger[currentSinger.length - 1]; // always use the last one | |
| // if (singer === targetSinger) return song;Source Singer | |
| const index = getSongs(singer).indexOf(song); // find index | |
| const newSong = getSongs(targetSinger)[index]; | |
| // console.log(`findCorrespondingSong: ${song} -> ${newSong}, index: ${index}, singer: ${singer}, targetSinger: ${targetSinger}, getSongs: ${getSongs(singer)}`) | |
| return newSong; | |
| } | |
| const getMultipleLable = () => { | |
| if (currentSinger.length > 1) { | |
| return ['sourcesinger', currentSinger] | |
| } else if (currentTargetSinger.length > 1) { | |
| return ['target', currentTargetSinger] | |
| } else if (currentSong.length > 1) { | |
| return ['song', currentSong] | |
| } | |
| return [null, null] | |
| } | |
| const autoLog = (value) => { | |
| // take log of value if it is too large | |
| if (value > 2) { | |
| return Math.log(value) / Math.log(4); | |
| } | |
| return value; | |
| } | |
| // === init UI bind functions === | |
| const bindVideo = (refId) => { | |
| let timer; | |
| const $video = $$(`video${refId}`) | |
| $video?.addEventListener('play', () => { | |
| clearInterval(timer) | |
| timer = setInterval(() => { | |
| const currentTime = $video.currentTime; | |
| charts.find((e) => e.id === refId)?.reset() | |
| charts.find((e) => e.id === refId)?.highlight(currentTime) | |
| }, 10) | |
| }); | |
| $video?.addEventListener('pause', () => { | |
| clearInterval(timer) | |
| charts.find((e) => e.id === refId)?.reset() | |
| }); | |
| $video?.addEventListener('ended', () => { | |
| clearInterval(timer) | |
| charts.find((e) => e.id === refId)?.reset() | |
| }); | |
| } | |
| const bindIcon = (refId, svg, color) => { | |
| d3.select(`#icon${refId}`) | |
| .attr("width", 20) | |
| .attr("height", 20) | |
| .append("path") | |
| .attr("d", svg.size(120)) | |
| .style("stroke", "white") | |
| .style("stroke-width", "2px") | |
| .attr("transform", `translate(10, 11)`) | |
| .style("fill", color) | |
| } | |
| const bindDiv = (refId, data = [], svgObject = null, color = "#000", close = () => { }, width = 345, height = 200) => { | |
| // trying to bind if exist: close, select, refresh, video, svg | |
| // bind close and select event | |
| $$(`close${refId}`)?.addEventListener('click', () => { | |
| close() | |
| }) | |
| $$(`select${refId}`)?.addEventListener('change', (e) => { | |
| const checked = e.target.checked; | |
| // mark this chart as selected in charts | |
| charts.find((c) => c.id === refId).selected = checked; | |
| checkCompare() | |
| }); | |
| // bind refresh event | |
| $$(`refresh${refId}`)?.addEventListener('click', () => { | |
| charts.forEach(c => c.sync && c.reset()) | |
| }) | |
| // when video is playing, highlight the chart | |
| bindVideo(refId) | |
| // draw svg using d3 | |
| if (svgObject) bindIcon(refId, svgObject, color); | |
| // data struct: | |
| // [ | |
| // {col1: 0.1, col2: 0.2, ...(x axis)}, | |
| // {col1: 0.1, col2: 0.2, ...}, | |
| // ...(y axis), | |
| // columns: ['col1', 'col2', ...] | |
| // ] | |
| if (data.length === 0) return; | |
| const arrayData = data.map(row => Object.values(row).map(d => +d)); | |
| // draw mel spectrogram | |
| plotMelSpectrogram(arrayData, refId, color, close, width, height, true, hidePitch); | |
| charts.find((e) => e.id === refId)?.zoom(zoomStart, zoomEnd) | |
| } | |
| const getDiv = (refId, src, color, title, subtitle, svg = false, card = true) => { | |
| const div = document.createElement('div'); | |
| const draggable = (isMultiMode() || userMode === "basic") ? '' : 'draggable="true"'; | |
| div.innerHTML = (card ? `<div class="card min-w-[305px] p-2 w-full flex flex-col gap-1" id="display${refId}" ${draggable}>` : '<div>') + | |
| `<div class="flex items-center">` + | |
| `<input id="select${refId}" type="checkbox" value="" class="checkbox mr-1">` + | |
| (svg ? `<svg id="icon${refId}" class="shrink-0 h-[20px] w-[20px]"></svg>` : '') + | |
| `<div class="flex flex-col ml-1 mr-1">` + | |
| `<h5 class="text-base font-bold tracking-tight mb-0 text-[${color}] line-clamp-1" id="title${refId}">${title}</h5>` + | |
| `<h5 class="text-xs tracking-tight mb-0 text-[${color}] line-clamp-1">${subtitle}</h5>` + | |
| `</div>` + | |
| (card ? `<div class="flex flex-row ml-auto">` + | |
| `<a class="btn-sec h-9 w-9 p-2.5 mb-0" id="refresh${refId}">${refreshIcon}</a>` + | |
| `<a class="btn-sec h-9 w-9 p-2.5 mb-0" id="close${refId}">${closeIcon}</a>` + | |
| `</div>` : '') + | |
| `</div>` + | |
| `<div class="mx-auto min-h-[200px]" id="mel${refId}">${loadingDiv}</div>` + | |
| `<audio class="w-full" id="video${refId}" controls src="${src}"></audio>` + | |
| `</div>`; | |
| return div.firstChild; | |
| } | |
| const cachedDifference = {} | |
| const getColor = (color) => { | |
| if (color > 1) { | |
| color = 1 | |
| } | |
| if (color < 0) { | |
| color = 0 | |
| } | |
| return d3.interpolateBlues(color) | |
| } | |
| const calculateStepDifference = (step1, step2, td) => { | |
| // return a number to represent the difference between two steps | |
| // the larger the number, the more different between two steps | |
| const csv1 = getCsvSrc(step1); | |
| const csv2 = getCsvSrc(step2); | |
| const key = `${currentSinger[0]}-${currentTargetSinger[0]}-${currentSong[0]}` | |
| let diff = 0; | |
| if (cachedDifference[`${key}-${step1}-${step2}`] || cachedDifference[`${key}-${step2}-${step1}`]) { | |
| diff = cachedDifference[`${key}-${step1}-${step2}`] ?? cachedDifference[`${key}-${step2}-${step1}`]; | |
| td.style.backgroundColor = getColor(diff); | |
| return; | |
| } | |
| d3.csv(csv1, (data1) => { | |
| const arrayData1 = data1.map(row => Object.values(row).map(d => +d)); | |
| d3.csv(csv2, (data2) => { | |
| const arrayData2 = data2.map(row => Object.values(row).map(d => +d)); | |
| // console.log(arrayData1, arrayData2) | |
| arrayData1.forEach((d1, i) => { | |
| const d2 = arrayData2[i]; | |
| d1.forEach((v1, j) => { | |
| const v2 = d2[j]; | |
| diff += (v1 - v2) ** 2; | |
| }) | |
| }) | |
| const normalizedDiff = Math.sqrt(diff) / 6000; | |
| cachedDifference[`${key}-${step1}-${step2}`] = normalizedDiff; | |
| td.style.backgroundColor = getColor(normalizedDiff); | |
| }) | |
| }) | |
| // console.log(`step difference between ${step1} and ${step2} is ${diff}`) | |
| // return diff; | |
| } | |
| let showTooltipFn = null | |
| const showTooltip = (content) => { | |
| // show a tool tip around the mouse | |
| const tooltip = $$("tooltip"); | |
| tooltip.innerHTML = content; | |
| tooltip.classList.remove("invisible"); | |
| showTooltipFn = (e) => { | |
| tooltip.style.left = `${e.pageX + 10}px`; | |
| tooltip.style.top = `${e.pageY + 10}px`; | |
| } | |
| document.addEventListener("mousemove", showTooltipFn) | |
| } | |
| const hideTooltip = () => { | |
| $$("tooltip").classList.add("invisible"); | |
| document.removeEventListener("mousemove", showTooltipFn); | |
| } | |
| const getGridDiv = () => { | |
| // a table with all selected steps lined up in cols and rows with step number | |
| const table = document.createElement('table'); | |
| table.classList.add('table-auto', 'h-fit', 'w-fit', 'border-collapse', 'border', 'border-gray-200', 'dark:border-gray-700'); | |
| for (let i = -1; i < gridSelected.length; i++) { | |
| const tr = document.createElement('tr'); | |
| tr.classList.add('border', 'border-gray-200', 'dark:border-gray-700'); | |
| for (let j = -1; j < gridSelected.length; j++) { | |
| const td = document.createElement('td'); | |
| td.classList.add('border', 'border-gray-200', 'dark:border-gray-700'); | |
| if (i === -1 || j === -1) { | |
| td.style.width = '35px'; | |
| td.style.minWidth = '35px'; | |
| td.style.height = '35px'; | |
| td.style.textAlign = 'center'; | |
| // table head | |
| if (i === -1 && j === -1) { | |
| tr.appendChild(td); | |
| continue; | |
| } | |
| if (i === -1) { | |
| td.innerText = `${gridSelected[j]}`; | |
| tr.appendChild(td); | |
| continue; | |
| } | |
| if (j === -1) { | |
| td.innerText = `${gridSelected[i]}`; | |
| tr.appendChild(td); | |
| continue; | |
| } | |
| continue; | |
| } | |
| const step = gridSelected[i]; | |
| const step2 = gridSelected[j]; | |
| if (step === step2) { | |
| // td.innerText = `N/A`; | |
| td.style.backgroundColor = getColor(0); | |
| tr.appendChild(td); | |
| continue; | |
| } | |
| calculateStepDifference(step, step2, td); | |
| td.style.cursor = 'pointer'; | |
| td.addEventListener('mouseover', () => { | |
| showTooltip(`Step ${step} vs Step ${step2}`) | |
| }) | |
| td.addEventListener('mouseout', () => { | |
| hideTooltip() | |
| }) | |
| td.addEventListener('click', () => { | |
| if (downloadingLock.length !== 0) { | |
| alert('Operating too fast, please wait for the previous operation to finish') // fix removing bug | |
| return | |
| } | |
| gridComparison.forEach((step) => { | |
| charts.filter(c => c.id.endsWith(step))?.forEach(c => c.close(true)); // close all previous comparison display | |
| }) | |
| // gridComparison = [`${step}`, `${step2}`]; | |
| gridComparison.push(`${step}`); | |
| gridComparison.push(`${step2}`); | |
| selectStep(step); | |
| selectStep(step2); | |
| }) | |
| tr.appendChild(td); | |
| } | |
| table.appendChild(tr); | |
| } | |
| // get outter div width and height, then scale the table | |
| if (gridSelected.length >= 6) { | |
| const tableHeight = 35 * (gridSelected.length + 1) + 1; | |
| const divHeight = 250; | |
| const factor = divHeight / tableHeight; | |
| console.log(factor) | |
| table.style.transform = `scale(${factor})`; | |
| } else { | |
| table.style.transform = `scale(1)`; | |
| } | |
| return table; | |
| } | |
| const loadGrid = () => { | |
| // generate two divs for comparison display: display${refId}, display${refId}. Undraggable, selectable, unclosable | |
| selectStep(-1) | |
| // gridSelected = [0, 10, 50, 100, 200, 999]; // default | |
| // gridSelected = Array.from({ length: 41 }, (v, i) => i * 5); // 0 - 200 | |
| // gridSelected = Array.from({ length: 21 }, (v, i) => i * 10); // 0 - 200 | |
| // gridSelected = Array.from({ length: 21 }, (v, i) => i * 10 + 200); // 200 - 400 | |
| // gridSelected = Array.from({ length: 21 }, (v, i) => (i * 30 + 400 === 1000) ? 999 : i * 30 + 400); // 400 - 999 | |
| gridSelected = [50, 250, 650, 850, 950]; // mid point + 950 | |
| // gridSelected = [0, 100, 200, 300, 600, 700, 800, 900]; // start and end point | |
| // gridSelected = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 999]; // exponential | |
| // gridSelected = Array.from({ length: 10 }, (v, i) => Math.pow(2, i)); | |
| // gridSelected = [0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987] // Fibonacci | |
| gridComparison = ["ph1", "ph2"]; | |
| if (gridComparison.length > 2) { | |
| alert('System error: Only 2 steps can be compared in grid mode') | |
| return | |
| } | |
| gridComparison.forEach((step, i) => { | |
| selectStep(step) | |
| }) | |
| // generate a div to save grid: grid_select | |
| // in the div, user can see a grid table with all selected steps lined up in cols and rows with step number | |
| // in each cell, user can see a color representing the difference between two steps (cols and rows steps) | |
| // user can click on the cell to select the step for comparison, which will be highlighted in the left map, and in the previous comparison display divs | |
| $$("grid_table").appendChild(getGridDiv()) | |
| $$("grid_table_legend").appendChild(legend({ | |
| color: d3.scaleSequential() | |
| .domain([0, 100]) | |
| .interpolator(d3.interpolateBlues), | |
| width: 30, | |
| tickSize: 2, | |
| height: 250, | |
| title: "Difference", | |
| tickFormat: (d) => `${d}%` | |
| })) | |
| } | |
| const addComparison = (step) => { | |
| // add the step into the gridSelected then update the grid table with new added information | |
| // then rerender the grid table | |
| let $table = $$("grid_table").querySelector("table"); | |
| step = +step; | |
| if (gridSelected.includes(step)) { | |
| // remove the step | |
| gridSelected = gridSelected.filter((s) => s !== step); | |
| } else { | |
| gridSelected.push(step); | |
| gridSelected = gridSelected.sort((a, b) => a - b); | |
| } | |
| const table = getGridDiv(); | |
| $table.replaceWith(table); | |
| $table = table; | |
| } | |
| // === init UI component functions === | |
| const initInterface = () => { | |
| availableMode = Object.keys(config.pathData).filter((k) => config.pathData[k].users.includes(userMode)); | |
| initSelect("mode_id", availableMode, 0); | |
| refreshOptions(); | |
| if (userMode === "basic") { | |
| resetDisplay(true, false, true); // clear the pin area | |
| // loadGrid(); // load the grid component in the pin area | |
| // $$("components").classList.add("hidden"); | |
| } | |
| if (userMode === "advanced") { | |
| resetDisplay(false, false, true); | |
| // $$("components").classList.remove("hidden"); | |
| selectStep(999); | |
| selectStep(100); | |
| selectStep(10); | |
| } | |
| $$("mode_change").textContent = userMode === "basic" ? "Switch to Advanced" : "Switch to Basic"; | |
| // set autoplay | |
| playMode = (localStorage.getItem('AUTO_PLAY') ?? "true") === "true"; | |
| updatePlayIcon(playMode) | |
| } | |
| const initAlert = () => { | |
| const show = () => { | |
| isAlertShown = true; | |
| $$("alert").classList.remove("hidden"); | |
| $$("alert").classList.add("flex"); | |
| } | |
| const hide = () => { | |
| isAlertShown = false; | |
| $$("alert").classList.add("hidden"); | |
| $$("alert").classList.remove("flex"); | |
| } | |
| window.alert = (title, msg = title) => { | |
| $$("alert_title").innerText = title === msg ? "Alert" : title; | |
| $$("alert_text").innerHTML = msg.replaceAll("\n", "<br />"); | |
| show(); | |
| $$("finish_alert").focus(); | |
| } | |
| $$("close_alert").addEventListener("click", () => { | |
| hide() | |
| }) | |
| $$("finish_alert").addEventListener("click", () => { | |
| hide() | |
| }) | |
| document.addEventListener("keydown", (e) => { | |
| if (isAlertShown && e.key === "Escape") { | |
| hide() | |
| } | |
| }) | |
| } | |
| const initOptions = (id, options) => { | |
| if (id === "sourcesinger_id") { | |
| currentSinger = [options[0]] | |
| } else if (id === "song_id") { | |
| currentSong = [options[0]] | |
| } else if (id === "target_id") { | |
| currentTargetSinger = [options[0]] | |
| } | |
| const $dropdown = $$(`${id}_dropdown`) | |
| if (!dropdowns.includes(id)) { | |
| dropdowns.push(id) | |
| $$(id).addEventListener("click", (e) => { | |
| if ($dropdown.classList.contains("hidden")) { | |
| dropdowns.forEach((dd) => { | |
| const d = $$(`${dd}_dropdown`) | |
| if (!d.classList.contains("hidden")) { | |
| d.classList.add("hidden") | |
| } | |
| }) | |
| $dropdown.classList.remove("hidden") | |
| } else { | |
| $dropdown.classList.add("hidden") | |
| } | |
| e.stopPropagation(); | |
| }); | |
| // Event listener for the blur event on the document | |
| document.addEventListener("click", (event) => { | |
| // Check if the clicked element is outside the dropdown | |
| if (!$dropdown.contains(event.target)) { | |
| if (!$dropdown.classList.contains("hidden")) { | |
| $dropdown.classList.add("hidden") | |
| } | |
| } | |
| }); | |
| } | |
| const $ul = $dropdown.querySelector("ul"); | |
| $ul.innerHTML = ''; | |
| options.forEach((o, i) => { | |
| const $li = document.createElement("li"); | |
| $li.classList.add("px-4", "flex", "items-center", "hover:bg-gray-100", "cursor-pointer", "dark:hover:bg-gray-600", "dark:hover:text-white"); | |
| const $input = document.createElement("input"); | |
| $input.id = `${id}_${o}`; | |
| $input.checked = i === 0 | |
| $input.type = "checkbox"; | |
| $input.value = o; | |
| $input.classList.add("checkbox"); | |
| const $label = document.createElement("label"); | |
| $label.htmlFor = `${id}_${o}`; | |
| $label.classList.add("w-full", "py-2", "ml-2", "text-sm", "text-gray-900", "dark:text-gray-300"); | |
| $label.innerText = autoMapFunc(o); | |
| $li.appendChild($input); | |
| $li.appendChild($label); | |
| $input.addEventListener("change", (e) => { | |
| // update current after click | |
| if (e.target.checked) { | |
| // enable | |
| if (id === "sourcesinger_id") { | |
| if ((!isSupportMultiMode(id) || isMultiMode()) && currentSinger.length > 0) { | |
| // already multi, cancel previous one | |
| $$(`${id}_${currentSinger.shift()}`).checked = false; | |
| } | |
| if (!refreshOptions(false, o, currentSinger.length > 0 ? currentSinger[0] : null)) { | |
| // no avaliable target singer | |
| alert('no avaliable target singer') | |
| return; | |
| } | |
| currentTargetSinger = [targetSingers[0]]; | |
| currentSinger.push(o); | |
| } else if (id === "song_id") { | |
| if ((!isSupportMultiMode(id) || isMultiMode()) && currentSong.length > 0) { | |
| $$(`${id}_${currentSong.shift()}`).checked = false; | |
| } | |
| currentSong.push(o); | |
| } else if (id === "target_id") { | |
| if ((!isSupportMultiMode(id) || isMultiMode()) && currentTargetSinger.length > 0) { | |
| $$(`${id}_${currentTargetSinger.shift()}`).checked = false; | |
| } | |
| currentTargetSinger.push(o); | |
| } | |
| } else if (id === "sourcesinger_id") { | |
| currentSinger = currentSinger.filter((s) => s !== o); | |
| if (currentSinger.length > 0) { | |
| refreshOptions(false, currentSinger[0]) | |
| } | |
| } else if (id === "song_id") { | |
| currentSong = currentSong.filter((s) => s !== o); | |
| } else if (id === "target_id") { | |
| currentTargetSinger = currentTargetSinger.filter((s) => s !== o); | |
| } | |
| resetDisplay(); | |
| }) | |
| $ul.appendChild($li); | |
| }) | |
| } | |
| const initSelect = (id, content, defaultIndex, limit = -1) => { | |
| const dropdown = $$(id); | |
| dropdown.innerHTML = ''; | |
| for (let i = 0; i < content.length; i++) { | |
| if (limit > 0 && i >= limit) break; | |
| const option = document.createElement("option"); | |
| option.value = content[i]; | |
| option.textContent = mapToSpaceFunc(content[i]); | |
| if (i === defaultIndex) option.selected = 1; | |
| dropdown.appendChild(option); | |
| } | |
| } | |
| const updateSelect = (id, content, key) => { | |
| const dropdown = $$(id); | |
| dropdown.selectedIndex = content.indexOf(key); | |
| } | |
| const updateOptions = (id, options) => { | |
| const $dropdown = $$(`${id}_dropdown`) | |
| const $ul = $dropdown.querySelector("ul"); | |
| const $inputs = $ul.querySelectorAll("input"); | |
| $inputs.forEach((i) => { | |
| if (options.includes(i.value)) { | |
| i.checked = true; | |
| } else { | |
| i.checked = false; | |
| } | |
| }) | |
| const $button = $$(`${id}_text`) | |
| options = options.map((o) => autoMapFunc(o)) | |
| if (options.length === 0) { | |
| $button.innerHTML = `<span class="text-red-500">Please select</span>`; | |
| } else if (options.length === 1) { | |
| $button.innerHTML = `${options[0]}`; | |
| } else { | |
| $button.innerHTML = `${options[0]} + ${options[1]}`; | |
| } | |
| } | |
| const refreshOptions = (reset = true, selectedSourceSinger, selectedSourceSinger2 = null) => { | |
| if (reset) { | |
| sourceSingers = config.pathData[currentMode].data.map((d) => Object.keys(d.pathMap).flat()).flat(); | |
| initOptions("sourcesinger_id", sourceSingers); | |
| selectedSourceSinger = selectedSourceSinger ?? sourceSingers[0]; | |
| } | |
| sourceSongs = config.pathData[currentMode].data.map((d) => d.pathMap[selectedSourceSinger]?.songs).flat().filter((s) => s !== undefined); | |
| initOptions("song_id", sourceSongs); // update avaliable source songs | |
| const avaliableTargetSingers = config.pathData[currentMode].data.map((d) => d.pathMap[selectedSourceSinger]?.targets).flat().filter((s) => s !== undefined); | |
| // take intersection | |
| console.log(selectedSourceSinger, selectedSourceSinger2) | |
| if (selectedSourceSinger2) { | |
| const avaliableTargetSingers2 = config.pathData[currentMode].data.map((d) => d.pathMap[selectedSourceSinger2]?.targets).flat().filter((s) => s !== undefined); | |
| targetSingers = avaliableTargetSingers2.filter((value) => avaliableTargetSingers.includes(value)); | |
| } else { | |
| targetSingers = avaliableTargetSingers | |
| } | |
| if (targetSingers === undefined || targetSingers.length === 0) { | |
| return false | |
| } | |
| initOptions("target_id", targetSingers); // update avaliable target singers | |
| return true | |
| } | |
| const lockOptions = () => { | |
| ["sourcesinger_id", "song_id", "target_id"].forEach((dd) => { | |
| const d = $$(dd) | |
| d.disabled = true; | |
| }) | |
| } | |
| const unlockOptions = () => { | |
| ["sourcesinger_id", "song_id", "target_id"].forEach((dd) => { | |
| const d = $$(dd) | |
| d.disabled = false; | |
| }) | |
| } | |
| const updatePlayIcon = (playMode) => { | |
| const $icon_play = $$("icon_play") | |
| const $icon_stop = $$("icon_stop") | |
| if (playMode) { | |
| $icon_play.style = "display: none"; | |
| $icon_stop.style = "display: block"; | |
| } else { | |
| $icon_play.style = "display: block"; | |
| $icon_stop.style = "display: none"; | |
| } | |
| } | |
| const drawHistogram = (data, id, width = 200, xlabel = "Metrics", ylable = "Performance", yside = "left") => { | |
| // shrink data to 0-1 by Z-score Normalization | |
| data = data.map((d, i) => { | |
| return { | |
| name: d.name, | |
| type: d.type, | |
| value: d.value, | |
| rawValue: d.value | |
| } | |
| }) | |
| // Declare the chart dimensions and margins. | |
| // const width = 200; | |
| const height = isMultiMode() ? (currentShowingPic == "encoded_step" ? 180 : 136) : 160; | |
| const rectWidth = 15; | |
| const marginTop = 20; | |
| const marginRight = yside === "left" ? 30 : 60; | |
| const marginBottom = 15; | |
| const marginLeft = yside === "left" ? 40 : 10; | |
| // Declare the x (horizontal position) scale. | |
| const x = d3.scalePoint() | |
| .domain(data.map((d) => d.name)) | |
| .range([width - marginRight, marginLeft + rectWidth]) | |
| // Declare the y (vertical position) scale. | |
| let y | |
| if (yside === "left") { | |
| y = d3.scaleLinear() | |
| .domain([0, 1]) | |
| .range([height - marginBottom, marginTop]); | |
| } else { | |
| // log | |
| y = d3.scaleLog() | |
| .domain([1, 100]) | |
| .range([height - marginBottom, marginTop]); | |
| } | |
| // Create the SVG container. | |
| d3.select(id).html(""); | |
| const svg = d3.select(id) | |
| .attr("style", `color: ${darkMode ? 'white' : 'black'};`) | |
| .append("svg") | |
| .attr("width", width) | |
| .attr("height", height) | |
| .attr("viewBox", [0, 0, width, height]) | |
| .attr("style", `max-width: 100%; height: auto;`); | |
| // Add a rect for each bin. | |
| svg.append("g") | |
| .selectAll() | |
| .data(data) | |
| .enter() | |
| .append("rect") | |
| .attr("fill", (_, i) => metricsColors[yside === "left" ? 1 - i : 4 - i]) | |
| .attr("id", (d) => `rect_${d.name}`) | |
| .attr("x", (d) => x(d.name) - rectWidth / 2) | |
| .attr("width", (d) => rectWidth) | |
| .attr("y", (d) => y(d.value)) | |
| .attr("height", (d) => height - marginBottom - y(d.value)) | |
| .on("mouseover", (e) => { | |
| if (currentHistogram.includes(e)) return; | |
| d3.select(`#rect_${e.name}`).attr("stroke", "gray").attr("stroke-width", 2) | |
| }) | |
| .on("mouseout", (e) => { | |
| if (currentHistogram.includes(e)) return; | |
| d3.select(`#rect_${e.name}`).attr("stroke", "none") | |
| }) | |
| .on("click", (e) => { | |
| if (currentHistogram.includes(e)) { | |
| currentHistogram = currentHistogram.filter((s) => s !== e); | |
| d3.select(`#rect_${e.name}`).attr("stroke", "none") | |
| } else { | |
| const previous_e = currentHistogram.shift(); | |
| if (previous_e) { | |
| d3.select(`#rect_${previous_e.name}`).attr("stroke", "none") | |
| } | |
| currentHistogram.push(e); | |
| d3.select(`#rect_${e.name}`).attr("stroke", "currentColor").attr("stroke-width", 2) | |
| } | |
| if (currentHistogram.length > 0) { | |
| showBestCase(); | |
| } else { | |
| unlockOptions(); | |
| } | |
| }) | |
| // Add a label for each bin. | |
| svg.append("g") | |
| .attr("fill", "currentColor") | |
| .attr("text-anchor", "middle") | |
| .attr("font-family", "sans-serif") | |
| .attr("font-size", 10) | |
| .selectAll("text") | |
| .data(data) | |
| .enter() | |
| .append("text") | |
| .attr("id", (d) => `label_${d.name}`) | |
| .attr("x", (d) => x(d.name)) | |
| .attr("y", (d) => y(d.value) - 4) | |
| .text((d) => d.rawValue.toFixed(2)); | |
| // Add the x-axis and label. | |
| svg.append("g") | |
| .attr("transform", `translate(0,${height - marginBottom})`) | |
| .call(d3.axisBottom(x).tickSizeInner(1).tickSizeOuter(0)) | |
| .call((g) => | |
| g.select(".domain").attr("stroke", "currentColor") | |
| .attr("d", yside === "left" ? "M40.5,0V0.5H115.5V0" : "M10.5,0V0.5H115.5V0") | |
| ) | |
| // Add the y-axis and label, and remove the domain line. | |
| if (yside === "left") { | |
| svg.append("g") | |
| .attr("transform", `translate(${marginLeft},0)`) | |
| .call(d3.axisLeft(y).ticks(height / 40)) | |
| .call((g) => g.select(".domain").attr("stroke", "currentColor")) | |
| .call((g) => g.append("text") | |
| .attr("x", -height / 2 + ylable.length * 2) | |
| .attr("y", -marginLeft + 10) | |
| .attr("font-size", 12) | |
| .attr("fill", "currentColor") | |
| .attr("transform", "rotate(-90)") | |
| .text(ylable)); | |
| } else { | |
| svg.append("g") | |
| .attr("transform", `translate(${width - marginRight + 15},0)`) | |
| .call(d3.axisRight(y).ticks(2).tickFormat(d3.format(".0f"))) | |
| .call((g) => g.select(".domain").attr("stroke", "currentColor")) | |
| .call((g) => g.append("text") | |
| .attr("x", height / 2 - ylable.length * 2) | |
| .attr("y", -30) | |
| .attr("font-size", 12) | |
| .attr("fill", "currentColor") | |
| .attr("transform", "rotate(90)") | |
| .text(ylable)); | |
| } | |
| svg.selectAll("text").attr("fill", "currentColor") | |
| svg.selectAll("line").attr("stroke", "currentColor") | |
| // Return the SVG element. | |
| return svg.node(); | |
| } | |
| const createColorLegend = (svg) => { | |
| const width = +svg.attr('width'); // Get width from SVG attribute | |
| const height = +svg.attr('height'); // Fixed height for the legend | |
| svg.attr('height', height); // Set the height of the SVG | |
| // Set up the color scale using the Red-Yellow-Blue interpolator | |
| const colorScale = d3.scaleSequential() | |
| .domain([999, 0]) | |
| .interpolator(d3.interpolateRdBu); | |
| // Gradient definition | |
| const defs = svg.append('defs'); | |
| const linearGradient = defs.append('linearGradient') | |
| .attr('id', 'linear-gradient'); | |
| // Define the gradient stops | |
| d3.range(0, 1.01, 0.01).forEach(t => { | |
| linearGradient.append('stop') | |
| .attr('offset', `${t * 100}%`) | |
| .attr('stop-color', colorScale(t * 1000)); | |
| }); | |
| // Draw the color rectangle | |
| svg.append('rect') | |
| .attr('width', width - 20) | |
| .attr('height', 8) | |
| .attr('x', 10) | |
| .attr('y', 2) | |
| .style('fill', 'url(#linear-gradient)'); | |
| // Add an axis to the legend | |
| const xScale = d3.scaleLinear() | |
| .domain([999, 0]) | |
| .range([10, width - 11]); | |
| // Custom ticks | |
| const ticks = [0, 250, 500, 750, 999]; | |
| const axis = d3.axisBottom(xScale) | |
| .tickValues(ticks) | |
| .tickFormat(d3.format(".0f")) | |
| .tickSize(10) | |
| .tickPadding(1); | |
| svg.append('g') | |
| .attr('transform', `translate(0, 2)`) | |
| .call(axis) | |
| .call(g => g.select('.domain').remove()) | |
| .selectAll('text') | |
| .attr('fill', darkMode ? "white" : "black") | |
| .attr('font-size', 8) | |
| } | |
| const drawContour = (data, SVG, width, height, id_i, x, y) => { | |
| is_large = id_i === "full"; | |
| is_filtered = data.length < 1000; | |
| const contours = d3.contourDensity() | |
| .x(function (d) { return x(d.heng); }) | |
| .y(function (d) { return y(d.shu); }) | |
| .size([width, height]) | |
| .cellSize(is_large ? 2 : 2) | |
| .bandwidth(is_large ? 5 : 3) // change bandwidth to adjust the contour density | |
| (data); | |
| const countourIndex = 1; | |
| const contour = SVG.append('g').attr("class", "zoom_g") | |
| contour.selectAll("path") | |
| .data([contours[countourIndex]]) | |
| .enter().append("path") | |
| .attr("d", d3.geoPath()) | |
| .attr("stroke", darkMode ? "white" : "black") | |
| .attr("fill", "none") | |
| // make a mask | |
| const mask = SVG.append('g').attr("class", "zoom_g") | |
| .append("mask") | |
| .attr("id", `contour_mask${id_i}`) | |
| mask.append("rect") | |
| .attr("width", width) | |
| .attr("height", height) | |
| .attr("fill", "black") | |
| mask.append("path") | |
| .attr("d", d3.geoPath()(contours[countourIndex])) | |
| .attr("fill", "white") | |
| // fill background color | |
| const backgroundScatters = SVG.append('g') | |
| .attr("class", "zoom_g") | |
| .attr("mask", `url(#contour_mask${id_i})`) | |
| backgroundScatters | |
| .selectAll("circle") | |
| .data(data) | |
| .enter() | |
| .append("circle") | |
| .attr("cx", (d) => x(d.heng)) | |
| .attr("cy", (d) => y(d.shu)) | |
| .attr("r", is_large ? 12 : 6) | |
| .style("opacity", is_filtered ? 1 : 0.6) // opacity for countour background | |
| .style("fill", (d) => { | |
| return d3.interpolateRdBu(d.index / 1000); | |
| }); | |
| } | |
| const drawScatter = (scatter_g, data, d, id_i, fill_color, x, y) => { | |
| is_large = id_i.includes("full"); | |
| scatter_g.selectAll("circle") | |
| .data(data).enter() | |
| .append("path") | |
| .attr("d", d) | |
| .attr("id", (d) => `point${id_i}_${d.index}`) | |
| .attr("transform", (d) => `translate(${x(d.heng)}, ${y(d.shu)})`) | |
| .style("fill", fill_color) | |
| .attr("stroke", "white") | |
| .style("opacity", stepOpacity) | |
| .style("stroke-width", `${is_large ? stepStrokeWidth : stepStrokeWidth * 0.5}px`) | |
| .on("click", (d) => { | |
| const step = Number(d.index).toFixed(0) | |
| selectStep(step); | |
| }) | |
| .on("mouseover", (d) => { | |
| const step = Number(d.index).toFixed(0) | |
| $range.value = 999 - step; | |
| lineChange(false); | |
| }) | |
| .on("mouseout", (d) => { | |
| const step = Number(d.index).toFixed(0) | |
| if (hoveredStep.filter(s => s === step).length > 0) { | |
| hoveredStep = hoveredStep.filter(s => s !== step) | |
| resetStep(step) | |
| } | |
| $$("current_step_display_number").innerHTML = ""; | |
| }); | |
| } | |
| const drawStepMap = (csvPath, csvPath2 = '') => { | |
| // if csvPath2 is not null: compare mode, display two data in same figure | |
| // console.log(csvPath, csvPath2) | |
| const width = 280; | |
| const height = ((isMultiMode() && currentShowingPic != "encoded_step") ? 240 : 316); | |
| SVG = d3.select("#dataviz_axisZoom") | |
| .append("svg") | |
| .attr("width", width) | |
| .attr("height", height) | |
| // init a unzoomed SVG to display the legend | |
| const legendSVG = d3.select("#dataviz_axisZoom") | |
| .append("svg") | |
| .attr("id", "color_legend") | |
| .attr("width", 150) | |
| .attr("height", 25) | |
| .style("position", "absolute") | |
| .style("bottom", "0") | |
| .style("right", "0") | |
| createColorLegend(legendSVG) | |
| let data1, data2 | |
| const downloadingData = () => { | |
| d3.csv(csvPath, (data) => { | |
| data1 = data; | |
| startDrawing() | |
| }); | |
| if (csvPath2 !== "") d3.csv(csvPath2, (data) => { | |
| data2 = data; | |
| startDrawing() | |
| }); | |
| } | |
| if (samplingSteps) { | |
| path = csvPath.split("/").pop() | |
| fetch(baseLink + `/process_map?input_path=${encodeURI(csvPath.replace(".csv", ".npy"))}&num_steps=${samplingNum}`) | |
| .then(response => response.json()) | |
| .then(json => { | |
| console.log(json) | |
| sampledSteps = json.selected_steps | |
| downloadingData() | |
| }) | |
| .catch((error) => { | |
| console.error('Error:', error); | |
| alert('Error: ' + error + ' Please try to reload the page') | |
| }); | |
| } else { | |
| downloadingData() | |
| } | |
| const startDrawing = () => { | |
| if (!csvPath && !data1) { | |
| // lack of data, waiting all data loaded | |
| return; | |
| } | |
| if (csvPath2 !== "" && !data2) { | |
| // lack of data, waiting all data loaded | |
| return; | |
| } | |
| // console.log(data1, data2) | |
| const x = d3.scaleLinear() | |
| .domain([0, 1]) | |
| .range([10, width - 10]); | |
| const y = d3.scaleLinear() | |
| .domain([0, 1]) | |
| .range(isMultiMode() ? [height - 10, 10] : [height - 25, 10]); | |
| const zoom = d3.zoom() | |
| .scaleExtent([0.5, 25]) | |
| .extent([[0, 0], [width, height]]) | |
| .wheelDelta((e) => -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * (event.ctrlKey ? 10 : 1)) | |
| .on("zoom", () => { | |
| SVG.selectAll(".zoom_g").attr("transform", d3.event.transform); | |
| }); | |
| SVG | |
| .append("rect") | |
| .attr("width", width) | |
| .attr("height", height) | |
| .style("fill", "none") | |
| .style("pointer-events", "all"); | |
| SVG.call(zoom); | |
| // draw line | |
| // SVG.append('g') | |
| // .selectAll(".line") | |
| // .data(data1) | |
| // .enter() | |
| // .append("line") | |
| // .attr("x1", (d, i) => x(data1[i - 1]?.heng ?? d.heng)) | |
| // .attr("y1", (d, i) => y(data1[i - 1]?.shu ?? d.shu)) | |
| // .attr("x2", (d, i) => x(d.heng)) | |
| // .attr("y2", (d, i) => y(d.shu)) | |
| // .attr("stroke", "black") | |
| // .attr("stroke-width", 1) | |
| // .attr("opacity", 0.5) | |
| // here to filter data | |
| if (samplingSteps) { | |
| data1 = data1.filter((d) => sampledSteps.includes(d.index)) | |
| if (data2) { | |
| data2 = data2.filter((d) => sampledSteps.includes(d.index)) | |
| } | |
| } | |
| if (data1 && !data2) drawContour(data1, SVG, width, height, "full", x, y) | |
| if (data1) { | |
| const scatter = SVG.append('g').attr("class", "zoom_g") | |
| drawScatter(scatter, data1, circleD3.size(64), "full", "#61a3a9", x, y) | |
| } | |
| if (data2) { | |
| const scatter2 = SVG.append('g').attr("class", "zoom_g") | |
| drawScatter(scatter2, data2, triangleD3.size(64), "full2", "#a961a3", x, y) | |
| } | |
| if (data1 && data2) { | |
| // create two small SVG | |
| const width2 = width / 2; | |
| const height2 = height / 2; | |
| const SVG1 = d3.select("#dataviz_axisZoom").append("svg") | |
| .attr("width", width2) | |
| .attr("height", height2) | |
| const SVG2 = d3.select("#dataviz_axisZoom").append("svg") | |
| .attr("width", width2) | |
| .attr("height", height2) | |
| const x2 = d3.scaleLinear().domain([-0.1, 1.1]).range([0, width2]); | |
| const y2 = d3.scaleLinear().domain([-0.1, 1.1]).range([height2, 0]); | |
| drawContour(data1, SVG1, width2, height2, "1", x2, y2) | |
| drawContour(data2, SVG2, width2, height2, "2", x2, y2) | |
| const scatter1 = SVG1.append('g').attr("class", "zoom_g") | |
| drawScatter(scatter1, data1, circleD3.size(24), "", "#61a3a9", x2, y2) | |
| const scatter2 = SVG2.append('g').attr("class", "zoom_g") | |
| drawScatter(scatter2, data2, triangleD3.size(24), "2", "#a961a3", x2, y2) | |
| // update the position and size of color legend | |
| d3.select("#color_legend") | |
| .style("right", "68px") | |
| .style("bottom", "-8px") | |
| .style("transform", "scale(0.7)") | |
| } else { | |
| d3.select("#color_legend") | |
| .style("right", "0") | |
| .style("bottom", "-2px") | |
| .style("transform", "scale(1)") | |
| } | |
| hoverStep = (step) => { | |
| step = `${step}` | |
| if (displaySteps.includes(step)) { | |
| d3.select(`#pointfull_${step}`).raise(); | |
| d3.select(`#pointfull2_${step}`).raise(); | |
| d3.select(`#point_${step}`).raise(); | |
| d3.select(`#point2_${step}`).raise(); | |
| return; | |
| } | |
| const color = (isMultiMode()) ? "#FFA500" : "#ff00ed" | |
| const color2 = (isMultiMode()) ? "#1C64F2" : "#ff00ed" | |
| d3.select(`#pointfull_${step}`) | |
| .style("fill", color) | |
| .style("stroke", "white") | |
| .style("stroke-width", `${stepStrokeWidth * 2}px`) | |
| .attr("d", circleD3.size(192)) | |
| .style("cursor", "pointer") | |
| .style("opacity", 1) | |
| .raise(); // move to front | |
| if (data2) d3.select(`#pointfull2_${step}`) | |
| .style("fill", color2) | |
| .style("stroke", "white") | |
| .style("stroke-width", `${stepStrokeWidth * 2}px`) | |
| .attr("d", triangleD3.size(192)) | |
| .style("opacity", 1) | |
| .style("cursor", "pointer") | |
| .raise(); // move to front | |
| if (data1 && data2) { | |
| d3.select(`#point_${step}`) | |
| .style("fill", color) | |
| .style("stroke", "white") | |
| .style("stroke-width", `${stepStrokeWidth}px`) | |
| .attr("d", circleD3.size(64)) | |
| .style("cursor", "pointer") | |
| .style("opacity", 1) | |
| .raise(); // move to front | |
| d3.select(`#point2_${step}`) | |
| .style("fill", color2) | |
| .style("stroke", "white") | |
| .style("stroke-width", `${stepStrokeWidth}px`) | |
| .attr("d", triangleD3.size(64)) | |
| .style("cursor", "pointer") | |
| .style("opacity", 1) | |
| .raise(); // move to front | |
| } | |
| $$("current_step_display_number").innerText = step; | |
| } | |
| highlightStep = (step, color = "#000", color2 = color) => { | |
| d3.select(`#pointfull_${step}`) | |
| .style("fill", color) | |
| .attr("d", circleD3.size(192)) | |
| .style("stroke", "white") | |
| .style("stroke-width", `${stepStrokeWidth * 2}px`) | |
| .style("cursor", "pointer") | |
| .style("opacity", 1) | |
| .raise(); // move to front | |
| if (data2) d3.select(`#pointfull2_${step}`) | |
| .style("fill", color2) | |
| .attr("d", triangleD3.size(192)) | |
| .style("stroke", "white") | |
| .style("stroke-width", `${stepStrokeWidth * 2}px`) | |
| .style("cursor", "pointer") | |
| .style("opacity", 1) | |
| .raise(); // move to front | |
| if (data1 && data2) { | |
| d3.select(`#point_${step}`) | |
| .style("fill", color) | |
| .attr("d", circleD3.size(24)) | |
| .style("stroke", "white") | |
| .style("stroke-width", `${stepStrokeWidth}px`) | |
| .style("cursor", "pointer") | |
| .style("opacity", 1) | |
| .raise(); // move to front | |
| d3.select(`#point2_${step}`) | |
| .style("fill", color2) | |
| .attr("d", triangleD3.size(24)) | |
| .style("stroke", "white") | |
| .style("stroke-width", `${stepStrokeWidth}px`) | |
| .style("cursor", "pointer") | |
| .style("opacity", 1) | |
| .raise(); // move to front | |
| } | |
| } | |
| resetStep = (step) => { | |
| step = `${step}` | |
| if (displaySteps.includes(step)) return; | |
| d3.select(`#pointfull_${step}`) | |
| .style("fill", "#61a3a9") | |
| .attr("d", circleD3.size(64)) | |
| .style("stroke-width", `${stepStrokeWidth}px`) | |
| .style("opacity", stepOpacity); | |
| if (data2) d3.select(`#pointfull2_${step}`) | |
| .style("fill", "#a961a3") | |
| .attr("d", triangleD3.size(64)) | |
| .style("stroke-width", `${stepStrokeWidth}px`) | |
| .style("opacity", stepOpacity); | |
| if (data1 && data2) { | |
| d3.select(`#point_${step}`) | |
| .style("fill", "#61a3a9") | |
| .attr("d", circleD3.size(24)) | |
| .style("stroke-width", `${stepStrokeWidth * 0.5}px`) | |
| .style("opacity", stepOpacity); | |
| d3.select(`#point2_${step}`) | |
| .style("fill", "#a961a3") | |
| .attr("d", triangleD3.size(24)) | |
| .style("stroke-width", `${stepStrokeWidth * 0.5}px`) | |
| .style("opacity", stepOpacity); | |
| } | |
| } | |
| charts.filter((c) => c.sync).forEach((c) => { | |
| if (c.step && c.color) highlightStep(c.step, c.color) | |
| }) | |
| let defaultStep = $$('value').value | |
| const color = (isMultiMode()) ? "#FFA500" : "#ff00ed" | |
| const color2 = (isMultiMode()) ? "#1C64F2" : "#ff00ed" | |
| highlightStep(defaultStep, color, color2) | |
| hoveredStep.push(defaultStep); | |
| } | |
| } | |
| const lineChange = (slow_mode = false) => { | |
| if (!isConfirmed()) return; | |
| const value = $$('range').value; | |
| let lValue = 999 - value; | |
| $$('value').value = lValue; | |
| // implement step change, only allowed step can be selected, or the nearest step will be selected | |
| if (samplingSteps) { | |
| const nearestStep = sampledSteps.reduce((a, b) => Math.abs(b - lValue) < Math.abs(a - lValue) ? b : a); | |
| if (nearestStep !== lValue) { | |
| $$('range').value = 999 - nearestStep; | |
| $$('value').value = nearestStep; | |
| lValue = nearestStep; | |
| } | |
| } | |
| if (lineChangeInterval) { | |
| clearInterval(lineChangeInterval); | |
| lineChangeInterval = null; | |
| } | |
| if (hoveredStep) { | |
| hoveredStep.forEach(s => resetStep(s)); | |
| hoveredStep = [] | |
| } | |
| if (hoverStep) { | |
| hoverStep(lValue); | |
| hoveredStep.push(lValue); | |
| } | |
| if ($$('titlepreview')) $$('titlepreview').textContent = 'Step: ' + lValue | |
| if ($$('titlepreview2')) $$('titlepreview2').textContent = 'Step: ' + lValue | |
| if (slow_mode === true) { | |
| slow_mode_count += 1; | |
| if (slow_mode_count < 4) { | |
| console.log('slow mode') | |
| return; | |
| } else { | |
| slow_mode_count = 0; | |
| } | |
| } | |
| if (Date.now() - lastDownload < 100) { | |
| console.log('too fast move, restart in 1 s') | |
| lineChangeInterval = setInterval(() => lineChange(), 1000) | |
| return | |
| } | |
| lastDownload = Date.now(); | |
| updatePreview(lValue) | |
| } | |
| const switchPreview = (multi = true) => { | |
| const $melpreview = $$('preview_container'); | |
| const $melpreview2 = $$('preview_container2'); | |
| if (multi) { | |
| $melpreview.classList.replace('w-[700px]', 'w-[320px]') | |
| $melpreview2.classList.remove('hidden') | |
| } else { | |
| $melpreview.classList.replace('w-[320px]', 'w-[700px]') | |
| $melpreview2.classList.add('hidden') | |
| } | |
| } | |
| const updatePreview = (sIndex = 999, reset = false) => { | |
| // TODO: update preview | |
| const color = (isMultiMode()) ? "#FFA500" : "#ff00ed" | |
| const color2 = (isMultiMode()) ? "#1C64F2" : "#ff00ed" | |
| let cards = [] | |
| const width = isMultiMode() ? 320 : 700; | |
| const height = 200; | |
| const indexMode = config.pathData[currentMode].indexMode ?? "key"; | |
| const previewCards = [ | |
| { | |
| id: '', display: true, | |
| svg: circleD3, | |
| csvSrc: () => { | |
| if (indexMode === "key") { | |
| return getCsvSrc(sIndex) | |
| } | |
| if (indexMode === "number") { | |
| return getCsvSrc(sIndex, currentSinger[0], currentTargetSinger[0], findCorrespondingSong(currentSinger[0])) | |
| } | |
| }, | |
| title: `Step: ${sIndex}`, | |
| titleColor: color, | |
| label: () => isMultiMode() ? `${mapToSongFunc(currentSong[0])}: ${mapToNameFunc(currentSinger[0])} -> ${mapToNameFunc(currentTargetSinger[0])}` : '' | |
| }, | |
| { | |
| id: '2', display: isMultiMode(), svg: triangleD3, | |
| csvSrc: () => { | |
| if (indexMode === "key") { | |
| return getCsvSrc(sIndex, currentSinger[currentSinger.length - 1], currentTargetSinger[currentTargetSinger.length - 1], currentSong[currentSong.length - 1]) | |
| } | |
| if (indexMode === "number") { | |
| return getCsvSrc(sIndex, currentSinger[currentSinger.length - 1], currentTargetSinger[currentTargetSinger.length - 1], findCorrespondingSong(currentSinger[currentSinger.length - 1])) | |
| } | |
| }, | |
| title: `Step: ${sIndex}`, | |
| titleColor: color2, | |
| label: () => `${mapToSongFunc(currentSong[currentSong.length - 1])}: ${mapToNameFunc(currentSinger[currentSinger.length - 1])} -> ${mapToNameFunc(currentTargetSinger[currentTargetSinger.length - 1])}` | |
| }, | |
| ] | |
| previewCards.forEach((card) => { | |
| if (!card.display) return; | |
| const { id, csvSrc, title, titleColor, label } = card; | |
| // generate div | |
| const refId = `preview${id}`; | |
| if (!reset) { | |
| // update audio | |
| if (changeVideoTimer[refId]) clearTimeout(changeVideoTimer[refId]) | |
| changeVideoTimer[refId] = setTimeout(() => { | |
| $$(`video${refId}`).src = csvSrc().replace('.csv', '.wav') | |
| }, 300); | |
| // update graph | |
| d3.csv(csvSrc(), (error, data) => { | |
| if (error) { console.error(error); return; } | |
| const arrayData = data.map(row => Object.values(row).map(d => +d)); | |
| charts = charts.filter((c) => c.id !== refId) | |
| // draw mel spectrogram | |
| plotMelSpectrogram(arrayData, refId, titleColor, close, width, height, true, hidePitch); | |
| }); | |
| return; | |
| } | |
| const div = getDiv(refId, csvSrc().replace('.csv', '.wav'), titleColor, title, label(), !!card.svg, false); | |
| const $container = $$(`preview_container${id}`); | |
| if ($container) { | |
| $container.innerText = ""; | |
| $container.appendChild(div); | |
| } | |
| // get data and bind div | |
| d3.csv(csvSrc(), (error, data) => { | |
| if (error) console.error(error); | |
| bindDiv(refId, data, card.svg ?? null, titleColor, () => { }, width, height) | |
| }); | |
| cards.push(div) | |
| }); | |
| switchPreview(isMultiMode()) | |
| } | |
| const plotMelSpectrogram = (melData, refId, title_color, close, width = 345, height = 200, sync = true, noPitch = false) => { | |
| const getColorMSE = (color1, color2) => { | |
| const c1 = d3.rgb(color1); | |
| const c2 = d3.rgb(color2); | |
| return Math.sqrt(d3.mean([c1.r - c2.r, c1.g - c2.g, c1.b - c2.b].map(d => d * d))) | |
| } | |
| const getDeltaEColor = (step) => { | |
| let r = 0.0; | |
| let g = 0.0; | |
| let b = 0.0; | |
| if (step <= 5) { | |
| step = 0; | |
| } | |
| if (step > 255) { | |
| step = 255; | |
| } | |
| if (step <= 10) { | |
| return [r, g, b]; | |
| } else if (step <= 11) { | |
| r = 0.75 - (step - 5) * 0.75 / 6.0; | |
| g = 0.375 - (step - 5) * 0.375 / 6.0; | |
| b = 1.0; | |
| } else if (step <= 19) { | |
| g = (step - 11) / 8.0; | |
| b = 1.0; | |
| } else if (step <= 27) { | |
| g = 1.0; | |
| b = 1.0 - (step - 19) / 8.0; | |
| } else if (step <= 37) { | |
| r = (step - 27) / 10.0; | |
| g = 1.0; | |
| } else if (step <= 47) { | |
| r = 1.0; | |
| g = 1.0 - (step - 37) * 0.5 / 10.0; | |
| } else if (step <= 255) { | |
| r = 1.0; | |
| g = 0.5 - (step - 47) * 0.5 / 208.0; | |
| } | |
| return [r * 255, g * 255, b * 255]; | |
| } | |
| // melData struct: | |
| // [ | |
| // [0.1, 0.2, 0.3, ...(x axis)], | |
| // [0.1, 0.2, 0.3, ...], | |
| // ...(y axis) | |
| // ] | |
| // compareMode: not draw pitch line | |
| let melData2 = null; | |
| if (melData.length === 2) { | |
| [melData, melData2] = melData; | |
| } | |
| // init | |
| const mmargin = { mtop: 5, mright: 40, mbottom: 35, mleft: 35 }; | |
| const mwidth = width - mmargin.mleft - mmargin.mright; | |
| const mheight = height - mmargin.mtop - mmargin.mbottom; | |
| let start = 0, end = melData[1].length, start_index = 0 | |
| // Create a Canvas element and append it to the container | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = (mwidth + mmargin.mleft + mmargin.mright); | |
| canvas.height = (mheight + mmargin.mtop + mmargin.mbottom); | |
| const context = canvas.getContext('2d'); | |
| document.querySelector(`#mel${refId}`).innerHTML = "" | |
| document.querySelector(`#mel${refId}`).appendChild(canvas); | |
| let x = d3.scaleLinear().range([mmargin.mleft, mmargin.mleft + mwidth]).domain([0, melData[1].length]); | |
| let y = d3.scaleLinear().range([mheight + mmargin.mtop, mmargin.mtop]).domain([0, melData.length - 1]); | |
| let color, color2, color_lock; | |
| if (!color_lock) { | |
| const min = d3.min(melData.filter((_, i) => i !== 0), array => d3.min(array)) | |
| const max = d3.max(melData.filter((_, i) => i !== 0), array => d3.max(array)) | |
| color = d3.scaleSequential(d3.interpolateViridis).domain([min, max]) | |
| if (melData2) { | |
| const min2 = d3.min(melData2.filter((_, i) => i !== 0), array => d3.min(array)) | |
| const max2 = d3.max(melData2.filter((_, i) => i !== 0), array => d3.max(array)) | |
| color2 = d3.scaleSequential(d3.interpolateViridis).domain([min2, max2]) | |
| color_lock = true | |
| } | |
| } | |
| const pitchYMax = 600; //d3.max(melData[0]); // TODO: make it to (step 0's top) * 1.5 | |
| const pitchDataY = d3.scaleLinear().range([mheight + mmargin.mtop, mmargin.mtop]).domain([0, pitchYMax]); | |
| // Draw the rectangles on the Canvas | |
| const drawRectangles = () => { | |
| let w = Math.ceil(mwidth / melData[0].length); | |
| if (w < 2) w = 2; // Make sure the rectangles are visible | |
| let h = Math.ceil(mheight / melData.length); | |
| // reset x axis after zoom | |
| x = d3.scaleLinear().range([mmargin.mleft, mmargin.mleft + mwidth]).domain([0, end - start]); | |
| // Draw the rectangles | |
| for (let i = 1; i < melData.length; i++) { | |
| for (let j = 0; j < melData[i].length; j++) { | |
| if (!showFrequency) { | |
| continue | |
| } | |
| if (showFrequency.length == 2) { | |
| [lower, upper] = showFrequency | |
| if (i > upper) continue; | |
| if (i < lower) continue; | |
| } | |
| if (melData2) { | |
| // calculate the difference using deltaE | |
| const color_1 = color(melData[i][j]); | |
| const color_2 = color2(melData2[i][j]); | |
| const deltaE = getColorMSE(color_1, color_2); | |
| const rgb = getDeltaEColor(deltaE); | |
| context.fillStyle = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; | |
| } else { | |
| context.fillStyle = color(melData[i][j]); | |
| } | |
| context.fillRect(x(j), y(i - 1) - h, w, h); | |
| } | |
| } | |
| if (!noPitch && !hidePitch) { | |
| // Draw pitch lines | |
| context.fillStyle = 'red'; | |
| context.strokeStyle = 'red'; | |
| context.lineWidth = 1; | |
| // get data from melData[0] | |
| const pitchData = melData[0]; | |
| const pitchDataLength = pitchData.length; | |
| for (let i = 0; i < pitchDataLength; i++) { | |
| const pitch = pitchData[i]; | |
| if (pitch === 0) continue; // skip 0 pitch | |
| context.fillRect(x(i), pitchDataY(pitch), w, h); | |
| } | |
| } | |
| // Color for the axis and labels | |
| context.fillStyle = darkMode ? 'white' : 'black'; | |
| context.strokeStyle = darkMode ? 'white' : 'black'; | |
| // Draw X-label text | |
| context.font = '12px Times New Roman'; | |
| context.fillText('Time (s)', canvas.width / 2 - 20, mheight + mmargin.mtop + mmargin.mbottom - 5); | |
| if (showFrequency) { | |
| // Draw Y-label text | |
| context.save(); | |
| context.rotate(-Math.PI / 2); | |
| context.font = '12px Times New Roman'; | |
| context.textAlign = 'center'; | |
| context.fillText('Channel', - canvas.height / 2 + 15, mmargin.mleft - 25); | |
| context.restore(); | |
| } | |
| const drawLine = (x1, y1, x2, y2) => { | |
| context.beginPath(); | |
| context.moveTo(x1, y1); | |
| context.lineTo(x2, y2); | |
| context.stroke(); | |
| } | |
| // Draw X-axis line | |
| drawLine(mmargin.mleft, mheight + mmargin.mtop, mwidth + mmargin.mleft, mheight + mmargin.mtop); | |
| if (showFrequency) { | |
| // Draw Y-axis line | |
| drawLine(mmargin.mleft, mmargin.mtop, mmargin.mleft, mheight + mmargin.mtop); | |
| } | |
| if (!noPitch && !hidePitch) { | |
| // Draw Y-label 2 | |
| context.save(); | |
| context.fillStyle = 'red'; | |
| context.strokeStyle = 'red'; | |
| context.textAlign = 'center'; | |
| context.rotate(Math.PI / 2) | |
| context.fillText('F0 (Hz)', canvas.height / 2 - 15, - canvas.width + 10); | |
| context.restore(); | |
| context.save(); | |
| context.fillStyle = 'red'; | |
| context.strokeStyle = 'red'; | |
| drawLine(mmargin.mleft + mwidth + 1, mmargin.mtop, mmargin.mleft + mwidth + 1, mheight + mmargin.mtop); | |
| context.restore(); | |
| } | |
| // Add X-axis labels | |
| const step = Math.round(melData[1].length / 5); | |
| const drawStep = []; | |
| for (let i = 0; i < melData[1].length; i++) { | |
| const text = (i + start_index) * 256 / 24000; | |
| if (start_index === 0) { | |
| if (text.toString().length > 3) continue; | |
| } else { | |
| if (i % step !== 0) continue; | |
| } | |
| drawStep.push(i); | |
| } | |
| const newDrawStep = []; | |
| if (drawStep.length > 5) { | |
| const step = Math.round(drawStep.length / 5); | |
| for (let i = 0; i < drawStep.length; i++) { | |
| if (i % step === 0) { | |
| newDrawStep.push(drawStep[i]); | |
| } | |
| } | |
| } else { | |
| newDrawStep.push(...drawStep); | |
| } | |
| for (let i = 0; i < newDrawStep.length; i++) { | |
| const text = (newDrawStep[i] + start_index) * 256 / 24000; | |
| const xPos = x(newDrawStep[i]); | |
| context.fillText(text.toFixed(2), xPos - 10, mheight + mmargin.mtop + 15); | |
| drawLine(xPos, mheight + mmargin.mtop, xPos, mheight + mmargin.mtop + 5); | |
| } | |
| // Add Y-axis labels | |
| if (showFrequency) { | |
| for (let i = 0; i <= melData.length; i++) { | |
| if (i % 20 !== 0) continue; | |
| const yPos = y(i); | |
| const yOff = i >= 100 ? -15 : i >= 10 ? -10 : -5; | |
| context.fillText(i, mmargin.mleft - 10 + yOff, yPos + 5); | |
| drawLine(mmargin.mleft - 5, yPos, mmargin.mleft, yPos) | |
| } | |
| } | |
| if (!noPitch && !hidePitch) { | |
| context.save(); | |
| context.fillStyle = 'red'; | |
| context.strokeStyle = 'red'; | |
| // Add Y-axis labels | |
| for (let i = 0; i <= pitchYMax; i++) { | |
| if (i % 100 !== 0) continue; | |
| const yPos = pitchDataY(i); | |
| context.fillText(i, mmargin.mleft + mwidth + 5, yPos + 5); | |
| drawLine(mmargin.mleft + mwidth, yPos, mmargin.mleft + mwidth + 5, yPos) | |
| } | |
| context.restore(); | |
| } | |
| } | |
| // Implement brushing manually | |
| let isBrushing = false; | |
| let startX, endX, lastX; | |
| // Initial drawing | |
| drawRectangles(); | |
| // Initial zooming | |
| canvas.addEventListener('mousedown', (e) => { | |
| isBrushing = true; | |
| startX = e.clientX - canvas.getBoundingClientRect().left; | |
| lastX = startX; | |
| }); | |
| canvas.addEventListener('mousemove', (e) => { | |
| if (isBrushing) { | |
| endX = e.clientX - canvas.getBoundingClientRect().left; | |
| // Draw the brush selection | |
| if (startX < endX) { | |
| // go right | |
| if (lastX > endX) { | |
| clear(); | |
| drawRectangles(); | |
| context.fillStyle = 'rgba(0, 0, 0, 0.4)'; | |
| context.fillRect(startX, 0, endX - startX, canvas.height); | |
| } else { | |
| context.fillStyle = 'rgba(0, 0, 0, 0.4)'; | |
| context.fillRect(lastX, 0, endX - lastX, canvas.height); | |
| } | |
| } else if (lastX < endX) { | |
| clear(); | |
| drawRectangles(); | |
| context.fillStyle = 'rgba(0, 0, 0, 0.4)'; | |
| context.fillRect(endX, 0, startX - endX, canvas.height); | |
| } else { | |
| context.fillStyle = 'rgba(0, 0, 0, 0.4)'; | |
| context.fillRect(endX, 0, lastX - endX, canvas.height); | |
| } | |
| lastX = endX; | |
| } | |
| }); | |
| canvas.addEventListener('mouseup', () => { | |
| isBrushing = false; | |
| if (!startX) return | |
| if (!endX) [startX, endX] = [startX - 30, startX + 30] | |
| // Reset the canvas to the original state | |
| clear() | |
| drawRectangles(); | |
| // Handle the selected range (startX to endX) | |
| // Clip the data to the selected range | |
| let start = Math.round(x.invert(startX)); | |
| let end = Math.round(x.invert(endX)); | |
| if (start === end) return; | |
| if (Math.abs(start - end) < 5) { | |
| console.log('Zoomed in too much!') | |
| return | |
| } | |
| // convert start and end into percentage | |
| start = start / melData[0].length; | |
| end = end / melData[0].length; | |
| if (sync) { | |
| charts.forEach(c => c.sync && c.zoom(start, end)) | |
| zoomStart = start | |
| zoomEnd = end | |
| } else { | |
| zoom(start, end) | |
| } | |
| }); | |
| // Reset the mel spectrogram when a new song is selected | |
| const reset = () => { | |
| let chart = charts.find(c => c.id === refId) | |
| if (!chart) { | |
| console.log('Reset fail: not find id', refId) | |
| return; | |
| } | |
| if (chart.sync) { | |
| zoomStart = 0; | |
| zoomEnd = 0; | |
| } | |
| startX = 0; | |
| endX = 0; | |
| lastX = 0; | |
| isBrushing = false; | |
| melData = chart.melData; | |
| if (chart.melData2) melData2 = chart.melData2; | |
| start = 0; | |
| end = melData[0].length; | |
| start_index = 0; | |
| clear(); | |
| drawRectangles(); | |
| } | |
| const clear = () => { | |
| context.clearRect(0, 0, canvas.width, canvas.height); | |
| } | |
| const zoom = (_start, _end) => { | |
| if (!_start || !_end) return; | |
| if (_start > _end) [_end, _start] = [_start, _end]; | |
| if (_start < 0) _start = 0; | |
| // convert start and end percentage into index | |
| _start = Math.round(_start * melData[0].length); | |
| _end = Math.round(_end * melData[0].length); | |
| if (_end - _start < 5) { | |
| return | |
| } | |
| start = _start | |
| end = _end | |
| melData = melData.map(row => row.slice(_start, _end)); | |
| if (melData2) { | |
| melData2 = melData2.map(row => row.slice(_start, _end)); | |
| } | |
| start_index += start; | |
| clear(); | |
| drawRectangles(); | |
| } | |
| const highlight = (time) => { | |
| const xPos = x(Math.round(time * 24000 / 256)); | |
| if (xPos < 0 || xPos > canvas.width - mmargin.mright) return; | |
| context.fillStyle = 'red'; | |
| context.fillRect(xPos, mmargin.mtop, 1.5, canvas.height - mmargin.mtop - mmargin.mbottom); | |
| } | |
| const chart = { | |
| id: refId, | |
| step: Number(refId.split('_')[1]), | |
| sync: sync, | |
| selected: false, | |
| canvas: canvas, | |
| x: x, | |
| y: y, | |
| color: title_color, | |
| melData: melData, | |
| melData2: melData2 ?? null, | |
| reset: reset, | |
| zoom: zoom, | |
| highlight: highlight, | |
| close: close | |
| }; | |
| charts.push(chart); | |
| } | |
| const drawCurve = (id, width, height) => { | |
| const singer = currentSinger[0]; | |
| const song = currentSong[0]; | |
| const target = currentTargetSinger[0]; | |
| const csvSrc = getMetricsSrc(singer, target, song); | |
| d3.csv(csvSrc, (error, data) => { | |
| if (error) { console.error(error); return; } | |
| const $container = $$(`metrics${id}`); | |
| const marginTop = 10; | |
| const marginRight = 50; | |
| const marginBottom = 30; | |
| const marginLeft = 40; | |
| const keysWithLable = [ | |
| "Dembed (↑)", | |
| "F0CORR (↑)", | |
| "FAD (↓)", | |
| "F0RMSE (↓)", | |
| "MCD (↓)", | |
| ]; | |
| const keys = keysWithLable.map((k) => k.split(' ')[0]); | |
| const values = keys.map((key) => data.map((d) => { | |
| const v = +d[key]; | |
| // const step = d.step; | |
| if (isNaN(v)) return null; | |
| // if (v < 0) return 0.1; | |
| return autoLog(v); | |
| })); | |
| const color = d3.scaleOrdinal() | |
| .domain(keysWithLable) | |
| .range(metricsColors); | |
| const $swatch = Swatches(color, { columns: "100px", marginLeft: 10 }); | |
| $container.appendChild($swatch); | |
| const squeezeArray = (array) => { | |
| let result = []; | |
| for (let i = 0; i < array.length; i++) { | |
| result = result.concat(array[i]); | |
| } | |
| return result; | |
| }; | |
| const squeezedValues = squeezeArray(values); | |
| const x = d3.scaleLog() | |
| .clamp(true).domain([0.5, d3.max(data, (d) => +d.step)]) | |
| .rangeRound([width - marginRight, marginLeft]) | |
| .base(2).nice(); | |
| const y = d3.scaleLinear() | |
| .domain([0, d3.max(squeezedValues)]) | |
| .rangeRound([height - marginBottom, marginTop]); | |
| const line = d3.line() | |
| .defined((y, i) => !isNaN(data[i].step) && !isNaN(y) && x(data[i].step) !== Infinity) | |
| .x((d, i) => x(data[i].step)) | |
| .y(y); | |
| // Function to find the closest point to the mouse | |
| const findClosestStep = (mouseX) => { | |
| let minDist = Infinity; | |
| let findClosestStep = null; | |
| data.forEach((d, i) => { | |
| const xVal = x(+d.step); | |
| const dist = Math.abs(mouseX - xVal); | |
| if (dist < minDist) { | |
| minDist = dist; | |
| findClosestStep = { x: xVal, step: d.step }; | |
| } | |
| }); | |
| return findClosestStep; | |
| } | |
| const svg = d3.select($container).append("svg") | |
| .attr("style", `color: ${darkMode ? 'white' : 'black'};`) | |
| .attr("width", width) | |
| .attr("height", height) | |
| .attr("viewBox", [0, 0, width, height]) | |
| .attr("style", "max-width: 100%; height: auto;"); | |
| svg.append("g") | |
| .attr("transform", `translate(0,${height - marginBottom})`) | |
| .call(d3.axisBottom(x).ticks(width / 80).tickFormat((d) => d >= 1 ? (d === 1024 ? 1000 : d) : 0)) | |
| .call((g) => g.select(".domain").attr("stroke", "currentColor")) | |
| .call((g) => g.append("text") | |
| .attr("x", width / 2) | |
| .attr("y", marginBottom - 4) | |
| .attr("fill", "currentColor") | |
| .attr("text-anchor", "center") | |
| .text("Step (log)")); | |
| const yAxis = svg.append("g") | |
| .attr("transform", `translate(${marginLeft},0)`) | |
| .call(d3.axisLeft(y).tickValues(d3.ticks(0.01, d3.max(squeezedValues), 10)).tickFormat((d) => d > 1 ? Math.pow(4, d).toFixed(0) : d.toFixed(1))) | |
| .call((g) => g.select(".domain").attr("stroke", "currentColor")) | |
| .call(g => g.append("text") | |
| .attr("x", -height / 2 + 30) | |
| .attr("y", -marginLeft + 10) | |
| .attr("fill", "currentColor") | |
| .attr("transform", "rotate(-90)") | |
| .text("Score")) | |
| .call(g => g.selectAll(".tick line").clone() | |
| .attr("x2", width - marginLeft - marginRight) | |
| .attr("stroke-opacity", 0.1)) | |
| svg.append("g") | |
| .attr("fill", "none") | |
| .attr("stroke-width", 1.5) | |
| .attr("stroke-linejoin", "round") | |
| .attr("stroke-linecap", "round") | |
| .selectAll() | |
| .data(values) | |
| .enter() | |
| .append("path") | |
| .attr("stroke", (d, i) => metricsColors[i]) | |
| .attr("d", line); | |
| svg.selectAll("text").attr("fill", "currentColor") | |
| svg.selectAll("line").attr("stroke", "currentColor") | |
| // Append a group element for the dot | |
| const dotGroup = svg.append("g") | |
| .style("display", "none"); | |
| dotGroup | |
| .append("g") | |
| .selectAll() | |
| .data(keys) | |
| .enter() | |
| .append("circle") | |
| .attr("r", 4) | |
| .attr("id", (d) => `dot_${d}`) | |
| .attr("fill", (d, i) => metricsColors[i]); | |
| dotGroup | |
| .append("g") | |
| .selectAll() | |
| .data(keys) | |
| .enter() | |
| .append("text") | |
| .attr("id", (d) => `dot_text_${d}`) | |
| .attr("fill", (d, i) => metricsColors[i]) | |
| .attr("font-size", 11) | |
| .attr("font-weight", 600) | |
| .attr("text-anchor", "start") | |
| .attr("stroke", "white") | |
| .attr("stroke-width", 3) | |
| .attr("font-family", "sans-serif") | |
| .attr("paint-order", "stroke") | |
| // .attr("alignment-baseline", "center") | |
| .text((d) => d); | |
| // Mouse move event listener | |
| svg.on("mousemove", function (event) { | |
| const [mouseX, mouseY] = d3.mouse(this) | |
| const closestStep = findClosestStep(mouseX); | |
| let yVals = []; | |
| if (closestStep) { | |
| // add a debunce to avoid too many update | |
| $range.value = 999 - closestStep.step; | |
| lineChange(false); | |
| dotGroup.style("display", null); | |
| keys.forEach((key, i) => { | |
| const yVal = +data.find((d) => d.step === closestStep.step)[key]; | |
| const drawYVal = y(autoLog(yVal)); | |
| yVals.push({ | |
| key: key, | |
| y: drawYVal, | |
| text: yVal.toFixed(2), | |
| difference: Math.abs(drawYVal - mouseY), | |
| }); | |
| d3.select(`#dot_${key}`) | |
| .attr("cx", closestStep.x) | |
| .attr("cy", drawYVal) | |
| .raise(); | |
| }); | |
| } | |
| // Draw the number (only the closest step) | |
| yVals = yVals.sort((a, b) => a.difference - b.difference); | |
| yVals.forEach((v, i) => { | |
| if (i === 0) { | |
| d3.select(`#dot_text_${v.key}`) | |
| .attr("x", closestStep.x + 10) | |
| .attr("y", v.y + 3) | |
| .text(v.text) | |
| .raise(); | |
| } else { | |
| d3.select(`#dot_text_${v.key}`).attr("x", -100).attr("y", -100); | |
| } | |
| }); | |
| }); | |
| // Mouse out event listener | |
| // svg.on("mouseout", function () { | |
| // dotGroup.style("display", "none"); | |
| // }); | |
| return svg.node(); | |
| }); | |
| } | |
| // ==== UI interaction functions ==== | |
| const resetDisplay = (clear = true, replot = true, keep_stepmap = false) => { | |
| // update choosed options | |
| updateSelect("mode_id", availableMode, currentMode); | |
| const pic_index = config.picTypes.indexOf(currentShowingPic) ?? 3; | |
| if (userMode == "basic") { | |
| initSelect("pic_id", config.picTypes, pic_index, 4); | |
| } else { | |
| initSelect("pic_id", config.picTypes, pic_index, -1); | |
| } | |
| // updateSelect("pic_id", , ); | |
| updateOptions("sourcesinger_id", currentSinger); | |
| updateOptions("target_id", currentTargetSinger); | |
| updateOptions("song_id", currentSong); | |
| if (!isConfirmed()) return; | |
| if (clear) { | |
| $$("mel_card_container").innerHTML = ""; | |
| $$("tips").style = 'display: block'; | |
| displaySteps = []; | |
| charts = []; | |
| usedColorList = []; | |
| } | |
| if (userMode === "basic") { | |
| if (currentMode === "Step Comparison" && !$$("grid_table")) loadGrid() | |
| $$("add_preview").classList.add("hidden"); | |
| } else { | |
| $$("add_preview").classList.remove("hidden"); | |
| } | |
| if (isSelectable()) { | |
| if (displaySteps.length == 0) { | |
| selectStep(-1) | |
| console.log('Single step mode init') | |
| } | |
| $$("add_preview").classList.add("hidden"); | |
| } else { | |
| $$("add_preview").classList.remove("hidden"); | |
| } | |
| if (keep_stepmap) return; | |
| $$("dataviz_axisZoom").innerHTML = ""; | |
| if (currentShowingPic === "encoded_step") { | |
| drawStepMap(`${baseLink}/data/mp_all/step_encoder_output.csv`); | |
| } else if (isMultiMode()) { | |
| const indexMode = (config.pathData[currentMode].indexMode ?? "key") === "number"; | |
| drawStepMap( | |
| getStepSrc( | |
| currentSinger[0], | |
| currentTargetSinger[0], | |
| indexMode ? findCorrespondingSong(currentSinger[0]) : currentSong[0] | |
| ), | |
| getStepSrc( | |
| currentSinger[currentSinger.length - 1], | |
| currentTargetSinger[currentTargetSinger.length - 1], | |
| indexMode ? findCorrespondingSong(currentSinger[currentSinger.length - 1]) : currentSong[currentSong.length - 1] | |
| )); | |
| } else { | |
| drawStepMap(getStepSrc()); | |
| } | |
| if (isMultiMode()) { | |
| $$("control_panel").classList.remove("gap-0.5"); | |
| $$("control_panel").classList.add("gap-1", "my-1"); | |
| } else { | |
| $$("control_panel").classList.remove("gap-1", "my-1"); | |
| $$("control_panel").classList.add("gap-0.5"); | |
| } | |
| updateHistogram(); | |
| $$("dataviz_axisZoom").addEventListener("mouseleave", () => { | |
| const step = 999 - $range.value; | |
| hoverStep(step) | |
| hoveredStep.push(step); | |
| }) | |
| $$("dataviz_axisZoom").addEventListener("mouseenter", () => { | |
| if (hoveredStep) { | |
| hoveredStep.forEach(step => resetStep(step)); | |
| hoveredStep = [] | |
| } | |
| }) | |
| if (!replot) return; | |
| $$("range").value = 0; | |
| $$("value").value = 999; | |
| updatePreview(999, true); | |
| } | |
| const showBestCase = () => { | |
| // find the best evaluation data by currentHistogram | |
| const selected = currentHistogram[0]["name"] ?? ""; | |
| if (selected === "") return; | |
| const bestCase = config.evaluation_data.find((d) => d.best.includes(selected)); | |
| // print the best one | |
| currentMode = "Metric Comparison"; | |
| refreshOptions(true, bestCase.sourcesinger); | |
| currentSinger = [bestCase.sourcesinger]; | |
| currentSong = [bestCase.song]; | |
| currentTargetSinger = [bestCase.target]; | |
| lockOptions(); | |
| resetDisplay(); | |
| } | |
| const selectStep = (sIndex) => { | |
| const width = 290; | |
| const height = 200; | |
| if (!isConfirmed()) { | |
| alert('Please select data first') | |
| return; | |
| } | |
| sIndex = `${sIndex}`; // ! conver to string | |
| // if (displaySteps.length === 0) { // clear the pin area | |
| // $$("mel_card_container").innerHTML = ""; | |
| // $$("tips").style = 'display: none' | |
| // } | |
| if (sIndex !== "-1" && displaySteps.indexOf(sIndex) !== -1) { | |
| charts.filter(c => c.id.endsWith(sIndex))?.forEach(c => c.close()); // close the card if the func called again | |
| return | |
| } | |
| if (`${sIndex}`.indexOf('ph') !== -1) { // if it is placeholder | |
| charts.filter(c => c.id.endsWith(sIndex))?.forEach(c => c.close()); // remove the placeholder if exist | |
| // generate a placeholder | |
| const div = document.createElement('div'); | |
| div.className = 'mel_card'; | |
| div.id = `mel_${sIndex}`; | |
| div.style.width = `${width}px`; | |
| div.style.height = `${height}px`; | |
| if (currentMode === 'Step Comparison' && userMode === 'basic') { | |
| $mel.insertBefore(div, $mel.lastChild); | |
| } else { | |
| $mel.appendChild(div); | |
| } | |
| charts.push({ // add a close function | |
| id: `_${sIndex}`, close: () => { | |
| charts = charts.filter(c => c.id !== `_${sIndex}`); | |
| displaySteps = displaySteps.filter(d => d !== sIndex) | |
| div.remove() | |
| } | |
| }); | |
| displaySteps.push(sIndex) // add to displaySteps | |
| return | |
| } | |
| if (displaySteps.length >= 3 && currentMode === "Step Comparison" && userMode === "basic") { | |
| // up to 2 is allowed in basic mode | |
| // other will be ignored | |
| console.log('basic mode only allows 2 steps') | |
| addComparison(sIndex) | |
| return | |
| } | |
| if (displaySteps.length >= 3 || (isSelectable() && sIndex !== "-1")) { | |
| // when slots full, only 3 cards can be displayed: more selected will be seen as hovered | |
| // for unselectable mode, none card will be displayed, only hovered | |
| // change hovered | |
| $$('range').value = 999 - sIndex; | |
| lineChange(); | |
| return | |
| } | |
| let color; | |
| displaySteps.push(sIndex) | |
| if (!isNaN(parseInt(sIndex)) && parseInt(sIndex) >= 0) { | |
| color = config.colorList.map(c => c).filter(c => !usedColorList.includes(c))[0] ?? "#000"; | |
| usedColorList.push(color); | |
| console.log('color', color) | |
| highlightStep(sIndex, color) // color needed | |
| } else { | |
| color = "#000" | |
| } | |
| let cards = [] | |
| if (!isMultiMode() && !(currentMode === "Step Comparison" && userMode === "basic")) { | |
| // Function to handle the mouse down event | |
| const dragStart = (e) => { | |
| currentCard = e.target | |
| setTimeout(() => { | |
| currentCard.classList.add('border-dashed', 'border-2', 'border-blue-500') | |
| }) | |
| } | |
| setTimeout(() => { | |
| const card = cards[0]; | |
| if (!card) return; | |
| card.addEventListener('dragstart', dragStart); | |
| const $mel_canvas = $$(`mel_${sIndex}`); | |
| $mel_canvas.addEventListener('mouseenter', () => { | |
| card.setAttribute('draggable', false) | |
| }); | |
| $mel_canvas.addEventListener('mouseout', () => { | |
| card.setAttribute('draggable', true) | |
| }); | |
| }) | |
| } | |
| const close = (skip = false) => { | |
| if (isMultiMode()) { | |
| alert('Card in multi mode cannot be closed.') | |
| return; | |
| } | |
| if (currentMode === 'Metric Comparison') { | |
| alert('Card in Metric Comparison mode cannot be closed.') | |
| return; | |
| } | |
| // color set to default | |
| if (currentMode === 'Step Comparison' && userMode === 'basic') { | |
| gridComparison = gridComparison.filter((d) => d !== sIndex) | |
| if (!skip) { | |
| if ($$("mel_ph1")) { | |
| gridComparison.push("ph2") | |
| selectStep("ph2") | |
| } else { | |
| gridComparison.push("ph1") | |
| selectStep("ph1") | |
| } | |
| } | |
| } | |
| displaySteps = displaySteps.filter((d) => d !== sIndex) | |
| usedColorList = usedColorList.filter(c => c !== color); | |
| resetStep(sIndex) | |
| charts = charts.filter(c => c.id !== `_${sIndex}` && c.id !== `2_${sIndex}`); | |
| cards.forEach((c) => c.remove()) | |
| } | |
| const [types] = getMultipleLable(); | |
| const indexMode = config.pathData[currentMode].indexMode ?? "key"; | |
| let index | |
| if (indexMode === "number") { | |
| // get index from config.pathData, then get corresponding song name | |
| const singer = currentSinger[currentSinger.length - 1]; | |
| const song = currentSong[0]; | |
| const songs = config.pathData[currentMode].data.map((d) => d.pathMap[singer]?.songs).flat().filter((s) => s !== undefined); | |
| index = songs.indexOf(song) | |
| } | |
| const referenceCards = [ | |
| { | |
| id: '3', display: isMultiMode() && enableReference, | |
| svg: isMultiMode() && enableReference && ((types == 'song' || types == 'sourcesinger') ? circleD3 : null), | |
| color: isMultiMode() && enableReference && ((types == 'song' || types == 'sourcesinger') ? "#FFA500" : (darkMode ? "#fff" : "#000")), | |
| csvSrc: () => { | |
| if (indexMode === "key") { | |
| return getReferenceCsvSrc(currentSinger[0], currentSong[0]) | |
| } | |
| if (indexMode === "number") { | |
| const correspondingSong = config.pathData[currentMode].data.find((d) => Object.keys(d.pathMap).includes(currentSinger[0])).pathMap[currentSinger[0]].songs[index] | |
| return getReferenceCsvSrc(currentSinger[0], correspondingSong) | |
| } | |
| }, | |
| title: () => 'Source', | |
| label: () => `${mapToSongFunc(currentSong[0])}: ${mapToNameFunc(currentSinger[0])}` | |
| }, | |
| { | |
| id: '4', display: isMultiMode() && enableReference && (types == 'song' || types == 'sourcesinger'), | |
| svg: triangleD3, | |
| color: "#1C64F2", | |
| csvSrc: () => { | |
| if (indexMode === "key") { | |
| return getReferenceCsvSrc(currentSinger[currentSinger.length - 1], currentSong[currentSong.length - 1]) | |
| } | |
| if (indexMode === "number") { | |
| const correspondingSong = config.pathData[currentMode].data.find((d) => Object.keys(d.pathMap).includes(currentSinger[currentSinger.length - 1])).pathMap[currentSinger[currentSinger.length - 1]].songs[index] | |
| return getReferenceCsvSrc(currentSinger[currentSinger.length - 1], correspondingSong) | |
| } | |
| }, | |
| title: () => 'Source', | |
| label: () => `${mapToSongFunc(currentSong[currentSong.length - 1])}: ${mapToNameFunc(currentSinger[currentSinger.length - 1])}` | |
| }, | |
| { | |
| id: '5', display: isMultiMode() && enableReference, | |
| svg: isMultiMode() && enableReference && ((types == 'target') ? circleD3 : null), | |
| color: isMultiMode() && enableReference && ((types == 'target') ? "#FFA500" : (darkMode ? "#fff" : "#000")), | |
| csvSrc: () => getTargetReferenceCsvSrc(currentTargetSinger[0], currentSong[0]), | |
| title: () => 'Target', | |
| label: () => mapToNameFunc(currentTargetSinger[0]) | |
| }, | |
| { | |
| id: '6', display: isMultiMode() && enableReference && (types == 'target'), | |
| svg: triangleD3, | |
| color: "#1C64F2", | |
| csvSrc: () => getTargetReferenceCsvSrc(currentTargetSinger[currentTargetSinger.length - 1], currentSong[currentSong.length - 1]), | |
| title: () => 'Target', | |
| label: () => mapToNameFunc(currentTargetSinger[currentTargetSinger.length - 1]) | |
| }, | |
| { | |
| id: '', display: !isMultiMode() && currentMode !== 'Metric Comparison' && sIndex !== "-1", | |
| color: color, | |
| svg: circleD3, | |
| csvSrc: () => getCsvSrc(sIndex), | |
| title: () => `Step: ${sIndex}`, | |
| label: () => '' | |
| }, | |
| { | |
| id: '2', display: !isMultiMode() && currentMode === 'Metric Comparison', | |
| div: (refId) => { | |
| const div = document.createElement('div'); | |
| div.innerHTML = `<div class="card p-2 w-full flex flex-col col-span-3 gap-1" id="display${refId}">` + | |
| `<div class="flex items-center">` + | |
| `<div class="flex flex-col ml-1 mr-1">` + | |
| `<h5 class="text-base font-bold tracking-tight mb-0 text-[black] line-clamp-1 dark:text-[white]" id="title${refId}">Metric Curve over Diffusion Step</h5>` + | |
| `</div>` + | |
| `</div>` + | |
| `<div class="mx-auto h-[250px] dark:text-[white]" id="metrics${refId}"></div>` + | |
| `</div>`; | |
| return div.firstChild; | |
| }, | |
| }, | |
| { | |
| id: '1', display: currentMode === 'Step Comparison' && userMode === 'basic' && sIndex === "-1", | |
| div: (refId) => { | |
| const div = document.createElement('div'); | |
| div.innerHTML = `<div class="card min-w-[305px] p-2 w-full flex flex-col gap-1" id="display${refId}">` + | |
| `<div class="flex items-center">` + | |
| `<div class="flex flex-col mx-auto">` + | |
| `<h5 class="text-base font-bold tracking-tight mb-0 text-[black] line-clamp-1 dark:text-[white]" id="title${refId}">Step Comparison Matrix</h5>` + | |
| `</div>` + | |
| `</div>` + | |
| `<div class="flex flex-row">` + | |
| `<div class="mx-auto grow-0 flex justify-center items-center w-[250px] h-[250px] dark:text-[white]" id="grid_table"></div>` + | |
| `<div class="grow-0 flex w-[50px]" id="grid_table_legend"></div>` + | |
| `</div>` + | |
| `</div>`; | |
| return div.firstChild; | |
| }, | |
| } | |
| ]; | |
| referenceCards.forEach((card) => { | |
| if (!card.display) return; | |
| const { id, div, csvSrc, title, label, color } = card; | |
| // generate div | |
| const refId = `${id}_${sIndex}`; | |
| if (div) { | |
| $mel.appendChild(div(refId)); | |
| if (currentMode === 'Metric Comparison') { | |
| // metrics bind | |
| drawCurve(refId, 900, 220); | |
| } | |
| return; | |
| } | |
| const divContent = getDiv(refId, csvSrc().replace('.csv', '.wav'), color, title(), label(), !!card.svg); | |
| // insert the div into the last two slots in $mel | |
| if (currentMode === 'Step Comparison' && userMode === 'basic') { | |
| $mel.insertBefore(divContent, $mel.lastChild); | |
| } else { | |
| $mel.appendChild(divContent); | |
| } | |
| cards.push(divContent) | |
| // get data and bind div | |
| downloadingLock.push(refId) | |
| d3.csv(csvSrc(), (error, data) => { | |
| downloadingLock = downloadingLock.filter((d) => d !== refId) | |
| if (error) console.error(error); | |
| bindDiv(refId, data, card.svg ?? null, color, close, width, height) | |
| }); | |
| }); | |
| } | |
| const checkCompare = () => { | |
| // check if there are 2 or more charts selected | |
| // if so, pop a compare window showing the difference of mel spectrogram | |
| const selectedCharts = charts.filter((c) => c.selected); | |
| if (selectedCharts.length < 2) return; | |
| if (selectedCharts.length >= 3) { | |
| alert('Please select 2 steps to compare.'); | |
| return; | |
| } | |
| selectedCharts.forEach((c) => { | |
| c.selected = false; | |
| $$(`select${c.id}`).checked = false; | |
| }); | |
| const compareId = compareNum++; | |
| const selectedData = selectedCharts.map((c) => c.melData); | |
| // pop a window | |
| const div = document.createElement('div'); | |
| div.innerHTML = `<div class="card flex flex-col absolute cursor-move z-10" id="compare${compareId}" draggable="true">` + | |
| `<div class="flex items-center">` + | |
| // `<input id="select${sIndex}" type="checkbox" value="" class="checkbox mb-2 mr-1">`+ | |
| `<h5 class="card-title">Mel Spectrogram Difference</h5>` + | |
| `<a class="btn-sec ml-auto h-9 w-14" id="refreshcompare${compareId}">${refreshIcon}</a>` + | |
| `<a class="btn-sec ml-2 h-9 w-14" id="closecompare${compareId}">${closeIcon}</a>` + | |
| `</div>` + | |
| `<div class="flex flex-row">` + | |
| `<div class="mx-auto min-w-[355px]" id="melcompare${compareId}"></div>` + | |
| `<div class="w-[26px]"><img src="img/difference_bar.jpg"></div>` + | |
| `</div>` + | |
| `</div>`; | |
| const domNode = div.firstChild; | |
| $$('step_preview').appendChild(domNode) | |
| // move to center of screen | |
| domNode.style.left = `${(window.innerWidth - 345) / 2}px`; | |
| domNode.style.top = `${window.scrollY + (window.innerHeight - 200) / 2}px`; | |
| const $mel_canvas = $$(`melcompare${compareId}`); | |
| $mel_canvas.addEventListener('mouseenter', () => { | |
| domNode.setAttribute('draggable', false) | |
| }); | |
| $mel_canvas.addEventListener('mouseout', () => { | |
| domNode.setAttribute('draggable', true) | |
| }); | |
| let offsetX, offsetY, isDragging = false; | |
| const dragStart = (e) => { | |
| // let windows follow cursor | |
| e.preventDefault() | |
| isDragging = true; | |
| offsetX = e.clientX - domNode.getBoundingClientRect().left; | |
| offsetY = e.clientY - domNode.getBoundingClientRect().top; | |
| domNode.style.zIndex = 2; // Bring the element to the front | |
| domNode.style.cursor = "grabbing"; | |
| } | |
| const drag = (e) => { | |
| if (isDragging) { | |
| const x = e.clientX - offsetX; | |
| const y = e.clientY - offsetY; | |
| // Ensure the draggable div stays within the viewport | |
| if (x >= 0 && x + domNode.offsetWidth <= window.innerWidth) { | |
| domNode.style.left = x + "px"; | |
| } | |
| if (y >= 0 && y + domNode.offsetHeight <= window.innerHeight) { | |
| domNode.style.top = (window.scrollY + y) + "px"; | |
| } | |
| } | |
| } | |
| const endDrag = () => { | |
| isDragging = false; | |
| domNode.style.zIndex = 1; // Restore the element's original z-index | |
| domNode.style.cursor = "move"; | |
| } | |
| domNode.addEventListener('dragstart', dragStart); | |
| document.addEventListener("mousemove", drag); | |
| document.addEventListener("mouseup", endDrag); | |
| const close = () => { | |
| $$(`compare${compareId}`).remove() | |
| } | |
| $$(`closecompare${compareId}`).addEventListener('click', () => { | |
| console.log(`closecompare${compareId}`, 'clicked') | |
| close() | |
| }) | |
| // plot the difference | |
| plotMelSpectrogram(selectedData, `compare${compareId}`, '#000', close, 345, 200, false, true); | |
| $$(`refreshcompare${compareId}`).addEventListener('click', () => { | |
| console.log(`refreshcompare${compareId}`, 'clicked') | |
| charts.find((c) => c.id === `compare${compareId}`)?.reset() | |
| }) | |
| } | |
| const preloadingFile = (src) => { | |
| // preloading data files for faster loading | |
| const img = new Image(); | |
| img.src = src; | |
| img.onload = () => { | |
| console.log('Preloaded:', src) | |
| } | |
| img.onerror = () => { | |
| console.log('Preloaded:', src) | |
| } | |
| } | |
| const preloading = () => { | |
| preloadingFile('img/difference_bar.jpg'); | |
| // loading all steps | |
| // Array.from({ length: 1000 }, (_, i) => i).forEach((i) => { | |
| // preloadingFile(getCsvSrc(i)); | |
| // preloadingFile(getCsvSrc(i).replace('.csv', '.wav')); | |
| // }); | |
| } |