`;
}
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 = `
`;
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'));
// });
}