Skip to content

Instantly share code, notes, and snippets.

@Hirosaji
Last active April 17, 2020 07:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Hirosaji/94637aa39176b1259a0c2b24f86f534a to your computer and use it in GitHub Desktop.
Save Hirosaji/94637aa39176b1259a0c2b24f86f534a to your computer and use it in GitHub Desktop.
Mapbox - storytelling demo
Mapbox - storytelling demo
license: mit
var config = {
style: 'mapbox://styles/hirosaji/ck8y5lf8x0vrt1ip5kt4szoi4',
accessToken: 'pk.eyJ1IjoiaGlyb3NhamkiLCJhIjoiY2szOWlqZWNzMDJueTNjcWhyNjhqdXBnOSJ9._6mJT202QqpnMuK-jvMr3g',
showMarkers: false,
theme: 'light',
alignment: 'left',
title: '東京都の犯罪統計 × Interactive Storytelling',
subtitle: '- Crimes Story Map with Olympic and Hot Town -',
byline: 'Hirosaji',
footer: '出典: 警視庁 - 区市町村の町丁別、罪種別及び手口別認知件数(H31), SUUMO - 住みたい街ランキング2020',
chapters: [
{
id: 'main',
title: 'まず、犯罪発生地域を地図に載せてみる',
// image: './path/to/image/source.png',
description: '犯罪の数の多さに合わせて地域を色/高さで表現して地図に載せました。新宿区や渋谷区などの繁華街の犯罪が群を抜いて多いのが見てとれます。',
location: {
center: [139.65269, 35.68924],
zoom: 11.00,
pitch: 60.00,
bearing: -36.80
},
onChapterEnter: [
{
layer: 'crime_total_3d',
opacity: 0.9
}
],
onChapterExit: [
{
layer: 'crime_total_3d',
opacity: 0
}
]
},
{
id: 'page01',
title: 'ここに五輪会場を載せる',
// image: './path/to/image/source.png',
description: '犯罪発生が少ない地域に五輪会場がプロットされているように見えます。',
location: {
center: [139.50469, 35.71926],
zoom: 9.31,
pitch: 0.00,
bearing: -9.60
},
onChapterEnter: [
{
layer: 'orinpic_plot',
opacity: 1
},
{
layer: 'crime_total',
opacity: 0.9
}
],
onChapterExit: [
{
layer: 'orinpic_plot',
opacity: 0
},
{
layer: 'crime_total',
opacity: 0
}
]
},
{
id: 'page02',
title: '五輪会場周辺の繁華街に注意',
// image: './path/to/image/source.png',
description: '日本はかなり治安が良いです。ただ、念のため五輪観戦の帰りの寄り道には注意するとよいかも。',
location: {
center: [139.73117, 35.69117],
zoom: 12.04,
pitch: 0.00,
bearing: 0.00
},
onChapterEnter: [
{
layer: 'orinpic_place',
opacity: 1
},
{
layer: 'crime_total',
opacity: 0.9
}
],
onChapterExit: [
{
layer: 'orinpic_place',
opacity: 0
},
{
layer: 'crime_total',
opacity: 0
}
]
},
{
id: 'page03',
title: '住みたい街ランキングも載せてみる',
// image: './path/to/image/source.png',
description: '住みたい街ランキング2020にランクインした街を載せてみました。これらの街も、犯罪発生率が少ない地域にプロットされているようです。',
location: {
center: [139.70666, 35.66718],
zoom: 11.50,
pitch: 0.00,
bearing: 0.00
},
onChapterEnter: [
{
layer: 'town_place',
opacity: 1
},
{
layer: 'crime_total',
opacity: 0.9
}
],
onChapterExit: [
{
layer: 'town_place',
opacity: 0
},
{
layer: 'crime_total',
opacity: 0
}
]
},
{
id: 'page04',
title: '空き巣等の犯罪だけに絞ってみる',
// image: './path/to/image/source.png',
description: '空き巣等の心配もなさそうです。',
location: {
center: [139.70666, 35.66718],
zoom: 11.50,
pitch: 0.00,
bearing: 0.00
},
onChapterEnter: [
{
layer: 'crime_sinnyu',
opacity: 0.9
},
{
layer: 'town_place',
opacity: 1
}
],
onChapterExit: [
{
layer: 'crime_sinnyu',
opacity: 0
},
{
layer: 'town_place',
opacity: 0
}
]
}
]
};
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<title>Scrollytelling Template</title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v1.5.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.5.0/mapbox-gl.css' rel='stylesheet' />
<script src="https://unpkg.com/intersection-observer@0.5.1/intersection-observer.js"></script>
<script src="https://unpkg.com/scrollama"></script>
<style>
body {
margin:0;
padding:0;
font-family: sans-serif;
}
a, a:hover, a:visited {
color: #41abf1;
}
#map {
top:0;
height: 100vh;
width:100vw;
position: fixed;
z-index: -5;
}
#header {
margin: 3vh auto;
width: 90vw;
padding: 2vh;
text-align: center;
}
#footer {
width: 100%;
min-height: 5vh;
padding-top: 2vh;
padding-bottom: 2vh;
text-align: center;
line-height: 25px;
font-size: 13px;
}
#features {
padding-top: 10vh;
padding-bottom: 10vh;
z-index: 100;
}
.centered {
width: 50vw;
margin: 0 auto;
}
.lefty {
width: 33vw;
margin-left: 5vw;
}
.righty {
width: 33vw;
margin-left: 62vw;
}
.light {
color: #ffffff;
background-color: #696969;
}
.dark {
color: #fafafa;
background-color: #444;
}
.step {
padding-bottom: 50vh;
/* margin-bottom: 10vh; */
opacity: 0.25;
}
.step.active {
opacity: 0.9;
}
.step div {
padding: 25px 50px;
line-height: 25px;
font-size: 13px;
}
.step img {
width: 100%;
}
@media (max-width: 750px) {
#features {
width: 90vw;
margin: 0 auto;
}
}
</style>
</head>
<body>
<div id="map"></div>
<div id="story"></div>
<script src="./config.js"></script>
<script>
var layerTypes = {
'fill': ['fill-opacity'],
'line': ['line-opacity'],
'circle': ['circle-opacity', 'circle-stroke-opacity'],
'symbol': ['icon-opacity', 'text-opacity'],
'raster': ['raster-opacity'],
'fill-extrusion': ['fill-extrusion-opacity']
}
var alignments = {
'left': 'lefty',
'center': 'centered',
'right': 'righty'
}
function getLayerPaintType(layer) {
var layerType = map.getLayer(layer).type;
return layerTypes[layerType];
}
function setLayerOpacity(layer) {
var paintProps = getLayerPaintType(layer.layer);
paintProps.forEach(function(prop) {
map.setPaintProperty(layer.layer, prop, layer.opacity);
});
}
var story = document.getElementById('story');
var features = document.createElement('div');
features.classList.add(alignments[config.alignment]);
features.setAttribute('id', 'features');
var header = document.createElement('div');
if (config.title) {
var titleText = document.createElement('h1');
titleText.innerText = config.title;
header.appendChild(titleText);
}
if (config.subtitle) {
var subtitleText = document.createElement('h2');
subtitleText.innerText = config.subtitle;
header.appendChild(subtitleText);
}
if (config.byline) {
var bylineText = document.createElement('p');
bylineText.innerHTML = 'created by <a href="https://github.com/Hirosaji">' + config.byline + '</a>';
header.appendChild(bylineText);
}
if (header.innerText.length > 0) {
header.classList.add(config.theme);
header.setAttribute('id', 'header');
story.appendChild(header);
}
config.chapters.forEach((record, idx) => {
var container = document.createElement('div');
var chapter = document.createElement('div');
if (record.title) {
var title = document.createElement('h3');
title.innerText = record.title;
chapter.appendChild(title);
}
if (record.image) {
var image = new Image();
image.src = record.image;
chapter.appendChild(image);
}
if (record.description) {
var story = document.createElement('p');
story.innerHTML = record.description;
chapter.appendChild(story);
}
container.setAttribute('id', record.id);
container.classList.add('step');
if (idx === 0) {
container.classList.add('active');
}
chapter.classList.add(config.theme);
container.appendChild(chapter);
features.appendChild(container);
});
story.appendChild(features);
var footer = document.createElement('div');
if (config.footer) {
var footerText = document.createElement('p');
footerText.innerHTML = config.footer;
footer.appendChild(footerText);
}
if (footer.innerText.length > 0) {
footer.classList.add(config.theme);
footer.setAttribute('id', 'footer');
story.appendChild(footer);
}
mapboxgl.accessToken = config.accessToken;
const transformRequest = (url) => {
const hasQuery = url.indexOf("?") !== -1;
const suffix = hasQuery ? "&pluginName=journalismScrollytelling" : "?pluginName=journalismScrollytelling";
return {
url: url + suffix
}
}
var map = new mapboxgl.Map({
container: 'map',
style: config.style,
center: config.chapters[0].location.center,
zoom: config.chapters[0].location.zoom,
bearing: config.chapters[0].location.bearing,
pitch: config.chapters[0].location.pitch,
scrollZoom: false,
transformRequest: transformRequest
});
var marker = new mapboxgl.Marker();
if (config.showMarkers) {
marker.setLngLat(config.chapters[0].location.center).addTo(map);
}
// instantiate the scrollama
var scroller = scrollama();
map.on("load", function() {
// setup the instance, pass callback functions
scroller
.setup({
step: '.step',
offset: 0.5,
progress: true
})
.onStepEnter(response => {
var chapter = config.chapters.find(chap => chap.id === response.element.id);
response.element.classList.add('active');
map.flyTo(chapter.location);
if (config.showMarkers) {
marker.setLngLat(chapter.location.center);
}
if (chapter.onChapterEnter.length > 0) {
chapter.onChapterEnter.forEach(setLayerOpacity);
}
})
.onStepExit(response => {
var chapter = config.chapters.find(chap => chap.id === response.element.id);
response.element.classList.remove('active');
if (chapter.onChapterExit.length > 0) {
chapter.onChapterExit.forEach(setLayerOpacity);
}
});
});
// setup resize event
window.addEventListener('resize', scroller.resize);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment