├── .gitignore ├── LICENSE ├── README.md ├── d3v4+jetpack.js ├── graph-scroll.js ├── index.html ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | *.zip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015, Adam Pearce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graph-scroll.js 2 | 3 | Simple scrolling events for [d3](https://github.com/mbostock/d3) graphs. Based on [stack](https://github.com/mbostock/stack.git) 4 | 5 | ### [Demo/Documentation](http://1wheel.github.io/graph-scroll/) 6 | 7 | *graph-scroll* takes a selection of explanatory text sections and dispatches `active` events as different sections are scrolled into to view. These `active` events can be used to update a chart's state. 8 | 9 | ``` 10 | d3.graphScroll() 11 | .sections(d3.selectAll('#sections > div')) 12 | .on('active', function(i){ console.log(i + 'th section active') }) 13 | ``` 14 | 15 | The top most element scrolled fully into view is classed `graph-scroll-active`. This makes it easy to highlight the active section with css: 16 | 17 | ``` 18 | #sections > div{ 19 | opacity: .3 20 | } 21 | 22 | #sections div.graph-scroll-active{ 23 | opacity: 1; 24 | } 25 | ``` 26 | 27 | To support headers and intro images/text, we use a container element containing the explanatory text and graph. 28 | 29 | ``` 30 |

Page Title 31 |
32 |
33 |
34 |
Section 0
35 |
Section 1
36 |
Section 2
37 |
38 |
39 |

Footer

40 | ``` 41 | 42 | If these elements are passed to graphScroll as selections with `container` and `graph`, every element in the graph selection will be classed `graph-scroll-graph` if the top of the container is out of view. 43 | 44 | ``` 45 | d3.graphScroll() 46 | .graph(d3.selectAll('#graph')) 47 | .container(d3.select('#container')) 48 | .sections(d3.selectAll('#sections > div')) 49 | .on('active', function(i){ console.log(i + 'th section active') }) 50 | 51 | ``` 52 | 53 | When the graph starts to scroll out of view, `position: sticky` keeps the graph element stuck to the top of the page while the text scrolls by. 54 | 55 | ``` 56 | #container{ 57 | position: relative; 58 | } 59 | 60 | #sections{ 61 | width: 340px; 62 | } 63 | 64 | #graph{ 65 | margin-left: 40px; 66 | width: 500px; 67 | position: sticky; 68 | top: 0px; 69 | float: right; 70 | } 71 | ``` 72 | 73 | 74 | On mobile centering the graph and sections while adding a some padding for the first slide is a good option: 75 | 76 | ``` 77 | @media (max-width: 925px) { 78 | #graph{ 79 | width: 100%; 80 | margin-left: 0px; 81 | float: none; 82 | } 83 | 84 | #sections{ 85 | position: relative; 86 | margin: 0px auto; 87 | padding-top: 400px; 88 | } 89 | } 90 | ``` 91 | 92 | Adjust the amount of pixels before a new section is triggered is also helpful on mobile (Defaults to 200 pixels): 93 | 94 | ``` 95 | graphScroll.offset(300) 96 | ``` 97 | 98 | To update or replace a graphScroll instance, pass a string to `eventId` to remove the old event listeners: 99 | 100 | ``` 101 | graphScroll.eventId('uniqueId1') 102 | ``` 103 | -------------------------------------------------------------------------------- /graph-scroll.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3')) : 3 | typeof define === 'function' && define.amd ? define(['exports', 'd3'], factory) : 4 | (factory((global.d3 = global.d3 || {}),global.d3)); 5 | }(this, function (exports,d3) { 'use strict'; 6 | 7 | function graphScroll(){ 8 | var windowHeight, 9 | dispatch = d3.dispatch("scroll", "active"), 10 | sections = d3.select('null'), 11 | i = NaN, 12 | sectionPos = [], 13 | n, 14 | graph = d3.select('null'), 15 | isFixed = null, 16 | isBelow = null, 17 | container = d3.select('body'), 18 | containerStart = 0, 19 | belowStart, 20 | eventId = Math.random(), 21 | offset = 200 22 | 23 | function reposition(){ 24 | var i1 = 0 25 | sectionPos.forEach(function(d, i){ 26 | if (d < pageYOffset - containerStart + offset) i1 = i 27 | }) 28 | i1 = Math.min(n - 1, i1) 29 | 30 | var isBelow1 = pageYOffset > belowStart 31 | if (isBelow != isBelow1){ 32 | isBelow = isBelow1 33 | container.classed('graph-scroll-below', isBelow) 34 | } 35 | var isFixed1 = !isBelow && pageYOffset > containerStart 36 | if (isFixed != isFixed1){ 37 | isFixed = isFixed1 38 | container.classed('graph-scroll-fixed', isFixed) 39 | } 40 | 41 | if (isBelow) i1 = n - 1 42 | 43 | if (i != i1){ 44 | sections.classed('graph-scroll-active', function(d, i){ return i === i1 }) 45 | 46 | dispatch.call('active', null, i1) 47 | 48 | i = i1 49 | } 50 | } 51 | 52 | function resize(){ 53 | sectionPos = [] 54 | var startPos 55 | sections.each(function(d, i){ 56 | if (!i) startPos = this.getBoundingClientRect().top 57 | sectionPos.push(this.getBoundingClientRect().top - startPos) 58 | }) 59 | 60 | var containerBB = container.node().getBoundingClientRect() 61 | var graphHeight = graph.node() ? graph.node().getBoundingClientRect().height : 0 62 | 63 | containerStart = containerBB.top + pageYOffset 64 | belowStart = containerBB.bottom - graphHeight + pageYOffset 65 | } 66 | 67 | function keydown() { 68 | if (!isFixed) return 69 | var delta 70 | switch (d3.event.keyCode) { 71 | case 39: // right arrow 72 | if (d3.event.metaKey) return 73 | case 40: // down arrow 74 | case 34: // page down 75 | delta = d3.event.metaKey ? Infinity : 1 ;break 76 | case 37: // left arrow 77 | if (d3.event.metaKey) return 78 | case 38: // up arrow 79 | case 33: // page up 80 | delta = d3.event.metaKey ? -Infinity : -1 ;break 81 | case 32: // space 82 | delta = d3.event.shiftKey ? -1 : 1 83 | ;break 84 | default: return 85 | } 86 | 87 | var i1 = Math.max(0, Math.min(i + delta, n - 1)) 88 | if (i1 == i) return // let browser handle scrolling past last section 89 | d3.select(document.documentElement) 90 | .interrupt() 91 | .transition() 92 | .duration(500) 93 | .tween("scroll", function() { 94 | var i = d3.interpolateNumber(pageYOffset, sectionPos[i1] + containerStart) 95 | return function(t) { scrollTo(0, i(t)) } 96 | }) 97 | 98 | d3.event.preventDefault() 99 | } 100 | 101 | 102 | var rv ={} 103 | 104 | rv.container = function(_x){ 105 | if (!_x) return container 106 | 107 | container = _x 108 | return rv 109 | } 110 | 111 | rv.graph = function(_x){ 112 | if (!_x) return graph 113 | 114 | graph = _x 115 | return rv 116 | } 117 | 118 | rv.eventId = function(_x){ 119 | if (!_x) return eventId 120 | 121 | eventId = _x 122 | return rv 123 | } 124 | 125 | rv.sections = function (_x){ 126 | if (!_x) return sections 127 | 128 | sections = _x 129 | n = sections.size() 130 | 131 | d3.select(window) 132 | .on('scroll.gscroll' + eventId, reposition) 133 | .on('resize.gscroll' + eventId, resize) 134 | .on('keydown.gscroll' + eventId, keydown) 135 | 136 | resize() 137 | if (window['gscrollTimer' + eventId]) window['gscrollTimer' + eventId].stop() 138 | window['gscrollTimer' + eventId] = d3.timer(reposition); 139 | 140 | return rv 141 | } 142 | 143 | rv.on = function() { 144 | var value = dispatch.on.apply(dispatch, arguments); 145 | return value === dispatch ? rv : value; 146 | } 147 | 148 | rv.offset = function(_x) { 149 | if(!_x) return offset 150 | 151 | offset = _x 152 | return rv 153 | } 154 | 155 | return rv 156 | } 157 | 158 | exports.graphScroll = graphScroll; 159 | 160 | Object.defineProperty(exports, '__esModule', { value: true }); 161 | 162 | })); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 95 | 96 |

graph-scroll.js

97 |

Simple scrolling events for d3 graphics

98 | 99 |
100 |
101 |
102 |
103 |

Connect text and graphics

104 | graph-scroll 105 | takes a selection of explanatory text sections and dispatches active events as different sections are scrolled into to view. These active events are used to update a graph's state. 106 |
107 | d3.graphScroll()
108 |   .sections(d3.selectAll('#sections > div'))
109 |   .on('active', function(i){
110 |     console.log(i + 'th section active') })
111 |       
112 | 113 |
114 | 115 |
116 |

Highlight active text

117 | The top most text section scrolled into view is classed graph-scroll-active. This makes it easy to highlight the active section with css: 118 |
119 | #sections > div{
120 |   opacity: .3
121 | } 
122 | 
123 | #sections div.graph-scroll-active{
124 |   opacity: 1;
125 | }
126 | 
127 |
128 | 129 | 130 |
131 |

