June 12, 2016

Reasons to use Polymer with Redux

In the recent project we were building analytics platform using web components. At the beginning of the project we have chosen Polymer framework to build our custom web components. After 3-4 month of work we have discovered several downsides of the framework:
1) it's not mature enough and we still have issues with basic functionality. For example dom-repeater element is for sure basic structure but for some reason it doesn't update child element correctly all the time.
2) Polymer gives us 2-way data binding which in big projects could give performance issues.
3) 2 way data binding system complicates developing (you have to place conditional guarders literally in every watch function) and debugging process.

To solve all mentioned above problems we decided to use Redux state management system. How Redux can help us:
1) Easy debuging including time-travel debugging and hotswapping state
2) Redux devTool app for Chrome allow to investigate your state after each action.
3) separation of logic from ui renderers. Our views can listen to changes to various pieces of Redux state and update the DOM accordingly.

How to use Redux with Polymer:
There is 'connection bridge' between Polymer and Redux - polymer-redux behavior
Using this behavior Polymer elements can listen to the State changes and render themself accordingly. It means that all the logic would be moved to Redux reducers and Polymer elements would be responsible only for rendering and internal 'dumb' logic.

As Redux state is read-only we can not use Polymer 2-way data binding to connect to the state. The only way to change the state is to dispatch actions. Doing this we eventually will use 1-way data binding which is easier to understand and debug.

After 2 month working with Redux we have concluded that this is the right way to go but we have still some issues with Polymer basic functionality which make us think that React + Redux might be a better solution.

April 12, 2016

Histogram + line combo charts

Recently I have faced a problem of creating histogram + line combo chart. My first attempt was using google-charts but the problem is that according to the documentation it is not possible to use histogram type in a combo charts. the same problem I had with Plotly.js library.  Fortunately these libraries are very flexible so I tried to use vertical bar (column) chart instead of the histogram.

Fig.1. Bar chart
Fig.2. Histogram + line combo chart


                                     






Nevertheless  bar chart x-axis has ticks (labels) for every bar (see Fig.1) instead of the “from-to” range as in normal histogram or line chart (Fig.2). So the post is about how to fix that for both libraries. The main trick is in shift of the plot to the right. lets see how it’s done in codepen for google-chart and for plotly.js

code for google-chart is below:

google.charts.load('current', {'packages':['corechart']});
google.charts.setOnLoadCallback(drawVisualization);
 
function drawVisualization() {
  var config = [{color: 'lightblue', label: 'Select'}, {color: 'blue', label: 'CTX'}, {color: 'green', label: '100%'}];
   
  var data = new google.visualization.DataTable();
  data.addColumn('number', 'Amount');
  data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
  data.addColumn('number', config[0].label); 
  data.addColumn('number', config[1].label);
  data.addColumn('number', config[2].label);
  data.addRows([
     [0.5, null, 50, 80, 111],
     [1.5, null, 30, 170, 200],
     [2.5, null, 300, 300, 300],
     [3.5, null, 250, 310, 370],
     [4.5, null, 150, 240, 250]
  ]);
 
  // setting tooltip
  for (var i = 0, rows = data.getNumberOfRows(), month; i < rows; i++) {
      data.setValue(i, 1,`<div class="gc-tooltip">
           <div style="color:${config[0].color;">
                Select:<span class="gc-right">${data.getValue(i,2)}</span>
           </div>
           <div style="color:${config[1].color;">
                CTX:<span class="gc-right">${data.getValue(i,3)}</span>
           </div>
           <div style="color:${config[2].color;">
                100%:<span class="gc-right">${data.getValue(i,4)}</span>
           </div>
           </div>`);
  }
 
  var options = {
    vAxis: {
        title: 'Trace Frequency',
      gridlines: {color: 'gainsboro'}
    },
    hAxis: {
      title: 'Amount', format: '#K',
      ticks: [0, 1, 2 , 3, 4, 5],
      gridlines: {color: 'white'}
    },
    series: {
      0: {type: 'bars', color: config[0].color},
        1: {type: 'line', color: config[1].color},
        2: {type: 'line', color: config[2].color}
    },
    legend: "none",
    bar: {groupWidth: "98%"},
    pointSize: 8,
    focusTarget: 'category',
    tooltip: {isHtml: true},
    width: 600,
    height: 400,
  };
 
  var chart = new google.visualization.ComboChart(document.getElementById('chart_div'));
  chart.draw(data, options);
}
code for plotly.js is below:
var trace0 = {
  
  x: [0.5, 1.5, 2.5, 3.5, 4.5], 
  y: [111,200,300,370,250], 
  type: 'scatter',
  marker: {
    color: 'green'
  },
  hoverinfo: 'y'
};
 
