diff --git a/static/scripts/barChart.mjs b/static/scripts/barChart.mjs index fdff01b..48be9d4 100644 --- a/static/scripts/barChart.mjs +++ b/static/scripts/barChart.mjs @@ -1,8 +1,16 @@ -import { filterData, cleanDataString, specialOrders, showStudyModal, defaultColors } from "./dataUtility.mjs"; +import { + filterData, + cleanDataString, + specialOrders, + showStudyModal, + defaultColors, +} from "./dataUtility.mjs"; // The available categories passed by the server for the bar charts const categories = $("body").data("filter-categories"); -const questionCirclePath = $("#toggle-menu-container").data("question-circle-path"); +const questionCirclePath = $("#toggle-menu-container").data( + "question-circle-path" +); const explanations = $("body").data("explanations"); /* @@ -15,7 +23,9 @@ const explanations = $("body").data("explanations"); // Function to create Modal HTML for a given category and label function createModalHTML(category, label) { - const activeData = filterData(JSON.parse(window.sessionStorage.getItem('filters'))); + const activeData = filterData( + JSON.parse(window.sessionStorage.getItem("filters")) + ); const fullCategory = getFullCategory(category); const tableHTML = ` @@ -32,11 +42,18 @@ function createModalHTML(category, label) {
- ${activeData.filter(entry => entry[fullCategory].toString().includes(label)).map(elem => - ` + ${activeData + .filter((entry) => entry[fullCategory].toString().includes(label)) + .map( + (elem) => + `No studies available for the selected sidebar filters. Please select some of the criteria from the sidebar at the right.
"); + $("#chartsContainer").html( + "No studies available for the selected sidebar filters. Please select some of the criteria from the sidebar at the right.
" + ); return; - }; + } if (activeCategories.length === 0) { // Reset the hidden charts message and list $("#hiddenChartsMessage").hide(); $("#hiddenChartsList").empty(); - $("#chartsContainer").html("No studies found for the selected filters. Please select some of the criteria from the toggle menu at the top.
"); + $("#chartsContainer").html( + "No studies found for the selected filters. Please select some of the criteria from the toggle menu at the top.
" + ); return; } // Create a bar chart for each active category for (const category of activeCategories) { // Only provide the data needed for the current category - const barData = activeData.map(entry => entry[category]); + const barData = activeData.map((entry) => entry[category]); // Create the bar chart for the current category createBarChart(barData, category); } - + // Update the visibility of the charts based on the maximum number of bars set in the dropdown menu updateVisibility(); } @@ -126,7 +150,7 @@ function createBarChart(barData, category) { chartTitleElement.className = "chart-title"; chartTitleElement.innerHTML = `Total connections: ${connectedNodes.length}
` - + const totalConnectionsHTML = `Total connections: ${connectedNodes.length}
`; + // Append to the connections container $("#connectionsContainer").empty(); $("#connectionsContainer").html(sourceHTML); @@ -201,47 +232,53 @@ function findSimilarStudies(links) { function generateGraphData(threshold) { const { studyIDs, similarityMatrix } = getCurrentSimilarityData(); const links = []; - + // Sort the nodes by category if a category is selected - const {sortedNodes, colorScale} = sortNodesByCategory(studyIDs, $("#similarityColorCategory").val()); - + const { sortedNodes, colorScale } = sortNodesByCategory( + studyIDs, + $("#similarityColorCategory").val() + ); + // Only check each pair once (i < j) for (let i = 0; i < sortedNodes.length; i++) { for (let j = i + 1; j < sortedNodes.length; j++) { const nodeA = sortedNodes[i]; const nodeB = sortedNodes[j]; - - const similarity = similarityMatrix[parseInt(nodeA) - 1][parseInt(nodeB) - 1]; + + const similarity = + similarityMatrix[parseInt(nodeA) - 1][parseInt(nodeB) - 1]; if (similarity && similarity >= threshold) { links.push({ sourceID: nodeA, targetID: nodeB, - value: similarity + value: similarity, }); } } } - + return { sortedNodes, links, colorScale }; -}; +} // Gets the current similarity data based on the selected type and the selected filters so only active studies are included, returns an object with the study IDs and the similarity matrix like this: {studyIDs: [...], similarityMatrix: [[...]]} function getCurrentSimilarityData() { const filters = JSON.parse(window.sessionStorage.getItem("filters")); // Get the IDs of all data studies that are currently active based on the selected filters - const activeDataIDs = filterData(filters).map(item => item["ID"].toString()); - + const activeDataIDs = filterData(filters).map((item) => + item["ID"].toString() + ); + if (similarityType === "abstract") { return { - studyIDs: abstractStudyIDs.filter(id => activeDataIDs.includes(id)), - similarityMatrix: abstractMatrix + studyIDs: abstractStudyIDs.filter((id) => activeDataIDs.includes(id)), + similarityMatrix: abstractMatrix, }; } else if (similarityType === "database") { return { - studyIDs: databaseStudyIDs.filter(id => activeDataIDs.includes(id)), - similarityMatrix: databaseMatrix + studyIDs: databaseStudyIDs.filter((id) => activeDataIDs.includes(id)), + similarityMatrix: databaseMatrix, }; - }; + } } /* @@ -261,345 +298,591 @@ function drawGraph(threshold) { $("#graphContainer").height("auto"); $("#legend").empty(); - // TODO: change to slider value const { sortedNodes, links, colorScale } = generateGraphData(threshold); const nodes = [...sortedNodes]; + const lengthLongestLabel = + nodes.length === 0 + ? 0 + : nodes.reduce((max, nodeID) => { + const author = getDataEntry(nodeID, "Main Author") || ""; + return Math.max(max, author.length); + }, 0); + // If there are no nodes, do not draw the graph if (nodes.length === 0) { - $("#graphContainer").append("No studies available for the selected sidebar filters. Please select some of the criteria from the sidebar at the right.
"); + $("#graphContainer").append( + "No studies available for the selected sidebar filters. Please select some of the criteria from the sidebar at the right.
" + ); return; } // Determine graph dimensions const useULayout = nodes.length > 50; // Use U-Layout for larger graphs - const height = useULayout ? 800 : 500; + // Breakpoint for vertical alignment of axes + const alignVertically = window.innerWidth <= 750; - $("#graphContainer").height(height); + // Define constants for the layout + const margin = alignVertically + ? { top: 10, right: 5, bottom: 10, left: 5 } + : { top: 10, right: 20, bottom: 10, left: 20 }; + + const nodeSpacing = 15; + const height = alignVertically + ? Math.max( + $("#graphContainer").width() * 1.5, + (useULayout ? nodes.length / 2 : nodes.length) * nodeSpacing + ) + : useULayout + ? (9 / 16) * $("#graphContainer").width() + 15 * lengthLongestLabel + : (9 / 16) * $("#graphContainer").width(); // Create SVG with calculated dimensions - const svg = d3.select("#graphContainer").append("svg") + const svg = d3 + .select("#graphContainer") + .append("svg") .attr("width", "100%") - .attr("height", height) .attr("viewBox", `0 0 ${$("#graphContainer").width()} ${height}`); - if (useULayout) { - drawULayout(svg, {nodes, links}, colorScale); - } else { - // Draw links, nodes, and labels for standard layout - drawStandardLayout(svg, {nodes, links}, colorScale); - } + // Choose layout based on number of nodes + const layoutFunction = useULayout ? drawULayout : drawStandardLayout; + layoutFunction(svg, margin, { nodes, links }, colorScale, !alignVertically); // Draw the legend - createLegend(nodes, colorScale, $("#similarityColorCategory").val(), $("#legend")); + createLegend( + nodes, + colorScale, + $("#similarityColorCategory").val(), + $("#legend") + ); findSimilarStudies(links); } -function drawULayout(container, graphData, colorScale) { +function drawULayout( + container, + margin, + graphData, + colorScale, + alignHorizontal +) { const { nodes, links } = graphData; - // Define constants for the layout - const margin = { top: 20, right: 20, bottom: 20, left: 20 }; const height = parseInt($("svg").height()) - margin.top - margin.bottom; const width = parseInt($("svg").width()) - margin.left - margin.right; - const nodeRadius = Math.floor(width / 150); - const topAxisHeight = height / 4; - const axisMiddle = height / 2; + const firstAxisPos = alignHorizontal ? height / 4 : width / 4; + const axisMiddle = alignHorizontal ? height / 2 : width / 2; + + // Base node radius + const nodeRadius = alignHorizontal ? 5.5 : 7; // Split the nodes into two groups based on their IDs - const topNodes = nodes.filter(node => nodes.indexOf(node) <= (nodes.length / 2)); - const bottomNodes = nodes.filter(node => nodes.indexOf(node) > (nodes.length / 2)); + const firstNodes = nodes.filter( + (node) => nodes.indexOf(node) <= nodes.length / 2 + ); + const secondNodes = nodes.filter( + (node) => nodes.indexOf(node) > nodes.length / 2 + ); + + const responsiveFontSize = getComputedStyle(document.body) + .getPropertyValue("--resp-font-ticks") + .trim(); // Create a scale for the top nodes - const topScale = d3.scalePoint() - .domain(topNodes) - .rangeRound([0, width]); + const firstScale = d3 + .scalePoint() + .domain(firstNodes) + .rangeRound([0, alignHorizontal ? width : height]); // Create a scale for the bottom nodes - const bottomScale = d3.scalePoint() - .domain(bottomNodes) - .rangeRound([0, width]); + const secondScale = d3 + .scalePoint() + .domain(secondNodes) + .rangeRound([0, alignHorizontal ? width : height]); // Create an arc generator for the nodes - const arc = d3.arc() - .innerRadius(0) - .outerRadius(nodeRadius); + const arc = d3.arc().innerRadius(0).outerRadius(nodeRadius); // Create the top axis for the nodes - const topAxis = d3.axisTop(topScale) - .tickValues(topNodes) - .tickFormat(d => "") - .tickSize(0) - .tickPadding(-4); + const firstAxis = alignHorizontal + ? d3 + .axisTop(firstScale) + .tickValues(firstNodes) + .tickFormat((d) => "") + .tickSize(0) + .tickPadding(-4) + : d3 + .axisLeft(firstScale) + .tickValues(firstNodes) + .tickFormat((d) => "") + .tickSize(0) + .tickPadding(8); // Create the bottom axis for the nodes - const bottomAxis = d3.axisBottom(bottomScale) - .tickValues(bottomNodes) - .tickFormat(d => "") - .tickSize(0) - .tickPadding(-4); + const secondAxis = alignHorizontal + ? d3 + .axisBottom(secondScale) + .tickValues(secondNodes) + .tickFormat((d) => "") + .tickSize(0) + .tickPadding(-4) + : d3 + .axisRight(secondScale) + .tickValues(secondNodes) + .tickFormat((d) => "") + .tickSize(0) + .tickPadding(8); + + // Append group element for zooming + const g = container + .append("g") + .attr("transform", `translate (${margin.left}, ${margin.top})`); // Draw the top axis - container.append("g") - .attr("transform", `translate(${margin.left}, ${topAxisHeight + margin.top})`) // Position the axis at the top + g.append("g") + .attr( + "transform", + alignHorizontal + ? `translate(0, ${firstAxisPos})` + : `translate(${firstAxisPos}, 0)` + ) // Position the first Axis .attr("class", "top-axis") - .call(topAxis); + .call(firstAxis); // Draw the bottom axis - container.append("g") - .attr("transform", `translate(${margin.left}, ${height - topAxisHeight})`) // Position the axis at the bottom + g.append("g") + .attr( + "transform", + alignHorizontal + ? `translate(0, ${3 * firstAxisPos})` + : `translate(${3 * firstAxisPos}, 0)` + ) // Position the axis at the bottom .attr("class", "bottom-axis") - .call(bottomAxis); + .call(secondAxis); // Add info circle and label to top axis ticks - container.selectAll(".top-axis text") - .html(d => `No connections found with the current filter settings.
"; + if ( + citingLinks.length === 0 && + citedByLinks.length === 0 && + coauthorLinks.length === 0 + ) { + connectionsHTML = + "No connections found with the current filter settings.
"; } else { connectionsHTML = `Total connections: ${citingLinks.length + citedByLinks.length + coauthorLinks.length}
+Total connections: ${ + citingLinks.length + citedByLinks.length + coauthorLinks.length + }
`; - }; + } // Append the generated HTML to the modal and show it $("#timelineConnectionsContainer").html(headerHTML); @@ -162,8 +203,8 @@ function formatConnectionType(value) { return "Cited By"; default: return "Shared Authors"; - }; -}; + } +} /* * Preparing the data for the timeline graph. @@ -171,41 +212,47 @@ function formatConnectionType(value) { */ function generateTimelineData() { // Get the currently active nodes based on the selected category and filters - const activeNodes = filterData(JSON.parse(window.sessionStorage.getItem("filters"))).map(item => item["ID"].toString()); + const activeNodes = filterData( + JSON.parse(window.sessionStorage.getItem("filters")) + ).map((item) => item["ID"].toString()); // Order the nodes by the selected category - const { sortedNodes, colorScale } = sortNodesByCategory(activeNodes, colorCategory); + const { sortedNodes, colorScale } = sortNodesByCategory( + activeNodes, + colorCategory + ); // Append the year to each node - const nodes = sortedNodes.map(node => { + const nodes = sortedNodes.map((node) => { return { id: node, year: getDataEntry(node, "Year"), - } + }; }); const years = {}; - nodes.forEach(node => { + nodes.forEach((node) => { if (!years[node.year]) { years[node.year] = [node.id]; } else { years[node.year].push(node.id); } }); - const maxYears = Math.max(...Object.keys(years).map(year => years[year].length)); - + const maxYears = Math.max( + ...Object.keys(years).map((year) => years[year].length) + ); + // Create links for co-authors and citations - const links = {coauthorLinks: [], citingLinks: [], citedByLinks: []}; + const links = { coauthorLinks: [], citingLinks: [], citedByLinks: [] }; for (const node of sortedNodes) { for (const other of sortedNodes) { - // Populate the links for co-authors if (coauthorMatrix[node][other]) { links.coauthorLinks.push({ sourceID: node, targetID: other, }); - }; + } // Populate the links for citations if (citationMatrix[node][other]) { @@ -213,7 +260,7 @@ function generateTimelineData() { sourceID: node, targetID: other, }); - }; + } if (citationMatrix[other][node]) { links.citedByLinks.push({ @@ -229,8 +276,8 @@ function generateTimelineData() { years, links, maxYears, - colorScale - } + colorScale, + }; } function drawTimelineGraph() { @@ -241,85 +288,148 @@ function drawTimelineGraph() { const { nodes, years, links, maxYears, colorScale } = generateTimelineData(); const { coauthorLinks, citingLinks, citedByLinks } = links; - const maxYearsCount = Math.max(...Object.values(years).map(year => year.length)); + const maxYearsCount = Math.max( + ...Object.values(years).map((year) => year.length) + ); // If there are no nodes, do not draw the graph if (nodes.length === 0) { - $("#timeline-graph-container").append("No studies available for the selected sidebar filters. Please select some of the criteria from the sidebar at the right.
"); + $("#timeline-graph-container").append( + "No studies available for the selected sidebar filters. Please select some of the criteria from the sidebar at the right.
" + ); return; } // Set up layout dimensions for the graph - const margin = { top: 20, right: 50, bottom: 50, left: 50 }; - const innerWidth = $("#timeline-graph-container").width() - margin.left - margin.right; - const nodeRadius = innerWidth / 150; - const height = Math.max(250, maxYearsCount * (nodeRadius * 4)); + let margin = + window.innerWidth <= 750 + ? { top: 5, right: 20, bottom: 30, left: 20 } + : { top: 20, right: 50, bottom: 20, left: 50 }; + const containerWidth = $("#timeline-graph-container").width(); + const innerWidth = containerWidth - margin.left - margin.right; + + // Base node radius + const nodeRadius = 7; + + // Calculate height based on max years count + const height = Math.max( + 400, + maxYearsCount * (nodeRadius * 4), + (9 / 16) * containerWidth + ); const innerHeight = height - margin.top - margin.bottom; const axisHeight = innerHeight; - // Create the svg container for the timeline graph - const svg = d3.select("#timeline-graph-container") + // get the current font size + const responsiveFontSize = getComputedStyle(document.body) + .getPropertyValue("--resp-font-ticks-bg") + .trim(); + + // Create the svg container for the timeline graph with responsive viewBox + const svg = d3 + .select("#timeline-graph-container") .append("svg") - .attr("width", $("#timeline-graph-container").width()) + .attr("width", containerWidth) .attr("height", height) - .attr("viewBox", `0 0 ${$("#timeline-graph-container").width()} ${height}`); + .attr("viewBox", `${margin.left} ${margin.top} ${innerWidth} ${height}`) + .attr("preserveAspectRatio", "xMidYMid meet"); // Create an x scale for the years - const xScale = d3.scalePoint() + const xScale = d3 + .scalePoint() .domain(Object.keys(years).map(Number)) .range([0, innerWidth]); // Create a y scale for the nodes - const yScale = d3.scaleLinear() + const yScale = d3 + .scaleLinear() .domain([0, maxYears]) .range([axisHeight - margin.bottom, 0]); + // Append group element to svg for zooming + const g = svg + .append("g") + .attr("transform", `translate (${margin.left}, ${margin.top})`); + // Create an x axis for the years - const xAxis = d3.axisBottom(xScale) + const xAxis = d3 + .axisBottom(xScale) .tickFormat(d3.format("d")) - .tickSize(5); + .tickSize(window.innerWidth <= 750 ? 0 : 5); // Set tickSize to 0 as we'll draw our own ticks - // Draw the x axis - svg.append("g") + // Draw just the x axis line + g.append("g") .attr("class", "x-axis") - .attr("transform", `translate(${margin.left}, ${axisHeight + margin.top})`) - .call(xAxis); - - // Format the labels for the axis - svg.selectAll(".x-axis text") - .style("font-size", "1.2em") - .style("user-select", "none"); + .attr("transform", `translate(0, ${axisHeight})`) + .call(xAxis) + .call((g) => { + // Keep only the domain line and remove default ticks when screen is small + if (window.innerWidth <= 750) { + g.select(".domain").attr("stroke", "#000"); + g.selectAll(".tick").remove(); + + // Add custom alternating ticks + const yearValues = Object.keys(years) + .map(Number) + .sort((a, b) => a - b); + + g.selectAll(".custom-tick") + .data(yearValues) + .join("g") + .attr("class", "custom-tick") + .attr("transform", (d) => `translate(${xScale(d)}, 0)`) + .each(function (d, i) { + const isAbove = i % 2 === 0; // Alternates based on index + const tick = d3.select(this); + + const tickSize = 5; + const textOffset = 8; + + // Add tick line + tick + .append("line") + .attr("class", "tick-line") + .attr("y1", 1) + .attr("y2", isAbove ? -tickSize : tickSize); + + // Add year text + tick + .append("text") + .attr("class", "tick-text") + .attr("text-anchor", "middle") + .attr("dy", isAbove ? -textOffset : textOffset + tickSize) + .style("font-size", responsiveFontSize) + .style("user-select", "none") + .text(d); + }); + } + }); // Create a link group for the links - const linkGroup = svg.append("g") - .attr("class", "links") - .attr("transform", `translate(${margin.left}, ${margin.top})`); + const linkGroup = g.append("g").attr("class", "links"); // Create a node group for the nodes - const nodeGroup = svg.append("g") - .attr("class", "nodes") - .attr("transform", `translate(${margin.left}, ${margin.top})`); + const nodeGroup = g.append("g").attr("class", "nodes"); - const arc = d3.arc() - .innerRadius(0) - .outerRadius(nodeRadius); + const arc = d3.arc().innerRadius(0).outerRadius(nodeRadius); // Draw the nodes on the timeline - nodeGroup.selectAll(".node") - .data(nodes.map(node => node.id)) + nodeGroup + .selectAll(".node") + .data(nodes.map((node) => node.id)) .join("g") .attr("class", "node") - .attr("transform", d => { - const year = nodes.find(node => node.id === d).year; + .attr("transform", (d) => { + const year = nodes.find((node) => node.id === d).year; return `translate(${xScale(year)}, ${yScale(years[year].indexOf(d))})`; }) - .each(function(d) { - drawNode(d3.select(this), colorCategory, arc, colorScale) + .each(function (d) { + drawNode(d3.select(this), colorCategory, arc, colorScale); }) - .on("click", function(event, d) { + .on("click", function (event, d) { showNetworkModal(d); }) - .on("mouseover", function(event, d) { + .on("mouseover", function (event, d) { // Show the tooltip next to the node nodeTooltip.style("visibility", "visible"); nodeTooltip.style("left", `${event.pageX + 15}px`); @@ -332,49 +442,57 @@ function drawTimelineGraph() {${entry["Main Author"]} (${entry["Year"]})
Location: ${entry["Location"]}
`); - + // Highlight the node and its connections - highlightNode(d, nodeRadius, citationMode === "cited-by" || citationMode === "cites"); + highlightNode( + d, + nodeRadius, + citationMode === "cited-by" || citationMode === "cites" + ); }) - .on("mouseout", function(event, d) { + .on("mouseout", function (event, d) { // Hide the tooltip when the mouse leaves the node nodeTooltip.style("visibility", "hidden"); removeHighlighting(nodeRadius); }); - - const nodeTooltip = d3.select("#timeline-graph-container").append("div") + const nodeTooltip = d3 + .select("#timeline-graph-container") + .append("div") .attr("class", "node-tooltip") .style("visibility", "hidden"); // Draw the links between the nodes if (showSharedAuthors === "true") { - linkGroup.selectAll(".link .coauthor") + linkGroup + .selectAll(".link .coauthor") .data(coauthorLinks) .join("path") .attr("class", "coauthor link") - .attr("d", d => drawLink(d)); - }; + .attr("d", (d) => drawLink(d)); + } if (citationMode === "both" || citationMode === "cites") { - linkGroup.selectAll(".link .citing") + linkGroup + .selectAll(".link .citing") .data(citingLinks) .join("path") .attr("class", "citing link") - .attr("d", d => drawLink(d)); - }; + .attr("d", (d) => drawLink(d)); + } if (citationMode === "both" || citationMode === "cited-by") { - linkGroup.selectAll(".link .cited-by") + linkGroup + .selectAll(".link .cited-by") .data(citedByLinks) .join("path") .attr("class", "cited-by link") - .attr("d", d => drawLink(d)); + .attr("d", (d) => drawLink(d)); } function drawLink(d) { - const sourceNode = nodes.find(node => node.id === d.sourceID); - const targetNode = nodes.find(node => node.id === d.targetID); + const sourceNode = nodes.find((node) => node.id === d.sourceID); + const targetNode = nodes.find((node) => node.id === d.targetID); const sourceX = xScale(sourceNode.year); const targetX = xScale(targetNode.year); const sourceY = yScale(years[sourceNode.year].indexOf(sourceNode.id)); @@ -383,12 +501,15 @@ function drawTimelineGraph() { if (sourceX === targetX) { // If the souce and target are in the same year, draw an arc const midY = (sourceY + targetY) / 2; - return `M ${sourceX},${sourceY} Q ${Math.min(sourceX + xScale.step(), $("#timeline-graph-container").width() - margin.right / 2)},${midY} ${targetX},${targetY}`; + return `M ${sourceX},${sourceY} Q ${Math.min( + sourceX + xScale.step(), + $("#timeline-graph-container").width() - margin.right / 2 + )},${midY} ${targetX},${targetY}`; } else { // For different years, create a bezier curve const dx = targetX - sourceX; const controlOffset = Math.min(Math.abs(dx) * 0.4, 100); // Limit control point offset - + // Calculate control points - higher curves for connections between distant years const controlX1 = sourceX + Math.sign(dx) * controlOffset; const controlY1 = sourceY - 40; // Curve upward @@ -397,72 +518,98 @@ function drawTimelineGraph() { // If the source and target are in different years, draw a bezier curve return `M ${sourceX},${sourceY} C ${controlX1},${controlY1} ${controlX2},${controlY2} ${targetX},${targetY}`; - }; - }; + } + } // Add hover title to links - linkGroup.selectAll(".citing") + linkGroup + .selectAll(".citing") .append("title") - .text(d => `[${d.sourceID}] cites [${d.targetID}]`); + .text((d) => `[${d.sourceID}] cites [${d.targetID}]`); - linkGroup.selectAll(".cited-by") + linkGroup + .selectAll(".cited-by") .append("title") - .text(d => `[${d.sourceID}] is cited by [${d.targetID}]`); + .text((d) => `[${d.sourceID}] is cited by [${d.targetID}]`); - linkGroup.selectAll(".coauthor") + linkGroup + .selectAll(".coauthor") .append("title") - .text(d => `[${d.sourceID}] shares authors with [${d.targetID}]`); + .text((d) => `[${d.sourceID}] shares authors with [${d.targetID}]`); // Create a legend for the colors - createLegend(nodes.map(node => node.id), colorScale, colorCategory, $("#legend")); + createLegend( + nodes.map((node) => node.id), + colorScale, + colorCategory, + $("#legend") + ); + + // Add zooming functionality at lower screen sizes + const mobileQuery = window.matchMedia("(max-width: 850px)"); + + if (mobileQuery.matches) { + const zoom = d3 + .zoom() + .scaleExtent([0.8, 10]) + .on("zoom", ({ transform }) => { + // On mobile allow panning/zooming + const x = margin.left + transform.x; + const y = margin.top + transform.y; + const k = transform.k; + g.attr("transform", `translate(${x}, ${y}) scale(${k})`); + }); + + svg.call(zoom).call(zoom.transform, d3.zoomIdentity); + } } -$(document).ready(function() { +$(document).ready(function () { drawTimelineGraph(); - $("#timeline-toggle-shared-authors").on("click", function() { + $("#timeline-toggle-shared-authors").on("click", function () { showSharedAuthors = $(this).is(":checked").toString(); window.sessionStorage.setItem("showSharedAuthors", showSharedAuthors); drawTimelineGraph(); - }) + }); - $("input[name='citation-mode']").on("click", function() { + $("input[name='citation-mode']").on("click", function () { citationMode = $(this).val(); window.sessionStorage.setItem("citationMode", citationMode); drawTimelineGraph(); }); // Set the coloring strategy to the session storage - $("#timelineColorCategory").on("change", function() { + $("#timelineColorCategory").on("change", function () { colorCategory = $(this).val(); window.sessionStorage.setItem("colorCategory", colorCategory); drawTimelineGraph(); }); - window.addEventListener("resize", function() { + window.addEventListener("resize", function () { drawTimelineGraph(); }); - $(".value-filter").on("change", function() { + $(".value-filter").on("change", function () { drawTimelineGraph(); }); - $(".exclusive-filter").on("click", function() { + $(".exclusive-filter").on("click", function () { drawTimelineGraph(); }); - $(".range-slider").each(function() { - this.noUiSlider.on("end", function() { + $(".range-slider").each(function () { + this.noUiSlider.on("end", function () { drawTimelineGraph(); - }) + }); }); // Handle clicks on the info circles in the connections modal - $("#timelineConnectionsContainer").on("click", ".info-circle", function() { + $("#timelineConnectionsContainer").on("click", ".info-circle", function () { const id = $(this).data("id"); // Close the network modal if it's open $("#timelineConnectionsModal").modal("hide"); showStudyModal(id); }); -}) \ No newline at end of file +}); diff --git a/static/styles/barChart.css b/static/styles/barChart.css index 349fe90..37d3366 100644 --- a/static/styles/barChart.css +++ b/static/styles/barChart.css @@ -5,6 +5,11 @@ gap: 1vw; } +#max-bars-container, +#max-bars-container select { + font-size: var(--font-bg); +} + .chart-wrapper { background: var(--color-gray-light); border-radius: 8px; @@ -20,7 +25,7 @@ .chart-container { position: relative; - min-height: 30vh; + min-height: 25vh; } .chart-title { @@ -35,16 +40,39 @@ margin: 0; display: inline-block; word-break: break-word; + font-size: var(--font-xl); +} + +.chart-title .question-circle { + width: var(--font-xl); } #hiddenChartsMessage { display: none; margin-top: 2em; padding: 1em; + font-size: var(--font-bg); +} + +#hiddenChartsMessage h6 { + font-size: var(--font-xl); +} + +#table-modal, +#table-modal button { + font-size: var(--font-md); +} + +#table-modal h5 { + font-size: var(--font-xl); +} + +#table-modal th { + font-size: var(--font-bg); } @media (max-width: 670px) { #visualization-warning { display: flex; } -} \ No newline at end of file +} diff --git a/static/styles/base.css b/static/styles/base.css index 4a7aa4b..69123af 100644 --- a/static/styles/base.css +++ b/static/styles/base.css @@ -1,5 +1,5 @@ :root { - --color-accent: #B89491; /* Earable's accent color */ + --color-accent: #b89491; /* Earable's accent color */ --color-accent-dark: #a37c79; --color-gray: #6c757d; --color-gray-light: #f8f9fa; @@ -11,7 +11,15 @@ body { padding: 0; display: flex; flex-direction: row; - font-size: clamp(0.75rem, 0.6642rem + 0.3661vw, 1.25rem); + + --resp-font-size: clamp(0.5rem, 0.3713rem + 0.5492vw, 1.25rem); + --resp-font-ticks: clamp(0.5rem, 0.3713rem + 0.5492vw, 1.25rem); + --resp-font-ticks-bg: calc(clamp(0.25rem, 0.1213rem + 0.5492vw, 1rem) * 1.5); + --font-xl: calc(var(--resp-font-size) * 1.25); + --font-bg: calc(var(--resp-font-size) * 1.15); + --font-md: var(--resp-font-size); + --font-sm: calc(var(--resp-font-size) * 0.85); + --resp-padding: clamp(0.5rem, 0.3713rem + 0.5492vw, 1.25rem); } /* Hide elements that are only relevant for lower screen resolution */ @@ -26,14 +34,16 @@ body { align-items: center; justify-content: center; background-color: rgb(255, 255, 175); - border: 2px solid #B89230; + border: 2px solid #b89230; padding: 0.5em; border-radius: 5px; - color: #B89230; + color: #b89230; + margin-bottom: 0.5rem; } #visualization-warning span { - text-align: center;; + text-align: center; + font-size: var(--font-bg); } header { @@ -41,7 +51,7 @@ header { flex-direction: row; justify-content: space-between; align-items: center; - gap: 1em; + gap: var(--resp-padding); } .left-section { @@ -61,6 +71,15 @@ nav { gap: 1em; } +#nav-r { + gap: 1em; +} + +#earXplore-repo, +#open-earable-repo { + font-size: var(--font-bg); +} + #earXplore-repo a { display: flex; align-items: center; @@ -76,11 +95,15 @@ nav { } .navbar-item { - padding: 1em; + padding: var(--resp-padding); text-align: center; border-radius: 5px; /* Rounded corners */ } +.navbar-text { + font-size: var(--font-bg); +} + .navbar-item:hover:not(.navbar-item-selected) { background-color: #f8e6e6; border-color: var(--color-accent); @@ -99,7 +122,7 @@ nav { } h3 { - font-size: 1.5em; + font-size: var(--font-xl); padding: 0; margin: 0; } @@ -117,7 +140,15 @@ h3 { padding: 5px; max-height: 100vh; overflow-y: scroll; - font-size: medium; +} + +.filter-group { + font-size: var(--font-md); +} + +.question-circle, +.info-circle { + width: var(--font-bg); } .panel { @@ -145,7 +176,7 @@ h3 { } /* Buttons */ -.add-study-button{ +.add-study-button { color: var(--color-accent); background-color: white; border: none; @@ -157,13 +188,13 @@ h3 { border: 1px solid var(--color-gray); color: white; padding: 0.1em 0.4em; - font-size: 1em; + font-size: var(--font-md); border-radius: 5px; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1); text-shadow: none; } -.exclusive-filter{ +.exclusive-filter { background-color: var(--color-accent); border-color: var(--color-accent); } @@ -174,9 +205,9 @@ h3 { } .btn-link, -.toggle-visibility-button { +.toggle-visibility-button { background-color: var(--color-accent); - font-size: 1em; + font-size: var(--font-md); color: white; border: none; text-decoration: none; @@ -193,6 +224,16 @@ h3 { font-weight: bold; } +.study-info-panel-header, +#study-info-header { + font-size: var(--font-xl); +} + +#study-info-modal-body, +#study-info-modal-footer button { + font-size: var(--font-bg); +} + .study-info-panel-header:not(:first-child) { margin-top: 20px; } @@ -209,7 +250,7 @@ h3 { border-radius: 5px; text-decoration: none; opacity: 1; - font-size: 1rem; + font-size: var(--font-md); border: 1px solid var(--color-accent); } @@ -226,6 +267,10 @@ h3 { /* General Styles */ +p { + font-size: var(--font-sm); +} + /* Style checkboxes to match the grey color of the modal close button */ input[type="checkbox"] { accent-color: var(--color-gray); /* Bootstrap's secondary color */ @@ -240,18 +285,19 @@ input[type="checkbox"] { opacity: 0.6; } -.form-check-input:focus { +.form-check-input:focus { outline: none; box-shadow: none; border: 1px solid var(--color-gray); } -a:link, a:visited { +a:link, +a:visited { text-decoration: none; color: inherit; } -@media (max-width: 1050px) { +@media (max-width: 1200px) { /* Hide the sidebar from the user */ #sidebar { position: fixed; @@ -264,13 +310,31 @@ a:link, a:visited { transition: right 0.3s ease; } + .left-section { + overflow-y: auto; + } + /* Make the sidebar appear when it should be visible */ #sidebar.visible-sidebar { right: 0; } /* Add two buttons to open and close the sidebar */ - #toggle-sidebar, + #toggle-sidebar { + display: flex; + justify-content: end; + font-size: var(--font-bg); + } + + #toggle-sidebar-btn { + display: flex; + border: none; + background-color: var(--color-gray-light); + border-radius: 30%; + opacity: 0.5; + gap: 0.5em; + } + #close-sidebar { display: block; border: none; @@ -285,7 +349,7 @@ a:link, a:visited { } #close-icon { - width: 2em; + width: 1.25rem; } /* Add a mask over the content when the sidebar is open */ @@ -317,7 +381,7 @@ a:link, a:visited { } } -@media (max-width: 630px) { +@media (max-width: 860px) { /* Change the navbar links from text to icons */ .navbar-text { display: none; @@ -335,11 +399,20 @@ a:link, a:visited { } } -@media (max-width: 450px) { +@media (max-width: 550px) { + #nav-r { + gap: 0.5rem; + } + #earXplore-repo { display: none; } -} - + .link-section { + gap: 0em; + } + header { + gap: 0; + } +} diff --git a/static/styles/filter.css b/static/styles/filter.css index 5b9f88b..a9bbcee 100644 --- a/static/styles/filter.css +++ b/static/styles/filter.css @@ -1,70 +1,74 @@ .small-button { - font-size: 0.75rem; /* Adjust the font size as needed */ - padding: 0.1rem 0.2rem; /* Adjust the padding as needed */ + font-size: 0.75rem; /* Adjust the font size as needed */ + padding: 0.1rem 0.2rem; /* Adjust the padding as needed */ } .select-buttons { - display: flex; - gap: 0.5em; - margin: 0.5em 0; + display: flex; + gap: 0.5em; + margin: 0.5em 0; } #select-filter-button { - display: none; + display: none; + font-size: var(--font-bg); } #reset-filters-button { - background-color: var(--color-accent); - border: 1px solid var(--color-accent); + background-color: var(--color-accent); + border: 1px solid var(--color-accent); } #columnToggles { - display: flex; - flex-wrap: wrap; - gap: 0.5em; + display: flex; + flex-wrap: wrap; + gap: 0.5em; } #toggle-menu-container { - font-size: medium; + font-size: var(--font-md); } @media (max-width: 1050px) { - #toggle-menu-container { - height: 0; - overflow: hidden; - transition: height 0.3s ease; - interpolate-size: allow-keywords; - } + #toggle-menu-container { + height: 0; + overflow: hidden; + transition: height 0.3s ease; + interpolate-size: allow-keywords; + } - #filter-icon { - width: 2em; - } + #filter-icon { + width: 2em; + } - #toggle-menu-container.visible-filters { - height: auto; - } - - #select-filter-button { - display: flex; - align-items: center; - gap: 0.5em; - margin: 1em 0 1em auto; - border: none; - background-color: var(--color-gray-light); - border-radius: 5%; - opacity: 0.5; - } + #toggle-menu-container.visible-filters { + height: auto; + } - .select-buttons { - justify-content: start; - } + #select-filter-button { + display: flex; + align-items: center; + gap: 0.5em; + margin: 1em 0 1em auto; + border: none; + background-color: var(--color-gray-light); + border-radius: 5%; + opacity: 0.5; + } + + .select-buttons { + justify-content: start; + } } .filter-wrapper { - background: #eee; - border: 1px solid transparent; - border-radius: 20px; - overflow: hidden; - margin: 0.5px; - padding: 2px 10px; -} \ No newline at end of file + background: #eee; + border: 1px solid transparent; + border-radius: 20px; + overflow: hidden; + margin: 0.5px; + padding: 2px 10px; + display: flex; + gap: 0.5rem; + align-items: center; +} diff --git a/static/styles/nouislider.css b/static/styles/nouislider.css index 175f99a..fb7075b 100644 --- a/static/styles/nouislider.css +++ b/static/styles/nouislider.css @@ -2,6 +2,7 @@ * These styles are required for noUiSlider to function. * You don't need to change these rules to apply your design. */ + .noUi-target, .noUi-target * { -webkit-touch-callout: none; @@ -14,6 +15,7 @@ user-select: none; -moz-box-sizing: border-box; box-sizing: border-box; + --resp-height: clamp(0.75rem, 0.3713rem + 0.5492vw, 1.25rem); } .noUi-target { position: relative; @@ -82,11 +84,12 @@ /* Slider size and handle placement; */ .noUi-horizontal { - height: 18px; + height: var(--resp-height); } .noUi-horizontal .noUi-handle { - width: 34px; - height: 28px; + width: 10%; + max-width: 30px; + height: calc(var(--resp-height) * 2); right: -17px; top: -6px; } @@ -107,16 +110,17 @@ * Giving the connect element a border radius causes issues with using transform: scale */ .noUi-target { - background: #FAFAFA; + background: #fafafa; border-radius: 4px; - border: 1px solid #D3D3D3; - box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB; + border: 1px solid #d3d3d3; + box-shadow: inset 0 1px 1px #f0f0f0, 0 3px 6px -5px #bbb; } + .noUi-connects { border-radius: 3px; } .noUi-connect { - background: #B89491; + background: #b89491; } /* Handles and cursors; */ @@ -127,14 +131,14 @@ cursor: ns-resize; } .noUi-handle { - border: 1px solid #D9D9D9; + border: 1px solid #d9d9d9; border-radius: 3px; - background: #FFF; + background: #fff; cursor: default; - box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #EBEBEB, 0 3px 6px -3px #BBB; + box-shadow: inset 0 0 1px #fff, inset 0 1px 7px #ebebeb, 0 3px 6px -3px #bbb; } .noUi-active { - box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #DDD, 0 3px 6px -3px #BBB; + box-shadow: inset 0 0 1px #fff, inset 0 1px 7px #ddd, 0 3px 6px -3px #bbb; } /* Handle stripes; */ @@ -143,14 +147,14 @@ content: ""; display: block; position: absolute; - height: 14px; + height: 65%; width: 1px; - background: #E8E7E6; - left: 14px; - top: 6px; + background: #e8e7e6; + left: 42%; + top: 20%; } .noUi-handle:after { - left: 17px; + left: 58%; } .noUi-vertical .noUi-handle:before, .noUi-vertical .noUi-handle:after { @@ -165,7 +169,7 @@ /* Disabled state; */ [disabled] .noUi-connect { - background: #B8B8B8; + background: #b8b8b8; } [disabled].noUi-target, [disabled].noUi-handle, @@ -201,13 +205,13 @@ */ .noUi-marker { position: absolute; - background: #CCC; + background: #ccc; } .noUi-marker-sub { - background: #AAA; + background: #aaa; } .noUi-marker-large { - background: #AAA; + background: #aaa; } /* Horizontal layout; * @@ -282,7 +286,7 @@ -webkit-transform: translate(-50%, 0); transform: translate(-50%, 0); left: 50%; - bottom: -120%; + top: calc(var(--resp-height) * 1.5); } .noUi-vertical .noUi-tooltip { -webkit-transform: translate(0, -50%); diff --git a/static/styles/similarity.css b/static/styles/similarity.css index 836ad6a..830cbc8 100644 --- a/static/styles/similarity.css +++ b/static/styles/similarity.css @@ -2,6 +2,11 @@ #similarityContainer { padding: 1.5em; margin: 1.5em 0; + font-size: var(--font-md); +} + +.form-select { + font-size: var(--font-md); } #categoryDropdownContainer { @@ -20,14 +25,13 @@ .btn-check, .btn-outline-secondary { - font-size: 1em; background-color: white; } -.btn-check:checked+.btn{ +.btn-check:checked + .btn { background-color: var(--color-accent); color: white; - border-color: var(--color-accent); + border-color: var(--color-accent); } .similarityControlContainer { @@ -35,6 +39,11 @@ flex-direction: column; align-items: center; gap: 0.5em; + font-size: var(--font-md); +} + +.similarityControlContainer label { + font-size: var(--font-md); } .btn-similarity { @@ -44,13 +53,13 @@ } .btn-check:checked + .btn-similarity:hover { - background-color: var(--color-accent) !important; + background-color: var(--color-accent) !important; border-color: var(--color-accent) !important; color: white !important; } .btn-similarity:hover { - background-color: #f8e6e6 !important; + background-color: #f8e6e6 !important; border-color: var(--color-accent) !important; color: #666 !important; } @@ -65,6 +74,7 @@ #thresholdSlider { width: 100%; + height: clamp(0.5rem, 0.3713rem + 0.5492vw, 1.25rem); } #thresholdValue { @@ -78,13 +88,17 @@ flex-direction: column; align-items: center; flex-shrink: 2; - font-size: 0.8em; + font-size: var(--font-md); +} + +#legend h4 { + font-size: var(--font-xl); } #legendNote { font-style: italic; - font-size: 0.8em; - margin-bottom: 0.5em + font-size: var(--font-sm); + margin-bottom: 0.5em; } #legendItems { @@ -126,6 +140,23 @@ opacity: 0.3; } +#thresholdInfoIcon { + width: var(--font-bg); +} + +#connectionsContainer, +#connectionsModal button { + font-size: var(--font-md); +} + +#connectionsContainer h5 { + font-size: var(--font-xl); +} + +#connectionsContainer th { + font-size: var(--font-bg); +} + .info-circle { font-weight: bold; color: var(--color-accent); @@ -145,4 +176,18 @@ #visualization-warning { display: flex; } -} \ No newline at end of file + + .controls { + flex-direction: column; + align-items: start; + } + + .sliderContainer { + width: 60%; + margin-bottom: 1em; + } + + .link { + stroke-width: 0.5; + } +} diff --git a/static/styles/tableView.css b/static/styles/tableView.css index 8c3abca..e0a5b89 100644 --- a/static/styles/tableView.css +++ b/static/styles/tableView.css @@ -16,7 +16,7 @@ background-color: var(--color-accent); color: white; border: none; - font-size: 1em; + font-size: var(--font-md); padding: 0.1em 0.4em; border-radius: 5px; cursor: pointer; @@ -41,7 +41,7 @@ color: #6c757d; opacity: 0.5; margin-left: 5px; - font-size: 0.8em; + font-size: var(--font-sm); white-space: pre; /* Preserves whitespace */ } @@ -49,4 +49,12 @@ .sort-arrows.active { color: inherit; opacity: 1; -} \ No newline at end of file +} + +#table thead { + font-size: var(--font-bg); +} + +#table tbody { + font-size: var(--font-md); +} diff --git a/static/styles/timeline.css b/static/styles/timeline.css index 8a68258..f26ae3c 100644 --- a/static/styles/timeline.css +++ b/static/styles/timeline.css @@ -2,7 +2,7 @@ padding: 1.5em; margin: 1.5em 0; } - + .timeline-controls { margin-bottom: 1.5em; display: flex; @@ -11,6 +11,21 @@ text-align: center; flex-wrap: wrap; gap: 1em; + font-size: var(--font-md); +} + +.timeline-controls label { + font-size: var(--font-md); + padding: calc(var(--resp-padding) * 0.5) calc(var(--resp-padding) * 2); + min-width: fit-content; +} + +.form-select { + font-size: var(--font-md); +} + +label[for="timelineColorCategory"] { + padding: 0; } .category-dropdown-container { @@ -27,7 +42,7 @@ .link { stroke: #999; - stroke-width: 1; + stroke-width: 0.5; stroke-opacity: 0.6; fill: none; } @@ -87,22 +102,6 @@ color: white; } -/* Timeline axis styling */ -.timeline-axis { - stroke: #ccc; - stroke-width: 2; -} - -.timeline-tick { - stroke: #ccc; - stroke-width: 1; -} - -.timeline-label { - font-size: 12px; - fill: #666; -} - /* Container dimensions */ #timeline-graph-container { width: 100%; @@ -125,7 +124,7 @@ .timeline-legend-item { display: flex; align-items: center; - font-size: 1em; + font-size: var(--font-md); } .timeline-legend-line { @@ -153,16 +152,16 @@ flex-direction: column; align-items: center; flex-shrink: 2; - font-size: 0.8em; + font-size: var(--font-md); } #legend h4 { - font-size: 1.2em; + font-size: var(--font-xl); } #legendNote { font-style: italic; - font-size: 0.8em; + font-size: var(--font-sm); margin-bottom: 0.5em; } @@ -180,10 +179,24 @@ align-items: center; } -#timelineConnectionsContainer, .centered-cell { +#timelineConnectionsContainer, +.centered-cell { text-align: center; vertical-align: middle; user-select: none; + font-size: var(--font-md); +} + +#timelineConnectionsModal button { + font-size: var(--font-md); +} + +#timelineConnectionsContainer th { + font-size: var(--font-bg); +} + +#timelineConnectionsContainer h5 { + font-size: var(--font-xl); } .node-tooltip { @@ -203,5 +216,25 @@ #visualization-warning { display: flex; } -} + .tick-line { + stroke: black; + } + + .tick-text { + fill: rgb(33, 37, 41); + } + + #timeline-container { + padding: var(--resp-padding); + } + + .timeline-controls { + flex-direction: column; + align-items: start; + } + + .link { + stroke-width: 0.5; + } +} diff --git a/templates/bar-chart.html b/templates/bar-chart.html index 1ea75dd..e66e61f 100644 --- a/templates/bar-chart.html +++ b/templates/bar-chart.html @@ -1,58 +1,87 @@ -{% extends "filter.html" %} - -{% block styles %} - {{ super() }} - -{% endblock styles %} - -{% block content %} - {{ super() }} -