Full time/Consulting. Available 3/3/2014.
Remote or the Boston area{"username": "rob_react", "weeks": {"1152446400": {"Pink": 2, "The Decemberists": 3, "Jerry Jeff Walker": 7, "The Clash": 2, "Jim Carroll Band": 2, "Big Audio Dynamite II": 1, "The Beatles": 12, "Cheap Trick": 1, "Rod Stewart": 3, "Ninja Tunes": 1, "Nas": 1, "Boogie Down Productions": 1, "Everclear": 8, "DANGERDOOM": 2, "Paul Simon": 1, "Macy Gray": 1, "Jem": 1, "Terror Danjah Ft. Riko, Bruza, D Double E And Hyper": 1, "Kelly Willis": 1, "Herman's Hermits": 1, "Tool": 1, "Sound Tracks": 1, "The Cure": 1, "Belle and Sebastian": 9, "Tom Waits": 2, "Nick Drake": 1, "Jammer Ft. Wiley, D Double E, Kano and Goodz": 2, "Oasis": 4, "Acid House Kings": 2, "Ralph Stanley & Friends": 1, "Joe Jackson": 1, "The Libertines": 7, "Wilco": 1, "Sublime": 5, "The Cardigans": 1, "Bill Hicks": 3, "Harry Nilsson": 1, "Operation Ivy": 1, "Bob Dylan": 11, "Joni Mitchell": 1, "Led Zeppelin": 1, "50 Cent": 1, "Black Crowes & Wilco": 1, "Kano": 1, "Mason Jennings": 7, "Dave Matthews Band": 1, "The White Stripes": 1, "Michael Penn": 4, "Ralph Stanley": 1, "Kirsty MacColl / The Pogues": 1, "Billy Bragg & Wilco": 2, "Misfits": 3, "The Pretenders": 2, "The Pogues": 12, "Sangue Misto": 1, "Rufus": 1, "The Beach Boys": 2, "Marvin Gaye": 2, "The Strokes": 3, "The Streets": 5, "The Stanley Brothers": 5, "Franz Ferdinand": 1, "Dave Matthews": 1, "My Morning Jacket": 1
SVG is vector graphics mixed up with XML.
Much like peanut butter and chocolate.When created in the context of an HTML document SVG elements are fancy DOM elements-- properties are stored as part of the DOM and access to individual elements and properties is available using the DOM.
It can also be styled with CSS.
In addition to being coded by hand or generated by software, SVG can also be output from a vector based drawing program. You can then use that .svg file as the src
of an <img>
element or as the background of an element as defined by CSS. Plus, it's still just a text format you can hand edit or pull into your document with XHR and still tweak with JavaScript.
I just solved the responsive images conundrum.
<svg width="200" height="200"
viewPort="0 0 200 200" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="75" fill="#fe57a1"/>
</svg>
<img src="img/circle.svg">
The canvas element provides a scriptable interface for drawing two-dimensional images in the browser.
One of the early stars of the HTML5 era.
<canvas id="circle" width="800" height="300"></canvas>
var ctx = document.getElementById( "circle" ).getContext( "2d" );
ctx.beginPath();
ctx.arc( 400, 150, 75, 0, Math.PI*2, true );
ctx.fillStyle = "#fe57a1";
ctx.closePath();
ctx.fill();
arc
is all you need to draw a circle when you have Math.PI*2
, right?
ctx.arc( 400, 150, 75, 0, Math.PI*2, true );
The arguments are "center x", "center y", "radius", "start angle, in radians", "end angle, in radians" and "clockwise, as a Boolean"
This issue could be a whole presentation all by itself. It's a nuanced topic. To help, here are a couple of articles which cover this question in greater depth:
Canvas, SVG, HTML elements + CSS animations, Google Maps, etc. It's all good as long as you're telling a story.
What follows is a cross section of the tools and approaches I've used over the past few years.
Working with Angular and SVG is a natural fit. Angular allows you to bind data to DOM elements. SVG elements are DOM elements.
Magic.
If Angular gives you hives, then squint and pretend it's some other framework. The basic idea, manipulating SVG attributes and values directly with JS still standsThis demo also shows that SVG doesn't need to be wholly generated. This is basically an SVG template output from Adobe Illustrator with some data plugged in and managed with Angular.
<div data-ng-app="comicsApp">
<div data-ng-controller="chartCtrl">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="100%" viewBox="0 0 980 600" enable-background="new 0 0 980 600" xml:space="preserve">
<filter id="drop-shadow">
<feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur"/>
<feOffset in="blur" dx="4" dy="4" result="offsetBlur"/>
<feMerge>
<feMergeNode in="offsetBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<g id="lines">
<line fill="none" stroke="#000000" stroke-miterlimit="10" x1="90.828" y1="-8" x2="90.828" y2="579.297"/>
<line fill="none" stroke="#B5B5B6" stroke-miterlimit="10" x1="0" y1="579.297" x2="981.996" y2="579.297"/>
<-- MANY LINES SNIPPED -->
</g>
<g id="text">
<text x="10" y="16">price in USD</text>
<text x="71" y="574">$0</text>
<text x="480" y="595">2003</text>
<text x="78" y="595">1993</text>
<text x="881" y="595">2013</text>
<text x="25" y="553">$100,000</text>
<text x="25" y="468">$500,000</text>
<text x="16" y="363">$1,000,000</text>
<text x="16" y="256">$1,500,000</text>
<text x="16" y="151">$2,000,000</text>
<text x="16" y="45" >$2,500,000</text>
</g>
<g id="dynamic">
<circle data-ng-repeat="it in items | filter : venues"
opacity="0.8" data-ng-attr-fill="{{colorPicker(it.venue)}}"
data-ng-attr-cx="{{it.date | xDate}}"
data-ng-mouseover="updateTooltip(it)"
data-ng-mouseout="updateTooltip(0)"
data-ng-attr-cy="{{it.price | yPrice}}"
r="7" />
<rect data-ng-show="tooltip.price"
data-ng-model="tooltip"
transform="translate(-55,-100)"
data-ng-attr-x="{{tooltip.date | xDate}}"
data-ng-attr-y="{{tooltip.price | yPrice}}"
width="150" height="80" fill="white"
stroke="" style="filter:url(#drop-shadow)" />
<text data-ng-show="tooltip.price"
data-ng-model="tooltip"
transform="translate(-45,-90)"
data-ng-attr-x="{{tooltip.date | xDate}}"
data-ng-attr-y="{{tooltip.price | yPrice}}" >{{tooltip.title}} {{tooltip.issue}}</text>
<text data-ng-show="tooltip.price"
data-ng-model="tooltip"
transform="translate(-45,-75)"
data-ng-attr-x="{{tooltip.date | xDate}}"
data-ng-attr-y="{{tooltip.price | yPrice}}" >{{tooltip.pedigree}}{{tooltip.collection}}{{tooltip.provenance}} {{tooltip.grade_src | srcFilter}} {{tooltip.grade}}</text>
<text data-ng-show="tooltip.price"
data-ng-model="tooltip"
transform="translate(-45,-60)"
data-ng-attr-x="{{tooltip.date | xDate}}"
data-ng-attr-y="{{tooltip.price | yPrice}}" >{{tooltip.price|currency}}</text>
<text data-ng-show="tooltip.price"
data-ng-model="tooltip"
transform="translate(-45,-45)"
data-ng-attr-x="{{tooltip.date | xDate}}"
data-ng-attr-y="{{tooltip.price | yPrice}}" >{{tooltip.date}}</text>
<text data-ng-show="tooltip.price"
data-ng-model="tooltip"
transform="translate(-45,-30)"
data-ng-attr-x="{{tooltip.date | xDate}}"
data-ng-attr-y="{{tooltip.price | yPrice}}" >{{tooltip.venue}}</text>
</g>
<g id="legend">
<rect x="125" y="5"
width="15" height="15" fill="#D95B43"
data-ng-mouseover="venues='comic connect'"
data-ng-mouseout="venues=''"></rect>
<text x="145" y="15"
class="legend">Comic Connect</text>
<rect x="245" y="5" width="15"
height="15" fill="#ECD078"
data-ng-mouseover="venues='heritage'"
data-ng-mouseout="venues=''"></rect>
<text x="265" y="15"
class="legend">Heritage</text>
<rect x="325" y="5" width="15"
height="15" fill="#C02942"
data-ng-mouseover="venues='comiclink'"
data-ng-mouseout="venues=''"></rect>
<text x="345" y="15"
class="legend">ComicLink</text>
<rect x="405" y="5" width="15"
height="15" fill="#542437"
data-ng-mouseover="venues='pedigree'"
data-ng-mouseout="venues=''"></rect>
<text x="425" y="15"
class="legend">Pedigree</text>
<rect x="485" y="5" width="15"
height="15" fill="#53777A"
data-ng-mouseover="venues='metropolis'"
data-ng-mouseout="venues=''"></rect>
<text x="505" y="15"
class="legend">Metropolis</text>
<rect x="565" y="5" width="15"
height="15" fill="#69D2E7"
data-ng-mouseover="venues='jp'"
data-ng-mouseout="venues=''"></rect>
<text x="585" y="15"
class="legend">JP/Mint</text>
<rect x="645" y="5" width="15"
height="15" fill="#FA6900"
data-ng-mouseover="venues='mastronet'"
data-ng-mouseout="venues=''"></rect>
<text x="665" y="15"
class="legend">Mastronet</text>
<rect x="725" y="5" width="15"
height="15" fill="#FE4365"
data-ng-mouseover="venues='pgc'"
data-ng-mouseout="venues=''"></rect>
<text x="745" y="15"
class="legend">PGCMint</text>
<rect x="805" y="5" width="15"
height="15" fill="#666666"
data-ng-mouseover="venues='unknown'"
data-ng-mouseout="venues=''"></rect>
<text x="825" y="15"
class="legend">Other/Unkown</text>
</g>
</svg>
</div>
</div>
angular.module('comicsApp.controllers', ['comicFilters','comicsFactories']).
.controller('chartCtrl', ["$scope",'dataService',
function( $scope, dataService ) {
//ACTUALLY JUST A PLAIN OLD JAVASCRIPT OBJECT.
//I'M JUST SHARING THE XHR CODE AND DATA MANIPULATION
$scope.items = dataService;
$scope.tooltip = {
price:0
}
$scope.updateTooltip = function(it) {
$scope.tooltip = {
price : it.price || 0,
venue : it.venue,
date : it.date,
title : it.title,
issue : it.issue,
pedigree : it.pedigree,
collection : it.collection,
provenance : it.provenance,
grade_src : it.grade_src,
grade : it.grade
}
}
$scope.colorPicker= function( venue ){
switch (venue) {
case "Heritage":
return $scope.colors[0];
case "Comic Connect":
return $scope.colors[1];
case "Comiclink":
return $scope.colors[2];
case "Pedigree":
return $scope.colors[3];
case "Metropolis":
return $scope.colors[4];
case "JP The Mint":
return $scope.colors[5];
case "Mastronet":
return $scope.colors[6];
case "PGCMint":
return $scope.colors[7];
default:
return $scope.colors[8];
}
}
$scope.colors = ["#ECD078","#D95B43","#C02942","#542437",
"#53777A","#69D2E7","#FA6900", "#FE4365","#666666"];
}
]);
angular.module('comicFilters', []).filter('xDate', function () {
"use strict";
return function (input) {
if (input !== undefined) {
var date = input.split("-");
var years = date[0] - 1993;
var months = (years * 12) + parseInt(date[1]);
return 90 + months * 3.3333;
}
};
}).filter('yPrice', function () {
"use strict";
return function (input) {
if (input){
return 579 - (input/4699.248120300752);
}
};
});
angular.module('comicsFactories', [])
.factory('dataService', function ($http) {
var records = [];
$http({"method" : "GET", "url" : "data/books.json"}).success(
function(data){
for (var i = 0, len = data.books.length; i < len; i++){
if (data.books[i].sales.length){
for (var j = 0, l = data.books[i].sales.length; j < l; j++){
if (parseFloat(data.books[i].sales[j].price) >= 100000){
records.push({
"title":data.books[i].title,
"issue": data.books[i].issue,
"pedigree": data.books[i].pedigree,
"collection": data.books[i].collection,
"provenance": data.books[i].provenance,
"grade": data.books[i].grade,
"grade_src":data.books[i].grade_src,
"uid": Math.floor(data.books[i].uid),
"date":data.books[i].sales[j].sale_date,
"venue":data.books[i].sales[j].venue,
"price": Math.floor(data.books[i].sales[j].price),
"link":data.books[i].sales[j].link
})
}
}
}
}
});
return records;
});
D3.js is a JavaScript library for manipulating documents based on data. D3 helps you bring data to life using HTML, SVG and CSS. D3’s emphasis on web standards gives you the full capabilities of modern browsers without tying yourself to a proprietary framework, combining powerful visualization components and a data-driven approach to DOM manipulation.
//What is the Matrix?
[/* ss td bpl boy back cc ken bea lw newb */
[2689, 508, 1170, 189, 1007, 187, 745, 248, 263, 2311], //ss
[1064, 121, 830, 323, 2473, 393, 453, 312, 533, 599], //td
[506, 296, 813, 530, 988, 540, 1936, 578, 747, 268], //bpl
[706, 311, 1568, 526, 1273, 371, 618, 694, 481, 227], //boy
[178, 701, 277, 176, 663, 227, 379, 284, 330, 111], //back
[550, 270, 548, 445, 196, 769, 868, 317, 1477, 195], //cc
[344, 141, 468, 955, 172, 346, 502, 388, 415, 97], //ken
[333, 207, 455, 545, 196, 1322, 618, 254, 659, 62], //bea
[655, 120, 301, 90, 2368, 108, 226, 99, 229, 875], //lw
[270, 221, 625, 436, 239, 278, 548, 1158, 320, 90] //newb
]
//from bl.ocks.org/mbostock/4062006 etc.
/*
* Create a chord layout
* Pass in our matrix
*/
var chord = d3.layout.chord()
.padding( .05 )
.sortSubgroups( d3.descending )
.matrix( matrix );
var width = 900,
height = 500,
innerRadius = Math.min( width, height ) * .35,
outerRadius = innerRadius * 1.1;
/*
* https://github.com/mbostock/d3/wiki/Ordinal-Scales
*/
var fill = d3.scale.ordinal()
.domain( d3.range(4) )
.range(["#336699", "#99ccff", "#6699cc", "#0066cc"]);
var svg = d3.select( "body" )
.append( "svg" )
.attr( "width", width )
.attr( "height", height )
.append( "g" )
.attr( "transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.append( "g" )
.selectAll( "path" )
.data( chord.groups )
.enter()
.append( "path" )
.style( "fill", function(d) { return fill(d.index); })
.style( "stroke", function(d) { return fill(d.index); })
.attr( "d", d3.svg
.arc()
.innerRadius( innerRadius )
.outerRadius( outerRadius ))
.on( "mouseover" , fade(.1) )
.on( "mouseout" , fade(1) );
var ticks = svg.append( "g" )
.selectAll( "g" )
.data( chord.groups )
.enter().append( "g" )
.selectAll( "g" )
.data( groupTicks )
.enter()
.append( "g" )
.attr( "transform", function( d ) {
return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
+ "translate(" + outerRadius + ",0)";
});
ticks.append( "line" )
.attr( "x1", 1 )
.attr( "y1", 0 )
.attr( "x2", 5 )
.attr( "y2", 0 )
.style( "stroke", "#000" );
var arc = d3.svg
.arc()
.innerRadius( innerRadius );
ticks.append( "text" )
.attr( "x", 8 )
.attr( "dy", ".35em" )
.attr( "transform", function(d) {
return d.angle > Math.PI ? "rotate(180)translate(-16)" : null; })
.style( "text-anchor", function(d) {
return d.angle > Math.PI ? "end" : null; })
.text( function(d) {
return d.label; });
svg.append( "g" )
.attr( "class", "chord" )
.selectAll( "path" )
.data( chord.chords )
.enter()
.append( "path" )
.attr( "d", d3.svg.chord().radius( innerRadius ))
.style( "fill", function(d) {
return fill(d.target.index); })
.style( "opacity", 1)
.outerRadius( outerRadius );
var g = svg.selectAll( "g.group" )
.data( chord.groups )
.enter()
.append( "svg:g" )
.attr( "class", "group")
.on( "mouseover", fade(.02))
.on( "mouseout", fade(.80));
g.append( "svg:path" )
.style( "stroke", function(d) {
return fill(d.index); })
.style( "fill", function(d) {
return fill(d.index); })
.attr( "d", arc);
/*
* Customized
*/
g.append("svg:text")
.each(function(d) { d.angle = (d.startAngle + d.endAngle) / 2; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
.attr("transform", function(d) {
/*
* Get the lengths of some triangle legs. Return the newX and newY of the label
*/
var newX = Math.sin( d.angle ) * (outerRadius +10),
newY = Math.cos( d.angle ) * (outerRadius+30);
return "translate(" + newX +","+ (-newY) + ")";
})
.text(function(d) { return names[d.index]; });
function groupTicks(d) {
var k = ( d.endAngle - d.startAngle ) / d.value;
return d3.range( 0, d.value, 1000 ).map( function(v, i) {
return {
angle: v * k + d.startAngle,
label: i % 5 ? null : v / 1000 + "k"
};
});
}
function fade( opacity ) {
return function( g, i ) {
svg.selectAll( ".chord path" )
.filter(function(d) { return d.source.index != i && d.target.index != i; })
.transition()
.style( "opacity", opacity );
};
}
This specification defines the 2D Context, Level 2 for the HTML canvas element. The 2D Context provides objects, methods, and properties to draw and manipulate graphics on a canvas drawing surface.
function draw( data ) {
ctx.fillRect( 0, 0, 900, 300 );
var len = data.length,
width = 900/len,
gradient = ctx.createLinearGradient( 0, 0, 0, 300 );
gradient.addColorStop( ".5", "#003366" );
gradient.addColorStop( "1.0", "#ccff00" );
ctx.fillStyle = gradient;
for ( var i=0; i<len; i++ ) {
ctx.fillRect( i, 300, width, -data[i] );
}
}
The Google Maps Javascript API lets you embed Google Maps in your own web pages. Version 3 of this API is especially designed to be faster and more applicable to mobile devices, as well as traditional desktop browser applications.
The Maps API is already rich, adding a layer of data on top of it opens up a wide range of possibilities
;( function( window, document, $, undefined ) {
"use strict";
if( window.HW === undefined ) {
window.HW = {};
};
var HW = window.HW;
HW.common = {
init: function() {
$( "#map" ).height( $( window ).height() );
var GM = google.maps,
defaultPosition = new GM.LatLng( 42.3520, -71.0560 ),
mapOptions = {
zoom: 12,
center: defaultPosition,
mapTypeId: GM.MapTypeId.ROADMAP
},
map = new GM.Map( document.getElementById( 'map' ), mapOptions ),
geocoder = new GM.Geocoder(),
position, endPosition, marker, polylineOptions, DD, DS;
$( "#map" ).data( "map" , map );
createMarker( 0 );
var i = 1, timer;
function render(){
if ( i == 10 ){
clearInterval( timer );
return;
}
createMarker( i );
i++;
}
timer = setInterval( render, 10000 );
function createMarker( i ){
var marker, position, start, end, dist;
position = new GM.LatLng( data.stations[i].latLng[0] , data.stations[i].latLng[1] );
marker = new GM.Marker({
position: position,
map: map,
title: data.stations[i].name,
data: data.stations[i].destinations
});
//http://www.movable-type.co.uk/scripts/latlong.html
//http://stackoverflow.com/questions/27928/how-do-i-calculate-distance-between-two-latitude-longitude-points
function distance( lat1, lon1, lat2, lon2) {
var R = 6371; // Radius of the earth in km
var dLat = deg2rad( lat2-lat1 ); // deg2rad below
var dLon = deg2rad( lon2-lon1 );
var a =
Math.sin( dLat/2 ) * Math.sin( dLat/2 ) +
Math.cos( deg2rad(lat1) ) * Math.cos( deg2rad( lat2 ) ) *
Math.sin( dLon/2 ) * Math.sin( dLon/2 );
var c = 2 * Math.atan2( Math.sqrt( a ), Math.sqrt( 1-a ) );
var d = R * c; // Distance in km
return d;
}
function deg2rad( deg ) {
return deg * ( Math.PI/180 )
}
var j = 0, len = data.stations[i].destinations.length, rendertimer;
function newrender(){
if ( j == len ){
clearInterval( rendertimer )
return;
}
dist = distance( data.stations[i].latLng[0],data.stations[i].latLng[1], data.stations[i].destinations[j].latLng[0], data.stations[i].destinations[j].latLng[1] )
createRoutes( i, j, position, dist);
j++;
}
rendertimer = setInterval( newrender, 2000 );
function createRoutes( i, j, start, distance) {
start = position;
end = data.stations[i].destinations[j].latLng;
var trips = data.stations[i].destinations[j].trips;
HW.common.renderer( start,end, trips, i, distance, data.stations[i].name, data.stations[i].destinations[j].name );
}
}
},
renderer: function( start, end, trips, index, distance, startName, endName ) {
var GM = google.maps,
polylineOptions, DD, DS, map = $( "#map" ).data( "map" ),
colors = ["#B22937","#DE5003","#80C837","#229F6E","#60B6CA","#6F6DA7", "#1F1D6D","#80529A","#A6358C","#A2395B"];
end = new GM.LatLng( end[0], end[1] );
var polylineOptions = new GM.Polyline({
strokeColor : colors[index],
strokeOpacity : .4,
strokeWeight : trips/100
}),
DS = new GM.DirectionsService(),
DD = new GM.DirectionsRenderer({
polylineOptions : polylineOptions,
markerOptions : { icon : "img/Hubwaylogo.png" }
});
DD.setMap( map );
var request = {
origin : start,
destination : end,
travelMode : GM.TravelMode.BICYCLING
};
DS.route( request, function( result, status ) {
if( status == GM.DirectionsStatus.OK ) {
DD.setDirections( result );
}
});
}
}
}( window, document, jQuery ));
A list of all these tools, plus more.
roblarsen on Github | @robreact/@roblarsenwww on Twitter | blog @ htmlcssjavascript.com