var trace1 = {
  x: [0.5, 1.5, 2.5, 3.5, 4.5], 
  y: [80, 170, 300, 310, 240], 
  type: 'scatter',
  marker: {
    color: 'blue'
  },
  hoverinfo: 'y'
};
 
var trace2 = {
  x: [0.5, 1.5, 2.5, 3.5, 4.5], 
  y: [50,30,300,250,150],
  type: 'bar',
  marker: {
    color: 'lightblue'
  },
  hoverinfo: 'y'
};
 
var layout = {
  xaxis: {
        zeroline: false,
        title: 'Amount',
        range: [0, 5],
        type: 'linear',
        dtick: 1,
        ticksuffix: 'K'
  },
  yaxis: {
        title: 'Trace Frequency',
        range: [0, 400],
        type: 'linear'
  },
  bargap :0.01,
  margin: {
    pad: 10
  },
  showlegend: false,
  hovermode: 'x',
}
 
Plotly.newPlot('myDiv', [trace0, trace1, trace2], layout);

Bootstrap 3 carousel with multiple items.

As you probably know bootstrap 3 carousel shows only one item at a time.
But what if I want it to show 2 or more at a time? or even make it responsive?
I’ve searched the internet and have some solutions which for some reason didn’t work correctly.
Instead of sliding items one by one it was sliding all of the shown items together replacing them with a new set of items (so pretty much the same behavior as in single item carousel).
At that time I had bootstrap v 3.3.2.
After spending some time on investigation how to fix that I figured out that newest updates to the bootstrap 3 css and script files changes the behavior of carousel.
I’ve found that with bootstrap 3.2.0 multiple items carousel behaves as I would expect this.

here is a codepen example of responsive multiple items carousel using bootstrap 3.2.0.
the code is below:

Html:
<div class="container">
 <div class="row">
  <div class="col-xs-11 col-md-10 col-centered">

   <div id="carousel" class="carousel slide" data-ride="carousel" data-type="multi" data-interval="2500">
    <div class="carousel-inner">
     <div class="item active">
      <div class="carousel-col">
       <div class="block red img-responsive"></div>
      </div>
     </div>
     <div class="item">
      <div class="carousel-col">
       <div class="block green img-responsive"></div>
      </div>
     </div>
     <div class="item">
      <div class="carousel-col">
       <div class="block blue img-responsive"></div>
      </div>
     </div>
     <div class="item">
      <div class="carousel-col">
       <div class="block yellow img-responsive"></div>
      </div>
     </div>
    </div>

    <!-- Controls -->
    <div class="left carousel-control">
     <a href="#carousel" role="button" data-slide="prev">
      <span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
      <span class="sr-only">Previous</span>
     </a>
    </div>
    <div class="right carousel-control">
     <a href="#carousel" role="button" data-slide="next">
      <span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
      <span class="sr-only">Next</span>
     </a>
    </div>
   </div>

  </div>
 </div>
</div>
Js:

$('.carousel[data-type="multi"] .item').each(function() {
 var next = $(this).next();
 if (!next.length) {
  next = $(this).siblings(':first');
 }
 next.children(':first-child').clone().appendTo($(this));

 for (var i = 0; i < 2; i++) {
  next = next.next();
  if (!next.length) {
   next = $(this).siblings(':first');
  }

  next.children(':first-child').clone().appendTo($(this));
 }
});
Css:

.col-centered {
    float: none;
    margin: 0 auto;
}

.carousel-control { 
    width: 8%;
    width: 0px;
}
.carousel-control.left,
.carousel-control.right { 
    margin-right: 40px;
    margin-left: 32px; 
    background-image: none;
    opacity: 1;
}
.carousel-control > a > span {
    color: white;
   font-size: 29px !important;
}

.carousel-col { 
    position: relative; 
    min-height: 1px; 
    padding: 5px; 
    float: left;
 }

 .active > div { display:none; }
 .active > div:first-child { display:block; }

