Verified Commit d425d3fa authored by Elias Häußler's avatar Elias Häußler 🐛

[FEATURE] Make project website responsive and add device notice refs #2

parent abdfeda7
......@@ -53,6 +53,12 @@ module.exports = {
*/
CHART_SELECTOR: ".visualization__chart",
/**
* Minimum device width which is necessary to place both map and chart next to each other
* @type {number}
*/
MINIMUM_DESKTOP_WIDTH: 1200,
/**
* Geometrical center of Germany
* @type {number[]}
......@@ -71,10 +77,34 @@ module.exports = {
*/
GEO_KEY_NAME: "NAME_1",
/**
* Key of semester name inside CSV file
* @type {string}
*/
DATA_KEY_SEMESTER: "",
/**
* Default fallback color for states
* @type {string}
*/
STATE_DEFAULT_COLOR: "#aaa"
STATE_DEFAULT_COLOR: "#aaa",
/**
* CSS selector for device notice confirm button
* @type {string}
*/
DEVICE_NOTICE_CONFIRM_SELECTOR: ".device-notice__confirm",
/**
* Class to describe the confirmation of the device notice
* @type {string}
*/
DEVICE_NOTICE_CONFIRMED_CLASS: "device-notice-confirmed",
/**
* Cookie name of the device confirmation
* @type {string}
*/
DEVICE_NOTICE_COOKIE: "device-notice-confirm"
};
......@@ -25,19 +25,26 @@ export class Chart
*/
this._chart = {};
/**
* Aspect ratio of the chart (height / width)
* @type {number}
* @private
*/
this._aspectRatio = 0.7;
/**
* Total width
* @type {number}
* @private
*/
this._w = 700;
this._w = 650;
/**
* Total height
* @type {number}
* @private
*/
this._h = 500;
this._h = this._w * this._aspectRatio;
/**
* Margins
......@@ -47,8 +54,8 @@ export class Chart
this._margin = {
top: 50,
right: 30,
bottom: 120,
left: 80
bottom: 70,
left: 50
};
/**
......@@ -93,6 +100,13 @@ export class Chart
*/
this._svg = null;
/**
* Reference to main graphics element
* @type {Selection}
* @private
*/
this._g = null;
/**
* Chart title of current selected data (contains the name of the current selected state)
* @type {string}
......@@ -126,7 +140,7 @@ export class Chart
* @type {Selection}
* @private
*/
this._g = null;
this._gChart = null;
/**
* Graphics element of x axis
......@@ -186,7 +200,7 @@ export class Chart
// Chart path element
if (!this._path) {
this._path = this._g.append("path");
this._path = this._gChart.append("path");
}
}
......@@ -211,7 +225,7 @@ export class Chart
for (let currentData of data) {
if (currentData.state === value) {
_d.push([]);
_d[i].x = currentData[""];
_d[i].x = currentData[Global.DATA_KEY_SEMESTER];
_d[i++].y = +currentData[this._key_x];
}
}
......@@ -256,7 +270,7 @@ export class Chart
.attr("class", "chart__text");
// Add line to chart
this._g.selectAll("path")
this._gChart.selectAll("path")
.transition()
.attrs({
"class": "chart__line",
......@@ -282,20 +296,20 @@ export class Chart
for(let currentData of data) {
if (currentData.state === value) {
tmp.push({
x: this._x(currentData[""]),
x: this._x(currentData[Global.DATA_KEY_SEMESTER]),
y: this._y(currentData[this._key_x]),
active: currentData[""] === this._key_y
active: currentData[Global.DATA_KEY_SEMESTER] === this._key_y
});
}
}
// Render dots
this._g.selectAll("circle")
this._gChart.selectAll("circle")
.data(tmp)
.enter()
.append("circle");
this._g.selectAll("circle")
this._gChart.selectAll("circle")
.data(tmp)
.transition()
.attrs({
......@@ -320,15 +334,21 @@ export class Chart
this._svg = d3.select(Global.CHART_SELECTOR)
.append("svg")
.attrs({
"width": this._width + this._margin.left + this._margin.right,
"height": this._height + this._margin.top + this._margin.bottom
"width": "100%",
"height": "100%",
"viewBox": `0 0 ${this._width + this._margin.left + this._margin.right} ${this._height + this._margin.top + this._margin.bottom}`
})
.style("visibility", "hidden");
}
// Create main graphics element
if (this._g == null) {
this._g = this._svg.append("g");
}
// Create svg chart axis graphics element
if (this._gX == null) {
this._gX = this._svg.append("g")
this._gX = this._g.append("g")
.attrs({
"class": "chart__axis chart__axis--x",
"transform": "translate(" + this._margin.left + ", " + (this._height + this._margin.top) + ")"
......@@ -336,7 +356,7 @@ export class Chart
}
if (this._gY == null) {
this._gY = this._svg.append("g")
this._gY = this._g.append("g")
.attrs({
"class": "chart__axis chart__axis--y",
"transform": "translate(" + this._margin.left + ", " + this._margin.top + ")"
......@@ -344,14 +364,14 @@ export class Chart
}
// Create svg chart graphics element
if (this._g == null) {
this._g = this._svg.append("g")
if (this._gChart == null) {
this._gChart = this._g.append("g")
.attr("transform", "translate(" + this._margin.left + "," + this._margin.top + ")");
}
// Create title text
if (!this._title) {
this._title = this._svg.append("text")
this._title = this._g.append("text")
.attrs({
"x": this._margin.left + this._width / 2,
"y": 15,
......@@ -361,7 +381,7 @@ export class Chart
// Create subtitle text
if (!this._subtitle) {
this._subtitle = this._svg.append("text")
this._subtitle = this._g.append("text")
.attrs({
"x": this._margin.left + this._width / 2,
"y": 40,
......
......@@ -127,6 +127,9 @@ export class Data
$(`.controls__${key}`).on('change', () => { this.update(); });
}
// Check if cookie for device-notice is set
Data.initDeviceNotice();
// Start visualization, then hide spinner
$.when(
this.map.data(this.dataFile),
......@@ -134,6 +137,9 @@ export class Data
this.update()
).done(Data.closeFullscreen);
// Add event for confirm button of device notice
$(Global.DEVICE_NOTICE_CONFIRM_SELECTOR).on('click', () => { Data.hideDeviceNotice(); });
// Change document title
document.title = `${document.title}: ${data.title}`;
})
......@@ -236,4 +242,34 @@ export class Data
{
$(Global.PAGE_WRAPPER_SELECTOR).show();
}
/**
* Initialize device notice.
*
* Checks whether the device notice confirmation cookie is already set. If true, the appropriate class is being added to the body element.
*/
static initDeviceNotice()
{
// Read all cookies
let cookies = document.cookie.split(';').filter(c => c.length > 0);
// Set confirm class if cookie is already set
if (cookies.some(c => c.indexOf(`${Global.DEVICE_NOTICE_COOKIE}=`) >= 0)) {
$('body').addClass(Global.DEVICE_NOTICE_CONFIRMED_CLASS);
}
}
/**
* Hide device notice.
*
* Sets the device notice confirmation cookie and adds the appropriate class to the body element.
*/
static hideDeviceNotice()
{
// Set cookie
document.cookie = `${Global.DEVICE_NOTICE_COOKIE}=true`;
// Add class to hide device notice
$('body').addClass(Global.DEVICE_NOTICE_CONFIRMED_CLASS);
}
}
......@@ -20,32 +20,32 @@ export class VisualizationMap
constructor()
{
/**
* Map object
* @type {Object}
* Reference to chart
* @type {Chart}
* @private
*/
this._map = {};
this._chart = null;
/**
* Reference to chart
* @type {Chart}
* Aspect ratio of the map (height / width)
* @type {number}
* @private
*/
this._chart = null;
this._aspectRatio = 1.35;
/**
* Width of svg element
* @type {number}
* @private
*/
this._width = 600;
this._width = 460;
/**
* Height of svg element
* @type {number}
* @private
*/
this._height = 700;
this._height = this._width * this._aspectRatio;
/**
* CSV data from source
......@@ -101,6 +101,13 @@ export class VisualizationMap
*/
this._svg = null;
/**
* Reference to garphics element
* @type {Selection}
* @private
*/
this._g = null;
/**
* Projection of data set
* @type {scale}
......@@ -162,13 +169,13 @@ export class VisualizationMap
*/
_renderMap()
{
// Loop through CSV dataset
// Loop through CSV data set
d3.csv(this._data).then(data =>
{
// Set color domain
this._colors.domain([
d3.min(data, d => { if (d[""] === this._key_y) return +d[this._key_x]; }),
d3.max(data, d => { if (d[""] === this._key_y) return +d[this._key_x]; })
d3.min(data, d => { if (d[Global.DATA_KEY_SEMESTER] === this._key_y) return +d[this._key_x]; }),
d3.max(data, d => { if (d[Global.DATA_KEY_SEMESTER] === this._key_y) return +d[this._key_x]; })
]);
d3.json(this._geo).then(json =>
......@@ -185,7 +192,7 @@ export class VisualizationMap
// Get data value
let value = +currentData[this._key_x];
if (currentData[""] !== this._key_y) {
if (currentData[Global.DATA_KEY_SEMESTER] !== this._key_y) {
continue;
}
......@@ -209,7 +216,7 @@ export class VisualizationMap
}
// Set state paths
this._svg.selectAll("path")
this._g.selectAll("path")
.data(json.features)
.enter()
.append("path")
......@@ -224,8 +231,8 @@ export class VisualizationMap
// Set tooltip content
let state = d.properties[Global.GEO_KEY_NAME];
let value = d.properties.value;
let prct = Math.round((value / this._total_count) * 10000) / 100;
this._tooltip.html(`<div>${state}</div>${value} (${prct}%)`);
let percentage = Math.round((value / this._total_count) * 10000) / 100;
this._tooltip.html(`<div>${state}</div>${value.toLocaleString()} (${percentage}%)`);
// Render chart
this._chart.render(state);
......@@ -258,7 +265,7 @@ export class VisualizationMap
});
// Fill states with color
this._svg.selectAll("path")
this._g.selectAll("path")
.transition()
.style("fill", d => {
let val = d.properties.value;
......@@ -280,13 +287,16 @@ export class VisualizationMap
this._svg = d3.select(Global.MAP_SELECTOR)
.append("svg")
.attrs({
"width": this._width,
"height": this._height
"width": "100%",
"height": "100%",
"viewBox": `0 0 ${this._width} ${this._height}`
});
}
// Set width of visualization controls
$(Global.CONTROLS_SELECTOR).css('width', this._width + "px");
// Create graphics element
if (this._g == null) {
this._g = this._svg.append("g");
}
// Define map projection settings
this.defineSettings();
......
......@@ -6,8 +6,10 @@
// noinspection CssUnknownTarget (source path included by Gulp)
@import '_reset.scss';
// Functions
// Helpers
@import 'modules/functions';
@import 'modules/mixins';
@import 'modules/device';
// Variables
@import 'modules/variables';
......@@ -15,7 +17,7 @@
// Partials
@import 'partials/general';
@import 'partials/code';
@import 'partials/grid';
@import 'partials/device-notice';
@import 'partials/input';
@import 'partials/link';
@import 'partials/page';
......
/*!
* Copyright (c) 2018 Elias Häußler <mail@elias-haeussler.de> (www.elias-haeussler.de).
*/
.landscape-only,
.portrait-only {
display: none;
}
@media screen and (orientation: landscape) {
.landscape-only {
display: initial;
}
}
@media screen and (orientation: portrait) {
.portrait-only {
display: initial;
}
}
......@@ -5,6 +5,7 @@
$z-index: (
chart__dot: 10,
map__tooltip: 20,
device-notice: 30,
fullscreen: 100
);
......@@ -12,6 +13,6 @@ $z-index: (
@if map-has-key($z-index, $key) {
@return map-get($z-index, $key);
} @else {
@return -1;
@error "Unknow key `#{$key}`. Use one of `#{map-keys($z-index)}` instead.";
}
}
/*!
* Copyright (c) 2018 Elias Häußler <mail@elias-haeussler.de> (www.elias-haeussler.de).
*/
$breakpoints: (
small: 640,
medium: 1024,
large: 1440
);
@mixin breakpoint($configuration)
{
// Get breakpoint and direction (default direction is "up")
$direction: up;
$breakpoint: null;
@if length($configuration) == 2 {
$breakpoint: nth($configuration, 1);
$dir: nth($configuration, 2);
@if index((up, down), $dir) {
$direction: $dir;
}
} @else {
$breakpoint: $configuration;
}
// Define size
$size: 0;
@if map-has-key($breakpoints, $breakpoint) {
$size: map-get($breakpoints, $breakpoint);
} @else {
@error "Unknown breakpoint `#{$breakpoint}`. Use one of `#{map-keys($breakpoints)}` instead.";
}
@if $direction == down {
@media screen and (max-width: #{$size - 1}px) {
@content;
}
} @else {
@media screen and (min-width: #{$size}px) {
@content;
}
}
}
......@@ -26,6 +26,20 @@ $transition: $transition-timing $transition-function;
// Code
$code-color: $dark-gray;
// Device Notice
$device-notice-breakpoint: medium;
$device-notice-background: rgba($dark-gray, 0.8);
$device-notice-message-background: $dark-gray;
$device-notice-message-color: $white;
$device-notice-message-padding: 2em;
$device-notice-message-line-height: 1.7;
$device-notice-confirm-background: $primary-color;
$device-notice-confirm-background-hover: darken($device-notice-confirm-background, 10%);
$device-notice-confirm-color: $white;
$device-notice-confirm-color-hover: $device-notice-confirm-color;
$device-notice-confirm-border-radius: 2px;
$device-notice-confirm-padding: 0.2em 0.8em;
// Input
$input-font-size: 1em;
$input-background: none;
......
/*!
* Copyright (c) 2018 Elias Häußler <mail@elias-haeussler.de> (www.elias-haeussler.de).
*/
.device-notice {
.device-notice-confirmed & {
display: none !important;
}
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: $device-notice-background;
z-index: z(device-notice);
@include breakpoint($device-notice-breakpoint down) {
display: initial;
}
&__message {
position: fixed;
top: 50%;
right: 2em;
left: 2em;
transform: translateY(-50%);
background: $device-notice-message-background;
color: $device-notice-message-color;
padding: $device-notice-message-padding;
line-height: $device-notice-message-line-height;
user-select: none;
p {
margin-top: 1em;
&:first-child {
margin-top: 0;
}
}
}
&__confirm {
display: inline-block;
background: $device-notice-confirm-background;
color: $device-notice-confirm-color;
border-radius: $device-notice-confirm-border-radius;
padding: $device-notice-confirm-padding;
transition: background $transition;
&:hover,
&:active,
&:focus {
background: $device-notice-confirm-background-hover;
color: $device-notice-confirm-color-hover;
}
}
}
/*!
* Copyright (c) 2018 Elias Häußler <mail@elias-haeussler.de> (www.elias-haeussler.de).
*/
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
grid-gap: 2em;
}
......@@ -3,33 +3,54 @@
*/
.visualization {
&__controls,
&__map,
&__chart {
margin: 1em 0;
@include breakpoint(medium) {
display: flex;
align-items: center;
}
@include breakpoint(medium down) {
flex-direction: column;
}
&:first-child {
margin-top: 0;
&__container {
flex: 1 0 auto;
&--map {
max-width: 460px;
}
&:last-child {
margin-bottom: 0;
&--chart {
max-width: 700px;
}
}
&__controls {
text-align: center;
margin-bottom: 1em;
}
&__map,
&__chart {
svg > g {
transform-origin: center;
}
}
&__chart {
@include breakpoint(medium) {
padding-left: 3em;
}
@include breakpoint(medium down) {
padding-top: 3em;
}
svg {
visibility: hidden;
opacity: 0;
transition: visibility $transition, opacity $transition;
}
}
&__chart {
.chart {
&__line {
fill: none;
......
......@@ -21,11 +21,13 @@
</header> <!-- eof: .page-header -->
<main class="page">
<section class="page__section page__section--visualization visualization">
<div class="visualization__controls controls" style="width: auto;"></div>
<div class="grid">
<div class="visualization__map map grid__cell"></div>
<div class="visualization__chart chart grid__cell"></div>
<section class="visualization">
<div class="visualization__container visualization__container--map">
<div class="visualization__controls controls"></div>
<div class="visualization__map map"></div>
</div>
<div class="visualization__container visualization__container--chart">
<div class="visualization__chart chart"></div>
</div>
</section> <!-- eof: .visualization -->
</main> <!-- eof: .page -->
......@@ -41,6 +43,23 @@
<div class="fullscreen"></div>
<div class="device-notice">
<div class="device-notice__message">
<p>
This website is not optimized for your devices' width.
<span class="portrait-only">
Please consider using it in landscape mode or select a device with larger screen size.
</span>
<span class="landscape-only">
Please consider using a device with larger screen size.
</span>
</p>
<p>
<a href="javascript:void(0);" class="device-notice__confirm">Got it!</a>
</p>
</div>
</div>
<script src="assets/js/main.js"></script>
</body>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment