cms/app-client/app/components/visu-langues.js
author ymh <ymh.work@gmail.com>
Fri, 09 Dec 2016 11:41:15 +0100
changeset 467 762fc0eb4946
parent 424 feb0d3e0fef9
child 532 1190ea937f2d
permissions -rw-r--r--
Migrate d3js to v4 and correct d3js visualisations i.e. bug 3.20. Breadcrumb navigation for the language treemap has been improved

import Ember from 'ember';
import * as d3 from  'd3-selection';
import * as d3h from 'd3-hierarchy';
import * as d3s from 'd3-scale';
import * as d3t from 'd3-transition';
import * as d3e from 'd3-ease';
import _ from 'lodash/lodash';

// inspired by http://bl.ocks.org/ganeshv/6a8e9ada3ab7f2d88022
// and https://bost.ocks.org/mike/treemap/
export default Ember.Component.extend({

  constants: Ember.inject.service(),
  filter: Ember.inject.service(),
  colors: Ember.inject.service(),
  utils: Ember.inject.service(),

  scale: Ember.computed('maxCount', 'minCount', function () {
    let maxCount = this.get('maxCount');
    let minCount = this.get('minCount');
    return this.get('colors').getScaleLinear(minCount, maxCount);
  }),
  maxCount: 0,
  minCount: 0,

  firstRender: true,

  filterLanguageObserver: Ember.observer('filter.language', function () {
    Ember.$('.node').removeClass("selected");
    Ember.$('.node[data-id="' + this.get('filter').get('language') + '"]').addClass("selected");
  }),

  filterOtherModified: false,

  filterOtherObserver: Ember.observer('filter.date.[]', 'filter.discourse.[]', 'filter.theme.[]', 'filter.location', function() {
    this.set('filterOtherModified', true);
    this.set('firstRender', true);
  }),


  didRender: function () {

    if(!this.get('filterOtherModified') && !this.get('firstRender')) {
      return;
    }
    this.set('firstRender', false);
    this.set('filterOtherModified', false);
    var self = this;

    var margin = { top: 30, right: 0, bottom: 0, left: 0 };
    var width = Ember.$('#' + this.get('elementId')).width();
    var height = Ember.$('#' + this.get('elementId')).height() - margin.top - margin.bottom;
    var ratio = (1+Math.sqrt(5))/2;

    var languages = this.get('languages');
    var languagesMap = languages.reduce(function (res, l) { res[l.get('id')] = l.get('count'); return res; }, {});

    var treemap = d3h.treemap()
      .size([width/ratio, height])
      .tile(d3h.treemapSquarify.ratio(1));

    var element = d3.select('#' + this.get('elementId'))
      .style("width", width + margin.left + margin.right + 'px')
      .style("height", height + margin.bottom + margin.top + 'px')
      .style("margin-left", -margin.left + "px")
      .style("margin-right", -margin.right + "px");

    var breadcrumbs = element.select("div.breadcrumbs");

    if(breadcrumbs.empty()) {
      breadcrumbs = element.insert("div", ":first-child")
        .attr("class", "breadcrumbs")
        .attr("y", -margin.top)
        .style("width", width + 'px')
        .style("height", margin.top + 'px');
    }

    var root = _.cloneDeep(this.constants.LANGUAGES_TREEMAP);

    function ancestors(d) {
      let ancestors = [];
      let currentNode = d;
      while (currentNode.parent) {
        ancestors.unshift(currentNode = currentNode.parent);
      }
      return ancestors;
    }

    // This function set the count and parent attributes
    function decorateData(d) {
      if (d.children) {
        d.count = d.children.reduce(function (p, v) { v.parent = d; return p + decorateData(v); }, 0);
      } else if (d.values) {
        d.count = d.values.reduce(function (p, v) { return p + (languagesMap[v] ? languagesMap[v] : 0); }, 0);
      } else {
        d.count = languagesMap[d.id] ? languagesMap[d.id] : 0;
      }
      return d.count;
    }
    decorateData(root);

    // Clean tree with empty nodes
    function cleanTree(n) {
      if(n.children) {
        n.children = n.children.filter(function(c) { return c.count !== 0;});
        n.children.forEach(function(c) {
          cleanTree(c);
        });
      }
    }
    cleanTree(root);

    var transitioning = false;

    display(root, 1);

    function transition(d, node, nodeWrapper) {
      if (transitioning || !d) {
        return;
      }

      selectHandler(d);
      transitioning = true;

      var t = d3t.transition().duration(750).ease(d3e.easeLinear);
      var newNode = display(d, 0);
      var transitionNodes = node.transition(t);
      var transitionNewNodes = newNode.transition(t);

      newNode.style("fill-opacity", 0);
      transitionNodes.style("opacity", 0).remove().on("end", function () {
        nodeWrapper.remove();
        transitioning = false;
      });
      transitionNewNodes.style("opacity", 1);

    }

    function selectHandler(d) {
      if (d.id) {
        self.get('filter').setFilter('language', d.id);
      }
    }

    function display(rData, opacity) {

      var countArray = rData.children.map(function (d) { return d.count; });
      var dMin = Math.min.apply(null, countArray);
      var dMax = Math.max.apply(null, countArray);
      var globalMin = rData.children.reduce(function minRec(m, c) {
        if(c.count < m) {
          m = c.count;
        }
        if(c.children) {
          m = c.children.reduce(minRec, m);
        }
        return m;
      }, dMax);
      var dataScale = d3s.scaleLinear().domain([globalMin, dMax]).range([globalMin, dMax]);

      var nameArray = rData.children.reduce(function (res, d) {
        res = res.concat(d.name.split(/[\s\-]/));
        return res;
      }, ["(nnnnn)"]).sort(function(a,b) { return b.length - a.length; });

      var w = self.get('utils').getWidthOfText(nameArray[0], '11px');
      var p = w*w/(width*height);

      if(dMin/dMax < p) {
       dataScale = dataScale.range([p*100,100]);
      }

      var rNode = d3h.hierarchy(rData)
        .sum(function (d) { return (d.children) ? 0 : dataScale(d.count); })
        .sort(function (a, b) { return b.value - a.value; });

      self.setProperties({ minCount: dMin, maxCount: dMax });
      var scale = self.get('scale');
      var backgroundColor = function (_d) { return scale(_d.data.count); };
      var colorClass = function (_d) { return (self.get('colors').getPerceptiveLuminance(backgroundColor(_d)) >= 0.5) ? 'light-color' : 'dark-color'; };

      var nodeWrapper = element.append("div")
        .attr("class", "nodes")
        .datum(rNode);

      var descendants = treemap(rNode).descendants().filter(function (c) { return c.depth === rNode.depth + 1; });

      var node = nodeWrapper.selectAll()
        .data(descendants)
        .enter()
        .append("div")
        .attr("data-id", function (d) { return d.data.id; });


      node.attr("class", function (_d) { return "node " + colorClass(_d) + (_d.id === self.get('filter').get('language') ? " selected" : ""); })
        .style("width", function (d) { return (Math.round(d.x1 * ratio) - Math.round(d.x0 * ratio)) + 'px'; })
        .style("height", function (d) { return (d.y1 - d.y0) + 'px'; })
        .style("left", function (d) { return Math.round(d.x0*ratio) + 'px'; })
        .style("top", function (d) { return d.y0 + 'px'; })
        .style("background-color", backgroundColor)
        .style('opacity', opacity)
        .attr('title', function (d) { return d.data.name + ' (' + d.data.count + ')'; })
        .on("click", function (d) { selectHandler(d.data); });

      node.filter(function (d) { return d.children; })
        .classed("children", true)
        .on("click", function (d) { transition(d.data, node, nodeWrapper); })
        .append("i")
        .attr("class", "fa fa-folder-o");

      node.append("span")
        .html(function (d) { return d.data.name + ' <span class="count">(' + d.data.count + ')</span>'; });

      breadcrumbs
        .selectAll("span")
        .remove();
      let i = 0;
      ancestors(rNode.data).forEach(function (a) {
        breadcrumbs
          .append("span")
          .attr('class', (i++) ? "level" : "root")
          .html(a.name)
          .datum(a)
          .on("click", function (d) { transition(d, node, nodeWrapper); });
      });
      breadcrumbs
        .append("span")
        .attr('class', (i) ? "level" : "root")
        .html(rNode.data.name)
        .datum(rNode.data);

      return node;
    }

  }

});