/*xs*/
@media (max-width: 767px) {
  .carousel-inner .active.left { left: -50%; }
  .carousel-inner .active.right { left: 50%; }
 .carousel-inner .next        { left:  50%; }
 .carousel-inner .prev       { left: -50%; }
  .carousel-col                { width: 50%; }
 .active > div:first-child + div { display:block; }
}

/*sm*/
@media (min-width: 768px) and (max-width: 991px) {
  .carousel-inner .active.left { left: -50%; }
  .carousel-inner .active.right { left: 50%; }
 .carousel-inner .next        { left:  50%; }
 .carousel-inner .prev       { left: -50%; }
  .carousel-col                { width: 50%; }
 .active > div:first-child + div { display:block; }
}

/*md*/
@media (min-width: 992px) and (max-width: 1199px) {
  .carousel-inner .active.left { left: -33%; }
  .carousel-inner .active.right { left: 33%; }
 .carousel-inner .next        { left:  33%; }
 .carousel-inner .prev       { left: -33%; }
  .carousel-col                { width: 33%; }
 .active > div:first-child + div { display:block; }
  .active > div:first-child + div + div { display:block; }
}

/*lg*/
@media (min-width: 1200px) {
  .carousel-inner .active.left { left: -25%; }
  .carousel-inner .active.right{ left:  25%; }
 .carousel-inner .next        { left:  25%; }
 .carousel-inner .prev       { left: -25%; }
  .carousel-col                { width: 25%; }
 .active > div:first-child + div { display:block; }
  .active > div:first-child + div + div { display:block; }
 .active > div:first-child + div + div + div { display:block; }
}

.block {
 width: 306px;
 height: 230px;
}

.red {background: red;}

.blue {background: blue;}

.green {background: green;}

.yellow {background: yellow;}

December 15, 2015

Pure css3 responsive masonry layout for items with fixed width.

Recently I had a task of creating Masonry style layout for items with fixed width but not fixed height. This suppose to be used within Polymer element to layout sub elements in masonry style. I didn't want to introduce any external library to do that so I decided to go with pure css. In Polymer my custom css styles would be scoped for my host element only and not visible for 'outside world'. Apparently it's not hard to do with just several lines of css:

the codepen example is here;

.masonry {
   -moz-column-gap: 1.5em;
   -webkit-column-gap: 1.5em;
   column-gap: 1.5em;
   font-size: .85em;
   width: 95%;
   margin: 3em auto;
}

.item {
   display: inline-block;
   width: 200px;
   background: silver;
   padding: 1em;
   margin: 0 0 1.5em;
   box-sizing: border-box;
   -moz-box-sizing: border-box;
   -webkit-box-sizing: border-box;
}

@media only screen and (min-width: 400px) { .masonry { -moz-column-count: 2; -webkit-column-count: 2; column-count: 2; }}

@media only screen and (min-width: 700px) { .masonry { -moz-column-count: 3; -webkit-column-count: 3; column-count: 3; }}

@media only screen and (min-width: 900px) { .masonry { -moz-column-count: 4; -webkit-column-count: 4; column-count: 4; }}

@media only screen and (min-width: 1100px) { .masonry { -moz-column-count: 5; -webkit-column-count: 5; column-count: 5; }}
and the usage is:

<class=masonry parent>
<class=item child>
<content/>
      ... 
</class=item child>
</class=masonry parent>

August 21, 2015

How to make website look like an app

Things that we want to achieve:
1) make customizable shortcut to a device’s desktop.
2) hide browser navigation bar

Lets start with creating a shortcut. for safari:

<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" sizes="72x72" href="72.png">
<link rel="apple-touch-icon" sizes="114x114" href="114.png">
<link rel="apple-touch-icon" href="57.png">
<link rel="apple-touch-startup-image" href="splash.png">
for chrome:

<meta name="mobile-web-app-capable" content="yes">
<link rel="icon" sizes="72x72" href="72.png">
<link rel="icon" sizes="114x114" href="114.png">
<link rel="icon" sizes="192x192" href="192.png">
<link rel="icon" sizes="57x57" href="57.png">
for IE10:

