diff --git a/static/css/base.css b/static/css/base.css index a38a1573b..0f4513ac8 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -91,12 +91,14 @@ button.link-button:focus { } #scatter-container { - width: 890px; - height: 425px; + width: 100%; + max-width: 890px; + height: 425px; /* Fallback for browsers predating aspect-ratio; see setScatterViewBox in decorate.js, which sets a precise aspect-ratio once the chart's real dimensions are known. */ position: relative; + overflow-x: auto; } -#scatter-bg, +#scatter-bg, #scatter-chart { width: 100%; height: 100%; @@ -105,6 +107,13 @@ button.link-button:focus { left: 0; } +#scatter-chart > svg, +#svg-bg { + display: block; + width: 100%; + height: auto; +} + #scatter-chart { z-index: 10; } @@ -187,6 +196,20 @@ button.link-button:focus { /* Maps and charts */ + +/* Scale the scatter plot and map's own SVGs to fit their container + (viewBox is added at render time, see congress.js/voteCharts.js) + instead of overflowing it. Deliberately scoped to just these two -- + applying this to every .dc-chart also shrank small sidebar charts + like #party-chart, which don't have a too-wide-for-mobile problem and + were previously rendered at native (legible) size regardless of their + column width. */ +#map-chart > svg { + display: block; + width: 100%; + height: auto; +} + .dc-chart g.row text { fill: black; } @@ -269,11 +292,11 @@ line.cutline { .tooltip > .tooltip-inner { opacity: 0.9; } .tooltip > .tooltip-arrow { opacity: 0.9; } -.left-tooltip + .tooltip > .tooltip-inner { text-align:left; min-width:400px;} +.left-tooltip + .tooltip > .tooltip-inner { text-align:left; width:400px; max-width:calc(100vw - 40px); } #memberList { - min-width: 800px; + min-width: 0; } ul#memberList.geography @@ -549,7 +572,7 @@ span.party_header { padding-right: 20px; } .roster_header { padding-bottom: 20px; } .roster_header h4 { display:inline; } #memberList { columns: auto 4; list-style-type: none; overflow: auto; width: 100%; margin-bottom: 60px; } -#memberTextList { margin-bottom: 40px; } +#memberTextList { margin-bottom: 40px; overflow-x: auto; } .save_icon { margin-left: 5px; font-size: 22px; vertical-align: middle; cursor: pointer; } #scatter-container { margin: 0 auto 10px auto; } #filterName { float: right; } @@ -568,7 +591,7 @@ span.party_header { padding-right: 20px; } #geoMap h4 { float: left; clear: none; vertical-align: middle; } #geoMap .glyphicon-save { margin-left: 5px; font-size: 18px; vertical-align: middle; cursor: pointer; } #geoMap .noteText { margin-left: 5px; width: 22px; vertical-align: middle; } -#geoMap #map-chart { margin-top: 10px; padding: 10px; vertical-align: bottom; } +#geoMap #map-chart { display: block; margin-top: 10px; padding: 10px; vertical-align: bottom; } #geoMap #map-chart #zoomIn { position: absolute; left: 25px; top: 40px; width: 30px; height: 30px; } #geoMap #map-chart #zoomOut { position: absolute; left: 25px; top: 80px; width: 30px; height: 30px; } #geoMap #map-chart #suppressMapControls { display: none; } @@ -586,6 +609,7 @@ span.party_header { padding-right: 20px; } #selectionFilterBar #sparse-selection { display: none; } .sortHeader { text-align:middle; padding-top: 3px; } .voteHeader { font-size: 19px; float: left; padding-right: 30px; text-align: middle; } +.voteListScroll { overflow-x: auto; } #voteList { margin-top: 15px; width: 100%; min-width: 1100px; } /* voteTable.js picks 1-4 columns based on how many members are listed @@ -704,7 +728,7 @@ h5.congSelector small { padding-left: 10px; } .bottomPad { padding-bottom: 20px; } .member_flag { width: 20px; vertical-align: middle; } .personSearch { padding-top: 10px; padding-bottom: 10px; clear: both; } -.loadVotes { float: right; padding-top: 12px; min-width: 400px; width: 400px; } +.loadVotes { float: right; padding-top: 12px; width: 400px; max-width: 100%; } .member_vote_load { position: fixed; top: 50%; diff --git a/static/js/congress.js b/static/js/congress.js index d88be8adb..e6bcd6867 100644 --- a/static/js/congress.js +++ b/static/js/congress.js @@ -171,6 +171,7 @@ function nomPlot() dc.filterAll(); dc.renderAll(); decorateNominate(nominateScatterChart, resultCache); + setScatterViewBox(nominateScatterChart); // Make brush box appear on click var scb = nominateScatterChart.select(".brush"); diff --git a/static/js/decorate.js b/static/js/decorate.js index 4df87829b..c2441dd11 100644 --- a/static/js/decorate.js +++ b/static/js/decorate.js @@ -1,6 +1,35 @@ var isDoingSelect=0; var delayUpdateToolip; +// Give the scatter plot's SVG a viewBox matching its configured drawing +// area, so CSS (#scatter-chart > svg in base.css) can scale the whole +// thing to fit its container instead of overflowing it. decorateNominate's +// own layout (axes, margins, heatmap) is computed directly from the +// chart's width()/height(), so those are exactly its true content bounds +// -- call this after decorateNominate() has run so the viewBox covers the +// decoration too, not just the scatter points dc.js draws on its own. +function setScatterViewBox(chart) { + var svg = chart.svg(); + if (!svg || !svg.node() || svg.attr("viewBox")) return; + svg.attr("viewBox", "0 0 " + chart.width() + " " + chart.height()) + .attr("preserveAspectRatio", "xMidYMid meet"); + + // #scatter-container has a fixed height (see base.css) as a fallback + // for browsers without aspect-ratio support. Once we know the chart's + // real width/height, size the container to match that ratio instead, + // so a narrow phone doesn't get a chart that's shrunk to fit its width + // while the container underneath stays at the fixed desktop height, + // leaving a large gap before whatever follows the chart on the page. + var container = document.getElementById("scatter-container"); + if (container) { + // aspect-ratio has no effect unless height is allowed to be + // computed from it -- base.css sets a fixed height as a fallback, + // so it has to be relaxed to auto here for the ratio to apply. + container.style.height = "auto"; + container.style.aspectRatio = chart.width() + " / " + chart.height(); + } +} + /* Add sponsor circle */ diff --git a/static/js/voteCharts.js b/static/js/voteCharts.js index a5fabb8eb..12d654534 100644 --- a/static/js/voteCharts.js +++ b/static/js/voteCharts.js @@ -10,6 +10,34 @@ var nominateScatterChart = dc.scatterPlot("#scatter-chart"); var globalPartyDimension = null; var globalData; +// Give the map's SVG a viewBox matching its actual rendered content +// (the union of all drawn path geometry) rather than mapChart's +// configured width()/height() -- the geo projection doesn't exactly fill +// that nominal canvas (it's inset a bit and slightly overflows the +// right/bottom edges), so using width()/height() directly clipped the +// map's right edge while leaving a blank margin on the left. Call this +// once the map's paths are actually in the DOM. +function setMapContentViewBox(chart, padding) { + var svg = chart.svg(); + if (!svg || !svg.node() || svg.attr("viewBox")) return; + padding = (padding === undefined) ? 4 : padding; + var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + svg.node().querySelectorAll("path").forEach(function(el) { + var b; + try { b = el.getBBox(); } catch(e) { return; } + if (b.width === 0 && b.height === 0) return; + minX = Math.min(minX, b.x); + minY = Math.min(minY, b.y); + maxX = Math.max(maxX, b.x + b.width); + maxY = Math.max(maxY, b.y + b.height); + }); + if (!isFinite(minX)) return; + svg.attr("viewBox", + (minX - padding) + " " + (minY - padding) + " " + + (maxX - minX + 2 * padding) + " " + (maxY - minY + 2 * padding)) + .attr("preserveAspectRatio", "xMidYMid meet"); +} + // Makes the bootstrap tooltip run for votes from before states were contiguous. $(document).ready(function(){$('[data-toggle="tooltip"]').tooltip();}); @@ -565,7 +593,9 @@ function drawWidgets(error, data, geodata, usaboundaries) // We are done defining everything, now let's just run our ancillary functions. dc.renderAll(); d3.select("div#geoMap > span#map-chart > svg").select("g.layer0").select("g").select("path").attr("opacity", 0.3).attr("stroke", "#666666"); + if(!failedMapLoad) setMapContentViewBox(mapChart); decorateNominate(nominateScatterChart, data); + setScatterViewBox(nominateScatterChart); addSponsorCircle(nominateScatterChart); if(!failedMapLoad) mapChart.on("filtered", pollFilters); votePartyChart.on("filtered", pollFilters); @@ -626,5 +656,6 @@ function doFullFilterReset() dc.redrawAll(); // Re-apply our decoration hack. decorateNominate(nominateScatterChart, globalData); + setScatterViewBox(nominateScatterChart); //updateVoteChart(); } diff --git a/views/congress.tpl b/views/congress.tpl index 6c73ea4a6..6a37bc25f 100644 --- a/views/congress.tpl +++ b/views/congress.tpl @@ -1,4 +1,6 @@ % STATIC_URL = "/static/" +% import random +% cache_breaker = random.randint(10000, 99999) % rcSuffix = lambda n: "%d%s" % (n,"tsnrhtdd"[(n//10%10!=1)*(n%10<4)*n%10::4]) % rebase('base.tpl', title='Congress View', extra_css=['map.css', 'scatter.css'], extra_js=["/static/js/libs/saveSvgAsPng.js", "/static/js/libs/jquery.tablesorter.min.js", "/static/js/libs/localStorage.js"]) % include('header.tpl') @@ -115,7 +117,7 @@ var tabular_view = 0; - - - - + + + + diff --git a/views/vote.tpl b/views/vote.tpl index 26a482c8d..2cdf86df3 100644 --- a/views/vote.tpl +++ b/views/vote.tpl @@ -1,4 +1,6 @@ % STATIC_URL = "/static/" +% import random +% cache_breaker = random.randint(10000, 99999) % rebase('base.tpl', title=plot_title, extra_css=["map.css","scatter.css", "bootstrap-slider.css"], extra_js=["/static/js/libs/saveSvgAsPng.js", "/static/js/libs/bootstrap-slider.min.js", "/static/js/libs/sticky-kit.min.js", "/static/js/stateMeta.js"]) % include('header.tpl') @@ -130,7 +132,9 @@ ) -
+
+
+
@@ -164,10 +168,10 @@ var nomBeta = {{ nom_beta }}; - - - - - - - + + + + + + +