pnts.us

Color Mixing Along A Gradient With Sass

design, interface, sass, and visualization

tldr; Here's how I used Sass to mix colors along a gradient and assign them to a value on a scale of 1 - 100.

Color-coded percentiles

Recently I worked on a prototype for a benchmarking app with the intent of helping organizations quickly understand how a given thing performed in a number of contexts across a variety of metrics. A few initial sketches and we ended up with a tabular display coupled with a bar chart visualizing the distance between a given benchmark and the corresponding average.

The initial grid was rather dull and lacked a macro-level visualization that would quickly communicate the overall 'goodness' or 'badness' for a set of benchmarks. After a few uninspiring directions, we hit on using the background color to communicate where a particular metric, displayed as a percentile, fell along a gradient scale. If the scores were largely on the higher end, they'd be one color, and if they were mostly on the lower end, they'd be a different color.

Color-coded percentiles

A grid of benchmark results, displayed as percentiles.

We used Ember, Ember Data (with fixtures), and Sass to build the prototype, which had the advantage of using real data from the get go, and which took care of assigning a percentile class to each table cell in the format of '.percentile-x'. From there, Sass took over, which required a bit of tinkering to get a system in place that would allow us to change the gradient and color encodings on the fly (because we all know how fickle stakeholders can be - amiriteorwhat?).

The two tricks that made this possible were 1) list functions to quickly generate a sequence of numbers given a starting number and an ending number, and 2) four color-stops to ensure percentiles in the mid-range weren't overwhelmingly brown.

List functions aside, this is all the code you need:

For more detail, check out the pen below.

/*------------------------------------------------------------------


Functions - list functions taken directly or inspired by http://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/


-------------------------------------------------------------------*/

@function calcRem($target) {
  @return ($target / 16) + rem;
}

@function calcEm($target, $context: 12) {
  @return ($target / $context) * 1em;
}

// create an arbitrary list of numbers
@function sequence($start, $end) {
  $seq: $start;

  @for $i from $start through $end {
    $x: nth($seq, length($seq)) + 1;
    $seq: append($seq, $x);
  }
 
  @return $seq;
}

// get first item in a list
@function first($list) {
  @return nth($list, 1);
}

// get last item in a list
@function last($list) {
  @return nth($list, length($list));
}


/*------------------------------------------------------------------


Variables


-------------------------------------------------------------------*/

$primary-color: #333;

$mobile: 76em;

$fonts: (junction-bold, junction-light, junction-regular, leaguegothic-condensed-italic, leaguegothic-condensed-regular, leaguegothic-italic, leaguegothic-regular);
@each $font in $fonts {
  @font-face {
    font-family: $font;
      src: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/1643/#{$font}.eot');
      src: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/1643/#{$font}.eot?#iefix') format('embedded-opentype'), 
           url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/1643/#{$font}.woff') format('woff'), 
           url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/1643/#{$font}.ttf')  format('truetype');
  }
}

$font-sizes: (1, 2, 3, 4, 5, 6, 7 ,8, 9, 10, 11, 12, 13, 14, 18, 20, 24, 30, 36, 48, 64, 72, 96, 144, 288);


/*------------------------------------------------------------------


Placeholders and Mixins


-------------------------------------------------------------------*/

%inlineBlock {
  display: -moz-inline-stack;
  display: inline-block;
  zoom: 1;
  *display: inline;
  vertical-align: top;
}

@each $font-size in $font-sizes {
  %font#{$font-size} {
    font-size: calcEm( $font-size )
  }
}

/*------------------------------------------------------------------


The meat and potatoes - relies on the list functions at the top of this file


-------------------------------------------------------------------*/

// set up the color stops - mess with these to change the gradient
$color-stop-1: hsl(107, 41.0526%, 62.7451%);
$color-stop-2: hsl(177, 42.9752%, 52.5490%);
$color-stop-3: hsl(263, 30.0000%, 60.7843%);
$color-stop-4: hsl(360, 74.8792%, 59.4118%);

// set up percentile ranges 
$top-tier: sequence(67, 99);
$mid-tier: sequence(34, 66);
$bottom-tier: sequence(1, 33);