<meta name="name" content="" />
<meta name="msapplication-TileColor" content="#000000" />
<meta name="msapplication-square70x70logo" content="72.png" />
<meta name="msapplication-square150x150logo" content="150.png" />
<meta name="msapplication-wide310x150logo" content="310x150.png" />
<meta name="msapplication-square310x310logo" content="310.png" />
if you need to support other browsers then it might be a good idea to use manup.js polifill library. In this case you have to create manifest file:

{
  "name": "app name",
  "short_name": "app",
  "icons": [{
        "src": "64.png",
        "sizes": "64x64" 
    }, {
        "src": "128.png",
        "sizes": "128x128"    
    }],
  "start_url": "index.html",
  "display": "fullscreen",
  "orientation": "landscape"
}
Next I will explain how to solve safari full screen mode problem You have created a shortcut for your website and opened it. You can see safari in full screen mode BUT as soon as you navigate using href links safari returns to default mode with a navigation bar. My first clue was to use target=”_parent” in all links which should force new pages open in the current frame but for some reason that doesn’t work for safari. solution I did find in here. Actually only thing you should do to fix that is to add some javascript code to your page:

(function(document,navigator,standalone) {   
   if ((standalone in navigator) && navigator[standalone]) {      
      var curnode, location=document.location, stop=/^(a|html)$/i;
         document.addEventListener('click', function(e) {
            curnode=e.target;
            while (!(stop).test(curnode.nodeName)) {
               curnode=curnode.parentNode;
            }            
            if('href' in curnode && ( curnode.href.indexOf('http') || ~curnode.href.indexOf(location.host) ) ) {
               e.preventDefault();
               location.href = curnode.href;
            }
      },false);
   }
})(document,window.navigator,'standalone');

March 30, 2015

Carousel (Slide Show) module for famo.us

Slide Show module is written in javaScript using famo.us framework.
The module is a modification of 'sliderShow' module for famo.us.
demo of non-modified version (Note: This demo works on old version of famo.us)

List of changes:
1) fixed bug with using old version of famo.us (otherwise doesn't work at all).
2) fixed bug: instead of Modifier use StateModifier.
3) changed CSS.
4) changed constructor feed parameters.
5) adjustable nav. bubble radius.
6) solved positioning bug for bubbles.
7) fixed translation bug: using translate(x, y,0.01) instead of translate(x,y,0).
8) adjustable size of the sliderShow container.


var slideshow = new Slidershow({
   width: window.innerWidth,
   height: 500,
   transition: {duration: 600, curve: 'easeInOut' },
   navDotRadius: window.innerWidth/100,
   navDotGap: window.innerWidth / 75,
   sliders: renderablesToShow
});

GISTS (modified version):
slidershow,
css

