diff --git a/static/scripts/barChart.mjs b/static/scripts/barChart.mjs index fdff01b..48be9d4 100644 --- a/static/scripts/barChart.mjs +++ b/static/scripts/barChart.mjs @@ -1,8 +1,16 @@ -import { filterData, cleanDataString, specialOrders, showStudyModal, defaultColors } from "./dataUtility.mjs"; +import { + filterData, + cleanDataString, + specialOrders, + showStudyModal, + defaultColors, +} from "./dataUtility.mjs"; // The available categories passed by the server for the bar charts const categories = $("body").data("filter-categories"); -const questionCirclePath = $("#toggle-menu-container").data("question-circle-path"); +const questionCirclePath = $("#toggle-menu-container").data( + "question-circle-path" +); const explanations = $("body").data("explanations"); /* @@ -15,7 +23,9 @@ const explanations = $("body").data("explanations"); // Function to create Modal HTML for a given category and label function createModalHTML(category, label) { - const activeData = filterData(JSON.parse(window.sessionStorage.getItem('filters'))); + const activeData = filterData( + JSON.parse(window.sessionStorage.getItem("filters")) + ); const fullCategory = getFullCategory(category); const tableHTML = ` @@ -32,11 +42,18 @@ function createModalHTML(category, label) { - ${activeData.filter(entry => entry[fullCategory].toString().includes(label)).map(elem => - ` + ${activeData + .filter((entry) => entry[fullCategory].toString().includes(label)) + .map( + (elem) => + ` - Info cirle for this row + Info cirle for this row ${elem["ID"]} ${elem["Main Author"]} @@ -46,7 +63,8 @@ function createModalHTML(category, label) { ${elem["Gesture"]} ` - ).join("")} + ) + .join("")} `; @@ -64,8 +82,10 @@ function createModalHTML(category, label) { // Creates all bar charts based on the data passed by the server and the currently active filters (categories and value filters) function createBarCharts() { $("#chartsContainer").empty(); // Clear the charts container - const filters = JSON.parse(window.sessionStorage.getItem('filters')); - const activeCategories = filters.categoryFilters.map(cat => getFullCategory(cat)).filter(cat => cat !== undefined); + const filters = JSON.parse(window.sessionStorage.getItem("filters")); + const activeCategories = filters.categoryFilters + .map((cat) => getFullCategory(cat)) + .filter((cat) => cat !== undefined); // Remove "Main Author" category if it is in the active categories const firstAuthorIndex = activeCategories.indexOf("Main Author"); if (firstAuthorIndex !== -1) { @@ -78,27 +98,31 @@ function createBarCharts() { if (activeData.length === 0) { $("#hiddenChartsMessage").hide(); $("#hiddenChartsList").empty(); - $("#chartsContainer").html("

No studies available for the selected sidebar filters. Please select some of the criteria from the sidebar at the right.

"); + $("#chartsContainer").html( + "

No studies available for the selected sidebar filters. Please select some of the criteria from the sidebar at the right.

" + ); return; - }; + } if (activeCategories.length === 0) { // Reset the hidden charts message and list $("#hiddenChartsMessage").hide(); $("#hiddenChartsList").empty(); - $("#chartsContainer").html("

No studies found for the selected filters. Please select some of the criteria from the toggle menu at the top.

"); + $("#chartsContainer").html( + "

No studies found for the selected filters. Please select some of the criteria from the toggle menu at the top.