Headers and footers

132 | To support headers and intro images/text, the explanatory text and graphic are wrapped with a container element: 133 |
134 | <h1>Page Title</div>
135 | <div id='container'>
136 |   <div id='graph'></div>
137 |   <div id='sections'>
138 |     <div>Section 0</div>
139 |     <div>Section 1</div>
140 |     <div>Section 2</div>
141 |   </div>
142 | </div>
143 | <h1>Footer</h1>
144 | 
145 | 146 | and passed to graphScroll 147 | 148 |
149 | d3.graphScroll()
150 |   .graph(d3.select('#graph'))
151 |   .container(d3.select('#container'))
152 | 
153 |     
154 | 155 |
156 |

Sticky graph

157 | When the graph starts to scroll out of view, postiion: sticky keeps the graph element stuck to the top of the page while the text scrolls by. 158 |
159 | #container{
160 |   position: relative;
161 | }
162 | 
163 | #sections{
164 |   width: 340px;
165 | }
166 | 
167 | #graph{
168 |   margin-left: 40px;
169 |   width: 500px;
170 |   position: sticky;
171 |   top: 0px;
172 |   float: right;
173 | }
174 | 
175 |
176 | 177 |
178 | 179 |
180 | 181 |

182 | Multiple graphs work too! 183 |