// mixin to mix the colors 
@mixin tileStyle($color1, $color2, $range) {
  @for $i from first($range) through last($range) {
    $value: index($range, $i) / length($range) * 100;
    $shade: darken(mix($color1, $color2, $value), 15%);
  
    .percentile-#{$i} {
      background: mix($color1, $color2, $value);
      .number {
        text-shadow: .080em .080em $shade;
      }
    }
  }
}

// call the mixin for each percentile grouping
.table {
  @include tileStyle($color-stop-1, $color-stop-2, $top-tier);
  @include tileStyle($color-stop-2, $color-stop-3, $mid-tier);
  @include tileStyle($color-stop-3, $color-stop-4, $bottom-tier);
}


/*------------------------------------------------------------------


Style all the things


-------------------------------------------------------------------*/

* {
   -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
           box-sizing: border-box;
}


body {
  background-size: cover;
  font-family: 'junction-light';
  font-size: calcEm( 14 );
  color: $primary-color;
  line-height: 1rem;
}

ul {
  list-style-type: none;
  padding: 0;
}

.search-result {
  color: $primary-color;
  margin: 1em 0 1em 0;
  padding: 0 1em;
}

.table {
  display: table;
  margin-top: 0;
}

.table-header, .table-body {
  display: table-header-group;
}

.table-header {
  @extend %font18;
  text-transform: uppercase;
  .th {
    padding-bottom: .5em;
    text-align: center;
    label {
      @extend %font6;
      display: block;
      margin-top: .7em;
    }
  }
}

.table-body {
  li {
    display: table-row;
    width: 100%;
  }
  .td {
    border: 2px solid #FFF;
    padding: .5em 0;
    line-height: 1em;
    &.metric {
      @extend %font18;
      border-bottom-color: #EFEFEF;
      border-top-color: #EFEFEF;
      label {
        @extend %font6;
        text-transform: uppercase;
        display: block;
      }
      span {
        @extend %font7;
        text-transform: uppercase;
      }
    }
    &:not(.metric) {
      color: #FFF;
      @extend %font24;
      line-height: 1.2em;
      .number {
        @extend %inlineBlock;
        padding-top: .3em;
      }
    }
  }
}

.tr {
  &:first-child {
    .metric {
      border-top: none;
    }
  }
  &:last-child {
    .metric {
      border-bottom: none;
    }
  }
}

.th, .td {
  display: table-cell;
  width: 16%;
  text-align: center;
}

legend {
  position: relative;
  margin: 2em 0 1em 0;
  width: 100%;
  height: 1em;
  background-image: linear-gradient(to left, $color-stop-1, $color-stop-2, $color-stop-3, $color-stop-4);
  &:before {
    position: absolute;
    content: '1st percentile';
    left: 0;
    margin-top: -1.5em;
    @extend %font8;
  }
  &:after {
    position: absolute;
    content: '99th percentile';
    right: 0;
    margin-top: -1.5em;
    @extend %font8;
  }
}

.benchmarks {
  @extend %inlineBlock;
  width: 100%;
}

See the Pen Dynamic background tiles by Andrea Mignolo (@pnts) on CodePen.

Animating Svg

design and gif

Animating SVG

Skeuomorphic Behavior

design and interface

I was in Portland last month for beer, beer, and EmberConf (mostly EmberConf), where I met a great group of people building ambitious applications in Ember. The talks are all available online, and if you're doing Ember development of any sort or interested in getting started, go watch them all now.

One of the more incidental takeaways that keeps popping into my head during idle moments came from Edward Faulkner's talk, "Animations and Transitions in an Ember App". In the first five minutes Edward uses the phrase, 'skeuomorphic behavior' to explain why scrolling has momentum in a medium with no physical limitations - our monkey brains just aren't quick enough to fully process instantanous changes in a system. If we're lucky we know *something* happened, but not exactly what. Ted Neslon said something similar a few years ago at Brooklyn Beta, which I may or not be correctly paraphrasing but remember it as, "animation is orientation in orthogonal space."

