From 2b03b44b1bd11799c2448e5aa5d83d89940ff219 Mon Sep 17 00:00:00 2001 From: uwptv Date: Tue, 14 Oct 2025 14:13:23 +0200 Subject: [PATCH 01/24] introduced fixed font size classes: xl, big (bg), medium (md) and small (sm); applied font size classes to elements --- static/styles/base.css | 45 ++++++++++------- static/styles/filter.css | 93 +++++++++++++++++++----------------- static/styles/similarity.css | 13 ++--- static/styles/tableView.css | 10 +++- static/styles/timeline.css | 7 +-- templates/base.html | 4 +- 6 files changed, 100 insertions(+), 72 deletions(-) diff --git a/static/styles/base.css b/static/styles/base.css index 4a7aa4b..ad4c61a 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,11 @@ body { padding: 0; display: flex; flex-direction: row; - font-size: clamp(0.75rem, 0.6642rem + 0.3661vw, 1.25rem); + --resp-font-size: clamp(0.75rem, 0.6642rem + 0.3661vw, 1.25rem); + --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); } /* Hide elements that are only relevant for lower screen resolution */ @@ -26,14 +30,14 @@ 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; } #visualization-warning span { - text-align: center;; + text-align: center; } header { @@ -81,6 +85,10 @@ nav { 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 +107,7 @@ nav { } h3 { - font-size: 1.5em; + font-size: var(--font-xl); padding: 0; margin: 0; } @@ -117,7 +125,10 @@ h3 { padding: 5px; max-height: 100vh; overflow-y: scroll; - font-size: medium; +} + +.filter-group { + font-size: var(--font-md); } .panel { @@ -145,7 +156,7 @@ h3 { } /* Buttons */ -.add-study-button{ +.add-study-button { color: var(--color-accent); background-color: white; border: none; @@ -157,13 +168,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,7 +185,7 @@ h3 { } .btn-link, -.toggle-visibility-button { +.toggle-visibility-button { background-color: var(--color-accent); font-size: 1em; color: white; @@ -226,6 +237,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,13 +255,14 @@ 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; } @@ -340,6 +356,3 @@ a:link, a:visited { display: none; } } - - - diff --git a/static/styles/filter.css b/static/styles/filter.css index 5b9f88b..9e08249 100644 --- a/static/styles/filter.css +++ b/static/styles/filter.css @@ -1,70 +1,73 @@ .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; } #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/similarity.css b/static/styles/similarity.css index 836ad6a..a6d746f 100644 --- a/static/styles/similarity.css +++ b/static/styles/similarity.css @@ -24,10 +24,10 @@ 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 +35,7 @@ flex-direction: column; align-items: center; gap: 0.5em; + font-size: var(--font-md); } .btn-similarity { @@ -44,13 +45,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; } @@ -84,7 +85,7 @@ #legendNote { font-style: italic; font-size: 0.8em; - margin-bottom: 0.5em + margin-bottom: 0.5em; } #legendItems { @@ -145,4 +146,4 @@ #visualization-warning { display: flex; } -} \ No newline at end of file +} diff --git a/static/styles/tableView.css b/static/styles/tableView.css index 8c3abca..365b9a2 100644 --- a/static/styles/tableView.css +++ b/static/styles/tableView.css @@ -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..8ecab7c 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,7 @@ text-align: center; flex-wrap: wrap; gap: 1em; + font-size: var(--font-md); } .category-dropdown-container { @@ -180,7 +181,8 @@ align-items: center; } -#timelineConnectionsContainer, .centered-cell { +#timelineConnectionsContainer, +.centered-cell { text-align: center; vertical-align: middle; user-select: none; @@ -204,4 +206,3 @@ display: flex; } } - diff --git a/templates/base.html b/templates/base.html index 4f8f695..fa61af0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -314,7 +314,9 @@

{{ panel.value }}