184 | 185 |
186 |
187 | 188 |
189 |
190 |

Mobile

191 | 192 | On mobile centering the graph and sections while adding a some padding for the first slide is a good option: 193 |
194 | @media (max-width: 925px)  {
195 |   #graph{
196 |     width: 100%;
197 |     transform: translate(-50%, 0);
198 |     margin-left: 50%;
199 |   }
200 | 
201 |   #sections{
202 |     position: relative;
203 |     margin: 0px auto;
204 |     padding-top: 400px;
205 |   }
206 | }
207 | 
208 |
209 | 210 |
211 |

Examples

212 |

Auto Sales

213 |

Measles

214 |

Coloring Maps

215 | 216 |

Global Warming

217 |

Hillary Clinton’s Debt to Feminism 218 |

219 |

The Year Ahead

220 |

Pace of Social Change

221 |

Red Meat

222 |
223 | 224 |
225 |

More reading

226 |

How To Scroll

227 |

So You Want to Build A Scroller

228 |

Making “Fewer Helmets, More Deaths”

229 |

Greenland Is Melting Away

230 |

Homan Square

231 |
232 | 233 |
234 |

Todos

235 |

- Simple examples 236 |

- Self explanatory graphics 237 |

- Swiper 238 |

239 |
240 | 241 | 242 |
243 | 244 |

contribute/view source on github

