diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/README.md b/README.md index 43fe8c6..91b6788 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,22 @@ Simple scrolling events for [d3](https://github.com/mbostock/d3) graphs. Based o ### [Demo/Documentation](http://1wheel.github.io/graph-scroll/) -`graphScroll` 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. +`graphScroll` 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. -``` +A `direction` is also passed on active. It is either `up`, `down` or `jump` (fired when you load a page from not at the time, like when you refresh). + +Set an `offset` to determine how far from the top the next section will be before it becomes active. + +```js graphScroll() .sections(d3.selectAll('#sections > div')) - .on('active', function(i){ console.log(i + 'th section active') }) + .offset(180) + .on('active', function(i, direction){ console.log(i + 'th section active', 'Moving ' + direction) }) ``` -The top most element scrolled fully into view is classed `graph-scroll-active`. This makes it easy to highlight the active section with css: +You can also set `.triggerAt('middle')` to activate the next section when it reaches the middle of the viewport. The top most element scrolled fully into view is classed `graph-scroll-active`. This makes it easy to highlight the active section with css: -``` +```css #sections > div{ opacity: .3 } @@ -26,7 +31,7 @@ The top most element scrolled fully into view is classed `graph-scroll-active`. To support headers and intro images/text, we use a container element containing the explanatory text and graph. -``` +```html

Page Title
@@ -42,19 +47,19 @@ To support headers and intro images/text, we use a container element containing 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. -``` +```js graphScroll() .graph(d3.selectAll('#graph')) .container(d3.select('#container')) .sections(d3.selectAll('#sections > div')) - .on('active', function(i){ console.log(i + 'th section active') }) + .on('active', function(i, direction){ console.log(i + 'th section active', 'Moving ' + direction) }) ``` With a little bit of css, the graph element snaps to the top of the page while the text scrolls by. -``` +```css #container{ position: relative; overflow: auto; @@ -81,7 +86,7 @@ With a little bit of css, the graph element snaps to the top of the page while t As the bottom of the container approaches the top of the page, the graph is classed with `graph-scroll-below`. A little more css allows the graph slide out of view gracefully: -``` +```css #graph.graph-scroll-below{ position: absolute; bottom: 0px; diff --git a/graph-scroll.js b/graph-scroll.js index 4e07b8f..f390197 100644 --- a/graph-scroll.js +++ b/graph-scroll.js @@ -1,158 +1,218 @@ -function graphScroll() { - var windowHeight, - dispatch = d3.dispatch("scroll", "active"), - sections = d3.select('null'), - i = -1, - sectionPos = [], - n, - graph = d3.select('null'), - isFixed = null, - isBelow = null, - container = d3.select('body'), - containerStart = 0, - belowStart, - eventId = Math.random(), - stickyTop - - function reposition(){ - var i1 = 0 - sectionPos.forEach(function(d, i){ - if (d < pageYOffset - containerStart + 180) i1 = i - }) - i1 = Math.min(n - 1, i1) - if (i != i1){ - sections.classed('graph-scroll-active', function(d, i){ return i === i1 }) - - dispatch.active(i1, i) - - i = i1 +(function(){ + var d3 = this.d3 || undefined + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + d3 = require('d3') + exports = module.exports = graphScroll; } + exports.graphScroll = graphScroll; + } else { + this.graphScroll = graphScroll; + } - var isBelow1 = pageYOffset > belowStart - 120 - if (isBelow != isBelow1){ - isBelow = isBelow1 - graph.classed('graph-scroll-below', isBelow) + function graphScroll() { + var windowHeight, + dispatch = d3.dispatch("scroll", "active"), + sections = d3.select('null'), + i = -1, + sectionPos = [], + n, + graph = d3.select('null'), + isFixed = null, + isBelow = null, + container = d3.select('body'), + containerStart = 0, + belowStart, + eventId = Math.random(), + stickyTop, + lastPageY = -Infinity, + triggerAt = 'top', + offset = 0; + + function reposition(){ + var i1 = 0 + var viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0) + + // If our lastPageY variable is our default but we're farther than the scroll top, then we are entering the page from somewhere not the top + if (lastPageY == -Infinity && pageYOffset > 0) { + direction = 'jump' + } else if (pageYOffset > lastPageY) { + direction = 'down' + } else { + direction = 'up' + } + + sectionPos.forEach(function(d, i){ + // Trigger active section when it gets to the middle of the viewport + if (triggerAt == 'middle' && d < (pageYOffset - containerStart + viewportHeight / 2 + offset) ) { + i1 = i + // Or at the top of the viewport + } else if (triggerAt == 'top' && d < pageYOffset - containerStart + offset) { + i1 = i + } + }) + + i1 = Math.min(n - 1, i1) + if (i != i1){ + sections.classed('graph-scroll-active', function(d, i){ return i === i1 }) + + dispatch.active.call(sections[0][i1], i1, direction) + + i = i1 + } + var yStickyOffset = stickyTop || 0 + + var isBelow1 = pageYOffset + yStickyOffset > belowStart + if (isBelow != isBelow1){ + isBelow = isBelow1 + graph.classed('graph-scroll-below', isBelow) + } + + var isFixed1 = !isBelow && pageYOffset > containerStart - yStickyOffset + if (isFixed != isFixed1){ + isFixed = isFixed1 + graph + .classed('graph-scroll-fixed', isFixed) + } + + var top + if (stickyTop){ + if (isBelow) { + top = 'auto' + } else if (isFixed) { + top = stickyTop + 'px' + } else { + top = '0px' + } + graph.style('top', top) + } + + lastPageY = pageYOffset } - var isFixed1 = !isBelow && pageYOffset > containerStart - 120 - if (isFixed != isFixed1){ - isFixed = isFixed1 - graph - .classed('graph-scroll-fixed', isFixed) + function resize(){ + sectionPos = [] + var startPos + sections.each(function(d, i){ + if (!i) startPos = this.getBoundingClientRect().top + sectionPos.push(this.getBoundingClientRect().top - startPos) }) + + var containerBB = container.node().getBoundingClientRect() + var graphBB = graph.node().getBoundingClientRect() + + containerStart = containerBB.top + pageYOffset + belowStart = containerBB.bottom - graphBB.height + pageYOffset } - if (stickyTop){ - graph.style('padding-top', (isBelow || isFixed ? stickyTop : 0)+ 'px') + function keydown() { + if (!isFixed) return + var delta + switch (d3.event.keyCode) { + case 39: // right arrow + if (d3.event.metaKey) return + case 40: // down arrow + case 34: // page down + delta = d3.event.metaKey ? Infinity : 1 ;break + case 37: // left arrow + if (d3.event.metaKey) return + case 38: // up arrow + case 33: // page up + delta = d3.event.metaKey ? -Infinity : -1 ;break + case 32: // space + delta = d3.event.shiftKey ? -1 : 1 + ;break + default: return + } + + var i1 = Math.max(0, Math.min(i + delta, n - 1)) + rv.scrollTo(i1) + + d3.event.preventDefault() } - } - function resize(){ - sectionPos = [] - var startPos - sections.each(function(d, i){ - if (!i) startPos = this.getBoundingClientRect().top - sectionPos.push(this.getBoundingClientRect().top - startPos) }) - var containerBB = container.node().getBoundingClientRect() - var graphBB = graph.node().getBoundingClientRect() + var rv ={} - containerStart = containerBB.top + pageYOffset - belowStart = containerBB.bottom - graphBB.height + pageYOffset - } + rv.scrollTo = function(_x){ + if (isNaN(_x)) return rv - function keydown() { - if (!isFixed) return - var delta - switch (d3.event.keyCode) { - case 39: // right arrow - if (d3.event.metaKey) return - case 40: // down arrow - case 34: // page down - delta = d3.event.metaKey ? Infinity : 1 ;break - case 37: // left arrow - if (d3.event.metaKey) return - case 38: // up arrow - case 33: // page up - delta = d3.event.metaKey ? -Infinity : -1 ;break - case 32: // space - delta = d3.event.shiftKey ? -1 : 1 - ;break - default: return + d3.select(document.documentElement) + .interrupt() + .transition() + .duration(500) + .tween("scroll", function() { + var i = d3.interpolateNumber(pageYOffset, sectionPos[_x] + containerStart) + return function(t) { scrollTo(0, i(t)) } + }) + return rv } - var i1 = Math.max(0, Math.min(i + delta, n - 1)) - rv.scrollTo(i1) - d3.event.preventDefault() - } + rv.container = function(_x){ + if (!_x) return container + container = _x + return rv + } - var rv ={} + rv.graph = function(_x){ + if (!_x) return graph - rv.scrollTo = function(_x){ - if (isNaN(_x)) return rv + graph = _x + return rv + } - d3.select(document.documentElement) - .interrupt() - .transition() - .duration(500) - .tween("scroll", function() { - var i = d3.interpolateNumber(pageYOffset, sectionPos[_x] + containerStart) - return function(t) { scrollTo(0, i(t)) } - }) - return rv - } + rv.triggerAt = function(_x){ + if (!_x) return triggerAt + triggerAt = _x + return rv + } - rv.container = function(_x){ - if (!_x) return container + rv.eventId = function(_x){ + if (!_x) return eventId - container = _x - return rv - } + eventId = _x + return rv + } - rv.graph = function(_x){ - if (!_x) return graph + rv.stickyTop = function(_x){ + if (!_x) return stickyTop - graph = _x - return rv - } + stickyTop = _x + return rv + } - rv.eventId = function(_x){ - if (!_x) return eventId + rv.offset = function(_x){ + if (!_x) return offset - eventId = _x - return rv - } + offset = _x + return rv + } - rv.stickyTop = function(_x){ - if (!_x) return stickyTop + rv.sections = function (_x){ + if (!_x) return sections - stickyTop = _x - return rv - } + sections = _x + n = sections.size() - rv.sections = function (_x){ - if (!_x) return sections + d3.select(window) + .on('scroll.gscroll' + eventId, reposition) + .on('resize.gscroll' + eventId, resize) + .on('keydown.gscroll' + eventId, keydown) - sections = _x - n = sections.size() + resize() + d3.timer(function() { + reposition() + return true + }) - d3.select(window) - .on('scroll.gscroll' + eventId, reposition) - .on('resize.gscroll' + eventId, resize) - .on('keydown.gscroll' + eventId, keydown) + return rv + } - resize() - d3.timer(function() { - reposition() - return true - }) + d3.rebind(rv, dispatch, "on") return rv } - - d3.rebind(rv, dispatch, "on") - - return rv -} \ No newline at end of file + +}).call(this) \ No newline at end of file diff --git a/index.html b/index.html index 1ddfb4d..e33354e 100644 --- a/index.html +++ b/index.html @@ -82,19 +82,21 @@

Simple scrolling events for d3 graphics.

Connect text and graphics

graph-scroll - 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. + 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.

A direction is also passed on active. It is either up, down or jump (fired when you load a page from not at the time, like when you refresh).

Set an offset to determine how far from the top the next section will be before it becomes active.
 graphScroll()
   .sections(d3.selectAll('#sections > div'))
-  .on('active', function(i){
-    console.log(i + 'th section active') })
+  .offset(180)
+  .on('active', function(i, direction){
+    console.log(i + 'th section active')
+    console.log('Moving ' + direction)
       

Highlight active text

- The top most text section scrolled into view is classed graph-scroll-active. This makes it easy to highlight the active section with css: + You can also set .triggerAt('middle') to activate the next section when it reaches the middle of the viewport. The top most text section scrolled into view is classed graph-scroll-active. This makes it easy to highlight the active section with css:
 #sections > div{
   opacity: .3
@@ -234,7 +236,9 @@ 

contribute/view .container(d3.select('#container')) .graph(d3.selectAll('#graph')) .sections(d3.selectAll('#sections > div')) - .on('active', function(i){ + .offset(180) + .triggerAt('top') + .on('active', function(i, direction){ var pos = [ {cx: width - r, cy: r}, {cx: r, cy: r}, {cx: width - r, cy: height - r}, @@ -257,7 +261,7 @@

contribute/view .container(d3.select('#container2')) .graph(d3.selectAll('#graph2')) .sections(d3.selectAll('#sections2 > div')) - .on('active', function(i){ + .on('active', function(i, direction){ var h = height var w = width var dArray = [ diff --git a/package.json b/package.json index e9641de..9dbe7ed 100644 --- a/package.json +++ b/package.json @@ -20,5 +20,8 @@ "bugs": { "url": "https://github.com/1wheel/graph-scroll/issues" }, - "homepage": "https://github.com/1wheel/graph-scroll" + "homepage": "https://github.com/1wheel/graph-scroll", + "dependencies": { + "d3": "^3.5.12" + } }