class="filter-group mt-2 category" data-col="{{ category.value }}" > -
+
{# Add an explanation circle for every category #}
{{ category.value.split('_')[-1] }} From 35f020633e4d5e578e860d56cefd9739fd253408 Mon Sep 17 00:00:00 2001 From: uwptv Date: Tue, 14 Oct 2025 15:05:41 +0200 Subject: [PATCH 02/24] Adjusted some font sizes and flex values for lower screen resolutions --- static/styles/base.css | 40 ++++++++++++++++++++++++++++++------ static/styles/similarity.css | 11 ++++++++++ static/styles/timeline.css | 14 +++++++++++++ templates/base.html | 10 ++++++--- 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/static/styles/base.css b/static/styles/base.css index ad4c61a..2309fcd 100644 --- a/static/styles/base.css +++ b/static/styles/base.css @@ -16,6 +16,7 @@ body { --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.75rem, 0.6642rem + 0.3661vw, 1.25rem); } /* Hide elements that are only relevant for lower screen resolution */ @@ -45,7 +46,7 @@ header { flex-direction: row; justify-content: space-between; align-items: center; - gap: 1em; + gap: var(--resp-padding); } .left-section { @@ -65,6 +66,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; @@ -80,7 +90,7 @@ nav { } .navbar-item { - padding: 1em; + padding: var(--resp-padding); text-align: center; border-radius: 5px; /* Rounded corners */ } @@ -267,7 +277,7 @@ a:visited { color: inherit; } -@media (max-width: 1050px) { +@media (max-width: 1200px) { /* Hide the sidebar from the user */ #sidebar { position: fixed; @@ -301,7 +311,7 @@ a:visited { } #close-icon { - width: 2em; + width: 1.25rem; } /* Add a mask over the content when the sidebar is open */ @@ -333,7 +343,7 @@ a:visited { } } -@media (max-width: 630px) { +@media (max-width: 860px) { /* Change the navbar links from text to icons */ .navbar-text { display: none; @@ -351,8 +361,26 @@ a:visited { } } -@media (max-width: 450px) { +@media (max-width: 650px) { + #nav-r { + gap: 0.5rem; + } + #earXplore-repo { display: none; } + + .link-section { + gap: 0em; + } +} + +@media (max-width: 560px) { + header { + gap: 0; + } + + #open-earable-repo { + display: none; + } } diff --git a/static/styles/similarity.css b/static/styles/similarity.css index a6d746f..84b6f65 100644 --- a/static/styles/similarity.css +++ b/static/styles/similarity.css @@ -2,6 +2,7 @@ #similarityContainer { padding: 1.5em; margin: 1.5em 0; + font-size: calc(var(--font-sm) * 1); } #categoryDropdownContainer { @@ -146,4 +147,14 @@ #visualization-warning { display: flex; } + + .controls { + flex-direction: column; + align-items: center; + } + + .sliderContainer { + width: 60%; + margin-bottom: 1em; + } } diff --git a/static/styles/timeline.css b/static/styles/timeline.css index 8ecab7c..3140ea7 100644 --- a/static/styles/timeline.css +++ b/static/styles/timeline.css @@ -14,6 +14,16 @@ 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; +} + +label[for="timelineColorCategory"] { + padding: 0; +} + .category-dropdown-container { display: flex; align-items: center; @@ -205,4 +215,8 @@ #visualization-warning { display: flex; } + + .timeline-controls { + flex-direction: column; + } } diff --git a/templates/base.html b/templates/base.html index fa61af0..40cdaa3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -98,7 +98,11 @@
{# Add Study Button #} -
+ {# Dynamic View Section #} {% block content %} {% endblock content %} From f27118fc8a13c92fbf1adc83d44e6f46b28df003 Mon Sep 17 00:00:00 2001 From: uwptv Date: Wed, 15 Oct 2025 14:59:22 +0200 Subject: [PATCH 03/24] adjusted visuals for d3 data graphs on lower screen resolutions, made font sizes consistent --- static/scripts/similarity.mjs | 463 ++++++++++++++++++++++------------ static/scripts/timeline.mjs | 332 ++++++++++++++++-------- static/styles/base.css | 2 + static/styles/similarity.css | 15 +- static/styles/timeline.css | 21 +- 5 files changed, 540 insertions(+), 293 deletions(-) diff --git a/static/scripts/similarity.mjs b/static/scripts/similarity.mjs index 3fe1d92..a0913d0 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, }; - }; + } } /* @@ -267,7 +304,9 @@ function drawGraph(threshold) { // 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; } @@ -279,20 +318,27 @@ function drawGraph(threshold) { $("#graphContainer").height(height); // 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); + drawULayout(svg, { nodes, links }, colorScale); } else { // Draw links, nodes, and labels for standard layout - drawStandardLayout(svg, {nodes, links}, colorScale); + drawStandardLayout(svg, { nodes, links }, colorScale); } // Draw the legend - createLegend(nodes, colorScale, $("#similarityColorCategory").val(), $("#legend")); + createLegend( + nodes, + colorScale, + $("#similarityColorCategory").val(), + $("#legend") + ); findSimilarStudies(links); } @@ -301,75 +347,106 @@ function drawULayout(container, graphData, colorScale) { const { nodes, links } = graphData; // Define constants for the layout - const margin = { top: 20, right: 20, bottom: 20, left: 20 }; + let margin; + if (window.innerWidth <= 650) { + margin = { top: 5, right: 5, bottom: 5, left: 5 }; + } else { + 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 topAxisHeight = height / 4; const axisMiddle = height / 2; // 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 topNodes = nodes.filter( + (node) => nodes.indexOf(node) <= nodes.length / 2 + ); + const bottomNodes = 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 topScale = d3.scalePoint().domain(topNodes).rangeRound([0, width]); // Create a scale for the bottom nodes - const bottomScale = d3.scalePoint() + const bottomScale = d3 + .scalePoint() .domain(bottomNodes) .rangeRound([0, width]); // 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) + const topAxis = d3 + .axisTop(topScale) .tickValues(topNodes) - .tickFormat(d => "") + .tickFormat((d) => "") .tickSize(0) .tickPadding(-4); // Create the bottom axis for the nodes - const bottomAxis = d3.axisBottom(bottomScale) + const bottomAxis = d3 + .axisBottom(bottomScale) .tickValues(bottomNodes) - .tickFormat(d => "") + .tickFormat((d) => "") .tickSize(0) .tickPadding(-4); // Draw the top axis - container.append("g") - .attr("transform", `translate(${margin.left}, ${topAxisHeight + margin.top})`) // Position the axis at the top + container + .append("g") + .attr( + "transform", + `translate(${margin.left}, ${topAxisHeight + margin.top})` + ) // Position the axis at the top .attr("class", "top-axis") .call(topAxis); // Draw the bottom axis - container.append("g") + container + .append("g") .attr("transform", `translate(${margin.left}, ${height - topAxisHeight})`) // Position the axis at the bottom .attr("class", "bottom-axis") .call(bottomAxis); // 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) => + `${formatTickLabel( + d + )}` + ); // 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) => + `${formatTickLabel( + d + )}` + ); // Rotate the axis labels for better readability and adjust the position - container.select(".top-axis") + 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") + container + .select(".bottom-axis") .selectAll("text") .attr("text-anchor", "end") .attr("transform", "rotate(-90)") @@ -377,91 +454,122 @@ function drawULayout(container, graphData, colorScale) { // 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") + const linkGroup = container + .append("g") .attr("class", "links") .attr("transform", `translate(${margin.left}, ${margin.top})`); // Create a group for the top and bottom nodes - const nodeGroup = container.append("g") + const nodeGroup = container + .append("g") .attr("class", "nodes") .attr("transform", `translate(${margin.left}, ${margin.top})`); // Draw the top nodes and add click and hover events - nodeGroup.selectAll(".node") - .data(topNodes, d => d) + nodeGroup + .selectAll(".node") + .data(topNodes, (d) => d) .enter() .append("g") .attr("class", "node") - .attr("transform", d => `translate(${topScale(d)}, ${topAxisHeight})`) - .each(function(d) { + .attr("transform", (d) => `translate(${topScale(d)}, ${topAxisHeight})`) + .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(bottomNodes, (d) => d) .enter() .append("g") .attr("class", "node") - .attr("transform", d => `translate(${bottomScale(d)}, ${height - topAxisHeight - margin.bottom})`) - .each(function(d) { + .attr( + "transform", + (d) => + `translate(${bottomScale(d)}, ${ + height - topAxisHeight - margin.bottom + })` + ) + .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); + 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; + 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}`; + 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}`; + return `M ${sourceX} ${sourceY} Q ${(sourceX + targetX) / 2} ${ + axisMiddle - margin.top + }, ${targetX} ${targetY}`; } else { return `M ${sourceX} ${sourceY} C ${sourceX} ${axisMiddle}, ${targetX} ${axisMiddle}, ${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 + }]` + ); } // Draws the standard layout for the similarity graph @@ -476,130 +584,149 @@ function drawStandardLayout(container, graphData, colorScale) { const nodeRadius = Math.floor(width / 150); // Create a scale for the node positions - const xScale = d3.scalePoint() - .domain(nodes) - .rangeRound([0, width]); + const xScale = d3.scalePoint().domain(nodes).rangeRound([0, width]); // Create an axis for the nodes to be displayed horizontally - const axis = d3.axisBottom(xScale) + const axis = d3 + .axisBottom(xScale) .tickValues(nodes) - .tickFormat(d => "") + .tickFormat((d) => "") .tickSize(0) .tickPadding(-4); // Draw the axis - container.append("g") + container + .append("g") .attr("class", "axis") .attr("transform", `translate(${margin.left}, ${axisHeight + margin.top})`) // 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("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") + const linkGroup = container + .append("g") .attr("transform", `translate(${margin.left}, ${margin.top})`) .attr("class", "links"); // Create a group for the nodes - const nodeGroup = container.append("g") + const nodeGroup = container + .append("g") .attr("transform", `translate(${margin.left}, ${axisHeight + margin.top})`) .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) => `translate(${xScale(d)}, 0)`) + .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 => { + .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}`; + return `M ${sourceX} ${axisHeight} Q ${(sourceX + targetX) / 2} ${ + axisHeight - arcHeight - 2 * margin.top + }, ${targetX} ${axisHeight}`; }); // 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 + }]` + ); } /* 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 +736,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 +745,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..21d90dd 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,127 @@ 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; + if (window.innerWidth <= 650) { + margin = { top: 5, right: 10, bottom: 30, left: 10 }; + } else { + margin = { top: 20, right: 50, bottom: 50, left: 50 }; + } + const containerWidth = $("#timeline-graph-container").width(); + const innerWidth = containerWidth - margin.left - margin.right; + + // Base node radius scaled to container width + const baseNodeRadius = innerWidth / 150; + + // Calculate height based on max years count + const height = Math.max(400, maxYearsCount * (baseNodeRadius * 4)); 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") + // Determine zoom factor based on screen width + let zoomFactor = 1; + let nodeRadius = baseNodeRadius; + + if (window.innerWidth >= 1200) { + // Medium desktop screens + zoomFactor = 0.85; + nodeRadius = baseNodeRadius * 0.95; + } + + // Calculate adjusted viewBox dimensions + const viewBoxWidth = containerWidth / zoomFactor; + const viewBoxHeight = height / zoomFactor; + + // Calculate viewBox offset to center the content + const xOffset = (containerWidth - viewBoxWidth) / 2; + const yOffset = (height - viewBoxHeight) / 2; + + // get the current font size + const responsiveFontSize = getComputedStyle(document.body) + .getPropertyValue("--resp-font-ticks") + .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", `${xOffset} ${yOffset} ${viewBoxWidth} ${viewBoxHeight}`) + .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]); // Create an x axis for the years - const xAxis = d3.axisBottom(xScale) - .tickFormat(d3.format("d")) - .tickSize(5); + const xAxis = d3.axisBottom(xScale).tickFormat(d3.format("d")).tickSize(5); // Draw the x axis - svg.append("g") + svg + .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") + svg + .selectAll(".x-axis text") + .style("font-size", responsiveFontSize) .style("user-select", "none"); // Create a link group for the links - const linkGroup = svg.append("g") + const linkGroup = svg + .append("g") .attr("class", "links") .attr("transform", `translate(${margin.left}, ${margin.top})`); // Create a node group for the nodes - const nodeGroup = svg.append("g") + const nodeGroup = svg + .append("g") .attr("class", "nodes") .attr("transform", `translate(${margin.left}, ${margin.top})`); - 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 +421,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 +480,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 +497,80 @@ 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") + ); } -$(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/base.css b/static/styles/base.css index 2309fcd..50d06ed 100644 --- a/static/styles/base.css +++ b/static/styles/base.css @@ -11,7 +11,9 @@ body { padding: 0; display: flex; flex-direction: row; + --resp-font-size: clamp(0.75rem, 0.6642rem + 0.3661vw, 1.25rem); + --resp-font-ticks: clamp(0.5rem, 0.4142rem + 0.3661vw, 1rem); --font-xl: calc(var(--resp-font-size) * 1.25); --font-bg: calc(var(--resp-font-size) * 1.15); --font-md: var(--resp-font-size); diff --git a/static/styles/similarity.css b/static/styles/similarity.css index 84b6f65..ad36eab 100644 --- a/static/styles/similarity.css +++ b/static/styles/similarity.css @@ -2,7 +2,11 @@ #similarityContainer { padding: 1.5em; margin: 1.5em 0; - font-size: calc(var(--font-sm) * 1); + font-size: var(--font-md); +} + +.form-select { + font-size: var(--font-md); } #categoryDropdownContainer { @@ -21,7 +25,6 @@ .btn-check, .btn-outline-secondary { - font-size: 1em; background-color: white; } @@ -80,12 +83,16 @@ 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; + font-size: var(--font-sm); margin-bottom: 0.5em; } diff --git a/static/styles/timeline.css b/static/styles/timeline.css index 3140ea7..62670b6 100644 --- a/static/styles/timeline.css +++ b/static/styles/timeline.css @@ -20,6 +20,10 @@ min-width: fit-content; } +.form-select { + font-size: var(--font-md); +} + label[for="timelineColorCategory"] { padding: 0; } @@ -109,11 +113,6 @@ label[for="timelineColorCategory"] { stroke-width: 1; } -.timeline-label { - font-size: 12px; - fill: #666; -} - /* Container dimensions */ #timeline-graph-container { width: 100%; @@ -136,7 +135,7 @@ label[for="timelineColorCategory"] { .timeline-legend-item { display: flex; align-items: center; - font-size: 1em; + font-size: var(--font-md); } .timeline-legend-line { @@ -164,16 +163,16 @@ label[for="timelineColorCategory"] { 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; } @@ -216,6 +215,10 @@ label[for="timelineColorCategory"] { display: flex; } + #timeline-container { + padding: var(--resp-padding); + } + .timeline-controls { flex-direction: column; } From 39c75a061f1dd3dfb948cbbbc44f4da01e2b9ce1 Mon Sep 17 00:00:00 2001 From: uwptv Date: Wed, 15 Oct 2025 15:02:05 +0200 Subject: [PATCH 04/24] font size properties are now used everywhere --- static/styles/base.css | 4 ++-- static/styles/tableView.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/static/styles/base.css b/static/styles/base.css index 50d06ed..ebbd61e 100644 --- a/static/styles/base.css +++ b/static/styles/base.css @@ -199,7 +199,7 @@ h3 { .btn-link, .toggle-visibility-button { background-color: var(--color-accent); - font-size: 1em; + font-size: var(--font-md); color: white; border: none; text-decoration: none; @@ -232,7 +232,7 @@ h3 { border-radius: 5px; text-decoration: none; opacity: 1; - font-size: 1rem; + font-size: var(--font-md); border: 1px solid var(--color-accent); } diff --git a/static/styles/tableView.css b/static/styles/tableView.css index 365b9a2..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 */ } From ccff0d389d71fb0b80927489fa377ecc281d09e3 Mon Sep 17 00:00:00 2001 From: uwptv Date: Thu, 16 Oct 2025 15:13:10 +0200 Subject: [PATCH 05/24] added zoom functionality --- static/scripts/similarity.mjs | 92 ++++++++++++++++++++--------------- static/scripts/timeline.mjs | 78 ++++++++++++++++++++++++----- static/styles/base.css | 2 +- 3 files changed, 119 insertions(+), 53 deletions(-) diff --git a/static/scripts/similarity.mjs b/static/scripts/similarity.mjs index a0913d0..da5dba8 100644 --- a/static/scripts/similarity.mjs +++ b/static/scripts/similarity.mjs @@ -298,7 +298,6 @@ function drawGraph(threshold) { $("#graphContainer").height("auto"); $("#legend").empty(); - // TODO: change to slider value const { sortedNodes, links, colorScale } = generateGraphData(threshold); const nodes = [...sortedNodes]; @@ -313,7 +312,7 @@ function drawGraph(threshold) { // Determine graph dimensions const useULayout = nodes.length > 50; // Use U-Layout for larger graphs - const height = useULayout ? 800 : 500; + const height = useULayout ? 1000 : 500; $("#graphContainer").height(height); @@ -322,7 +321,6 @@ function drawGraph(threshold) { .select("#graphContainer") .append("svg") .attr("width", "100%") - .attr("height", height) .attr("viewBox", `0 0 ${$("#graphContainer").width()} ${height}`); if (useULayout) { @@ -351,7 +349,7 @@ function drawULayout(container, graphData, colorScale) { if (window.innerWidth <= 650) { margin = { top: 5, right: 5, bottom: 5, left: 5 }; } else { - margin = { top: 20, right: 20, bottom: 20, left: 20 }; + margin = { top: 50, right: 20, bottom: 50, left: 20 }; } const height = parseInt($("svg").height()) - margin.top - margin.bottom; const width = parseInt($("svg").width()) - margin.left - margin.right; @@ -399,20 +397,18 @@ function drawULayout(container, graphData, colorScale) { .tickSize(0) .tickPadding(-4); + // Append group element for zooming + const g = container.append("g"); + // 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", `translate(0, ${topAxisHeight})`) // Position the axis at the top .attr("class", "top-axis") .call(topAxis); // 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", `translate(0, ${3 * topAxisHeight})`) // Position the axis at the bottom .attr("class", "bottom-axis") .call(bottomAxis); @@ -462,16 +458,10 @@ function drawULayout(container, graphData, colorScale) { .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 @@ -501,10 +491,7 @@ function drawULayout(container, graphData, colorScale) { .attr("class", "node") .attr( "transform", - (d) => - `translate(${bottomScale(d)}, ${ - height - topAxisHeight - margin.bottom - })` + (d) => `translate(${bottomScale(d)}, ${3 * topAxisHeight})` ) .each(function (d) { drawNode(d3.select(this), colorCategory, arc, colorScale); @@ -538,12 +525,8 @@ function drawULayout(container, graphData, colorScale) { : 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; + const sourceY = isSourceTop ? topAxisHeight : 3 * topAxisHeight; + const targetY = isTargetTop ? topAxisHeight : 3 * topAxisHeight; if (sourceX === targetX && isSourceTop) { return `M ${sourceX} ${sourceY} Q ${(sourceX + targetX) / 2} ${ @@ -570,6 +553,19 @@ function drawULayout(container, graphData, colorScale) { d.targetID }]` ); + + const zoom = d3.zoom().scaleExtent([0.8, 10]).on("zoom", zoomed); + + container.call(zoom).call(zoom.transform, d3.zoomIdentity); + + function zoomed({ transform }) { + g.attr( + "transform", + `translate (${margin.left + transform.x}, ${ + margin.top + transform.y + }) scale(${transform.k})` + ); + } } // Draws the standard layout for the similarity graph @@ -594,11 +590,17 @@ function drawStandardLayout(container, graphData, colorScale) { .tickSize(0) .tickPadding(-4); + 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", `translate(0, ${axisHeight})`) // Position the axis in the middle of the graph .call(axis); d3.selectAll("text").html( @@ -624,15 +626,12 @@ function drawStandardLayout(container, graphData, colorScale) { .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 + const nodeGroup = g .append("g") - .attr("transform", `translate(${margin.left}, ${axisHeight + margin.top})`) + .attr("transform", `translate(0, ${axisHeight})`) .attr("class", "nodes"); const arc = d3.arc().innerRadius(0).outerRadius(nodeRadius); @@ -685,6 +684,19 @@ function drawStandardLayout(container, graphData, colorScale) { d.targetID }]` ); + + const zoom = d3.zoom().scaleExtent([1, 10]).on("zoom", zoomed); + + container.call(zoom).call(zoom.transform, d3.zoomIdentity); + + function zoomed({ transform }) { + g.attr( + "transform", + `translate(${margin.left + transform.x}, ${ + margin.top + transform.y + }) scale(${transform.k})` + ); + } } /* diff --git a/static/scripts/timeline.mjs b/static/scripts/timeline.mjs index 21d90dd..b9a47ed 100644 --- a/static/scripts/timeline.mjs +++ b/static/scripts/timeline.mjs @@ -362,30 +362,68 @@ function drawTimelineGraph() { .domain([0, maxYears]) .range([axisHeight - margin.bottom, 0]); + // Append group element to svg for zooming + const g = svg.append("g"); + // Create an x axis for the years - const xAxis = d3.axisBottom(xScale).tickFormat(d3.format("d")).tickSize(5); + const xAxis = d3.axisBottom(xScale).tickFormat(d3.format("d")).tickSize(0); // 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); + .call(xAxis) + .call((g) => { + // Keep only the domain line and remove default ticks + g.select(".domain").attr("stroke", "#000"); + g.selectAll(".tick text").remove(); + g.selectAll(".tick line").remove(); + }); - // Format the labels for the axis - svg - .selectAll(".x-axis text") - .style("font-size", responsiveFontSize) - .style("user-select", "none"); + // 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(${margin.left + xScale(d)}, ${axisHeight + margin.top})` + ) + .each(function (d, i) { + const isAbove = i % 2 === 0; // Alternates based on index + const tick = d3.select(this); + + // Add tick line + tick + .append("line") + .attr("class", "tick-line") + .attr("y1", isAbove ? 0.5 : -0.5) + .attr("y2", isAbove ? -5 : 5) + .attr("stroke", "#000"); + + // Add year text + tick + .append("text") + .attr("class", "tick-text") + .attr("text-anchor", "middle") + .attr("dy", isAbove ? "-0.71em" : "1.4em") + .style("font-size", responsiveFontSize) + .style("user-select", "none") + .text(d); + }); // Create a link group for the links - const linkGroup = svg + const linkGroup = g .append("g") .attr("class", "links") .attr("transform", `translate(${margin.left}, ${margin.top})`); // Create a node group for the nodes - const nodeGroup = svg + const nodeGroup = g .append("g") .attr("class", "nodes") .attr("transform", `translate(${margin.left}, ${margin.top})`); @@ -523,6 +561,22 @@ function drawTimelineGraph() { colorCategory, $("#legend") ); + + // Add zooming functionality + const zoom = d3 + .zoom() + .scaleExtent([1, 10]) + .translateExtent([ + [0, 0], + [containerWidth, height], + ]) + .on("zoom", zoomed); + + svg.call(zoom); + + function zoomed({ target, type, transform, sourceEvent }) { + g.attr("transform", transform); + } } $(document).ready(function () { diff --git a/static/styles/base.css b/static/styles/base.css index ebbd61e..d93ee4e 100644 --- a/static/styles/base.css +++ b/static/styles/base.css @@ -13,7 +13,7 @@ body { flex-direction: row; --resp-font-size: clamp(0.75rem, 0.6642rem + 0.3661vw, 1.25rem); - --resp-font-ticks: clamp(0.5rem, 0.4142rem + 0.3661vw, 1rem); + --resp-font-ticks: clamp(0.25rem, 0.1213rem + 0.5492vw, 1rem); --font-xl: calc(var(--resp-font-size) * 1.25); --font-bg: calc(var(--resp-font-size) * 1.15); --font-md: var(--resp-font-size); From febfffb599bcd44d6e48f43d071e4ec6abcc8dd7 Mon Sep 17 00:00:00 2001 From: uwptv Date: Thu, 16 Oct 2025 15:27:05 +0200 Subject: [PATCH 06/24] adjusted tick sizes and labels for timeline view --- static/scripts/timeline.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/scripts/timeline.mjs b/static/scripts/timeline.mjs index b9a47ed..1e7e2da 100644 --- a/static/scripts/timeline.mjs +++ b/static/scripts/timeline.mjs @@ -402,7 +402,7 @@ function drawTimelineGraph() { .append("line") .attr("class", "tick-line") .attr("y1", isAbove ? 0.5 : -0.5) - .attr("y2", isAbove ? -5 : 5) + .attr("y2", isAbove ? "-0.5rem" : "0.5rem") .attr("stroke", "#000"); // Add year text @@ -410,7 +410,7 @@ function drawTimelineGraph() { .append("text") .attr("class", "tick-text") .attr("text-anchor", "middle") - .attr("dy", isAbove ? "-0.71em" : "1.4em") + .attr("dy", isAbove ? "-0.5rem" : "1rem") .style("font-size", responsiveFontSize) .style("user-select", "none") .text(d); From 92e4f49301cf1bee7589f13497699fd85e9329ee Mon Sep 17 00:00:00 2001 From: uwptv Date: Thu, 16 Oct 2025 15:33:48 +0200 Subject: [PATCH 07/24] changed ticks text size in timeline view --- static/scripts/timeline.mjs | 2 +- static/styles/base.css | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/static/scripts/timeline.mjs b/static/scripts/timeline.mjs index 1e7e2da..11a4d7c 100644 --- a/static/scripts/timeline.mjs +++ b/static/scripts/timeline.mjs @@ -338,7 +338,7 @@ function drawTimelineGraph() { // get the current font size const responsiveFontSize = getComputedStyle(document.body) - .getPropertyValue("--resp-font-ticks") + .getPropertyValue("--resp-font-ticks-bg") .trim(); // Create the svg container for the timeline graph with responsive viewBox diff --git a/static/styles/base.css b/static/styles/base.css index d93ee4e..2873655 100644 --- a/static/styles/base.css +++ b/static/styles/base.css @@ -14,6 +14,7 @@ body { --resp-font-size: clamp(0.75rem, 0.6642rem + 0.3661vw, 1.25rem); --resp-font-ticks: clamp(0.25rem, 0.1213rem + 0.5492vw, 1rem); + --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); From f3fa36bf034bebc41c2ea01d8e0860546e1ee7b2 Mon Sep 17 00:00:00 2001 From: uwptv Date: Fri, 17 Oct 2025 11:49:36 +0200 Subject: [PATCH 08/24] Alternating ticks only appear on lower screen resolutions --- static/scripts/timeline.mjs | 106 ++++++++++++++++++------------------ static/styles/timeline.css | 8 +++ 2 files changed, 62 insertions(+), 52 deletions(-) diff --git a/static/scripts/timeline.mjs b/static/scripts/timeline.mjs index 11a4d7c..c1a2060 100644 --- a/static/scripts/timeline.mjs +++ b/static/scripts/timeline.mjs @@ -366,67 +366,64 @@ function drawTimelineGraph() { const g = svg.append("g"); // Create an x axis for the years - const xAxis = d3.axisBottom(xScale).tickFormat(d3.format("d")).tickSize(0); // Set tickSize to 0 as we'll draw our own ticks + const xAxis = d3 + .axisBottom(xScale) + .tickFormat(d3.format("d")) + .tickSize(window.innerWidth <= 750 ? 0 : 5); // Set tickSize to 0 as we'll draw our own ticks // Draw just the x axis line g.append("g") .attr("class", "x-axis") - .attr("transform", `translate(${margin.left}, ${axisHeight + margin.top})`) + .attr("transform", `translate(0, ${axisHeight})`) .call(xAxis) .call((g) => { - // Keep only the domain line and remove default ticks - g.select(".domain").attr("stroke", "#000"); - g.selectAll(".tick text").remove(); - g.selectAll(".tick line").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(${margin.left + xScale(d)}, ${axisHeight + margin.top})` - ) - .each(function (d, i) { - const isAbove = i % 2 === 0; // Alternates based on index - const tick = d3.select(this); - - // Add tick line - tick - .append("line") - .attr("class", "tick-line") - .attr("y1", isAbove ? 0.5 : -0.5) - .attr("y2", isAbove ? "-0.5rem" : "0.5rem") - .attr("stroke", "#000"); - - // Add year text - tick - .append("text") - .attr("class", "tick-text") - .attr("text-anchor", "middle") - .attr("dy", isAbove ? "-0.5rem" : "1rem") - .style("font-size", responsiveFontSize) - .style("user-select", "none") - .text(d); + // 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 = g - .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 = g - .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); @@ -572,10 +569,15 @@ function drawTimelineGraph() { ]) .on("zoom", zoomed); - svg.call(zoom); + svg.call(zoom).call(zoom.transform, d3.zoomIdentity); function zoomed({ target, type, transform, sourceEvent }) { - g.attr("transform", transform); + g.attr( + "transform", + `translate(${margin.left + transform.x}, ${ + margin.top + transform.y + }) scale(${transform.k})` + ); } } diff --git a/static/styles/timeline.css b/static/styles/timeline.css index 62670b6..3bf5896 100644 --- a/static/styles/timeline.css +++ b/static/styles/timeline.css @@ -215,6 +215,14 @@ label[for="timelineColorCategory"] { display: flex; } + .tick-line { + stroke: black; + } + + .tick-text { + fill: rgb(33, 37, 41); + } + #timeline-container { padding: var(--resp-padding); } From 4b2ba3b3cb96581dcb1421e2c253a18d2b2846d5 Mon Sep 17 00:00:00 2001 From: uwptv Date: Fri, 17 Oct 2025 14:00:44 +0200 Subject: [PATCH 09/24] nodes now increase in radius on smaller screens --- static/scripts/similarity.mjs | 36 +++++++++++++++------------ static/scripts/timeline.mjs | 47 ++++++++--------------------------- static/styles/timeline.css | 15 +++-------- 3 files changed, 35 insertions(+), 63 deletions(-) diff --git a/static/scripts/similarity.mjs b/static/scripts/similarity.mjs index da5dba8..6b74ea7 100644 --- a/static/scripts/similarity.mjs +++ b/static/scripts/similarity.mjs @@ -312,9 +312,13 @@ function drawGraph(threshold) { // Determine graph dimensions const useULayout = nodes.length > 50; // Use U-Layout for larger graphs - const height = useULayout ? 1000 : 500; + // Define constants for the layout + const margin = + window.innerWidth <= 750 + ? { top: 5, right: 5, bottom: 5, left: 5 } + : { top: 50, right: 20, bottom: 50, left: 20 }; - $("#graphContainer").height(height); + const height = $("#graphContainer").width(); // Create SVG with calculated dimensions const svg = d3 @@ -324,10 +328,10 @@ function drawGraph(threshold) { .attr("viewBox", `0 0 ${$("#graphContainer").width()} ${height}`); if (useULayout) { - drawULayout(svg, { nodes, links }, colorScale); + drawULayout(svg, margin, { nodes, links }, colorScale); } else { // Draw links, nodes, and labels for standard layout - drawStandardLayout(svg, { nodes, links }, colorScale); + drawStandardLayout(svg, margin, { nodes, links }, colorScale); } // Draw the legend @@ -341,22 +345,19 @@ function drawGraph(threshold) { findSimilarStudies(links); } -function drawULayout(container, graphData, colorScale) { +function drawULayout(container, margin, graphData, colorScale) { const { nodes, links } = graphData; - // Define constants for the layout - let margin; - if (window.innerWidth <= 650) { - margin = { top: 5, right: 5, bottom: 5, left: 5 }; - } else { - margin = { top: 50, right: 20, bottom: 50, 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; + // Base node radius + const baseNodeRadius = 5; + // Node radius should scale with smaller screen size, 7 is maximum since nodes would overlap + const nodeRadius = Math.min(7, baseNodeRadius + 800 / width); + // Split the nodes into two groups based on their IDs const topNodes = nodes.filter( (node) => nodes.indexOf(node) <= nodes.length / 2 @@ -569,15 +570,18 @@ function drawULayout(container, graphData, colorScale) { } // Draws the standard layout for the similarity graph -function drawStandardLayout(container, graphData, colorScale) { +function drawStandardLayout(container, margin, graphData, colorScale) { 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); + + // Base node radius + const baseNodeRadius = 5; + // Node radius should scale with smaller screen size, 7 is maximum since nodes would overlap + const nodeRadius = Math.min(7, baseNodeRadius + 800 / width); // Create a scale for the node positions const xScale = d3.scalePoint().domain(nodes).rangeRound([0, width]); diff --git a/static/scripts/timeline.mjs b/static/scripts/timeline.mjs index c1a2060..a604a47 100644 --- a/static/scripts/timeline.mjs +++ b/static/scripts/timeline.mjs @@ -301,41 +301,23 @@ function drawTimelineGraph() { } // Set up layout dimensions for the graph - let margin; - if (window.innerWidth <= 650) { - margin = { top: 5, right: 10, bottom: 30, left: 10 }; - } else { - margin = { top: 20, right: 50, bottom: 50, left: 50 }; - } + let margin = + window.innerWidth <= 750 + ? { top: 5, right: 10, bottom: 30, left: 10 } + : { top: 20, right: 50, bottom: 20, left: 50 }; const containerWidth = $("#timeline-graph-container").width(); const innerWidth = containerWidth - margin.left - margin.right; - // Base node radius scaled to container width - const baseNodeRadius = innerWidth / 150; + // Base node radius + const baseNodeRadius = 5; + // Node radius should scale with smaller screen size, 7 is maximum since nodes would overlap + const nodeRadius = Math.min(7, baseNodeRadius + 800 / containerWidth); // Calculate height based on max years count - const height = Math.max(400, maxYearsCount * (baseNodeRadius * 4)); + const height = Math.max(400, maxYearsCount * (nodeRadius * 4)); const innerHeight = height - margin.top - margin.bottom; const axisHeight = innerHeight; - // Determine zoom factor based on screen width - let zoomFactor = 1; - let nodeRadius = baseNodeRadius; - - if (window.innerWidth >= 1200) { - // Medium desktop screens - zoomFactor = 0.85; - nodeRadius = baseNodeRadius * 0.95; - } - - // Calculate adjusted viewBox dimensions - const viewBoxWidth = containerWidth / zoomFactor; - const viewBoxHeight = height / zoomFactor; - - // Calculate viewBox offset to center the content - const xOffset = (containerWidth - viewBoxWidth) / 2; - const yOffset = (height - viewBoxHeight) / 2; - // get the current font size const responsiveFontSize = getComputedStyle(document.body) .getPropertyValue("--resp-font-ticks-bg") @@ -347,7 +329,7 @@ function drawTimelineGraph() { .append("svg") .attr("width", containerWidth) .attr("height", height) - .attr("viewBox", `${xOffset} ${yOffset} ${viewBoxWidth} ${viewBoxHeight}`) + .attr("viewBox", `${margin.left} ${margin.top} ${innerWidth} ${height}`) .attr("preserveAspectRatio", "xMidYMid meet"); // Create an x scale for the years @@ -560,14 +542,7 @@ function drawTimelineGraph() { ); // Add zooming functionality - const zoom = d3 - .zoom() - .scaleExtent([1, 10]) - .translateExtent([ - [0, 0], - [containerWidth, height], - ]) - .on("zoom", zoomed); + const zoom = d3.zoom().scaleExtent([0.5, 10]).on("zoom", zoomed); svg.call(zoom).call(zoom.transform, d3.zoomIdentity); diff --git a/static/styles/timeline.css b/static/styles/timeline.css index 3bf5896..2acacc1 100644 --- a/static/styles/timeline.css +++ b/static/styles/timeline.css @@ -102,17 +102,6 @@ label[for="timelineColorCategory"] { color: white; } -/* Timeline axis styling */ -.timeline-axis { - stroke: #ccc; - stroke-width: 2; -} - -.timeline-tick { - stroke: #ccc; - stroke-width: 1; -} - /* Container dimensions */ #timeline-graph-container { width: 100%; @@ -230,4 +219,8 @@ label[for="timelineColorCategory"] { .timeline-controls { flex-direction: column; } + + .link { + stroke-width: 0.5; + } } From 23ffe6bd0a6abb97f25adc42677954664a88a003 Mon Sep 17 00:00:00 2001 From: uwptv Date: Fri, 17 Oct 2025 14:09:31 +0200 Subject: [PATCH 10/24] decreased stroke width so focus is more on nodes --- static/styles/similarity.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/styles/similarity.css b/static/styles/similarity.css index ad36eab..38d0e60 100644 --- a/static/styles/similarity.css +++ b/static/styles/similarity.css @@ -150,6 +150,10 @@ margin-bottom: 1em; /* Add some space below the visualization */ } +.link { + stroke-width: 0.5; +} + @media (max-width: 850px) { #visualization-warning { display: flex; From 626281a09c700f4557bc6828fddd918b879005fe Mon Sep 17 00:00:00 2001 From: uwptv Date: Fri, 17 Oct 2025 16:19:14 +0200 Subject: [PATCH 11/24] similarity view now has a vertical mdoe which serves on lower screen widths --- static/scripts/similarity.mjs | 370 ++++++++++++++++++++++------------ static/styles/base.css | 2 +- 2 files changed, 246 insertions(+), 126 deletions(-) diff --git a/static/scripts/similarity.mjs b/static/scripts/similarity.mjs index 6b74ea7..fd3fe19 100644 --- a/static/scripts/similarity.mjs +++ b/static/scripts/similarity.mjs @@ -312,13 +312,22 @@ function drawGraph(threshold) { // Determine graph dimensions const useULayout = nodes.length > 50; // Use U-Layout for larger graphs - // Define constants for the layout - const margin = - window.innerWidth <= 750 - ? { top: 5, right: 5, bottom: 5, left: 5 } - : { top: 50, right: 20, bottom: 50, left: 20 }; + // Breakpoint for vertical alignment of axes + const alignVertically = window.innerWidth <= 750; - const height = $("#graphContainer").width(); + // Define constants for the layout + const margin = alignVertically + ? { top: 10, right: 5, bottom: 10, left: 5 } + : { top: 10, right: 20, bottom: 10, left: 20 }; + + // TODO: Parametarise the height with amount of nodes + const nodeSpacing = 25; + const height = alignVertically + ? Math.max( + $("#graphContainer").width() * 1.5, + (useULayout ? nodes.length / 2 : nodes.length) * nodeSpacing + ) + : (9 / 16) * $("#graphContainer").width(); // Create SVG with calculated dimensions const svg = d3 @@ -327,12 +336,9 @@ function drawGraph(threshold) { .attr("width", "100%") .attr("viewBox", `0 0 ${$("#graphContainer").width()} ${height}`); - if (useULayout) { - drawULayout(svg, margin, { nodes, links }, colorScale); - } else { - // Draw links, nodes, and labels for standard layout - drawStandardLayout(svg, margin, { 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( @@ -345,13 +351,19 @@ function drawGraph(threshold) { findSimilarStudies(links); } -function drawULayout(container, margin, graphData, colorScale) { +function drawULayout( + container, + margin, + graphData, + colorScale, + alignHorizontal +) { const { nodes, links } = graphData; const height = parseInt($("svg").height()) - margin.top - margin.bottom; const width = parseInt($("svg").width()) - margin.left - margin.right; - 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 baseNodeRadius = 5; @@ -359,10 +371,10 @@ function drawULayout(container, margin, graphData, colorScale) { const nodeRadius = Math.min(7, baseNodeRadius + 800 / width); // Split the nodes into two groups based on their IDs - const topNodes = nodes.filter( + const firstNodes = nodes.filter( (node) => nodes.indexOf(node) <= nodes.length / 2 ); - const bottomNodes = nodes.filter( + const secondNodes = nodes.filter( (node) => nodes.indexOf(node) > nodes.length / 2 ); @@ -371,83 +383,116 @@ function drawULayout(container, margin, graphData, colorScale) { .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 + const secondScale = d3 .scalePoint() - .domain(bottomNodes) - .rangeRound([0, width]); + .domain(secondNodes) + .rangeRound([0, alignHorizontal ? width : height]); // Create an arc generator for the nodes 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"); // Draw the top axis g.append("g") - .attr("transform", `translate(0, ${topAxisHeight})`) // Position the axis at the top + .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 g.append("g") - .attr("transform", `translate(0, ${3 * topAxisHeight})`) // Position the axis at the bottom + .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") @@ -467,11 +512,15 @@ function drawULayout(container, margin, graphData, colorScale) { // Draw the top nodes and add click and hover events nodeGroup .selectAll(".node") - .data(topNodes, (d) => d) + .data(firstNodes, (d) => d) .enter() .append("g") .attr("class", "node") - .attr("transform", (d) => `translate(${topScale(d)}, ${topAxisHeight})`) + .attr("transform", (d) => + alignHorizontal + ? `translate(${firstScale(d)}, ${firstAxisPos})` + : `translate(${firstAxisPos}, ${firstScale(d)})` + ) .each(function (d) { drawNode(d3.select(this), colorCategory, arc, colorScale); }) @@ -486,13 +535,14 @@ function drawULayout(container, margin, graphData, colorScale) { // Draw the bottom nodes and add click and hover events nodeGroup .selectAll(".node") - .data(bottomNodes, (d) => d) + .data(secondNodes, (d) => d) .enter() .append("g") .attr("class", "node") - .attr( - "transform", - (d) => `translate(${bottomScale(d)}, ${3 * topAxisHeight})` + .attr("transform", (d) => + alignHorizontal + ? `translate(${secondScale(d)}, ${3 * firstAxisPos})` + : `translate(${3 * firstAxisPos}, ${secondScale(d)})` ) .each(function (d) { drawNode(d3.select(this), colorCategory, arc, colorScale); @@ -514,31 +564,54 @@ function drawULayout(container, margin, graphData, colorScale) { .attr("class", "link") .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 : 3 * topAxisHeight; - const targetY = isTargetTop ? topAxisHeight : 3 * topAxisHeight; - - 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 vertical line + if (sourceX === targetX) { + const midPointY = + axisMiddle + (isSourceFirst ? margin.bottom : -margin.top); + return `M ${sourceX} ${sourceY} Q ${sourceX} ${midPointY}, ${targetX} ${targetY}`; + } + // Normal case - nodes on different vertical lines return `M ${sourceX} ${sourceY} C ${sourceX} ${axisMiddle}, ${targetX} ${axisMiddle}, ${targetX} ${targetY}`; + } else { + // When the nodes are on the same horizontal line + if (sourceY === targetY) { + const midPointX = + axisMiddle + (isSourceFirst ? margin.left : -margin.right); + return `M ${sourceX} ${sourceY} Q ${midPointX} ${sourceY}, ${targetX} ${targetY}`; + } + // Normal case - nodes on different horizontal lines + return `M ${sourceX} ${sourceY} C ${axisMiddle} ${sourceY}, ${axisMiddle} ${targetY}, ${targetX} ${targetY}`; } }); @@ -562,7 +635,7 @@ function drawULayout(container, margin, graphData, colorScale) { function zoomed({ transform }) { g.attr( "transform", - `translate (${margin.left + transform.x}, ${ + `translate(${margin.left + transform.x}, ${ margin.top + transform.y }) scale(${transform.k})` ); @@ -570,13 +643,19 @@ function drawULayout(container, margin, graphData, colorScale) { } // Draws the standard layout for the similarity graph -function drawStandardLayout(container, margin, graphData, colorScale) { +function drawStandardLayout( + container, + margin, + graphData, + colorScale, + alignHorizontal +) { const { nodes, links } = graphData; // Define constants for the layout const height = parseInt($("svg").height()) - margin.top - margin.bottom; const width = parseInt($("svg").width()) - margin.left - margin.right; - const axisHeight = height / 2; + const axisMiddle = alignHorizontal ? height / 2 : width / 2; // Base node radius const baseNodeRadius = 5; @@ -584,15 +663,24 @@ function drawStandardLayout(container, margin, graphData, colorScale) { const nodeRadius = Math.min(7, baseNodeRadius + 800 / width); // Create a scale for the node positions - const xScale = d3.scalePoint().domain(nodes).rangeRound([0, width]); + const nodeScale = d3 + .scalePoint() + .domain(nodes) + .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") @@ -604,7 +692,12 @@ function drawStandardLayout(container, margin, graphData, colorScale) { // Draw the axis g.append("g") .attr("class", "axis") - .attr("transform", `translate(0, ${axisHeight})`) // 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( @@ -615,12 +708,14 @@ function drawStandardLayout(container, margin, graphData, colorScale) { ); // 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", responsiveFontSize) - .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") @@ -635,7 +730,12 @@ function drawStandardLayout(container, margin, graphData, colorScale) { // Create a group for the nodes const nodeGroup = g .append("g") - .attr("transform", `translate(0, ${axisHeight})`) + .attr( + "transform", + alignHorizontal + ? `translate(0, ${axisMiddle})` + : `translate(${axisMiddle}, 0)` + ) .attr("class", "nodes"); const arc = d3.arc().innerRadius(0).outerRadius(nodeRadius); @@ -646,7 +746,11 @@ function drawStandardLayout(container, margin, graphData, colorScale) { .data(nodes) .join("g") .attr("class", "node") - .attr("transform", (d) => `translate(${xScale(d)}, 0)`) + .attr("transform", (d) => + alignHorizontal + ? `translate(${nodeScale(d)}, 0)` + : `translate(0, ${nodeScale(d)})` + ) .each(function (d) { drawNode(d3.select(this), colorCategory, arc, colorScale); }) @@ -666,14 +770,30 @@ function drawStandardLayout(container, margin, graphData, colorScale) { .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}`; + 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 diff --git a/static/styles/base.css b/static/styles/base.css index 2873655..7f99f27 100644 --- a/static/styles/base.css +++ b/static/styles/base.css @@ -13,7 +13,7 @@ body { flex-direction: row; --resp-font-size: clamp(0.75rem, 0.6642rem + 0.3661vw, 1.25rem); - --resp-font-ticks: clamp(0.25rem, 0.1213rem + 0.5492vw, 1rem); + --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); From 0fe91f8bffb0cc65f97478762f8f4b29a7dea00a Mon Sep 17 00:00:00 2001 From: uwptv Date: Sat, 25 Oct 2025 12:48:13 +0200 Subject: [PATCH 12/24] Labels in ULayout are now not overflowing on certain resolutions --- static/scripts/similarity.mjs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/static/scripts/similarity.mjs b/static/scripts/similarity.mjs index fd3fe19..3b96335 100644 --- a/static/scripts/similarity.mjs +++ b/static/scripts/similarity.mjs @@ -301,6 +301,15 @@ function drawGraph(threshold) { 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); + console.log(lengthLongestLabel); + // If there are no nodes, do not draw the graph if (nodes.length === 0) { $("#graphContainer").append( @@ -320,13 +329,14 @@ function drawGraph(threshold) { ? { top: 10, right: 5, bottom: 10, left: 5 } : { top: 10, right: 20, bottom: 10, left: 20 }; - // TODO: Parametarise the height with amount of nodes const nodeSpacing = 25; 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 From d22437911776faec31be396fcc155358a469e3ba Mon Sep 17 00:00:00 2001 From: uwptv Date: Sat, 25 Oct 2025 13:18:23 +0200 Subject: [PATCH 13/24] decreased some font sizes and padding --- static/styles/base.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/styles/base.css b/static/styles/base.css index 7f99f27..b2bed45 100644 --- a/static/styles/base.css +++ b/static/styles/base.css @@ -12,14 +12,14 @@ body { display: flex; flex-direction: row; - --resp-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.75rem, 0.6642rem + 0.3661vw, 1.25rem); + --resp-padding: clamp(0.5rem, 0.3713rem + 0.5492vw, 1.25rem); } /* Hide elements that are only relevant for lower screen resolution */ From 02a005eed2833ae7b89fad27246d0f9976f982b9 Mon Sep 17 00:00:00 2001 From: uwptv Date: Sat, 25 Oct 2025 13:22:23 +0200 Subject: [PATCH 14/24] made timeline container have normal aspect ratio --- static/scripts/timeline.mjs | 6 +++++- static/styles/timeline.css | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/static/scripts/timeline.mjs b/static/scripts/timeline.mjs index a604a47..29656ef 100644 --- a/static/scripts/timeline.mjs +++ b/static/scripts/timeline.mjs @@ -314,7 +314,11 @@ function drawTimelineGraph() { const nodeRadius = Math.min(7, baseNodeRadius + 800 / containerWidth); // Calculate height based on max years count - const height = Math.max(400, maxYearsCount * (nodeRadius * 4)); + const height = Math.max( + 400, + maxYearsCount * (nodeRadius * 4), + (9 / 16) * containerWidth + ); const innerHeight = height - margin.top - margin.bottom; const axisHeight = innerHeight; diff --git a/static/styles/timeline.css b/static/styles/timeline.css index 2acacc1..b03d6f4 100644 --- a/static/styles/timeline.css +++ b/static/styles/timeline.css @@ -42,7 +42,7 @@ label[for="timelineColorCategory"] { .link { stroke: #999; - stroke-width: 1; + stroke-width: 0.5; stroke-opacity: 0.6; fill: none; } From 6307be1e6513874a192c60d485f26d1f92d2f592 Mon Sep 17 00:00:00 2001 From: uwptv Date: Wed, 29 Oct 2025 11:46:52 +0100 Subject: [PATCH 15/24] zooming only at lower screen sizes --- static/scripts/similarity.mjs | 53 ++++++++++++++++++----------------- static/scripts/timeline.mjs | 26 +++++++++-------- static/styles/similarity.css | 8 +++--- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/static/scripts/similarity.mjs b/static/scripts/similarity.mjs index 3b96335..0c9c52a 100644 --- a/static/scripts/similarity.mjs +++ b/static/scripts/similarity.mjs @@ -376,9 +376,7 @@ function drawULayout( const axisMiddle = alignHorizontal ? height / 2 : width / 2; // Base node radius - const baseNodeRadius = 5; - // Node radius should scale with smaller screen size, 7 is maximum since nodes would overlap - const nodeRadius = Math.min(7, baseNodeRadius + 800 / width); + const nodeRadius = 7; // Split the nodes into two groups based on their IDs const firstNodes = nodes.filter( @@ -638,18 +636,21 @@ function drawULayout( }]` ); - const zoom = d3.zoom().scaleExtent([0.8, 10]).on("zoom", zoomed); + // Add zoom for smaller screen widths + const mobileQuery = window.matchMedia("(max-width: 850px)"); + + const zoom = d3 + .zoom() + .scaleExtent([0.8, 10]) + .on("zoom", ({ transform }) => { + // On mobile allow panning/zooming; on larger screens keep a fixed margin offset + const x = margin.left + (mobileQuery.matches ? transform.x : 0); + const y = margin.top + (mobileQuery.matches ? transform.y : 0); + const k = mobileQuery.matches ? transform.k : 1; + g.attr("transform", `translate(${x}, ${y}) scale(${k})`); + }); container.call(zoom).call(zoom.transform, d3.zoomIdentity); - - function zoomed({ transform }) { - g.attr( - "transform", - `translate(${margin.left + transform.x}, ${ - margin.top + transform.y - }) scale(${transform.k})` - ); - } } // Draws the standard layout for the similarity graph @@ -668,9 +669,7 @@ function drawStandardLayout( const axisMiddle = alignHorizontal ? height / 2 : width / 2; // Base node radius - const baseNodeRadius = 5; - // Node radius should scale with smaller screen size, 7 is maximum since nodes would overlap - const nodeRadius = Math.min(7, baseNodeRadius + 800 / width); + const nodeRadius = 7; // Create a scale for the node positions const nodeScale = d3 @@ -819,18 +818,20 @@ function drawStandardLayout( }]` ); - const zoom = d3.zoom().scaleExtent([1, 10]).on("zoom", zoomed); + const mobileQuery = window.matchMedia("(max-width: 850px)"); + + const zoom = d3 + .zoom() + .scaleExtent([0.8, 10]) + .on("zoom", ({ transform }) => { + // On mobile allow panning/zooming; on larger screens keep a fixed margin offset + const x = margin.left + (mobileQuery.matches ? transform.x : 0); + const y = margin.top + (mobileQuery.matches ? transform.y : 0); + const k = mobileQuery.matches ? transform.k : 1; + g.attr("transform", `translate(${x}, ${y}) scale(${k})`); + }); container.call(zoom).call(zoom.transform, d3.zoomIdentity); - - function zoomed({ transform }) { - g.attr( - "transform", - `translate(${margin.left + transform.x}, ${ - margin.top + transform.y - }) scale(${transform.k})` - ); - } } /* diff --git a/static/scripts/timeline.mjs b/static/scripts/timeline.mjs index 29656ef..e1cc8dd 100644 --- a/static/scripts/timeline.mjs +++ b/static/scripts/timeline.mjs @@ -545,19 +545,21 @@ function drawTimelineGraph() { $("#legend") ); - // Add zooming functionality - const zoom = d3.zoom().scaleExtent([0.5, 10]).on("zoom", zoomed); - - svg.call(zoom).call(zoom.transform, d3.zoomIdentity); + // Add zooming functionality at lower screen sizes + const mobileQuery = window.matchMedia("(max-width: 850px)"); + + const zoom = d3 + .zoom() + .scaleExtent([0.8, 10]) + .on("zoom", ({ transform }) => { + // On mobile allow panning/zooming; on larger screens keep a fixed margin offset + const x = margin.left + (mobileQuery.matches ? transform.x : 0); + const y = margin.top + (mobileQuery.matches ? transform.y : 0); + const k = mobileQuery.matches ? transform.k : 1; + g.attr("transform", `translate(${x}, ${y}) scale(${k})`); + }); - function zoomed({ target, type, transform, sourceEvent }) { - g.attr( - "transform", - `translate(${margin.left + transform.x}, ${ - margin.top + transform.y - }) scale(${transform.k})` - ); - } + g.call(zoom).call(zoom.transform, d3.zoomIdentity); } $(document).ready(function () { diff --git a/static/styles/similarity.css b/static/styles/similarity.css index 38d0e60..92b249e 100644 --- a/static/styles/similarity.css +++ b/static/styles/similarity.css @@ -150,10 +150,6 @@ margin-bottom: 1em; /* Add some space below the visualization */ } -.link { - stroke-width: 0.5; -} - @media (max-width: 850px) { #visualization-warning { display: flex; @@ -168,4 +164,8 @@ width: 60%; margin-bottom: 1em; } + + .link { + stroke-width: 0.5; + } } From 0e56b5885a400b9ae74726cd1c2f3ba114ca6e48 Mon Sep 17 00:00:00 2001 From: uwptv Date: Wed, 29 Oct 2025 11:48:53 +0100 Subject: [PATCH 16/24] aligned controls for similarity and timeline at the left on lower screen sizes --- static/styles/similarity.css | 2 +- static/styles/timeline.css | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/static/styles/similarity.css b/static/styles/similarity.css index 92b249e..566a92e 100644 --- a/static/styles/similarity.css +++ b/static/styles/similarity.css @@ -157,7 +157,7 @@ .controls { flex-direction: column; - align-items: center; + align-items: start; } .sliderContainer { diff --git a/static/styles/timeline.css b/static/styles/timeline.css index b03d6f4..c84510b 100644 --- a/static/styles/timeline.css +++ b/static/styles/timeline.css @@ -218,6 +218,7 @@ label[for="timelineColorCategory"] { .timeline-controls { flex-direction: column; + align-items: start; } .link { From 88845f046067cc2dfd33da9da1767cb4fb59cd06 Mon Sep 17 00:00:00 2001 From: uwptv Date: Wed, 29 Oct 2025 12:03:42 +0100 Subject: [PATCH 17/24] Info circles scale with font size now --- static/scripts/similarity.mjs | 1 - static/styles/base.css | 5 ++ static/styles/similarity.css | 8 +++ templates/base.html | 2 + templates/similarity.html | 94 ++++++++++++++++++++++++----------- 5 files changed, 81 insertions(+), 29 deletions(-) diff --git a/static/scripts/similarity.mjs b/static/scripts/similarity.mjs index 0c9c52a..4049589 100644 --- a/static/scripts/similarity.mjs +++ b/static/scripts/similarity.mjs @@ -308,7 +308,6 @@ function drawGraph(threshold) { const author = getDataEntry(nodeID, "Main Author") || ""; return Math.max(max, author.length); }, 0); - console.log(lengthLongestLabel); // If there are no nodes, do not draw the graph if (nodes.length === 0) { diff --git a/static/styles/base.css b/static/styles/base.css index b2bed45..21fb28e 100644 --- a/static/styles/base.css +++ b/static/styles/base.css @@ -42,6 +42,7 @@ body { #visualization-warning span { text-align: center; + font-size: var(--font-bg); } header { @@ -144,6 +145,10 @@ h3 { font-size: var(--font-md); } +.question-circle { + width: var(--font-bg); +} + .panel { background-color: var(--color-gray-light); padding: 1em; diff --git a/static/styles/similarity.css b/static/styles/similarity.css index 566a92e..b2036f9 100644 --- a/static/styles/similarity.css +++ b/static/styles/similarity.css @@ -42,6 +42,10 @@ font-size: var(--font-md); } +.similarityControlContainer label { + font-size: var(--font-md); +} + .btn-similarity { background: white; border: 1px solid var(--color-accent); @@ -135,6 +139,10 @@ opacity: 0.3; } +#thresholdInfoIcon { + width: var(--font-bg); +} + .info-circle { font-weight: bold; color: var(--color-accent); diff --git a/templates/base.html b/templates/base.html index 40cdaa3..4ea5e90 100644 --- a/templates/base.html +++ b/templates/base.html @@ -299,6 +299,7 @@

{{ panel.value }}

Question Mark Icon within a circle {% endif %} @@ -328,6 +329,7 @@

{{ panel.value }}

Question Mark Icon within a circle {% endif %} diff --git a/templates/similarity.html b/templates/similarity.html index b9f5172..f8e35f6 100644 --- a/templates/similarity.html +++ b/templates/similarity.html @@ -1,41 +1,62 @@ -{% extends "base.html" %} - -{% block styles %} - -{% endblock styles %} - -{% block content %} +{% extends "base.html" %} {% block styles %} + +{% endblock styles %} {% block content %}
-
Similarity Type:
- - - - - + + + + +
- Similarity Threshold (σ): + Similarity Threshold (σ): - Question Mark Icon within a circle -
-
- {# Insert slider here #} + Question Mark Icon within a circle
+
{# Insert slider here #}
-
+
- - - - - - - - - -
-
- -
-
-
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:
    +
      +
      -
      From aaee179fffb342a4650c3a90cb046552a1fc62fd Mon Sep 17 00:00:00 2001 From: uwptv Date: Wed, 29 Oct 2025 16:34:15 +0100 Subject: [PATCH 22/24] slider is now responsive --- static/styles/nouislider.css | 46 ++++++++++++++++++++---------------- static/styles/similarity.css | 1 + 2 files changed, 26 insertions(+), 21 deletions(-) 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 efede0a..830cbc8 100644 --- a/static/styles/similarity.css +++ b/static/styles/similarity.css @@ -74,6 +74,7 @@ #thresholdSlider { width: 100%; + height: clamp(0.5rem, 0.3713rem + 0.5492vw, 1.25rem); } #thresholdValue { From 286c2dd8101202ecaffa134edd7aa1b1e4519d5b Mon Sep 17 00:00:00 2001 From: uwptv Date: Wed, 29 Oct 2025 16:37:17 +0100 Subject: [PATCH 23/24] increased min-height of barcharts to 25vh --- static/styles/barChart.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/styles/barChart.css b/static/styles/barChart.css index 1b8fe49..37d3366 100644 --- a/static/styles/barChart.css +++ b/static/styles/barChart.css @@ -25,7 +25,7 @@ .chart-container { position: relative; - min-height: 21vh; + min-height: 25vh; } .chart-title { From b7cf081341203a5fde78b1def9dd151c65d356bc Mon Sep 17 00:00:00 2001 From: uwptv Date: Sun, 23 Nov 2025 15:37:57 +0100 Subject: [PATCH 24/24] adjusted node sizes so they dont overlap --- static/scripts/similarity.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/scripts/similarity.mjs b/static/scripts/similarity.mjs index d3c0b3f..cf082e0 100644 --- a/static/scripts/similarity.mjs +++ b/static/scripts/similarity.mjs @@ -375,7 +375,7 @@ function drawULayout( const axisMiddle = alignHorizontal ? height / 2 : width / 2; // Base node radius - const nodeRadius = 7; + const nodeRadius = alignHorizontal ? 5.5 : 7; // Split the nodes into two groups based on their IDs const firstNodes = nodes.filter(