245 | 246 | 247 | 248 | 249 | 331 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | 3 | export function graphScroll(){ 4 | var windowHeight, 5 | dispatch = d3.dispatch("scroll", "active"), 6 | sections = d3.select('null'), 7 | i = NaN, 8 | sectionPos = [], 9 | n, 10 | graph = d3.select('null'), 11 | isFixed = null, 12 | isBelow = null, 13 | container = d3.select('body'), 14 | containerStart = 0, 15 | belowStart, 16 | eventId = Math.random(), 17 | offset = 200 18 | 19 | function reposition(){ 20 | var i1 = 0 21 | sectionPos.forEach(function(d, i){ 22 | if (d < pageYOffset - containerStart + offset) i1 = i 23 | }) 24 | i1 = Math.min(n - 1, i1) 25 | 26 | var isBelow1 = pageYOffset > belowStart 27 | if (isBelow != isBelow1){ 28 | isBelow = isBelow1 29 | container.classed('graph-scroll-below', isBelow) 30 | } 31 | var isFixed1 = !isBelow && pageYOffset > containerStart 32 | if (isFixed != isFixed1){ 33 | isFixed = isFixed1 34 | container.classed('graph-scroll-fixed', isFixed) 35 | } 36 | 37 | if (isBelow) i1 = n - 1 38 | 39 | if (i != i1){ 40 | sections.classed('graph-scroll-active', function(d, i){ return i === i1 }) 41 | 42 | dispatch.call('active', null, i1) 43 | 44 | i = i1 45 | } 46 | } 47 | 48 | function resize(){ 49 | sectionPos = [] 50 | var startPos 51 | sections.each(function(d, i){ 52 | if (!i) startPos = this.getBoundingClientRect().top 53 | sectionPos.push(this.getBoundingClientRect().top - startPos) }) 54 | 55 | var containerBB = container.node().getBoundingClientRect() 56 | var graphHeight = graph.node() ? graph.node().getBoundingClientRect().height : 0 57 | 58 | containerStart = containerBB.top + pageYOffset 59 | belowStart = containerBB.bottom - graphHeight + pageYOffset 60 | } 61 | 62 | function keydown() { 63 | if (!isFixed) return 64 | var delta 65 | switch (d3.event.keyCode) { 66 | case 39: // right arrow 67 | if (d3.event.metaKey) return 68 | case 40: // down arrow 69 | case 34: // page down 70 | delta = d3.event.metaKey ? Infinity : 1 ;break 71 | case 37: // left arrow 72 | if (d3.event.metaKey) return 73 | case 38: // up arrow 74 | case 33: // page up 75 | delta = d3.event.metaKey ? -Infinity : -1 ;break 76 | case 32: // space 77 | delta = d3.event.shiftKey ? -1 : 1 78 | ;break 79 | default: return 80 | } 81 | 82 | var i1 = Math.max(0, Math.min(i + delta, n - 1)) 83 | if (i1 == i) return // let browser handle scrolling past last section 84 | d3.select(document.documentElement) 85 | .interrupt() 86 | .transition() 87 | .duration(500) 88 | .tween("scroll", function() { 89 | var i = d3.interpolateNumber(pageYOffset, sectionPos[i1] + containerStart) 90 | return function(t) { scrollTo(0, i(t)) } 91 | }) 92 | 93 | d3.event.preventDefault() 94 | } 95 | 96 | 97 | var rv ={} 98 | 99 | rv.container = function(_x){ 100 | if (!_x) return container 101 | 102 | container = _x 103 | return rv 104 | } 105 | 106 | rv.graph = function(_x){ 107 | if (!_x) return graph 108 | 109 | graph = _x 110 | return rv 111 | } 112 | 113 | rv.eventId = function(_x){ 114 | if (!_x) return eventId 115 | 116 | eventId = _x 117 | return rv 118 | } 119 | 120 | rv.sections = function (_x){ 121 | if (!_x) return sections 122 | 123 | sections = _x 124 | n = sections.size() 125 | 126 | d3.select(window) 127 | .on('scroll.gscroll' + eventId, reposition) 128 | .on('resize.gscroll' + eventId, resize) 129 | .on('keydown.gscroll' + eventId, keydown) 130 | 131 | resize() 132 | if (window['gscrollTimer' + eventId]) window['gscrollTimer' + eventId].stop() 133 | window['gscrollTimer' + eventId] = d3.timer(reposition); 134 | 135 | return rv 136 | } 137 | 138 | rv.on = function() { 139 | var value = dispatch.on.apply(dispatch, arguments); 140 | return value === dispatch ? rv : value; 141 | } 142 | 143 | rv.offset = function(_x) { 144 | if(!_x) return offset 145 | 146 | offset = _x 147 | return rv 148 | } 149 | 150 | return rv 151 | } 152 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graph-scroll", 3 | "version": "1.0.2", 4 | "description": "experiments using scrolling instead of steppers", 5 | "keywords": [ 6 | "d3", 7 | "d3-module", 8 | "scroll" 9 | ], 10 | "license": "MIT", 11 | "main": "graph-scroll.js", 12 | "author": "Adam Pearce", 13 | "jsnext:main": "index", 14 | "module": "index", 15 | "homepage": "http://1wheel.github.io/graph-scroll/", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/1wheel/graph-scroll.git" 19 | }, 20 | "scripts": { 21 | "pretest": "rollup -f umd -n d3 -g d3:d3 -o graph-scroll.js -- index.js", 22 | "test": "echo 'no tests'", 23 | "prepublish": "npm run pretest", 24 | "postpublish": "zip -j graph-scroll.zip -- LICENSE README.md graph-scroll.js" 25 | }, 26 | "devDependencies": { 27 | "rollup": "0.27", 28 | "tape": "4", 29 | "rollup": "0.27" 30 | }, 31 | "dependencies": { 32 | "d3": "4" 33 | } 34 | } 35 | --------------------------------------------------------------------------------