define(function (require, exports, module) {
 'use strict';

 var Surface = require('famous/core/Surface');
 var StateModifier = require('famous/modifiers/stateModifier');
 var Transform = require('famous/core/Transform');
 var EventHandler = require('famous/core/EventHandler');
 var OptionsManager = require('famous/core/OptionsManager');
 var View = require('famous/core/View');
 var ContainerSurface = require('famous/surfaces/ContainerSurface');
 var Utility = require('famous/utilities/Utility');

 function Slidershow(options) {
  this.options = Object.create(Slidershow.DEFAULT_OPTIONS);
  this._optionsManager = new OptionsManager(this.options);

  if (options) this.setOptions(options);

  this.total = this.options.sliders.length;
  this.page = 1;
  this._sliders = [];
  this._rwdDelay = undefined;
  this._currentSlideId = 0;
  this._navigationSurf = [];

  this._eventOutput = new EventHandler();
  EventHandler.setOutputHandler(this, this._eventOutput);

  _createSlider.call(this);
 }

 Slidershow.DEFAULT_OPTIONS = {
  width: window.innerWidth,
  height: 500,
  transition: {
   duration: 600,
   curve: 'easeInOut'
  },
  navDotRadius: window.innerWidth/100,
  navDotGap: window.innerWidth / 75
 };

 function _createSlider() {
  this.container = new ContainerSurface({
   size: [(this.options.width === window.innerWidth ? undefined : this.options.width), this.options.height],
   properties: {
    overflow: 'hidden',
    backgroundColor: 'rgb(33, 33, 33)'
   }
  });

  this.sliderContainer = new ContainerSurface({
   size: [(this.options.width === window.innerWidth ? undefined : this.options.width), this.options.height],
   properties: {
    overflow: 'hidden'
   }
  });

  // Create slide items
  _createSlideItem.call(this);

  // Navigation
  if (this.total > 1) _createNavigation.call(this);

  this.container.add(this.sliderContainer);

  if (this.options.width === window.innerWidth) {
   window.addEventListener('resize', function () {
    this.options.width = window.innerWidth;
   }.bind(this), false);
  }
 }

 function _createNavButton(direction) {     // Prev, Next button
  var container, surf, mod;

  container = new ContainerSurface({
   size: [true, undefined],
   properties: {
    background: 'rgba(33,33,33,0.4)',
    padding: '2em'
   }
  });
  surf = new Surface({
   size: [true, true],
   content: '',
   classes: ['projects-assets-nav-button']
  });
  mod = new StateModifier({
   align: [0.5, 0.5],
   origin: [0.5, 0.5]
  });
  container.add(mod).add(surf);

  mod = new StateModifier({
   align: [(direction === 'prev' ? 0 : 1), 0.5],
   origin: [(direction === 'prev' ? 0 : 1), 0.5]
  });

  container.on('click', function (e) {
   this.slidingPage(direction);
  }.bind(this));

  this.container.add(mod).add(container);
 }

 function _createNavBubble(bubble, view) {
  var surf, mod;

  surf = new Surface({
   classes: ['projects-assets-bubble-button'],
   properties: {
    borderWidth: this.options.navDotRadius + 'px',
    borderRadius: bubble === 0 ? 2 * this.options.navDotRadius + 'px' : this.options.navDotRadius + 'px',
    padding: bubble === 0 ? this.options.navDotRadius + 'px' : '0px'
   }
  });

  var dist = (bubble != 0) ? bubble * (this.options.navDotRadius * 2 + this.options.navDotGap) - this.options.navDotRadius : -2 * this.options.navDotRadius;

  mod = new StateModifier({
   size: (bubble === 0 ? [this.options.navDotRadius * 3, this.options.navDotRadius * 3] : [this.options.navDotRadius, this.options.navDotRadius]),
   transform: Transform.translate(dist, 0, 0.01),
   origin: [0, 0.5]
  });
  view._add(mod).add(surf);

  surf.on('click', function (e) {
   this.jumpToSlide(bubble);
  }.bind(this));

  this._navigationSurf.push({ view: view, surf: surf });
 }

 function _createNavigation() {
  var surf, mod, container, view, i;

  _createNavButton.call(this, 'prev');
  _createNavButton.call(this, 'next');

  container = new ContainerSurface({
   size: [(this.total - 1) * (this.options.navDotGap + 2 * this.options.navDotRadius), this.options.navDotRadius]
  });

  view = new View();
  for (i = 0; i < this.total; i++) {
   _createNavBubble.call(this, i, view);
  }
  container.add(view);

  mod = new StateModifier({
   origin: [0.5, 0.5],
   align: [0.5, 0.9]
  });

  this.container.add(mod).add(container);
 }

 function _createSlideItem() {
  for (var i = (this.total - 1) ; i >= 0; i--) {

   var container = new ContainerSurface({ size: [this.options.width, this.options.height] });

   if (typeof this.options.sliders[i] === 'object') container.add(this.options.sliders[i]);

   var view = new View({});

   var dx; // calculate nitial x position of the slide
   if (i === 0) { //first item
    dx = 0;
   } else {
    if (i === 1) { //next item
     dx = this.options.width;
    } else if (i === this.total - 1) { // prev item
     dx = -this.options.width;
    } else {
     dx = -10 * this.options.width; //all other items
    }
   }

   view.add(new StateModifier({
    transform: Transform.translate(dx, 0, 0.01)
   })).add(container);

   this._sliders.push(view);
   this.sliderContainer.add(view);
  }
 }

 function _resetNavigation(slideId) {
  var activated = {
   borderRadius: 2 * this.options.navDotRadius + 'px',
   padding: this.options.navDotRadius + 'px'
  }
  var deactivated = {
   borderRadius: this.options.navDotRadius + 'px',
   padding: '0px'
  }
  var nav = this._navigationSurf[this._currentSlideId];
  nav.surf.setOptions({ properties: deactivated });
  nav.view._node._child[this._currentSlideId].get().setSize([this.options.navDotRadius, this.options.navDotRadius]);
  var dist = this._currentSlideId * (this.options.navDotRadius * 2 + this.options.navDotGap) - this.options.navDotRadius;
  nav.view._node._child[this._currentSlideId].get().setTransform(Transform.translate(dist, 0, 0.01));
  nav.view._node._child[this._currentSlideId].get().setOrigin([0, 0.5]);

  nav = this._navigationSurf[slideId];
  nav.surf.setOptions({ properties: activated });
  nav.view._node._child[slideId].get().setSize([this.options.navDotRadius * 3, this.options.navDotRadius * 3]);
  var dist = slideId * (this.options.navDotRadius * 2 + this.options.navDotGap) - 2 * this.options.navDotRadius;
  nav.view._node._child[slideId].get().setTransform(Transform.translate(dist, 0, 0.01));
  nav.view._node._child[slideId].get().setOrigin([0, 0.5]);

  this._currentSlideId = slideId;
 }

 Slidershow.prototype.getTotal = function getTotal() {
  return this.total;
 };

 Slidershow.prototype.getPage = function getPage() {
  return this.page;
 };

 Slidershow.prototype.jumpToSlide = function jumpToSlide(slide, direction /* only for prev, next */) {
  var current = this.total - this.page >= this.total ?
      this.total - 1 : this.total - this.page <= 0 ?
      0 : this.total - this.page,
   next = current - 1 < 0 ? this.total - 1 : current - 1,
   prev = current + 1 >= this.total ? 0 : current + 1,
   feature = this.total - (slide + 1);

  if (current === feature) return true;
  var direction = direction || 'feature';

  _resetNavigation.call(this, slide);

  if (Math.abs(current - feature) !== (this.total - 1)) {
   this._sliders[next]._node.get().setTransform(
     Transform.translate(this.options.width * -10, 0, 0.01),
     undefined
   );
   this._sliders[prev]._node.get().setTransform(
     Transform.translate(this.options.width * -10, 0, 0.01),
     undefined
   );
  }

  this._sliders[feature]._node.get().setTransform(
    Transform.translate(this.options.width * (current > feature ? 1 : -1), 0, 0.01)
  );
  this._sliders[feature]._node.get().setTransform(
    Transform.translate(0, 0, 0.01),
    this.options.transition
  );
  this._sliders[current]._node.get().setTransform(
    Transform.translate(this.options.width * (current > feature ? -1 : 1), 0, 0.01),
    this.options.transition
  );

  if (Math.abs(current - feature) !== (this.total - 1)) {
   next = feature - 1 < 0 ? this.total - 1 : feature - 1;
   prev = feature + 1 >= this.total ? 0 : feature + 1;

   if (current > feature) {
    this._sliders[next]._node.get().setTransform(
      Transform.translate(this.options.width, 0, 0.01),
      undefined
    );
   } else {
    this._sliders[prev]._node.get().setTransform(
      Transform.translate(this.options.width * -1, 0, 0.01),
      undefined
    );
   }
  }

  this.page = slide + 1;
  this.page = (this.page > this.total) ? 1 : (this.page < 1) ? this.total : this.page;

  this._eventOutput.emit('pageChange', { page: this.page, direction: direction });
 };

 Slidershow.prototype.slidingPage = function slidingPage(direction) {
  if (direction === 'next') {
   this.jumpToSlide((this.page >= this.total) ? 0 : this.page, direction);
  } else if (direction === 'prev') {
   this.jumpToSlide((this.page <= 1) ? this.total - 1 : this.page - 2, direction);
  }
 };

 Slidershow.prototype.setOptions = function setOptions(options) {
  if (options.sliders === undefined || options.sliders.length === 0) {
   options.sliders = [];
  }
  if (options.width === undefined || typeof options.width !== 'number') {
   options.width = window.innerWidth;
  }
  if (options.height === undefined || typeof options.height !== 'number') {
   options.height = 500;
  }
  if (options.transition === undefined || typeof options.transition !== 'object') {
   options.transition = {
    duration: 600,
    curve: 'easeInOut'
   };
  }

  this._optionsManager.setOptions(options);
 };

 Slidershow.prototype.render = function render() {
  if (this.total === 0) return null;

  return [
    {
     target: this.container.render()
    }
  ];
 };

 module.exports = Slidershow;
});
CSS:

