From 43aa25d986575ad3d82db6aa691be9e10859b0f5 Mon Sep 17 00:00:00 2001 From: Felix Claessen <30658763+Flix6x@users.noreply.github.com> Date: Sat, 25 Jun 2022 14:05:14 +0200 Subject: [PATCH] Sensor chart ux improvements (#447) Introduce a collapsible sidepanel on the left of the sensor page, activated by hover or swipe, depending on the device. Also implements many smaller UX improvements: - Show spinner while fetching new data - Show single month and fewer custom ranges - Rotate y-axis labels to improve legibility (addresses Issue #442) - Update sensor data and annotations together instead of first come first serve - Streamline calendar styling - Streamline use of box shadows - Fix positioning of chart actions button - Move units to right side of tooltip - Cancel previous request upon a new calendar selection - Style navbar logo to have a consistent height and adjust the width of the navbar-header accordingly - Actually load the font intended to be used in 0.10 - Make sure time axis labels aren't too close to each other * Show updated sensor data and annotations together Signed-off-by: F.N. Claessen * Show spinner while fetching new data Signed-off-by: F.N. Claessen * Switch from id-based styling to class-based styling Signed-off-by: F.N. Claessen * Move styling to css, and lower spinner Signed-off-by: F.N. Claessen * Simplify and streamline datepicker fontsize Signed-off-by: F.N. Claessen * Streamline datepicker margins Signed-off-by: F.N. Claessen * Add margins and side panel activated on hover Signed-off-by: F.N. Claessen * Correct margins and padding of side panel to allow for custom ranges at the bottom of the calendar Signed-off-by: F.N. Claessen * Show single month Signed-off-by: F.N. Claessen * Fewer custom ranges Signed-off-by: F.N. Claessen * Side panel rounded similar to buttons rather than similar to cards Signed-off-by: F.N. Claessen * Align box shadows of cards and calendar Signed-off-by: F.N. Claessen * Non-transparent cards Signed-off-by: F.N. Claessen * Simplified padding notation Signed-off-by: F.N. Claessen * Move chart actions buttons away from the card's corner (negative margin) Signed-off-by: F.N. Claessen * Rotate y-axis labels to improve legibility Signed-off-by: F.N. Claessen * Remove sensor chart title if the same information is already contained in the y-axis label Signed-off-by: F.N. Claessen * Move unit to right side of tooltip Signed-off-by: F.N. Claessen * Style predefined datetime ranges Signed-off-by: F.N. Claessen * Raise column to top without requiring flex display Signed-off-by: F.N. Claessen * Rename sidepanel class and separate styling specific to the sidepanel being on the left Signed-off-by: F.N. Claessen * Show spinner only while the promise is being fulfilled Signed-off-by: F.N. Claessen * Cancel previous request when the user makes a new request Signed-off-by: F.N. Claessen * Do not let spinner block the full page height, so the sensor table navigation can still be used Signed-off-by: F.N. Claessen * Change header and label colors inside the sidepanel to contrast against the sidepanel background Signed-off-by: F.N. Claessen * Style navbar logo to have a consistent height and adjust the width of the navbar-header accordingly Signed-off-by: F.N. Claessen * Actually load intended font Signed-off-by: F.N. Claessen * Enforce separation of time axis labels Signed-off-by: F.N. Claessen * Add return type annotation and docs: applying chart defaults returns a dictionary with vega-lite specs Signed-off-by: F.N. Claessen * Resolve hover glitch when exiting either the list of months or the list of years with the pointer. This stops the side panel from collapsing and reopening. Signed-off-by: F.N. Claessen * Enable swiping for left sidepanel Signed-off-by: F.N. Claessen * Stop using redundant litepicker plugin, which was messing with calendar styling Signed-off-by: F.N. Claessen * Fix test Signed-off-by: F.N. Claessen * Changelog entry Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 1 + .../data/models/charts/belief_charts.py | 10 +- flexmeasures/data/models/charts/defaults.py | 6 +- .../data/models/charts/test_chart_defaults.py | 2 +- flexmeasures/data/models/time_series.py | 4 +- flexmeasures/ui/static/css/flexmeasures.css | 147 +++++++++++++----- flexmeasures/ui/static/js/flexmeasures.js | 11 ++ .../ui/static/js/swiped-events.min.js | 9 ++ flexmeasures/ui/templates/base.html | 3 +- flexmeasures/ui/templates/views/sensors.html | 100 +++++++----- 10 files changed, 206 insertions(+), 87 deletions(-) create mode 100644 flexmeasures/ui/static/js/swiped-events.min.js diff --git a/documentation/changelog.rst b/documentation/changelog.rst index a09037ea3..731e6b5d7 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -8,6 +8,7 @@ v0.11.0 | June XX, 2022 New features ------------- * Individual sensor charts show available annotations [see `PR #428 `_] +* Collapsible sidepanel (hover/swipe) used for date selection on sensor charts, and various styling improvements [see `PR #447 `_] Bugfixes ----------- diff --git a/flexmeasures/data/models/charts/belief_charts.py b/flexmeasures/data/models/charts/belief_charts.py index d14623e7a..aebb86bb0 100644 --- a/flexmeasures/data/models/charts/belief_charts.py +++ b/flexmeasures/data/models/charts/belief_charts.py @@ -9,13 +9,14 @@ def bar_chart( unit = sensor.unit if sensor.unit else "a.u." event_value_field_definition = dict( title=f"{capitalize(sensor.sensor_type)} ({unit})", - format=".3s", + format=[".3s", unit], + formatType="quantityWithUnitFormat", stack=None, **FIELD_DEFINITIONS["event_value"], ) chart_specs = { "description": "A simple bar chart.", - "title": capitalize(sensor.name), + "title": capitalize(sensor.name) if sensor.name != sensor.sensor_type else None, "mark": "bar", "encoding": { "x": FIELD_DEFINITIONS["event_start"], @@ -25,7 +26,10 @@ def bar_chart( "opacity": {"value": 0.7}, "tooltip": [ FIELD_DEFINITIONS["full_date"], - event_value_field_definition, + { + **event_value_field_definition, + **dict(title=f"{capitalize(sensor.sensor_type)}"), + }, FIELD_DEFINITIONS["source"], ], }, diff --git a/flexmeasures/data/models/charts/defaults.py b/flexmeasures/data/models/charts/defaults.py index 1f4bb3274..6cc35c043 100644 --- a/flexmeasures/data/models/charts/defaults.py +++ b/flexmeasures/data/models/charts/defaults.py @@ -17,11 +17,13 @@ field="event_start", type="temporal", title=None, + axis={"labelOverlap": True, "labelSeparation": 1}, ), "event_end": dict( field="event_end", type="temporal", title=None, + axis={"labelOverlap": True, "labelSeparation": 1}, ), "event_value": dict( field="event_value", @@ -105,6 +107,7 @@ titleFontSize=FONT_SIZE, labelFontSize=FONT_SIZE, ), + axisY={"titleAngle": 0, "titleAlign": "left", "titleY": -15, "titleX": -40}, title=dict( fontSize=FONT_SIZE, ), @@ -128,7 +131,8 @@ def apply_chart_defaults(fn): @wraps(fn) - def decorated_chart_specs(*args, **kwargs): + def decorated_chart_specs(*args, **kwargs) -> dict: + """:returns: dict with vega-lite specs, even when applied to an Altair chart.""" dataset_name = kwargs.pop("dataset_name", None) include_annotations = kwargs.pop("include_annotations", None) if isinstance(fn, Callable): diff --git a/flexmeasures/data/models/charts/test_chart_defaults.py b/flexmeasures/data/models/charts/test_chart_defaults.py index 4a08a328c..1042e2245 100644 --- a/flexmeasures/data/models/charts/test_chart_defaults.py +++ b/flexmeasures/data/models/charts/test_chart_defaults.py @@ -6,4 +6,4 @@ def test_default_encodings(): """Check default encodings for valid vega-lite specifications.""" for field_name, field_definition in FIELD_DEFINITIONS.items(): - assert alt.StringFieldDefWithCondition(**field_definition) + assert alt.PositionFieldDef(**field_definition) diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index 210a1dbaf..03700c34a 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -366,9 +366,7 @@ def chart( # Set up chart specification if dataset_name is None: dataset_name = "sensor_" + str(self.id) - self.sensor_type = ( - self.name - ) # todo remove this placeholder when sensor types are modelled + self.sensor_type = self.get_attribute("sensor_type", self.name) chart_specs = chart_type_to_chart_specs( chart_type, sensor=self, diff --git a/flexmeasures/ui/static/css/flexmeasures.css b/flexmeasures/ui/static/css/flexmeasures.css index d2c40bf25..38e970410 100644 --- a/flexmeasures/ui/static/css/flexmeasures.css +++ b/flexmeasures/ui/static/css/flexmeasures.css @@ -33,7 +33,30 @@ --delete-color: var(--red); } -@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); +/* devanagari */ +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/poppins/v20/pxiEyp8kv8JHgFVrJJbecmNE.woff2) format('woff2'); + unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; +} +/* latin-ext */ +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/poppins/v20/pxiEyp8kv8JHgFVrJJnecmNE.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/poppins/v20/pxiEyp8kv8JHgFVrJJfecg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} body { padding-top: 100px; /* This default is overridden by flexmeasures.js to support a fluid navbar */ @@ -43,6 +66,12 @@ body *:not(.fa, .glyphicon) { font-family: 'Poppins', sans-serif; } +@media (min-width: 768px) { + .on-top-md { + z-index: 1010; + } +} + h1 { font-size: 35px; color: var(--primary-color); @@ -63,6 +92,12 @@ h1, h2, h3 { font-weight: 600; color: var(--primary-color); } +.sidepanel h1, +.sidepanel h2, +.sidepanel h3, +.sidepanel label { + color: var(--white) !important; +} hr { border-top: none; @@ -108,8 +143,14 @@ form button[type="submit"] { } #spinner { - padding-top: 50px !important; - padding-bottom: 50px !important; + padding-top: 200px; + text-align: center; + position: absolute; + z-index: 10; + font-size: 8px; + top: 0; + left: 0; + width: 100%; } .legend { @@ -170,7 +211,6 @@ p.error { /* --- Nav Bar --- */ .navbar-tool-name { - margin-right: 30px; color: var(--secondary-color); } @@ -220,8 +260,8 @@ p.error { .navbar-brand { display: inline-block; - width: 200px; height: 100%; + padding: 10px 15px; } .navbar-brand a { @@ -233,10 +273,6 @@ p.error { color: var(--secondary-color) !important; } -.navbar-brand a span img { - width: 200px; -} - .navbar-default .navbar-collapse, .navbar-default .navbar-form { border-color: var(--primary-border-color); } @@ -264,8 +300,8 @@ p.error { @media (min-width: 768px) { .navbar-nav>li>a { - padding-top: 28px; - padding-bottom: 28px; + padding-top: 20px; + padding-bottom: 20px; } .navbar-default .dropdown-menu a { @@ -301,8 +337,7 @@ p.error { } #navbar-logo { - height: 50px; - position: fixed; top: 0; left: 0; + height: 40px; } /* --- End Nav Bar --- */ @@ -877,13 +912,48 @@ body .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { /* ---- End Modal ---- */ -/* ---- Date picker ---- */ +/* ---- Side panel ---- */ -#datepicker { - margin-top: 50px; - margin-bottom: 30px; +@media (min-width: 768px) { + .sidepanel-container { + z-index: 20; + } + .sidepanel { + background: var(--nav-default-background-color); + width: calc(var(--litepicker-day-width) * 9); + margin: 15px; + padding: 20px 15px; + transition: .3s; + -webkit-transition: .3s; + -moz-transition: .3s; + -ms-transition: .3s; + -o-transition: .3s; + } + .left-sidepanel { + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + border-top-right-radius: 15px; + border-bottom-right-radius: 15px; + } + .sidepanel-container > .left-sidepanel:not(.sidepanel-show) { + transform: translateX(-106%); + } + @media (hover: hover) { + .sidepanel-container:hover > .left-sidepanel, + .sidepanel-container:focus-within > .left-sidepanel { + transform: translateX(-30px); + } + } + @media (hover: none) { + .sidepanel-container > .left-sidepanel.sidepanel-show { + transform: translateX(-30px); + } + } } + +/* ---- Date picker ---- */ + .datetimepicker input { width: 100%; } @@ -897,7 +967,6 @@ body .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { outline: none; border-radius: 4px; border: 1px solid #ddd; - font-size: 13px; color: var(--nav-default-color); background: var(--nav-default-background-color); -webkit-border-radius: 4px; @@ -907,8 +976,9 @@ body .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { } .container__predefined-ranges { - padding: 20px; - margin-right: 10px; + padding: 0px; + margin-top: 15px; + justify-content: space-around; box-shadow: 0 0 10px rgba(0,0,0,.1) !important; border-radius: 6px !important; -webkit-border-radius: 6px !important; @@ -918,12 +988,17 @@ body .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { } .container__predefined-ranges button { - font-size: 14px; font-weight: 500; cursor: pointer !important; + border-right: 1px solid var(--primary-border-color) !important; + flex: 1 1 auto; +} +.container__predefined-ranges > button:last-child { + border-right: none !important; } .container__months { + box-shadow: 0 0 10px rgba(0,0,0,.1) !important; border-radius: 6px !important; -webkit-border-radius: 6px !important; -moz-border-radius: 6px !important; @@ -932,14 +1007,6 @@ body .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { justify-content: center; } -.month-item-weekdays-row { - font-size: 14px; -} - -.week-number { - font-size: 14px !important; -} - .litepicker .container__days .day-item.is-today { color: var(--nav-current-color); background: var(--secondary-transparent); @@ -947,7 +1014,6 @@ body .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { .litepicker .container__days .day-item { color: var(--litepicker-day-color); - font-size: 14px; cursor: pointer; width: 38px; height: 38px; @@ -1058,11 +1124,9 @@ body .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { fill: var(--secondary-color) !important; } -@media (max-width:525px) { +@media (max-width: 525px) { #datepicker .container__main { flex-wrap: wrap; - margin-left: 15px; - margin-right: 15px; } #datepicker .container__predefined-ranges { @@ -1077,12 +1141,10 @@ body .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { /* --- Sensor Data --- */ -.sensor-data .sensor-chart-main { - padding: 0; -} - -#sensorchart { - padding: 20px 15px 20px 15px; +.card { + background: var(--white); + margin: 15px; + padding: 20px 15px; box-shadow: 0 0 10px rgba(0,0,0,.1); border-radius: 6px; -webkit-border-radius: 6px; @@ -1090,6 +1152,9 @@ body .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { -ms-border-radius: 6px; -o-border-radius: 6px; } +.card.vega-embed summary { + transform: translate(-15px, 15px); +} .role-title-text text { font-size: 20px; @@ -1100,10 +1165,6 @@ body .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { margin: 0 !important; } -#sensorchart { - margin-bottom: 40px; -} - #vg-tooltip-element { font-size: 16px; } diff --git a/flexmeasures/ui/static/js/flexmeasures.js b/flexmeasures/ui/static/js/flexmeasures.js index 8dd0becba..7e68b1c9a 100644 --- a/flexmeasures/ui/static/js/flexmeasures.js +++ b/flexmeasures/ui/static/js/flexmeasures.js @@ -310,3 +310,14 @@ function submit_market() { function submit_sensor_type() { $("#sensor_type-form").attr("action", empty_location).submit(); } + +/** Tooltips: Register custom formatter for quantities incl. units + Usage: + { + 'format': [, ], + 'formatType': 'quantityWithUnitFormat' + } +*/ +vega.expressionFunction('quantityWithUnitFormat', function(datum, params) { + return d3.format(params[0])(datum) + " " + params[1]; +}); diff --git a/flexmeasures/ui/static/js/swiped-events.min.js b/flexmeasures/ui/static/js/swiped-events.min.js new file mode 100644 index 000000000..e3bac8952 --- /dev/null +++ b/flexmeasures/ui/static/js/swiped-events.min.js @@ -0,0 +1,9 @@ +/*! + * swiped-events.js - v1.1.6 + * Pure JavaScript swipe events + * https://github.com/john-doherty/swiped-events + * @inspiration https://stackoverflow.com/questions/16348031/disable-scrolling-when-touch-moving-certain-element + * @author John Doherty + * @license MIT + */ +!function(t,e){"use strict";"function"!=typeof t.CustomEvent&&(t.CustomEvent=function(t,n){n=n||{bubbles:!1,cancelable:!1,detail:void 0};var a=e.createEvent("CustomEvent");return a.initCustomEvent(t,n.bubbles,n.cancelable,n.detail),a},t.CustomEvent.prototype=t.Event.prototype),e.addEventListener("touchstart",function(t){if("true"===t.target.getAttribute("data-swipe-ignore"))return;s=t.target,r=Date.now(),n=t.touches[0].clientX,a=t.touches[0].clientY,u=0,i=0},!1),e.addEventListener("touchmove",function(t){if(!n||!a)return;var e=t.touches[0].clientX,r=t.touches[0].clientY;u=n-e,i=a-r},!1),e.addEventListener("touchend",function(t){if(s!==t.target)return;var e=parseInt(l(s,"data-swipe-threshold","20"),10),o=parseInt(l(s,"data-swipe-timeout","500"),10),c=Date.now()-r,d="",p=t.changedTouches||t.touches||[];Math.abs(u)>Math.abs(i)?Math.abs(u)>e&&c0?"swiped-left":"swiped-right"):Math.abs(i)>e&&c0?"swiped-up":"swiped-down");if(""!==d){var b={dir:d.replace(/swiped-/,""),touchType:(p[0]||{}).touchType||"direct",xStart:parseInt(n,10),xEnd:parseInt((p[0]||{}).clientX||-1,10),yStart:parseInt(a,10),yEnd:parseInt((p[0]||{}).clientY||-1,10)};s.dispatchEvent(new CustomEvent("swiped",{bubbles:!0,cancelable:!0,detail:b})),s.dispatchEvent(new CustomEvent(d,{bubbles:!0,cancelable:!0,detail:b}))}n=null,a=null,r=null},!1);var n=null,a=null,u=null,i=null,r=null,s=null;function l(t,n,a){for(;t&&t!==e.documentElement;){var u=t.getAttribute(n);if(u)return u;t=t.parentNode}return a}}(window,document); \ No newline at end of file diff --git a/flexmeasures/ui/templates/base.html b/flexmeasures/ui/templates/base.html index 7d5e65041..3f57baeb9 100644 --- a/flexmeasures/ui/templates/base.html +++ b/flexmeasures/ui/templates/base.html @@ -73,7 +73,7 @@ {% if menu_logo %} {% else %} - FlexMeasures + {% endif %} @@ -227,6 +227,7 @@ + {% if show_datepicker %} diff --git a/flexmeasures/ui/templates/views/sensors.html b/flexmeasures/ui/templates/views/sensors.html index 3238e55f5..53188bdd3 100644 --- a/flexmeasures/ui/templates/views/sensors.html +++ b/flexmeasures/ui/templates/views/sensors.html @@ -8,11 +8,20 @@
-
-
-
-
+
+
+
+
+
+
+
+
+ + Loading... +
+
+

@@ -25,7 +34,6 @@ - {% endblock %} \ No newline at end of file