" + ); return; } // Create a bar chart for each active category for (const category of activeCategories) { // Only provide the data needed for the current category - const barData = activeData.map(entry => entry[category]); + const barData = activeData.map((entry) => entry[category]); // Create the bar chart for the current category createBarChart(barData, category); } - + // Update the visibility of the charts based on the maximum number of bars set in the dropdown menu updateVisibility(); } @@ -126,7 +150,7 @@ function createBarChart(barData, category) { chartTitleElement.className = "chart-title"; chartTitleElement.innerHTML = `
${chartTitle}
- Information about the category of this chart + Information about the category of this chart `; const chartContainer = document.createElement("div"); @@ -137,7 +161,6 @@ function createBarChart(barData, category) { canvas.id = "chart-" + category.replaceAll(" ", "€"); chartContainer.appendChild(canvas); - // Append the chart container to the element for all charts $("#chartsContainer").append(chartWrapper); chartWrapper.appendChild(chartTitleElement); @@ -165,7 +188,7 @@ function createBarChartData(barData, category) { // The keys of the occurrences will be the labels for the chart const labels = Object.keys(occurrences).sort((a, b) => { // Check if the labels are all convertable to numbers - if (Object.keys(occurrences).every(key => !isNaN(key))) { + if (Object.keys(occurrences).every((key) => !isNaN(key))) { return parseFloat(a) - parseFloat(b); } @@ -176,13 +199,17 @@ function createBarChartData(barData, category) { return { labels: labels, - datasets: [{ - data: labels.map(label => occurrences[label]), - backgroundColor: labels.map((_, index) => defaultColors[index % defaultColors.length]), - barThickness: "flex", - maxBarThickness: 50, - }] - } + datasets: [ + { + data: labels.map((label) => occurrences[label]), + backgroundColor: labels.map( + (_, index) => defaultColors[index % defaultColors.length] + ), + barThickness: "flex", + maxBarThickness: 50, + }, + ], + }; } // Creating the chart options based on the category @@ -196,43 +223,45 @@ function createChartOptions(category) { }, tooltip: { callbacks: { - label: function(tooltipItem) { + label: function (tooltipItem) { return `${tooltipItem.raw} studies. Click for list!`; - } - } + }, + }, }, }, scales: { x: { ticks: { autoSkip: false, - callback: function(value, index, ticks) { + callback: function (value, index, ticks) { const label = this.getLabelForValue(value); // Limit the number of characters displayed on the x-axis - return label.length > 20 ? label.slice(0, 20) + '...' : label; + return label.length > 20 ? label.slice(0, 20) + "..." : label; }, maxRotation: 40, padding: 2, - } + }, }, - y: { - beginAtZero: true, - ticks: { + y: { + beginAtZero: true, + ticks: { stepSize: 1, - } - } + }, + }, }, - onClick: function(event, elements, chart) { + onClick: function (event, elements, chart) { if (elements.length === 0) return; const label = chart.data.labels[elements[0].index]; const tableHTML = createModalHTML(category, label); - $("#modal-header-info").text(`Studies for ${category.split("_").pop()} filtered by "${label}"`); + $("#modal-header-info").text( + `Studies for ${category.split("_").pop()} filtered by "${label}"` + ); $("#rowDetailsContainerBarCharts").html(tableHTML); $("#table-modal").modal("show"); }, - } + }; } // Finds the full category name based on the short name provided by the checkbox @@ -249,8 +278,10 @@ function getFullCategory(category) { function updateVisibility() { const maxBars = parseInt($("#maxBarsDropdown").val()); - const filters = JSON.parse(window.sessionStorage.getItem('filters')); - const activeCategories = filters.categoryFilters.map(cat => getFullCategory(cat)).filter(cat => cat !== undefined); + const filters = JSON.parse(window.sessionStorage.getItem("filters")); + const activeCategories = filters.categoryFilters + .map((cat) => getFullCategory(cat)) + .filter((cat) => cat !== undefined); // Remove "Main Author" category if it is in the active categories const firstAuthorIndex = activeCategories.indexOf("Main Author"); if (firstAuthorIndex !== -1) { @@ -264,7 +295,9 @@ function updateVisibility() { // Hide all charts that exceed the maximum number of bars for (const category of activeCategories) { const chart = Chart.getChart("chart-" + category.replaceAll(" ", "€")); - const chartWrapper = document.getElementById("chart-wrapper-" + category.replaceAll(" ", "€")); + const chartWrapper = document.getElementById( + "chart-wrapper-" + category.replaceAll(" ", "€") + ); // if there are no labels, dont show the chart if (chart.data.labels.length === 0) { @@ -278,13 +311,19 @@ function updateVisibility() { chartWrapper.style.display = "none"; // Add a message to the hidden charts list - $("#hiddenChartsList").append(`
  • ${category.split("_").pop()}: ${chart.data.labels.length} bars (exceeds threshold of ${maxBars})
  • `); + $("#hiddenChartsList").append( + `
  • ${category + .split("_") + .pop()}: ${ + chart.data.labels.length + } bars (exceeds threshold of ${maxBars})
  • ` + ); } else { // Show the chart if it does not exceed the maximum number of bars chartWrapper.style.display = "flex"; } } - + // If there is at least one hidden chart, show the hidden charts message if ($("#hiddenChartsList").children().length > 0) { $("#hiddenChartsMessage").show(); @@ -299,40 +338,40 @@ function updateVisibility() { - Modals are set up to show study details when a info-circle is clicked - When values in the sidebar are changed, the charts are updated accordingly */ -$(document).ready(function() { +$(document).ready(function () { // Create bar charts for each category that is currently selected createBarCharts(); // When the maximum number of displayed bars is changed, create the view again - $("#maxBarsDropdown").on("change", function() { + $("#maxBarsDropdown").on("change", function () { // Remove all existing charts and hidden messages updateVisibility(); - }) + }); // Add an event listener to the every category checkbox - $(".form-check-input").on("change", function () { + $(".form-check-input").on("change", function () { createBarCharts(); }); // Add an event listener to each value filter checkbox - $(".value-filter").on("change", function() { + $(".value-filter").on("change", function () { createBarCharts(); }); // Add an event listener to the exclusive filters button - $(".exclusive-filter").on("click", function() { + $(".exclusive-filter").on("click", function () { createBarCharts(); }); // Use Event Delegation to handle clicks on the info-circle images - $("#rowDetailsContainerBarCharts").on("click", ".info-circle", function(e) { + $("#rowDetailsContainerBarCharts").on("click", ".info-circle", function (e) { const id = e.target.getAttribute("data-id"); showStudyModal(id); }); - $(".range-slider").each(function() { - this.noUiSlider.on("end", function() { + $(".range-slider").each(function () { + this.noUiSlider.on("end", function () { createBarCharts(); }); }); -}); \ No newline at end of file +}); diff --git a/static/scripts/similarity.mjs b/static/scripts/similarity.mjs index 3fe1d92..cf082e0 100644 --- a/static/scripts/similarity.mjs +++ b/static/scripts/similarity.mjs @@ -1,16 +1,30 @@ -import { filterData, getDataEntry, showStudyModal, sortNodesByCategory } from "./dataUtility.mjs"; -import { createLegend, highlightNode, removeHighlighting, drawNode } from "./d3DrawingUtility.mjs"; +import { + filterData, + getDataEntry, + showStudyModal, + sortNodesByCategory, +} from "./dataUtility.mjs"; +import { + createLegend, + highlightNode, + removeHighlighting, + drawNode, +} from "./d3DrawingUtility.mjs"; // Load the similarity data from the HTML element const similarityData = $("#graphContainer").data("similarity"); // Load the categories of the dropdown menu const filterCategories = $("body").data("filter-categories"); -const excluded_categories = $("#categoryDropdownContainer").data("excluded-categories"); +const excluded_categories = $("#categoryDropdownContainer").data( + "excluded-categories" +); const infoCirclePath = $("#graphContainer").data("info-circle-path"); // Define some texts for the tooltips -const abstractTooltip = "This visualization shows semantic similarity between paper abstracts. Similarities were calculated using Google Gemini embeddings (gemini-embedding-exp-03-07) with cosine similarity and then z-standardized. Values above 0 indicate above-average similarity (0=mean, 1=one standard deviation above mean). Higher thresholds show only the most similar papers."; -const databaseTooltip = "This visualization shows similarity between studies based on features extracted from the database. Features were normalized and similarity was calculated based on their values."; +const abstractTooltip = + "This visualization shows semantic similarity between paper abstracts. Similarities were calculated using Google Gemini embeddings (gemini-embedding-exp-03-07) with cosine similarity and then z-standardized. Values above 0 indicate above-average similarity (0=mean, 1=one standard deviation above mean). Higher thresholds show only the most similar papers."; +const databaseTooltip = + "This visualization shows similarity between studies based on features extracted from the database. Features were normalized and similarity was calculated based on their values."; const abstractStudyIDs = similarityData["abstract_study_ids"]; const abstractMatrix = similarityData["abstract_matrix"]; @@ -18,15 +32,19 @@ const databaseStudyIDs = similarityData["database_study_ids"]; const databaseMatrix = similarityData["database_matrix"]; // Set the default similarity type from session storage or fallback to "database" -let similarityType = window.sessionStorage.getItem("similarityType") || "database"; +let similarityType = + window.sessionStorage.getItem("similarityType") || "database"; $(`input[value='${similarityType}']`).prop("checked", true); // Add tooltip text based on the selected similarity type -$("#thresholdInfoIcon").attr("title", similarityType === "abstract" ? abstractTooltip : databaseTooltip); +$("#thresholdInfoIcon").attr( + "title", + similarityType === "abstract" ? abstractTooltip : databaseTooltip +); // Populate the color nodes dropdown menu -filterCategories.forEach(category => { - if (excluded_categories.includes(category)) return; +filterCategories.forEach((category) => { + if (excluded_categories.includes(category)) return; const shortCategory = category.split("_").pop(); $("#similarityColorCategory").append( `` @@ -34,40 +52,45 @@ filterCategories.forEach(category => { }); let colorCategory = window.sessionStorage.getItem("colorCategory") || ""; -$(`#similarityColorCategory > option[value="${colorCategory}"]`).prop("selected", true); +$(`#similarityColorCategory > option[value="${colorCategory}"]`).prop( + "selected", + true +); -let similarityThreshold = parseFloat(window.sessionStorage.getItem("similarityThreshold")) || 1; +let similarityThreshold = + parseFloat(window.sessionStorage.getItem("similarityThreshold")) || 1; // Set the displayed threshold value in the UI $("#thresholdValue").text(similarityThreshold.toFixed(2)); // Create the slider const slider = document.getElementById("thresholdSlider"); -noUiSlider.create(slider, { - start: [similarityThreshold], // Default to 1 steddev - connect: [true, false], // Connect to the left - range: { - 'min': -3, // Typically -3 standard deviations - 'max': 3 // Typically +3 standard deviations - }, - step: 0.1, - tooltips: [true], // Show tooltip - format: { - to: function (value) { - return value.toFixed(2); +noUiSlider + .create(slider, { + start: [similarityThreshold], // Default to 1 steddev + connect: [true, false], // Connect to the left + range: { + min: -3, // Typically -3 standard deviations + max: 3, // Typically +3 standard deviations }, - from: function (value) { + step: 0.1, + tooltips: [true], // Show tooltip + format: { + to: function (value) { + return value.toFixed(2); + }, + from: function (value) { return parseFloat(value); - } - }, -}) -.on("change", function(values, handle) { - similarityThreshold = parseFloat(values[handle]); - // Update the threshold text - $("#thresholdValue").text(similarityThreshold.toFixed(2)); - // Draw the graph with the new threshold - drawGraph(similarityThreshold); - window.sessionStorage.setItem("similarityThreshold", similarityThreshold); -}); + }, + }, + }) + .on("change", function (values, handle) { + similarityThreshold = parseFloat(values[handle]); + // Update the threshold text + $("#thresholdValue").text(similarityThreshold.toFixed(2)); + // Draw the graph with the new threshold + drawGraph(similarityThreshold); + window.sessionStorage.setItem("similarityThreshold", similarityThreshold); + }); /* Section for showing the modal @@ -76,12 +99,14 @@ noUiSlider.create(slider, { */ function openNetworkDetails(nodeID, links) { const nodeData = getDataEntry(nodeID); - const connectedNodes = links.filter(link => link.sourceID === nodeID || link.targetID === nodeID).map(link => { - return { - id: link.sourceID === nodeID ? link.targetID : link.sourceID, - similarity: link.value, - } - }); + const connectedNodes = links + .filter((link) => link.sourceID === nodeID || link.targetID === nodeID) + .map((link) => { + return { + id: link.sourceID === nodeID ? link.targetID : link.sourceID, + similarity: link.value, + }; + }); // Sort connected nodes by similarity connectedNodes.sort((a, b) => b.similarity - a.similarity); @@ -123,7 +148,7 @@ function openNetworkDetails(nodeID, links) { ${nodeData["Main Author"]} ${nodeData["Year"]} ${nodeData["Location"]} - ${nodeData['Input Body Part']} + ${nodeData["Input Body Part"]} ${nodeData["Gesture"]} @@ -150,12 +175,16 @@ function openNetworkDetails(nodeID, links) { - ${connectedNodes.length > 0 ? - connectedNodes.map(node => { - const nodeData = getDataEntry(node.id); - return ` + ${ + connectedNodes.length > 0 + ? connectedNodes + .map((node) => { + const nodeData = getDataEntry(node.id); + return ` - Info cirle for this row + Info cirle for this row ${nodeData["ID"]} ${nodeData["Main Author"]} ${nodeData["Year"]} @@ -164,16 +193,18 @@ function openNetworkDetails(nodeID, links) { ${nodeData["Gesture"]} ${node.similarity.toFixed(2)} - ` - }).join("") : - `No connected studies found.`} + `; + }) + .join("") + : `No connected studies found.` + } - ` + `; // Add information about the total number of connections - const totalConnectionsHTML = `

    Total connections: ${connectedNodes.length}

    ` - + const totalConnectionsHTML = `

    Total connections: ${connectedNodes.length}

    `; + // Append to the connections container $("#connectionsContainer").empty(); $("#connectionsContainer").html(sourceHTML); @@ -201,47 +232,53 @@ function findSimilarStudies(links) { function generateGraphData(threshold) { const { studyIDs, similarityMatrix } = getCurrentSimilarityData(); const links = []; - + // Sort the nodes by category if a category is selected - const {sortedNodes, colorScale} = sortNodesByCategory(studyIDs, $("#similarityColorCategory").val()); - + const { sortedNodes, colorScale } = sortNodesByCategory( + studyIDs, + $("#similarityColorCategory").val() + ); + // Only check each pair once (i < j) for (let i = 0; i < sortedNodes.length; i++) { for (let j = i + 1; j < sortedNodes.length; j++) { const nodeA = sortedNodes[i]; const nodeB = sortedNodes[j]; - - const similarity = similarityMatrix[parseInt(nodeA) - 1][parseInt(nodeB) - 1]; + + const similarity = + similarityMatrix[parseInt(nodeA) - 1][parseInt(nodeB) - 1]; if (similarity && similarity >= threshold) { links.push({ sourceID: nodeA, targetID: nodeB, - value: similarity + value: similarity, }); } } } - + return { sortedNodes, links, colorScale }; -}; +} // Gets the current similarity data based on the selected type and the selected filters so only active studies are included, returns an object with the study IDs and the similarity matrix like this: {studyIDs: [...], similarityMatrix: [[...]]} function getCurrentSimilarityData() { const filters = JSON.parse(window.sessionStorage.getItem("filters")); // Get the IDs of all data studies that are currently active based on the selected filters - const activeDataIDs = filterData(filters).map(item => item["ID"].toString()); - + const activeDataIDs = filterData(filters).map((item) => + item["ID"].toString() + ); + if (similarityType === "abstract") { return { - studyIDs: abstractStudyIDs.filter(id => activeDataIDs.includes(id)), - similarityMatrix: abstractMatrix + studyIDs: abstractStudyIDs.filter((id) => activeDataIDs.includes(id)), + similarityMatrix: abstractMatrix, }; } else if (similarityType === "database") { return { - studyIDs: databaseStudyIDs.filter(id => activeDataIDs.includes(id)), - similarityMatrix: databaseMatrix + studyIDs: databaseStudyIDs.filter((id) => activeDataIDs.includes(id)), + similarityMatrix: databaseMatrix, }; - }; + } } /* @@ -261,345 +298,591 @@ function drawGraph(threshold) { $("#graphContainer").height("auto"); $("#legend").empty(); - // TODO: change to slider value const { sortedNodes, links, colorScale } = generateGraphData(threshold); const nodes = [...sortedNodes]; + const lengthLongestLabel = + nodes.length === 0 + ? 0 + : nodes.reduce((max, nodeID) => { + const author = getDataEntry(nodeID, "Main Author") || ""; + return Math.max(max, author.length); + }, 0); + // If there are no nodes, do not draw the graph if (nodes.length === 0) { - $("#graphContainer").append("

    No studies available for the selected sidebar filters. Please select some of the criteria from the sidebar at the right.

    "); + $("#graphContainer").append( + "

    No studies available for the selected sidebar filters. Please select some of the criteria from the sidebar at the right.

    " + ); return; } // Determine graph dimensions const useULayout = nodes.length > 50; // Use U-Layout for larger graphs - const height = useULayout ? 800 : 500; + // Breakpoint for vertical alignment of axes + const alignVertically = window.innerWidth <= 750; - $("#graphContainer").height(height); + // Define constants for the layout + const margin = alignVertically + ? { top: 10, right: 5, bottom: 10, left: 5 } + : { top: 10, right: 20, bottom: 10, left: 20 }; + + const nodeSpacing = 15; + const height = alignVertically + ? Math.max( + $("#graphContainer").width() * 1.5, + (useULayout ? nodes.length / 2 : nodes.length) * nodeSpacing + ) + : useULayout + ? (9 / 16) * $("#graphContainer").width() + 15 * lengthLongestLabel + : (9 / 16) * $("#graphContainer").width(); // Create SVG with calculated dimensions - const svg = d3.select("#graphContainer").append("svg") + const svg = d3 + .select("#graphContainer") + .append("svg") .attr("width", "100%") - .attr("height", height) .attr("viewBox", `0 0 ${$("#graphContainer").width()} ${height}`); - if (useULayout) { - drawULayout(svg, {nodes, links}, colorScale); - } else { - // Draw links, nodes, and labels for standard layout - drawStandardLayout(svg, {nodes, links}, colorScale); - } + // Choose layout based on number of nodes + const layoutFunction = useULayout ? drawULayout : drawStandardLayout; + layoutFunction(svg, margin, { nodes, links }, colorScale, !alignVertically); // Draw the legend - createLegend(nodes, colorScale, $("#similarityColorCategory").val(), $("#legend")); + createLegend( + nodes, + colorScale, + $("#similarityColorCategory").val(), + $("#legend") + ); findSimilarStudies(links); } -function drawULayout(container, graphData, colorScale) { +function drawULayout( + container, + margin, + graphData, + colorScale, + alignHorizontal +) { const { nodes, links } = graphData; - // Define constants for the layout - const margin = { top: 20, right: 20, bottom: 20, left: 20 }; const height = parseInt($("svg").height()) - margin.top - margin.bottom; const width = parseInt($("svg").width()) - margin.left - margin.right; - const nodeRadius = Math.floor(width / 150); - const topAxisHeight = height / 4; - const axisMiddle = height / 2; + const firstAxisPos = alignHorizontal ? height / 4 : width / 4; + const axisMiddle = alignHorizontal ? height / 2 : width / 2; + + // Base node radius + const nodeRadius = alignHorizontal ? 5.5 : 7; // Split the nodes into two groups based on their IDs - const topNodes = nodes.filter(node => nodes.indexOf(node) <= (nodes.length / 2)); - const bottomNodes = nodes.filter(node => nodes.indexOf(node) > (nodes.length / 2)); + const firstNodes = nodes.filter( + (node) => nodes.indexOf(node) <= nodes.length / 2 + ); + const secondNodes = nodes.filter( + (node) => nodes.indexOf(node) > nodes.length / 2 + ); + + const responsiveFontSize = getComputedStyle(document.body) + .getPropertyValue("--resp-font-ticks") + .trim(); // Create a scale for the top nodes - const topScale = d3.scalePoint() - .domain(topNodes) - .rangeRound([0, width]); + const firstScale = d3 + .scalePoint() + .domain(firstNodes) + .rangeRound([0, alignHorizontal ? width : height]); // Create a scale for the bottom nodes - const bottomScale = d3.scalePoint() - .domain(bottomNodes) - .rangeRound([0, width]); + const secondScale = d3 + .scalePoint() + .domain(secondNodes) + .rangeRound([0, alignHorizontal ? width : height]); // Create an arc generator for the nodes - const arc = d3.arc() - .innerRadius(0) - .outerRadius(nodeRadius); + const arc = d3.arc().innerRadius(0).outerRadius(nodeRadius); // Create the top axis for the nodes - const topAxis = d3.axisTop(topScale) - .tickValues(topNodes) - .tickFormat(d => "") - .tickSize(0) - .tickPadding(-4); + const firstAxis = alignHorizontal + ? d3 + .axisTop(firstScale) + .tickValues(firstNodes) + .tickFormat((d) => "") + .tickSize(0) + .tickPadding(-4) + : d3 + .axisLeft(firstScale) + .tickValues(firstNodes) + .tickFormat((d) => "") + .tickSize(0) + .tickPadding(8); // Create the bottom axis for the nodes - const bottomAxis = d3.axisBottom(bottomScale) - .tickValues(bottomNodes) - .tickFormat(d => "") - .tickSize(0) - .tickPadding(-4); + const secondAxis = alignHorizontal + ? d3 + .axisBottom(secondScale) + .tickValues(secondNodes) + .tickFormat((d) => "") + .tickSize(0) + .tickPadding(-4) + : d3 + .axisRight(secondScale) + .tickValues(secondNodes) + .tickFormat((d) => "") + .tickSize(0) + .tickPadding(8); + + // Append group element for zooming + const g = container + .append("g") + .attr("transform", `translate (${margin.left}, ${margin.top})`); // Draw the top axis - container.append("g") - .attr("transform", `translate(${margin.left}, ${topAxisHeight + margin.top})`) // Position the axis at the top + g.append("g") + .attr( + "transform", + alignHorizontal + ? `translate(0, ${firstAxisPos})` + : `translate(${firstAxisPos}, 0)` + ) // Position the first Axis .attr("class", "top-axis") - .call(topAxis); + .call(firstAxis); // Draw the bottom axis - container.append("g") - .attr("transform", `translate(${margin.left}, ${height - topAxisHeight})`) // Position the axis at the bottom + g.append("g") + .attr( + "transform", + alignHorizontal + ? `translate(0, ${3 * firstAxisPos})` + : `translate(${3 * firstAxisPos}, 0)` + ) // Position the axis at the bottom .attr("class", "bottom-axis") - .call(bottomAxis); + .call(secondAxis); // Add info circle and label to top axis ticks - container.selectAll(".top-axis text") - .html(d => `${formatTickLabel(d)}`); + container.selectAll(".top-axis text").html((d) => { + const label = formatTickLabel(d); + const infoCircle = ''; + const labelSpan = `${label}`; + + return alignHorizontal + ? `${labelSpan} ${infoCircle}` + : `${infoCircle} ${labelSpan}`; + }); // Add info circle and label to bottom axis ticks - container.selectAll(".bottom-axis text") - .html(d => `${formatTickLabel(d)}`); + container.selectAll(".bottom-axis text").html((d) => { + const label = formatTickLabel(d); + const infoCircle = ''; + const labelSpan = `${label}`; + + return alignHorizontal + ? `${infoCircle} ${labelSpan}` + : `${labelSpan} ${infoCircle}`; + }); - // Rotate the axis labels for better readability and adjust the position - container.select(".top-axis") - .selectAll("text") - .attr("text-anchor", "start") - .attr("transform", "rotate(-90)") - .attr("dx", "3em"); - - // Rotate the axis labels for better readability - container.select(".bottom-axis") - .selectAll("text") - .attr("text-anchor", "end") - .attr("transform", "rotate(-90)") - .attr("dx", "-3em"); // Adjust label position + // Rotate the axis labels for better readability and adjust the position for bigger screens + if (alignHorizontal) { + container + .select(".top-axis") + .selectAll("text") + .attr("text-anchor", "start") + .attr("transform", "rotate(-90)") + .attr("dx", "2em"); + } + + // Rotate the axis labels for better readability for bigger screens + if (alignHorizontal) { + container + .select(".bottom-axis") + .selectAll("text") + .attr("text-anchor", "end") + .attr("transform", "rotate(-90)") + .attr("dx", "-2em"); // Adjust label position + } // Add click event to the axis ticks, so that clicking on a node opens the study modal d3.selectAll(".tick") - .on("click", function(event, d) { + .on("click", function (event, d) { showStudyModal(d); }) .style("cursor", "pointer") - .style("font-size", "1.2em") + .style("font-size", responsiveFontSize) .style("user-select", "none"); // Change cursor to pointer for better UX // Create a group for the links - const linkGroup = container.append("g") - .attr("class", "links") - .attr("transform", `translate(${margin.left}, ${margin.top})`); + const linkGroup = g.append("g").attr("class", "links"); // Create a group for the top and bottom nodes - const nodeGroup = container.append("g") - .attr("class", "nodes") - .attr("transform", `translate(${margin.left}, ${margin.top})`); + const nodeGroup = g.append("g").attr("class", "nodes"); // Draw the top nodes and add click and hover events - nodeGroup.selectAll(".node") - .data(topNodes, d => d) + nodeGroup + .selectAll(".node") + .data(firstNodes, (d) => d) .enter() .append("g") .attr("class", "node") - .attr("transform", d => `translate(${topScale(d)}, ${topAxisHeight})`) - .each(function(d) { + .attr("transform", (d) => + alignHorizontal + ? `translate(${firstScale(d)}, ${firstAxisPos})` + : `translate(${firstAxisPos}, ${firstScale(d)})` + ) + .each(function (d) { drawNode(d3.select(this), colorCategory, arc, colorScale); }) - .on("click", function(event, d) { + .on("click", function (event, d) { openNetworkDetails(d, links); }) - .on("mouseover", function(event, d) { + .on("mouseover", function (event, d) { highlightNode(d, nodeRadius); }) .on("mouseout", () => removeHighlighting(nodeRadius)); // Draw the bottom nodes and add click and hover events - nodeGroup.selectAll(".node") - .data(bottomNodes, d => d) + nodeGroup + .selectAll(".node") + .data(secondNodes, (d) => d) .enter() .append("g") .attr("class", "node") - .attr("transform", d => `translate(${bottomScale(d)}, ${height - topAxisHeight - margin.bottom})`) - .each(function(d) { + .attr("transform", (d) => + alignHorizontal + ? `translate(${secondScale(d)}, ${3 * firstAxisPos})` + : `translate(${3 * firstAxisPos}, ${secondScale(d)})` + ) + .each(function (d) { drawNode(d3.select(this), colorCategory, arc, colorScale); }) - .on("click", function(event, d) { + .on("click", function (event, d) { openNetworkDetails(d, links); }) - .on("mouseover", function(event, d) { + .on("mouseover", function (event, d) { highlightNode(d, nodeRadius); }) .on("mouseout", () => removeHighlighting(nodeRadius)); // Draw the links - linkGroup.selectAll(".link") + linkGroup + .selectAll(".link") .data(links) .enter() .append("path") .attr("class", "link") - .attr("d", d => { + .attr("d", (d) => { // Check on which axis the source and target nodes are located - const isSourceTop = topNodes.includes(d.sourceID); - const isTargetTop = topNodes.includes(d.targetID); - - // Retrieve the correct x position based on the axis - const sourceX = isSourceTop ? topScale(d.sourceID): bottomScale(d.sourceID); - const targetX = isTargetTop ? topScale(d.targetID): bottomScale(d.targetID); - - // Retrieve the correct y position based on the axis - const sourceY = isSourceTop ? topAxisHeight: height - topAxisHeight - margin.bottom; - const targetY = isTargetTop ? topAxisHeight: height - topAxisHeight - margin.bottom; - - if (sourceX === targetX && isSourceTop) { - return `M ${sourceX} ${sourceY} Q ${(sourceX + targetX) / 2} ${axisMiddle + margin.bottom}, ${targetX} ${targetY}`; - } else if (sourceX === targetX && !isSourceTop) { - return `M ${sourceX} ${sourceY} Q ${(sourceX + targetX) / 2} ${axisMiddle - margin.top}, ${targetX} ${targetY}`; - } else { + const isSourceFirst = firstNodes.includes(d.sourceID); + const isTargetFirst = firstNodes.includes(d.targetID); + + // Get scale based on node position (first or second group) + const sourceScale = isSourceFirst ? firstScale : secondScale; + const targetScale = isTargetFirst ? firstScale : secondScale; + + // Get positions based on orientation and scale + const sourceX = alignHorizontal + ? sourceScale(d.sourceID) + : isSourceFirst + ? firstAxisPos + : 3 * firstAxisPos; + const targetX = alignHorizontal + ? targetScale(d.targetID) + : isTargetFirst + ? firstAxisPos + : 3 * firstAxisPos; + const sourceY = alignHorizontal + ? isSourceFirst + ? firstAxisPos + : 3 * firstAxisPos + : sourceScale(d.sourceID); + const targetY = alignHorizontal + ? isTargetFirst + ? firstAxisPos + : 3 * firstAxisPos + : targetScale(d.targetID); + + // Create the path + if (alignHorizontal) { + // When the nodes are on the same horizontal line + if (sourceY === targetY) { + const midPointY = + axisMiddle + (isSourceFirst ? margin.top : -margin.top) * 15; + return `M ${sourceX} ${sourceY} Q ${ + (sourceX + targetX) / 2 + } ${midPointY}, ${targetX} ${targetY}`; + } + // Normal case - nodes on different horizontal lines return `M ${sourceX} ${sourceY} C ${sourceX} ${axisMiddle}, ${targetX} ${axisMiddle}, ${targetX} ${targetY}`; + } else { + // When the nodes are on the same vertical line + if (sourceX === targetX) { + const midPointX = + axisMiddle + (isSourceFirst ? margin.left : -margin.right) * 20; + return `M ${sourceX} ${sourceY} Q ${midPointX} ${ + (sourceY + targetY) / 2 + }, ${targetX} ${targetY}`; + } + // Normal case - nodes on different vertical lines + return `M ${sourceX} ${sourceY} C ${axisMiddle} ${sourceY}, ${axisMiddle} ${targetY}, ${targetX} ${targetY}`; } }); // Add tooltips to the links - linkGroup.selectAll(".link") + linkGroup + .selectAll(".link") .append("title") - .text(d => `${similarityType === "database" ? "Database" : "Abstract"} Similarity: ${d.value.toFixed(2)} between [${d.sourceID}] and [${d.targetID}]`); + .text( + (d) => + `${ + similarityType === "database" ? "Database" : "Abstract" + } Similarity: ${d.value.toFixed(2)} between [${d.sourceID}] and [${ + d.targetID + }]` + ); + + // Add zoom for smaller screen widths + const mobileQuery = window.matchMedia("(max-width: 850px)"); + + if (mobileQuery.matches) { + const zoom = d3 + .zoom() + .scaleExtent([0.8, 10]) + .on("zoom", ({ transform }) => { + // On mobile allow panning/zooming + const x = margin.left + transform.x; + const y = margin.top + transform.y; + const k = transform.k; + g.attr("transform", `translate(${x}, ${y}) scale(${k})`); + }); + + container.call(zoom).call(zoom.transform, d3.zoomIdentity); + } } // Draws the standard layout for the similarity graph -function drawStandardLayout(container, graphData, colorScale) { +function drawStandardLayout( + container, + margin, + graphData, + colorScale, + alignHorizontal +) { const { nodes, links } = graphData; // Define constants for the layout - const margin = { top: 20, right: 20, bottom: 20, left: 20 }; const height = parseInt($("svg").height()) - margin.top - margin.bottom; const width = parseInt($("svg").width()) - margin.left - margin.right; - const axisHeight = height / 2; - const nodeRadius = Math.floor(width / 150); + const axisMiddle = alignHorizontal ? height / 2 : width / 2; + + // Base node radius + const nodeRadius = 7; // Create a scale for the node positions - const xScale = d3.scalePoint() + const nodeScale = d3 + .scalePoint() .domain(nodes) - .rangeRound([0, width]); + .rangeRound([0, alignHorizontal ? width : height]); // Create an axis for the nodes to be displayed horizontally - const axis = d3.axisBottom(xScale) - .tickValues(nodes) - .tickFormat(d => "") - .tickSize(0) - .tickPadding(-4); + const axis = alignHorizontal + ? d3 + .axisBottom(nodeScale) + .tickValues(nodes) + .tickFormat((d) => "") + .tickSize(0) + .tickPadding(-4) + : d3 + .axisLeft(nodeScale) + .tickFormat((d) => "") + .tickSize(0) + .tickPadding(8); + + const responsiveFontSize = getComputedStyle(document.body) + .getPropertyValue("--resp-font-ticks") + .trim(); + + // Append group element for zooming + const g = container.append("g"); // Draw the axis - container.append("g") + g.append("g") .attr("class", "axis") - .attr("transform", `translate(${margin.left}, ${axisHeight + margin.top})`) // Position the axis in the middle of the graph + .attr( + "transform", + alignHorizontal + ? `translate(0, ${axisMiddle})` + : `translate(${axisMiddle}, 0)` + ) // Position the axis in the middle of the graph .call(axis); - d3.selectAll("text") - .html(d => `${formatTickLabel(d)}`); + d3.selectAll("text").html( + (d) => + `${formatTickLabel( + d + )}` + ); // Rotate the axis labels for better readability and adjust the position - d3.selectAll("text") - .attr("text-anchor", "end") - .attr("transform", "rotate(-90)") - .attr("dx", "-2em") - .style("font-size", "1.2em") - .style("user-select", "none"); - + if (alignHorizontal) { + d3.selectAll("text") + .attr("text-anchor", "end") + .attr("transform", "rotate(-90)") + .attr("dx", "-2em") + .style("font-size", responsiveFontSize) + .style("user-select", "none"); + } + // Add click event to the axis ticks, so that clicking on a node opens the study modal d3.selectAll(".tick") - .on("click", function(event, d) { + .on("click", function (event, d) { showStudyModal(d); }) .style("cursor", "pointer"); // Change cursor to pointer for better UX // Create a group for the links - const linkGroup = container.append("g") - .attr("transform", `translate(${margin.left}, ${margin.top})`) - .attr("class", "links"); + const linkGroup = g.append("g").attr("class", "links"); // Create a group for the nodes - const nodeGroup = container.append("g") - .attr("transform", `translate(${margin.left}, ${axisHeight + margin.top})`) + const nodeGroup = g + .append("g") + .attr( + "transform", + alignHorizontal + ? `translate(0, ${axisMiddle})` + : `translate(${axisMiddle}, 0)` + ) .attr("class", "nodes"); - const arc = d3.arc() - .innerRadius(0) - .outerRadius(nodeRadius); + const arc = d3.arc().innerRadius(0).outerRadius(nodeRadius); // Draw the nodes and add click and hover events - nodeGroup.selectAll(".node") - .data(nodes) - .join("g") - .attr("class", "node") - .attr("transform", d => `translate(${xScale(d)}, 0)`) - .each(function(d) { + nodeGroup + .selectAll(".node") + .data(nodes) + .join("g") + .attr("class", "node") + .attr("transform", (d) => + alignHorizontal + ? `translate(${nodeScale(d)}, 0)` + : `translate(0, ${nodeScale(d)})` + ) + .each(function (d) { drawNode(d3.select(this), colorCategory, arc, colorScale); }) - .on("click", function(event, d) { - openNetworkDetails(d, links); - }) - .on("mouseover", function(event, d) { - highlightNode(d, nodeRadius); - }) - .on("mouseout", () => removeHighlighting(nodeRadius)); + .on("click", function (event, d) { + openNetworkDetails(d, links); + }) + .on("mouseover", function (event, d) { + highlightNode(d, nodeRadius); + }) + .on("mouseout", () => removeHighlighting(nodeRadius)); // Draw the links - linkGroup.selectAll(".link") + linkGroup + .selectAll(".link") .data(links) .enter() .append("path") .attr("class", "link") - .attr("d", d => { - const sourceX = xScale(d.sourceID); - const targetX = xScale(d.targetID); - const arcHeight = Math.min(Math.abs(sourceX - targetX) * 15, height / 3); - - // Draw an quadratic curve from the source to the target node - return `M ${sourceX} ${axisHeight} Q ${(sourceX + targetX) / 2} ${axisHeight - arcHeight - 2 * margin.top}, ${targetX} ${axisHeight}`; + .attr("d", (d) => { + if (alignHorizontal) { + const sourceX = nodeScale(d.sourceID); + const targetX = nodeScale(d.targetID); + const midX = (sourceX + targetX) / 2; + const arcHeight = Math.min( + Math.abs(sourceX - targetX) * 0.4, + height / 3 + ); + + // Draw a curved path between nodes + return `M ${sourceX} ${axisMiddle} Q ${midX} ${ + axisMiddle - arcHeight + }, ${targetX} ${axisMiddle}`; + } else { + const sourceY = nodeScale(d.sourceID); + const targetY = nodeScale(d.targetID); + const midY = (sourceY + targetY) / 2; + const arcWidth = Math.min(Math.abs(sourceY - targetY) * 2, width / 2); + + // Draw a curved path between nodes + return `M ${axisMiddle} ${sourceY} Q ${ + axisMiddle + arcWidth + } ${midY}, ${axisMiddle} ${targetY}`; + } }); // Add tooltips to the links - linkGroup.selectAll(".link") + linkGroup + .selectAll(".link") .append("title") - .text(d => `${similarityType === "database" ? "Database" : "Abstract"} Similarity: ${d.value.toFixed(2)} between [${d.sourceID}] and [${d.targetID}]`); + .text( + (d) => + `${ + similarityType === "database" ? "Database" : "Abstract" + } Similarity: ${d.value.toFixed(2)} between [${d.sourceID}] and [${ + d.targetID + }]` + ); + + const mobileQuery = window.matchMedia("(max-width: 850px)"); + + if (mobileQuery.matches) { + const zoom = d3 + .zoom() + .scaleExtent([0.8, 10]) + .on("zoom", ({ transform }) => { + // On mobile allow panning/zooming + const x = margin.left + transform.x; + const y = margin.top + transform.y; + const k = transform.k; + g.attr("transform", `translate(${x}, ${y}) scale(${k})`); + }); + + container.call(zoom).call(zoom.transform, d3.zoomIdentity); + } } /* Interaction section Here the event listeners for the interaction possibilities of the similarity graph are set up */ -$(document).ready(function() { +$(document).ready(function () { drawGraph(similarityThreshold); // Initial draw of the graph // Add event listener for similarity type change - $("input[name='similarityType']").on("change", function() { + $("input[name='similarityType']").on("change", function () { similarityType = $(this).val(); window.sessionStorage.setItem("similarityType", similarityType); // Update the tooltip text based on the selected similarity type - $("#thresholdInfoIcon").attr("title", similarityType === "abstract" ? abstractTooltip : databaseTooltip); + $("#thresholdInfoIcon").attr( + "title", + similarityType === "abstract" ? abstractTooltip : databaseTooltip + ); drawGraph(similarityThreshold); // Redraw the graph with the new similarity type }); // Add event listener for the category dropdown change - $("#similarityColorCategory").on("change", function() { + $("#similarityColorCategory").on("change", function () { colorCategory = $(this).val(); window.sessionStorage.setItem("colorCategory", colorCategory); drawGraph(similarityThreshold); // Redraw the graph with the new color category }); - window.addEventListener("resize", function() { + window.addEventListener("resize", function () { drawGraph(similarityThreshold); // Redraw the graph on window resize }); - $(".value-filter").on("change", function() { + $(".value-filter").on("change", function () { drawGraph(similarityThreshold); // Redraw the graph when a value filter changes }); - $(".exclusive-filter").on("click", function() { + $(".exclusive-filter").on("click", function () { drawGraph(similarityThreshold); // Redraw the graph when an exclusive filter is applied }); - $("#connectionsContainer").on("click", ".info-circle", function() { + $("#connectionsContainer").on("click", ".info-circle", function () { const id = $(this).data("id"); if (this.classList.contains("network-information")) { @@ -609,8 +892,8 @@ $(document).ready(function() { showStudyModal(id); }); - $(".range-slider").each(function() { - this.noUiSlider.on("end", function(values, handle) { + $(".range-slider").each(function () { + this.noUiSlider.on("end", function (values, handle) { drawGraph(similarityThreshold); }); }); @@ -618,4 +901,4 @@ $(document).ready(function() { $("#connectionsModal").on("hidden.bs.modal", function () { window.sessionStorage.removeItem("modalID"); }); -}); \ No newline at end of file +}); diff --git a/static/scripts/timeline.mjs b/static/scripts/timeline.mjs index eeb5018..f8645a8 100644 --- a/static/scripts/timeline.mjs +++ b/static/scripts/timeline.mjs @@ -1,17 +1,29 @@ -import { filterData, sortNodesByCategory, getDataEntry, showStudyModal } from "./dataUtility.mjs"; -import { createLegend, drawNode, highlightNode, removeHighlighting } from "./d3DrawingUtility.mjs"; +import { + filterData, + sortNodesByCategory, + getDataEntry, + showStudyModal, +} from "./dataUtility.mjs"; +import { + createLegend, + drawNode, + highlightNode, + removeHighlighting, +} from "./d3DrawingUtility.mjs"; // Load data from the backend const coauthorMatrix = $("#timeline-graph-container").data("coauthor"); const citationMatrix = $("#timeline-graph-container").data("citation"); const filterCategories = $("body").data("filter-categories"); -const excludedCategories = $(".category-dropdown-container").data("excluded-categories"); +const excludedCategories = $(".category-dropdown-container").data( + "excluded-categories" +); const infoCirclePath = $("#timelineConnectionsModal").data("info-circle-path"); // Citing order -const citingOrder = {"Cites": 0, "Cited By": 1, "Coauthor": 2}; +const citingOrder = { Cites: 0, "Cited By": 1, Coauthor: 2 }; // Populate the timeline dropdown menu -filterCategories.forEach(category => { +filterCategories.forEach((category) => { if (excludedCategories.includes(category)) return; const shortCategory = category.split("_").pop(); $("#timelineColorCategory").append( @@ -24,8 +36,12 @@ let colorCategory = window.sessionStorage.getItem("colorCategory") || ""; $("#timelineColorCategory").val(colorCategory); // Set the shared authors option -let showSharedAuthors = window.sessionStorage.getItem("showSharedAuthors") || "true"; -$("#timeline-toggle-shared-authors").prop("checked", showSharedAuthors === "true"); +let showSharedAuthors = + window.sessionStorage.getItem("showSharedAuthors") || "true"; +$("#timeline-toggle-shared-authors").prop( + "checked", + showSharedAuthors === "true" +); // Set the current citation mode let citationMode = window.sessionStorage.getItem("citationMode") || "both"; @@ -36,28 +52,40 @@ function showNetworkModal(id) { const entry = getDataEntry(id); // Get links which represent a citing relationship (the source cites the target) - const citingLinks = d3.selectAll(".citing") - .filter(d => d.sourceID === id).data(); + const citingLinks = d3 + .selectAll(".citing") + .filter((d) => d.sourceID === id) + .data(); // Get links which represent a cited by relationship (the source is cited by the target) - const citedByLinks = d3.selectAll(".cited-by") - .filter(d => d.sourceID === id).data(); + const citedByLinks = d3 + .selectAll(".cited-by") + .filter((d) => d.sourceID === id) + .data(); // Get the links that represent a coauthor relationship - const coauthorLinks = d3.selectAll(".coauthor") - .filter(d => d.sourceID === id).data(); + const coauthorLinks = d3 + .selectAll(".coauthor") + .filter((d) => d.sourceID === id) + .data(); // Combine target IDs that appear in more than one type of link const targetIDs = {}; - citingLinks.forEach(link => (targetIDs[link.targetID] = (targetIDs[link.targetID] || [])).push("Cites")); - citedByLinks.forEach(link => (targetIDs[link.targetID] = (targetIDs[link.targetID] || [])).push("Cited By")); - coauthorLinks.forEach(link => (targetIDs[link.targetID] = (targetIDs[link.targetID] || [])).push("Coauthor")); + citingLinks.forEach((link) => + (targetIDs[link.targetID] = targetIDs[link.targetID] || []).push("Cites") + ); + citedByLinks.forEach((link) => + (targetIDs[link.targetID] = targetIDs[link.targetID] || []).push("Cited By") + ); + coauthorLinks.forEach((link) => + (targetIDs[link.targetID] = targetIDs[link.targetID] || []).push("Coauthor") + ); const orderedIDs = Object.keys(targetIDs).sort((a, b) => { if (targetIDs[a].length !== targetIDs[b].length) { return targetIDs[b].length - targetIDs[a].length; // Sort by number of connections } return citingOrder[targetIDs[a][0]] - citingOrder[targetIDs[b][0]]; // Sort by connection type priority - }) + }); const colgroupHTML = ` @@ -106,8 +134,13 @@ function showNetworkModal(id) { `; let connectionsHTML; - if (citingLinks.length === 0 && citedByLinks.length === 0 && coauthorLinks.length === 0) { - connectionsHTML = "
    Study Network

    No connections found with the current filter settings.

    "; + if ( + citingLinks.length === 0 && + citedByLinks.length === 0 && + coauthorLinks.length === 0 + ) { + connectionsHTML = + "
    Study Network

    No connections found with the current filter settings.

    "; } else { connectionsHTML = `
    Study Network
    @@ -127,26 +160,34 @@ function showNetworkModal(id) { - ${orderedIDs.map(targetID => { - const entry = getDataEntry(targetID); - return ` + ${orderedIDs + .map((targetID) => { + const entry = getDataEntry(targetID); + return ` - Info cirle for this row + Info cirle for this row ${entry["ID"]} ${entry["Main Author"]} ${entry["Year"]} ${entry["Location"]} ${entry["Input Body Part"]} ${entry["Gesture"]} - ${targetIDs[targetID].map(formatConnectionType).join(", ")} + ${targetIDs[targetID] + .map(formatConnectionType) + .join(", ")} `; - }).join("\n")} + }) + .join("\n")} -

    Total connections: ${citingLinks.length + citedByLinks.length + coauthorLinks.length}

    +

    Total connections: ${ + citingLinks.length + citedByLinks.length + coauthorLinks.length + }

    `; - }; + } // Append the generated HTML to the modal and show it $("#timelineConnectionsContainer").html(headerHTML); @@ -162,8 +203,8 @@ function formatConnectionType(value) { return "Cited By"; default: return "Shared Authors"; - }; -}; + } +} /* * Preparing the data for the timeline graph. @@ -171,41 +212,47 @@ function formatConnectionType(value) { */ function generateTimelineData() { // Get the currently active nodes based on the selected category and filters - const activeNodes = filterData(JSON.parse(window.sessionStorage.getItem("filters"))).map(item => item["ID"].toString()); + const activeNodes = filterData( + JSON.parse(window.sessionStorage.getItem("filters")) + ).map((item) => item["ID"].toString()); // Order the nodes by the selected category - const { sortedNodes, colorScale } = sortNodesByCategory(activeNodes, colorCategory); + const { sortedNodes, colorScale } = sortNodesByCategory( + activeNodes, + colorCategory + ); // Append the year to each node - const nodes = sortedNodes.map(node => { + const nodes = sortedNodes.map((node) => { return { id: node, year: getDataEntry(node, "Year"), - } + }; }); const years = {}; - nodes.forEach(node => { + nodes.forEach((node) => { if (!years[node.year]) { years[node.year] = [node.id]; } else { years[node.year].push(node.id); } }); - const maxYears = Math.max(...Object.keys(years).map(year => years[year].length)); - + const maxYears = Math.max( + ...Object.keys(years).map((year) => years[year].length) + ); + // Create links for co-authors and citations - const links = {coauthorLinks: [], citingLinks: [], citedByLinks: []}; + const links = { coauthorLinks: [], citingLinks: [], citedByLinks: [] }; for (const node of sortedNodes) { for (const other of sortedNodes) { - // Populate the links for co-authors if (coauthorMatrix[node][other]) { links.coauthorLinks.push({ sourceID: node, targetID: other, }); - }; + } // Populate the links for citations if (citationMatrix[node][other]) { @@ -213,7 +260,7 @@ function generateTimelineData() { sourceID: node, targetID: other, }); - }; + } if (citationMatrix[other][node]) { links.citedByLinks.push({ @@ -229,8 +276,8 @@ function generateTimelineData() { years, links, maxYears, - colorScale - } + colorScale, + }; } function drawTimelineGraph() { @@ -241,85 +288,148 @@ function drawTimelineGraph() { const { nodes, years, links, maxYears, colorScale } = generateTimelineData(); const { coauthorLinks, citingLinks, citedByLinks } = links; - const maxYearsCount = Math.max(...Object.values(years).map(year => year.length)); + const maxYearsCount = Math.max( + ...Object.values(years).map((year) => year.length) + ); // If there are no nodes, do not draw the graph if (nodes.length === 0) { - $("#timeline-graph-container").append("

    No studies available for the selected sidebar filters. Please select some of the criteria from the sidebar at the right.

    "); + $("#timeline-graph-container").append( + "

    No studies available for the selected sidebar filters. Please select some of the criteria from the sidebar at the right.

    " + ); return; } // Set up layout dimensions for the graph - const margin = { top: 20, right: 50, bottom: 50, left: 50 }; - const innerWidth = $("#timeline-graph-container").width() - margin.left - margin.right; - const nodeRadius = innerWidth / 150; - const height = Math.max(250, maxYearsCount * (nodeRadius * 4)); + let margin = + window.innerWidth <= 750 + ? { top: 5, right: 20, bottom: 30, left: 20 } + : { top: 20, right: 50, bottom: 20, left: 50 }; + const containerWidth = $("#timeline-graph-container").width(); + const innerWidth = containerWidth - margin.left - margin.right; + + // Base node radius + const nodeRadius = 7; + + // Calculate height based on max years count + const height = Math.max( + 400, + maxYearsCount * (nodeRadius * 4), + (9 / 16) * containerWidth + ); const innerHeight = height - margin.top - margin.bottom; const axisHeight = innerHeight; - // Create the svg container for the timeline graph - const svg = d3.select("#timeline-graph-container") + // get the current font size + const responsiveFontSize = getComputedStyle(document.body) + .getPropertyValue("--resp-font-ticks-bg") + .trim(); + + // Create the svg container for the timeline graph with responsive viewBox + const svg = d3 + .select("#timeline-graph-container") .append("svg") - .attr("width", $("#timeline-graph-container").width()) + .attr("width", containerWidth) .attr("height", height) - .attr("viewBox", `0 0 ${$("#timeline-graph-container").width()} ${height}`); + .attr("viewBox", `${margin.left} ${margin.top} ${innerWidth} ${height}`) + .attr("preserveAspectRatio", "xMidYMid meet"); // Create an x scale for the years - const xScale = d3.scalePoint() + const xScale = d3 + .scalePoint() .domain(Object.keys(years).map(Number)) .range([0, innerWidth]); // Create a y scale for the nodes - const yScale = d3.scaleLinear() + const yScale = d3 + .scaleLinear() .domain([0, maxYears]) .range([axisHeight - margin.bottom, 0]); + // Append group element to svg for zooming + const g = svg + .append("g") + .attr("transform", `translate (${margin.left}, ${margin.top})`); + // Create an x axis for the years - const xAxis = d3.axisBottom(xScale) + const xAxis = d3 + .axisBottom(xScale) .tickFormat(d3.format("d")) - .tickSize(5); + .tickSize(window.innerWidth <= 750 ? 0 : 5); // Set tickSize to 0 as we'll draw our own ticks - // Draw the x axis - svg.append("g") + // Draw just the x axis line + g.append("g") .attr("class", "x-axis") - .attr("transform", `translate(${margin.left}, ${axisHeight + margin.top})`) - .call(xAxis); - - // Format the labels for the axis - svg.selectAll(".x-axis text") - .style("font-size", "1.2em") - .style("user-select", "none"); + .attr("transform", `translate(0, ${axisHeight})`) + .call(xAxis) + .call((g) => { + // Keep only the domain line and remove default ticks when screen is small + if (window.innerWidth <= 750) { + g.select(".domain").attr("stroke", "#000"); + g.selectAll(".tick").remove(); + + // Add custom alternating ticks + const yearValues = Object.keys(years) + .map(Number) + .sort((a, b) => a - b); + + g.selectAll(".custom-tick") + .data(yearValues) + .join("g") + .attr("class", "custom-tick") + .attr("transform", (d) => `translate(${xScale(d)}, 0)`) + .each(function (d, i) { + const isAbove = i % 2 === 0; // Alternates based on index + const tick = d3.select(this); + + const tickSize = 5; + const textOffset = 8; + + // Add tick line + tick + .append("line") + .attr("class", "tick-line") + .attr("y1", 1) + .attr("y2", isAbove ? -tickSize : tickSize); + + // Add year text + tick + .append("text") + .attr("class", "tick-text") + .attr("text-anchor", "middle") + .attr("dy", isAbove ? -textOffset : textOffset + tickSize) + .style("font-size", responsiveFontSize) + .style("user-select", "none") + .text(d); + }); + } + }); // Create a link group for the links - const linkGroup = svg.append("g") - .attr("class", "links") - .attr("transform", `translate(${margin.left}, ${margin.top})`); + const linkGroup = g.append("g").attr("class", "links"); // Create a node group for the nodes - const nodeGroup = svg.append("g") - .attr("class", "nodes") - .attr("transform", `translate(${margin.left}, ${margin.top})`); + const nodeGroup = g.append("g").attr("class", "nodes"); - const arc = d3.arc() - .innerRadius(0) - .outerRadius(nodeRadius); + const arc = d3.arc().innerRadius(0).outerRadius(nodeRadius); // Draw the nodes on the timeline - nodeGroup.selectAll(".node") - .data(nodes.map(node => node.id)) + nodeGroup + .selectAll(".node") + .data(nodes.map((node) => node.id)) .join("g") .attr("class", "node") - .attr("transform", d => { - const year = nodes.find(node => node.id === d).year; + .attr("transform", (d) => { + const year = nodes.find((node) => node.id === d).year; return `translate(${xScale(year)}, ${yScale(years[year].indexOf(d))})`; }) - .each(function(d) { - drawNode(d3.select(this), colorCategory, arc, colorScale) + .each(function (d) { + drawNode(d3.select(this), colorCategory, arc, colorScale); }) - .on("click", function(event, d) { + .on("click", function (event, d) { showNetworkModal(d); }) - .on("mouseover", function(event, d) { + .on("mouseover", function (event, d) { // Show the tooltip next to the node nodeTooltip.style("visibility", "visible"); nodeTooltip.style("left", `${event.pageX + 15}px`); @@ -332,49 +442,57 @@ function drawTimelineGraph() {

    ${entry["Main Author"]} (${entry["Year"]})

    Location: ${entry["Location"]}

    `); - + // Highlight the node and its connections - highlightNode(d, nodeRadius, citationMode === "cited-by" || citationMode === "cites"); + highlightNode( + d, + nodeRadius, + citationMode === "cited-by" || citationMode === "cites" + ); }) - .on("mouseout", function(event, d) { + .on("mouseout", function (event, d) { // Hide the tooltip when the mouse leaves the node nodeTooltip.style("visibility", "hidden"); removeHighlighting(nodeRadius); }); - - const nodeTooltip = d3.select("#timeline-graph-container").append("div") + const nodeTooltip = d3 + .select("#timeline-graph-container") + .append("div") .attr("class", "node-tooltip") .style("visibility", "hidden"); // Draw the links between the nodes if (showSharedAuthors === "true") { - linkGroup.selectAll(".link .coauthor") + linkGroup + .selectAll(".link .coauthor") .data(coauthorLinks) .join("path") .attr("class", "coauthor link") - .attr("d", d => drawLink(d)); - }; + .attr("d", (d) => drawLink(d)); + } if (citationMode === "both" || citationMode === "cites") { - linkGroup.selectAll(".link .citing") + linkGroup + .selectAll(".link .citing") .data(citingLinks) .join("path") .attr("class", "citing link") - .attr("d", d => drawLink(d)); - }; + .attr("d", (d) => drawLink(d)); + } if (citationMode === "both" || citationMode === "cited-by") { - linkGroup.selectAll(".link .cited-by") + linkGroup + .selectAll(".link .cited-by") .data(citedByLinks) .join("path") .attr("class", "cited-by link") - .attr("d", d => drawLink(d)); + .attr("d", (d) => drawLink(d)); } function drawLink(d) { - const sourceNode = nodes.find(node => node.id === d.sourceID); - const targetNode = nodes.find(node => node.id === d.targetID); + const sourceNode = nodes.find((node) => node.id === d.sourceID); + const targetNode = nodes.find((node) => node.id === d.targetID); const sourceX = xScale(sourceNode.year); const targetX = xScale(targetNode.year); const sourceY = yScale(years[sourceNode.year].indexOf(sourceNode.id)); @@ -383,12 +501,15 @@ function drawTimelineGraph() { if (sourceX === targetX) { // If the souce and target are in the same year, draw an arc const midY = (sourceY + targetY) / 2; - return `M ${sourceX},${sourceY} Q ${Math.min(sourceX + xScale.step(), $("#timeline-graph-container").width() - margin.right / 2)},${midY} ${targetX},${targetY}`; + return `M ${sourceX},${sourceY} Q ${Math.min( + sourceX + xScale.step(), + $("#timeline-graph-container").width() - margin.right / 2 + )},${midY} ${targetX},${targetY}`; } else { // For different years, create a bezier curve const dx = targetX - sourceX; const controlOffset = Math.min(Math.abs(dx) * 0.4, 100); // Limit control point offset - + // Calculate control points - higher curves for connections between distant years const controlX1 = sourceX + Math.sign(dx) * controlOffset; const controlY1 = sourceY - 40; // Curve upward @@ -397,72 +518,98 @@ function drawTimelineGraph() { // If the source and target are in different years, draw a bezier curve return `M ${sourceX},${sourceY} C ${controlX1},${controlY1} ${controlX2},${controlY2} ${targetX},${targetY}`; - }; - }; + } + } // Add hover title to links - linkGroup.selectAll(".citing") + linkGroup + .selectAll(".citing") .append("title") - .text(d => `[${d.sourceID}] cites [${d.targetID}]`); + .text((d) => `[${d.sourceID}] cites [${d.targetID}]`); - linkGroup.selectAll(".cited-by") + linkGroup + .selectAll(".cited-by") .append("title") - .text(d => `[${d.sourceID}] is cited by [${d.targetID}]`); + .text((d) => `[${d.sourceID}] is cited by [${d.targetID}]`); - linkGroup.selectAll(".coauthor") + linkGroup + .selectAll(".coauthor") .append("title") - .text(d => `[${d.sourceID}] shares authors with [${d.targetID}]`); + .text((d) => `[${d.sourceID}] shares authors with [${d.targetID}]`); // Create a legend for the colors - createLegend(nodes.map(node => node.id), colorScale, colorCategory, $("#legend")); + createLegend( + nodes.map((node) => node.id), + colorScale, + colorCategory, + $("#legend") + ); + + // Add zooming functionality at lower screen sizes + const mobileQuery = window.matchMedia("(max-width: 850px)"); + + if (mobileQuery.matches) { + const zoom = d3 + .zoom() + .scaleExtent([0.8, 10]) + .on("zoom", ({ transform }) => { + // On mobile allow panning/zooming + const x = margin.left + transform.x; + const y = margin.top + transform.y; + const k = transform.k; + g.attr("transform", `translate(${x}, ${y}) scale(${k})`); + }); + + svg.call(zoom).call(zoom.transform, d3.zoomIdentity); + } } -$(document).ready(function() { +$(document).ready(function () { drawTimelineGraph(); - $("#timeline-toggle-shared-authors").on("click", function() { + $("#timeline-toggle-shared-authors").on("click", function () { showSharedAuthors = $(this).is(":checked").toString(); window.sessionStorage.setItem("showSharedAuthors", showSharedAuthors); drawTimelineGraph(); - }) + }); - $("input[name='citation-mode']").on("click", function() { + $("input[name='citation-mode']").on("click", function () { citationMode = $(this).val(); window.sessionStorage.setItem("citationMode", citationMode); drawTimelineGraph(); }); // Set the coloring strategy to the session storage - $("#timelineColorCategory").on("change", function() { + $("#timelineColorCategory").on("change", function () { colorCategory = $(this).val(); window.sessionStorage.setItem("colorCategory", colorCategory); drawTimelineGraph(); }); - window.addEventListener("resize", function() { + window.addEventListener("resize", function () { drawTimelineGraph(); }); - $(".value-filter").on("change", function() { + $(".value-filter").on("change", function () { drawTimelineGraph(); }); - $(".exclusive-filter").on("click", function() { + $(".exclusive-filter").on("click", function () { drawTimelineGraph(); }); - $(".range-slider").each(function() { - this.noUiSlider.on("end", function() { + $(".range-slider").each(function () { + this.noUiSlider.on("end", function () { drawTimelineGraph(); - }) + }); }); // Handle clicks on the info circles in the connections modal - $("#timelineConnectionsContainer").on("click", ".info-circle", function() { + $("#timelineConnectionsContainer").on("click", ".info-circle", function () { const id = $(this).data("id"); // Close the network modal if it's open $("#timelineConnectionsModal").modal("hide"); showStudyModal(id); }); -}) \ No newline at end of file +}); diff --git a/static/styles/barChart.css b/static/styles/barChart.css index 349fe90..37d3366 100644 --- a/static/styles/barChart.css +++ b/static/styles/barChart.css @@ -5,6 +5,11 @@ gap: 1vw; } +#max-bars-container, +#max-bars-container select { + font-size: var(--font-bg); +} + .chart-wrapper { background: var(--color-gray-light); border-radius: 8px; @@ -20,7 +25,7 @@ .chart-container { position: relative; - min-height: 30vh; + min-height: 25vh; } .chart-title { @@ -35,16 +40,39 @@ margin: 0; display: inline-block; word-break: break-word; + font-size: var(--font-xl); +} + +.chart-title .question-circle { + width: var(--font-xl); } #hiddenChartsMessage { display: none; margin-top: 2em; padding: 1em; + font-size: var(--font-bg); +} + +#hiddenChartsMessage h6 { + font-size: var(--font-xl); +} + +#table-modal, +#table-modal button { + font-size: var(--font-md); +} + +#table-modal h5 { + font-size: var(--font-xl); +} + +#table-modal th { + font-size: var(--font-bg); } @media (max-width: 670px) { #visualization-warning { display: flex; } -} \ No newline at end of file +} diff --git a/static/styles/base.css b/static/styles/base.css index 4a7aa4b..69123af 100644 --- a/static/styles/base.css +++ b/static/styles/base.css @@ -1,5 +1,5 @@ :root { - --color-accent: #B89491; /* Earable's accent color */ + --color-accent: #b89491; /* Earable's accent color */ --color-accent-dark: #a37c79; --color-gray: #6c757d; --color-gray-light: #f8f9fa; @@ -11,7 +11,15 @@ body { padding: 0; display: flex; flex-direction: row; - font-size: clamp(0.75rem, 0.6642rem + 0.3661vw, 1.25rem); + + --resp-font-size: clamp(0.5rem, 0.3713rem + 0.5492vw, 1.25rem); + --resp-font-ticks: clamp(0.5rem, 0.3713rem + 0.5492vw, 1.25rem); + --resp-font-ticks-bg: calc(clamp(0.25rem, 0.1213rem + 0.5492vw, 1rem) * 1.5); + --font-xl: calc(var(--resp-font-size) * 1.25); + --font-bg: calc(var(--resp-font-size) * 1.15); + --font-md: var(--resp-font-size); + --font-sm: calc(var(--resp-font-size) * 0.85); + --resp-padding: clamp(0.5rem, 0.3713rem + 0.5492vw, 1.25rem); } /* Hide elements that are only relevant for lower screen resolution */ @@ -26,14 +34,16 @@ body { align-items: center; justify-content: center; background-color: rgb(255, 255, 175); - border: 2px solid #B89230; + border: 2px solid #b89230; padding: 0.5em; border-radius: 5px; - color: #B89230; + color: #b89230; + margin-bottom: 0.5rem; } #visualization-warning span { - text-align: center;; + text-align: center; + font-size: var(--font-bg); } header { @@ -41,7 +51,7 @@ header { flex-direction: row; justify-content: space-between; align-items: center; - gap: 1em; + gap: var(--resp-padding); } .left-section { @@ -61,6 +71,15 @@ nav { gap: 1em; } +#nav-r { + gap: 1em; +} + +#earXplore-repo, +#open-earable-repo { + font-size: var(--font-bg); +} + #earXplore-repo a { display: flex; align-items: center; @@ -76,11 +95,15 @@ nav { } .navbar-item { - padding: 1em; + padding: var(--resp-padding); text-align: center; border-radius: 5px; /* Rounded corners */ } +.navbar-text { + font-size: var(--font-bg); +} + .navbar-item:hover:not(.navbar-item-selected) { background-color: #f8e6e6; border-color: var(--color-accent); @@ -99,7 +122,7 @@ nav { } h3 { - font-size: 1.5em; + font-size: var(--font-xl); padding: 0; margin: 0; } @@ -117,7 +140,15 @@ h3 { padding: 5px; max-height: 100vh; overflow-y: scroll; - font-size: medium; +} + +.filter-group { + font-size: var(--font-md); +} + +.question-circle, +.info-circle { + width: var(--font-bg); } .panel { @@ -145,7 +176,7 @@ h3 { } /* Buttons */ -.add-study-button{ +.add-study-button { color: var(--color-accent); background-color: white; border: none; @@ -157,13 +188,13 @@ h3 { border: 1px solid var(--color-gray); color: white; padding: 0.1em 0.4em; - font-size: 1em; + font-size: var(--font-md); border-radius: 5px; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1); text-shadow: none; } -.exclusive-filter{ +.exclusive-filter { background-color: var(--color-accent); border-color: var(--color-accent); } @@ -174,9 +205,9 @@ h3 { } .btn-link, -.toggle-visibility-button { +.toggle-visibility-button { background-color: var(--color-accent); - font-size: 1em; + font-size: var(--font-md); color: white; border: none; text-decoration: none; @@ -193,6 +224,16 @@ h3 { font-weight: bold; } +.study-info-panel-header, +#study-info-header { + font-size: var(--font-xl); +} + +#study-info-modal-body, +#study-info-modal-footer button { + font-size: var(--font-bg); +} + .study-info-panel-header:not(:first-child) { margin-top: 20px; } @@ -209,7 +250,7 @@ h3 { border-radius: 5px; text-decoration: none; opacity: 1; - font-size: 1rem; + font-size: var(--font-md); border: 1px solid var(--color-accent); } @@ -226,6 +267,10 @@ h3 { /* General Styles */ +p { + font-size: var(--font-sm); +} + /* Style checkboxes to match the grey color of the modal close button */ input[type="checkbox"] { accent-color: var(--color-gray); /* Bootstrap's secondary color */ @@ -240,18 +285,19 @@ input[type="checkbox"] { opacity: 0.6; } -.form-check-input:focus { +.form-check-input:focus { outline: none; box-shadow: none; border: 1px solid var(--color-gray); } -a:link, a:visited { +a:link, +a:visited { text-decoration: none; color: inherit; } -@media (max-width: 1050px) { +@media (max-width: 1200px) { /* Hide the sidebar from the user */ #sidebar { position: fixed; @@ -264,13 +310,31 @@ a:link, a:visited { transition: right 0.3s ease; } + .left-section { + overflow-y: auto; + } + /* Make the sidebar appear when it should be visible */ #sidebar.visible-sidebar { right: 0; } /* Add two buttons to open and close the sidebar */ - #toggle-sidebar, + #toggle-sidebar { + display: flex; + justify-content: end; + font-size: var(--font-bg); + } + + #toggle-sidebar-btn { + display: flex; + border: none; + background-color: var(--color-gray-light); + border-radius: 30%; + opacity: 0.5; + gap: 0.5em; + } + #close-sidebar { display: block; border: none; @@ -285,7 +349,7 @@ a:link, a:visited { } #close-icon { - width: 2em; + width: 1.25rem; } /* Add a mask over the content when the sidebar is open */ @@ -317,7 +381,7 @@ a:link, a:visited { } } -@media (max-width: 630px) { +@media (max-width: 860px) { /* Change the navbar links from text to icons */ .navbar-text { display: none; @@ -335,11 +399,20 @@ a:link, a:visited { } } -@media (max-width: 450px) { +@media (max-width: 550px) { + #nav-r { + gap: 0.5rem; + } + #earXplore-repo { display: none; } -} - + .link-section { + gap: 0em; + } + header { + gap: 0; + } +} diff --git a/static/styles/filter.css b/static/styles/filter.css index 5b9f88b..a9bbcee 100644 --- a/static/styles/filter.css +++ b/static/styles/filter.css @@ -1,70 +1,74 @@ .small-button { - font-size: 0.75rem; /* Adjust the font size as needed */ - padding: 0.1rem 0.2rem; /* Adjust the padding as needed */ + font-size: 0.75rem; /* Adjust the font size as needed */ + padding: 0.1rem 0.2rem; /* Adjust the padding as needed */ } .select-buttons { - display: flex; - gap: 0.5em; - margin: 0.5em 0; + display: flex; + gap: 0.5em; + margin: 0.5em 0; } #select-filter-button { - display: none; + display: none; + font-size: var(--font-bg); } #reset-filters-button { - background-color: var(--color-accent); - border: 1px solid var(--color-accent); + background-color: var(--color-accent); + border: 1px solid var(--color-accent); } #columnToggles { - display: flex; - flex-wrap: wrap; - gap: 0.5em; + display: flex; + flex-wrap: wrap; + gap: 0.5em; } #toggle-menu-container { - font-size: medium; + font-size: var(--font-md); } @media (max-width: 1050px) { - #toggle-menu-container { - height: 0; - overflow: hidden; - transition: height 0.3s ease; - interpolate-size: allow-keywords; - } + #toggle-menu-container { + height: 0; + overflow: hidden; + transition: height 0.3s ease; + interpolate-size: allow-keywords; + } - #filter-icon { - width: 2em; - } + #filter-icon { + width: 2em; + } - #toggle-menu-container.visible-filters { - height: auto; - } - - #select-filter-button { - display: flex; - align-items: center; - gap: 0.5em; - margin: 1em 0 1em auto; - border: none; - background-color: var(--color-gray-light); - border-radius: 5%; - opacity: 0.5; - } + #toggle-menu-container.visible-filters { + height: auto; + } - .select-buttons { - justify-content: start; - } + #select-filter-button { + display: flex; + align-items: center; + gap: 0.5em; + margin: 1em 0 1em auto; + border: none; + background-color: var(--color-gray-light); + border-radius: 5%; + opacity: 0.5; + } + + .select-buttons { + justify-content: start; + } } .filter-wrapper { - background: #eee; - border: 1px solid transparent; - border-radius: 20px; - overflow: hidden; - margin: 0.5px; - padding: 2px 10px; -} \ No newline at end of file + background: #eee; + border: 1px solid transparent; + border-radius: 20px; + overflow: hidden; + margin: 0.5px; + padding: 2px 10px; + display: flex; + gap: 0.5rem; + align-items: center; +} diff --git a/static/styles/nouislider.css b/static/styles/nouislider.css index 175f99a..fb7075b 100644 --- a/static/styles/nouislider.css +++ b/static/styles/nouislider.css @@ -2,6 +2,7 @@ * These styles are required for noUiSlider to function. * You don't need to change these rules to apply your design. */ + .noUi-target, .noUi-target * { -webkit-touch-callout: none; @@ -14,6 +15,7 @@ user-select: none; -moz-box-sizing: border-box; box-sizing: border-box; + --resp-height: clamp(0.75rem, 0.3713rem + 0.5492vw, 1.25rem); } .noUi-target { position: relative; @@ -82,11 +84,12 @@ /* Slider size and handle placement; */ .noUi-horizontal { - height: 18px; + height: var(--resp-height); } .noUi-horizontal .noUi-handle { - width: 34px; - height: 28px; + width: 10%; + max-width: 30px; + height: calc(var(--resp-height) * 2); right: -17px; top: -6px; } @@ -107,16 +110,17 @@ * Giving the connect element a border radius causes issues with using transform: scale */ .noUi-target { - background: #FAFAFA; + background: #fafafa; border-radius: 4px; - border: 1px solid #D3D3D3; - box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB; + border: 1px solid #d3d3d3; + box-shadow: inset 0 1px 1px #f0f0f0, 0 3px 6px -5px #bbb; } + .noUi-connects { border-radius: 3px; } .noUi-connect { - background: #B89491; + background: #b89491; } /* Handles and cursors; */ @@ -127,14 +131,14 @@ cursor: ns-resize; } .noUi-handle { - border: 1px solid #D9D9D9; + border: 1px solid #d9d9d9; border-radius: 3px; - background: #FFF; + background: #fff; cursor: default; - box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #EBEBEB, 0 3px 6px -3px #BBB; + box-shadow: inset 0 0 1px #fff, inset 0 1px 7px #ebebeb, 0 3px 6px -3px #bbb; } .noUi-active { - box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #DDD, 0 3px 6px -3px #BBB; + box-shadow: inset 0 0 1px #fff, inset 0 1px 7px #ddd, 0 3px 6px -3px #bbb; } /* Handle stripes; */ @@ -143,14 +147,14 @@ content: ""; display: block; position: absolute; - height: 14px; + height: 65%; width: 1px; - background: #E8E7E6; - left: 14px; - top: 6px; + background: #e8e7e6; + left: 42%; + top: 20%; } .noUi-handle:after { - left: 17px; + left: 58%; } .noUi-vertical .noUi-handle:before, .noUi-vertical .noUi-handle:after { @@ -165,7 +169,7 @@ /* Disabled state; */ [disabled] .noUi-connect { - background: #B8B8B8; + background: #b8b8b8; } [disabled].noUi-target, [disabled].noUi-handle, @@ -201,13 +205,13 @@ */ .noUi-marker { position: absolute; - background: #CCC; + background: #ccc; } .noUi-marker-sub { - background: #AAA; + background: #aaa; } .noUi-marker-large { - background: #AAA; + background: #aaa; } /* Horizontal layout; * @@ -282,7 +286,7 @@ -webkit-transform: translate(-50%, 0); transform: translate(-50%, 0); left: 50%; - bottom: -120%; + top: calc(var(--resp-height) * 1.5); } .noUi-vertical .noUi-tooltip { -webkit-transform: translate(0, -50%); diff --git a/static/styles/similarity.css b/static/styles/similarity.css index 836ad6a..830cbc8 100644 --- a/static/styles/similarity.css +++ b/static/styles/similarity.css @@ -2,6 +2,11 @@ #similarityContainer { padding: 1.5em; margin: 1.5em 0; + font-size: var(--font-md); +} + +.form-select { + font-size: var(--font-md); } #categoryDropdownContainer { @@ -20,14 +25,13 @@ .btn-check, .btn-outline-secondary { - font-size: 1em; background-color: white; } -.btn-check:checked+.btn{ +.btn-check:checked + .btn { background-color: var(--color-accent); color: white; - border-color: var(--color-accent); + border-color: var(--color-accent); } .similarityControlContainer { @@ -35,6 +39,11 @@ flex-direction: column; align-items: center; gap: 0.5em; + font-size: var(--font-md); +} + +.similarityControlContainer label { + font-size: var(--font-md); } .btn-similarity { @@ -44,13 +53,13 @@ } .btn-check:checked + .btn-similarity:hover { - background-color: var(--color-accent) !important; + background-color: var(--color-accent) !important; border-color: var(--color-accent) !important; color: white !important; } .btn-similarity:hover { - background-color: #f8e6e6 !important; + background-color: #f8e6e6 !important; border-color: var(--color-accent) !important; color: #666 !important; } @@ -65,6 +74,7 @@ #thresholdSlider { width: 100%; + height: clamp(0.5rem, 0.3713rem + 0.5492vw, 1.25rem); } #thresholdValue { @@ -78,13 +88,17 @@ flex-direction: column; align-items: center; flex-shrink: 2; - font-size: 0.8em; + font-size: var(--font-md); +} + +#legend h4 { + font-size: var(--font-xl); } #legendNote { font-style: italic; - font-size: 0.8em; - margin-bottom: 0.5em + font-size: var(--font-sm); + margin-bottom: 0.5em; } #legendItems { @@ -126,6 +140,23 @@ opacity: 0.3; } +#thresholdInfoIcon { + width: var(--font-bg); +} + +#connectionsContainer, +#connectionsModal button { + font-size: var(--font-md); +} + +#connectionsContainer h5 { + font-size: var(--font-xl); +} + +#connectionsContainer th { + font-size: var(--font-bg); +} + .info-circle { font-weight: bold; color: var(--color-accent); @@ -145,4 +176,18 @@ #visualization-warning { display: flex; } -} \ No newline at end of file + + .controls { + flex-direction: column; + align-items: start; + } + + .sliderContainer { + width: 60%; + margin-bottom: 1em; + } + + .link { + stroke-width: 0.5; + } +} diff --git a/static/styles/tableView.css b/static/styles/tableView.css index 8c3abca..e0a5b89 100644 --- a/static/styles/tableView.css +++ b/static/styles/tableView.css @@ -16,7 +16,7 @@ background-color: var(--color-accent); color: white; border: none; - font-size: 1em; + font-size: var(--font-md); padding: 0.1em 0.4em; border-radius: 5px; cursor: pointer; @@ -41,7 +41,7 @@ color: #6c757d; opacity: 0.5; margin-left: 5px; - font-size: 0.8em; + font-size: var(--font-sm); white-space: pre; /* Preserves whitespace */ } @@ -49,4 +49,12 @@ .sort-arrows.active { color: inherit; opacity: 1; -} \ No newline at end of file +} + +#table thead { + font-size: var(--font-bg); +} + +#table tbody { + font-size: var(--font-md); +} diff --git a/static/styles/timeline.css b/static/styles/timeline.css index 8a68258..f26ae3c 100644 --- a/static/styles/timeline.css +++ b/static/styles/timeline.css @@ -2,7 +2,7 @@ padding: 1.5em; margin: 1.5em 0; } - + .timeline-controls { margin-bottom: 1.5em; display: flex; @@ -11,6 +11,21 @@ text-align: center; flex-wrap: wrap; gap: 1em; + font-size: var(--font-md); +} + +.timeline-controls label { + font-size: var(--font-md); + padding: calc(var(--resp-padding) * 0.5) calc(var(--resp-padding) * 2); + min-width: fit-content; +} + +.form-select { + font-size: var(--font-md); +} + +label[for="timelineColorCategory"] { + padding: 0; } .category-dropdown-container { @@ -27,7 +42,7 @@ .link { stroke: #999; - stroke-width: 1; + stroke-width: 0.5; stroke-opacity: 0.6; fill: none; } @@ -87,22 +102,6 @@ color: white; } -/* Timeline axis styling */ -.timeline-axis { - stroke: #ccc; - stroke-width: 2; -} - -.timeline-tick { - stroke: #ccc; - stroke-width: 1; -} - -.timeline-label { - font-size: 12px; - fill: #666; -} - /* Container dimensions */ #timeline-graph-container { width: 100%; @@ -125,7 +124,7 @@ .timeline-legend-item { display: flex; align-items: center; - font-size: 1em; + font-size: var(--font-md); } .timeline-legend-line { @@ -153,16 +152,16 @@ flex-direction: column; align-items: center; flex-shrink: 2; - font-size: 0.8em; + font-size: var(--font-md); } #legend h4 { - font-size: 1.2em; + font-size: var(--font-xl); } #legendNote { font-style: italic; - font-size: 0.8em; + font-size: var(--font-sm); margin-bottom: 0.5em; } @@ -180,10 +179,24 @@ align-items: center; } -#timelineConnectionsContainer, .centered-cell { +#timelineConnectionsContainer, +.centered-cell { text-align: center; vertical-align: middle; user-select: none; + font-size: var(--font-md); +} + +#timelineConnectionsModal button { + font-size: var(--font-md); +} + +#timelineConnectionsContainer th { + font-size: var(--font-bg); +} + +#timelineConnectionsContainer h5 { + font-size: var(--font-xl); } .node-tooltip { @@ -203,5 +216,25 @@ #visualization-warning { display: flex; } -} + .tick-line { + stroke: black; + } + + .tick-text { + fill: rgb(33, 37, 41); + } + + #timeline-container { + padding: var(--resp-padding); + } + + .timeline-controls { + flex-direction: column; + align-items: start; + } + + .link { + stroke-width: 0.5; + } +} diff --git a/templates/bar-chart.html b/templates/bar-chart.html index 1ea75dd..e66e61f 100644 --- a/templates/bar-chart.html +++ b/templates/bar-chart.html @@ -1,58 +1,87 @@ -{% extends "filter.html" %} - -{% block styles %} - {{ super() }} - -{% endblock styles %} - -{% block content %} - {{ super() }} -
    - - -
    -
    - -
    -
    -
    Some charts exceed the maximum bar threshold and are not displayed:
    - -
    +{% extends "filter.html" %} {% block styles %} {{ super() }} + +{% endblock styles %} {% block content %} {{ super() }} +
    + + +
    +
    + +
    +
    +
    Some charts exceed the maximum bar threshold and are not displayed:
    + +
    -