.projects-assets-nav-button {
 text-align: center;
 color: rgba(255,255,255,0.8);
 cursor: pointer;
 font-size: 4em;
 line-height: 1em;
}

.projects-assets-nav-button:hover {
 color: rgb(252, 236, 60);
}
 
.projects-assets-bubble-button {
 border-color: rgba(252, 236, 60, 0.8);
 cursor: pointer;
 box-shadow: 10px 6px 35px 0px rgb(33, 33, 33);
 border-style: solid;
}

.projects-assets-bubble-button:hover {
 border-color: rgb(252, 236, 60);
}

March 26, 2015

JavaScript Fixed Masonry responsive layout.

The idea of this responsive layout is to present all surfaces on a custom viewport preserving the aspect ratios and keeping all surfaces as big as possible.

The codePen of the working responsive layout example is here.

This layout function work with famo.us framework and  famous-flex lib.
Gist of the layout function is here.

/**
 * This Source Code is licensed under the MIT license. If a copy of the
 * MIT-license was not distributed with this file, You can obtain one at:
 * http://opensource.org/licenses/mit-license.html.
 *
 * @author: Oleksandr Zinchenko (Qvatra)
 * @license MIT
 */

/*global console*/
/*eslint no-console: 0*/

/**
 * Fits (sets max possible size) a collection of renderables with a given aspect ratios to the context size from left to right, and when the right edge is reached,
 * continues at the next row.
 *
 * |options|type|description|
 * |---|---|---|
 * |`[cellRatios]`|Array.Number|Array of the dataSource elements aspect ratios|
 *
 * Example:
 *
 * ```javascript
 * var FixedMasonryLayout = require('FixedMasonryLayout');
 *
 * var layoutController = new LayoutController({
 *   layout: FixedMasonryLayout,
 *   layoutOptions: {
 *     cellRatios: [1, 3, 1, 2]
 *   },
 *   dataSource: [
 *     new Surface({content: '1', properties:{background:'red'}}),
 *     new Surface({content: '2', properties:{background:'blue'}}),
 *     new Surface({content: '3', properties:{background:'green'}}),
 *     new Surface({content: '4', properties:{background:'yellow'}})
 *   ]
 * });
 * ```
 * @module
 */
