/** * 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`
${domain.map(value => { const label = `${format(value)}`; return htl.html`
${label}
`; })}
`; return htl.html`
${domain.map(value => htl.html`${format(value)}`)}
`; } 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 ? `
` : '
') + `
` + `` + (svg ? `` : '') + `
` + `
${title}
` + `
${subtitle}
` + `
` + (card ? `` : '') + `
` + `
${loadingDiv}
` + `` + `
`; 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", "
"); 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 = `Please select`; } 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 = `
` + `
` + `
` + `
Metric Curve over Diffusion Step
` + `
` + `
` + `
` + `
`; return div.firstChild; }, }, { id: '1', display: currentMode === 'Step Comparison' && userMode === 'basic' && sIndex === "-1", div: (refId) => { const div = document.createElement('div'); div.innerHTML = `
` + `
` + `
` + `
Step Comparison Matrix
` + `
` + `
` + `
` + `
` + `
` + `
` + `
`; 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 = `
` + `
` + // ``+ `
Mel Spectrogram Difference
` + `${refreshIcon}` + `${closeIcon}` + `
` + `
` + `
` + `
` + `
` + `
`; 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')); // }); }