├── .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 |
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 |
223 |
224 |
232 |
233 |
234 |
Todos
235 |
- Simple examples
236 |
- Self explanatory graphics
237 |
- Swiper
238 |
239 |
240 |
241 |
242 |
243 |
244 |
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 |
--------------------------------------------------------------------------------