What I like about the phrase 'skeuomorphic behavior' is the nuance. For a while, debates about skeuomorphism by and large referred to visual ornamentation to aid in understanding the what of digital objects. Teasing apart the definition and applying it to wider systems of interaction changes skeuomorphism from a passing visual trend into a useful tool for thinking about digital systems. Sound is also a good candidate for skeuomorphic structures that assist in digital wayfinding - apps like Tweetbot and Airmail make nuanced use of sound to communicate the type of action being taken.

For more, check out Edwards Ember talk:

Medians And Messages

data and visualization

We still carry the historical baggage of a Platonic heritage that seeks sharp essences and definite boundaries...This Platonic heritage, with its emphasis in clear distinctions and separated immutable entities, leads us to view statistical measures of central tendency wrongly, indeed opposite to the appropriate interpretation in our actual world of variation, shadings, and continua. In short, we view means and medians as the hard "realities," and the variation that permits their calculation as a set of transient and imperfect measurements of this hidden essence. If the median is the reality and variation around the median just a device for its calculation, the "I will probably be dead in eight months" may pass as a reasonable interpretation.
Variation itself is nature's only irreducible essence. Variation is the hard reality, not a set of imperfect measures for a central tendency. Means and medians are the abstractions.

Stephen Jay Gould: The Median Isn't the Message

The Story Of A Streamgraph

design and visualization

At SimpleReach we spend a lot of time investigating, among other things, the motley social behavior of digital content. We ingest the links people share in a perpetual, frantic choreography of likes, pins, +1s, and tweets, and look for patterns that can predict the next few hours of social traffic to any given url. We see a lot of content and sharing in a 24 hour period which, internally, we look at all day in the form of charts, graphs, leaderboards, logs, and reports. Publically we’ve written a few blog posts about our research and spent a beer-fueled week building the sponsored content leaderboard, but other than that, the data is pretty much under lock and key.

When we started designing our first official marketing site (upgraded from a sign-up-form-slash-holding page lacquered in lighting effects, box shadows, and texture) one of the requirements was to incorporate our data in some way. After a bit of exploration we found the best approach was to visualize the relative sharing activity between social networks over a 24 hour period.

We looked into a number of different visualization techniques before settling on the streamgraph - a type of stacked area graph - for its visual appeal and architectural interestingness.

The horizontal flow of the river represents the flow of time. Each vertical section of the river corresponds to an ordered time slice. Each theme is represented by a colored current that runs horizontally within the river. The width of a current changes to reflect the strength of the corresponding theme over time. As the occurrence of a theme increases over time, the corresponding current widens. As a theme’s occurrence decreases over time, the corresponding current narrows.

Theme River: In Search of Trends, Patterns, and Relationships

The streamgraph on our marketing site visualizes the frequency of social actions over time across six major social networks. The graph displays data over the last 24 hours and updates every 10 minutes causing the height of the stream to undulate across the width of the screen throughout the day. It’s an interesting way to gauge the ebbs and flows of social actions - each day surfaces different shapes, bumps, and wiggles.

I check the site throughout the day to get a sense of how things are going on a macro level. Facebook always dominates, while the other five social networks alternate between 2nd and 3rd place. StumbleUpon occasionally has spikes of activity, and Twitter waxes and wanes. Unsurprisingly, if you remove Facebook from the visualization, the relationships between the other networks becomes more dynamic.

We work with a lot of large publishers who drive significant traffic on a daily basis. Sometimes when we onboard a particularly large one and the implementation isn’t quite right, it can have some interesting effects:

The streamgraph has been surprising in the variety of ways it can be used beyond our original intent. There are, of course, a number of things we could improve upon with the current graph - add the ability to toggle which networks are visible or select custom time frames such as the last 7 or 30 days. We could also spend some time investigating layout algorithms that cater more specifically to the statistical properties of our particular data set.

Additional reading:

  1. Theme River: In Search of Trends, Patterns, and Relationships - Susan Havre, Beth Hetzler, and Lucy Nowell
  2. Stacked Graphs, Geometry & Aesthetics - Lee Byron & Margin Wattenberg
  3. D3 and Streamgraphs

pnts.us is fueled by the intersection of mishaps, happenstance, books, curiousity, and beer.

New York, NY