define(function (require, exports, module) {
    // Define capabilities of this layout function
    var capabilities = {
        sequence: true,
        scrolling: false
    };

    // data
    var size;       // layout container size 
    var index;       // iterator
    var cellRatios;      // integer ratios of elements
    var gridArea;       // summ of all elements areas. used for calculation of the gridSize
    var grid;       // binary grid array. 0 means empty cell - 1 means occupied cell
    var gridRatio;      // aspect ratio of the grid
    var gridSize;      // size of the grid regarding to the gridArea parameter (not pixels)
    var nodes;       // array of all elements (nodes)
    var node;       // current element
    var set = {       // size and position of an element
        size: [0, 0],
        translate: [0, 0, 0.01]
    };

    // returns true if element of size=size could be fitted in to the grid at coordinates [x, y]
    function _canBeFitted(x, y, itemSize) {
        for (var j = y; j < y + itemSize[1]; j++) {
            for (var i = x; i < x + itemSize[0]; i++) {
                if (i > grid.length - 1 || j > grid[0].length - 1 || grid[i][j] == 1) {
                    return false;
                }
            }
        }
        return true;
    }

    // markes occupied grid area with '1'
    function _reservePlace(x, y, itemSize) {
        for (var j = y; j < y + itemSize[1]; j++) {
            for (var i = x; i < x + itemSize[0]; i++) {
                grid[i][j] = 1;
            }
        }
    }

    // returns position on the grid if element could be fitted or undefined otherwise
    function _tryToFit(itemSize) {
        for (var j = 0; j < grid[0].length; j++) {
            for (var i = 0; i < grid.length; i++) {
                if (_canBeFitted(i, j, itemSize)) {
                    _reservePlace(i, j, itemSize);
                    return [i, j];
                }
            }
        }
        return undefined;
    }

    // calculate recursively next possible position on the grid for the given element
    function _calculatePosition(size) {
        var gridPosition = _tryToFit(size);
        if (!gridPosition) { // make grid bigger (add additional row and col) 
            grid.forEach(function (col) { //adding extra row
                col.push(0);
            });
            var col = [];
            grid[0].forEach(function () { //adding extra col
                col.push(0);
            });
            grid.push(col);
            return _calculatePosition(size);
        }
        return gridPosition;
    }

    // calculate canvas size = min size of a rectangle that can fit all elements
    function _calculateCanvasSize() {
        var canvasSize = [0, 0];
        for (var i = 0; i < nodes.length; i++) {
            canvasSize[0] = Math.max(nodes[i].gridPosition[0] + nodes[i].aspectRatio, canvasSize[0]);
            canvasSize[1] = Math.max(nodes[i].gridPosition[1] + 1, canvasSize[1]);
        }
        return canvasSize;
    }

    // height align calculation
    function _alignHeight(scale, viewSize, canvasSize) {
        return (viewSize[1] - canvasSize[1] * scale) / (canvasSize[1] + 1);
    }

    // width align calculation 
    function _alignWidth(scale, viewSize, canvasSize) {
        var gutterInRow = [];
        var freeSpaceInRow = [];
        var numCellsInRow = [];
        var lastCellInRow = [];

        for (var i = 0; i < canvasSize[1]; i++) {
            numCellsInRow.push(0);
            freeSpaceInRow.push(0);
            gutterInRow.push(0);
            lastCellInRow.push(null);
        }

        for (var i = 0; i < nodes.length; i++) {
            var row = nodes[i].gridPosition[1];
            numCellsInRow[row] += 1;
            nodes[i]['positionInRow'] = numCellsInRow[row];
            lastCellInRow[row] = (lastCellInRow[row] === null || nodes[i].gridPosition[0] > lastCellInRow[row].gridPosition[0]) ? nodes[i] : lastCellInRow[row];
        }

        freeSpaceInRow = lastCellInRow.map(function (cell) {
            return viewSize[0] - (cell.gridPosition[0] + cell.aspectRatio) * scale;
        })

        for (var i = 0; i < freeSpaceInRow.length; i++) {
            gutterInRow[i] = Math.floor(freeSpaceInRow[i] / (numCellsInRow[i] + 1));
        }

        return gutterInRow;
    }

    // Layout function
    function FixedMasonryLayout(context, options) {
        // init
        size = context.size;
        cellRatios = options.cellRatios;
        reflowTransition = options.reflowTransition;

        // calculate total area of elements
        gridArea = 0;
        cellRatios.forEach(function (cellRatio) {
            gridArea += cellRatio * 1;          // we assume that all nodes have height = 1
        })

        // prepare grid
        grid = [];
        gridRatio = size[0] / size[1];
        gridSize = [Math.ceil(Math.sqrt(gridRatio * gridArea)), Math.ceil(Math.sqrt(gridArea / gridRatio))];
        for (var i = 0; i < gridSize[0]; i++) {
            grid.push([]);
            for (var j = 0; j < gridSize[1]; j++) {
                grid[i].push(0);           // init of the grid with '0'
            }
        }

        // 1st loop; calculate positions
        node = context.next();
        nodes = [];
        index = 0;
        while (node && (index < cellRatios.length)) {
            node['aspectRatio'] = cellRatios[index];
            node['gridPosition'] = _calculatePosition([cellRatios[index], 1]);
            nodes.push(node);

            // Move to next renderable
            index++;
            node = context.next();
        }

        var canvasSize = _calculateCanvasSize();
        var scale = (size[0] / size[1] > canvasSize[0] / canvasSize[1]) ? size[1] / canvasSize[1] : size[0] / canvasSize[0];
        var widthAlign = _alignWidth(scale, size, canvasSize);
        var heightAlign = _alignHeight(scale, size, canvasSize);

        // 2nd loop; set node options
        nodes.forEach(function (node) {
            set.size = [node.aspectRatio * scale, 1 * scale];
            set.translate[0] = node.gridPosition[0] * scale + widthAlign[node.gridPosition[1]] * node.positionInRow;
            set.translate[1] = node.gridPosition[1] * scale + heightAlign * (node.gridPosition[1] + 1);
            context.set(node, set);
        })
    }

    FixedMasonryLayout.Capabilities = capabilities;
    module.exports = FixedMasonryLayout;
});