Source: v-chart-plugin.js

/** 
 *  @fileOverview Chart component containing all of the generic components required for charts
 *
 *  @author       Brian Greig
 *
 *  @requires     NPM:d3:Vue
 */

/* eslint-env browser */
import barChart from './import/barChart';
import vBarChart from './import/vBarChart';
import lineGraph from './import/lineGraph';
import scatterPlot from './import/scatterPlot';
import pieChart from './import/pieChart';
import areaChart from './import/areaChart';
import bubbleChart from './import/bubbleChart';

const d3 = Object.assign({},
  require('d3-selection'));

/**
 *  Chart is the generic component used for any chart type
 *  @namespace
 */
  
const Chart = {
  install(Vue) {
    Vue.component('v-chart', {
      props: ['chartData'],
      data() {
        return {
          selector: `${this.chartData.selector}-${this.chartData.chartType}`,
        };
      },
      methods: {
        /**
         * Generate a new Chart of type chartType
         * @memberOf Chart
         */
        initalizeChart() {
          const cs = this[this.chartData.chartType]('init');
          this.drawTitle();
          this.generateAxisLabels(cs);
          this.generateLegend(cs);
        },
        /**
         * Redraw the Chart when the data is recycled
         * @memberOf Chart
         */
        refreshChart() {
          this.clearAxis();
          this[this.chartData.chartType]('refresh');
        },
        /**
         * Redraw the Chart when the data is recycled
         * @memberOf Chart
         */
        drawGrid(cs) {
          if (this.chartData.grid && this.chartData.grid.enabled === true) {
            const grid = {
              x: [],
              y: []
            }
            for (let i = this.header; i < (this.height - this.header) * .80; i += this.gridTicks) {
              grid.y.push(i);
            }
            d3.select(`#${this.chartData.selector}`)
            .selectAll('line.gridLine')
            .data(grid.y).enter()
            .append('line')
            .attr('class', 'gridLine')
            .attr('x1', cs.y.axisWidth)
            .attr('x2', this.width)
            .attr('y1', d => d)
            .attr('y2', d => d)
            .style('stroke', '#D3D3D3')
            .style('stroke-width', 1)
          }
        },
        /**
         * Remove x and y axes
         * @memberOf Chart
         */
        clearAxis() {
          d3.select(`#${this.chartData.selector}`).selectAll('.axis').remove();
        },
        /**
         * Remove all content from the SVG
         * @memberOf Chart
         */
        clearCanvas() {
          d3.select(`#${this.chartData.selector}`).selectAll('*').remove();
        },
        /**
         * Appends title and subtitle to the chart
         * @memberOf Chart
         */
        drawTitle() {
          d3.select(`#${this.chartData.selector}`)
            .append('text')
            .attr('font-size', '20')
            .attr('x', this.width / 2)
            .attr('y', this.titleHeight - this.titleHeight * 0.1)
            .style('text-anchor', 'middle')
            .text(this.chartData.title);

          d3.select(`#${this.chartData.selector}`)
            .append('text')
            .attr('font-size', '12')
            .attr('x', this.width / 2)
            .attr('y', this.titleHeight - this.titleHeight * 0.1 + this.subtitleHeight)
            .style('text-anchor', 'middle')
            .text(this.chartData.subtitle);
        },
        /**
         * Adds a tooltip to the SVG
         * @memberOf Chart
         * @param {Object} d dataset
         * @param {Object} e event x and y coordinates
         */
        addTooltip(d, e) {
          d3.select(`#${this.chartData.selector}`)
            .append('rect')
            .attr('x', e.offsetX - 5 - 50)
            .attr('y', e.offsetY - 13 - 25)
            .attr('height', '16px')
            .attr('width', '80px')
            .attr('class', 'tt')
            .attr('fill', 'white');

          d3.select(`#${this.chartData.selector}`)
            .append('text')
            .attr('x', e.offsetX - 50)
            .attr('y', e.offsetY - 25)
            .attr('class', 'tt')
            .attr('font-size', '10px')
            .text(`${d.dim}:${d.metric}`);
        },
        /**
         * Removes all tooltips from the SVG
         * @memberOf Chart
         * @param {Object} d dataset
         */
        removeTooltip() {
          d3.select(`#${this.chartData.selector}`)
            .selectAll('.tt').remove();
        },
        /**
         * Override default values 
         * @param {Object} cs configuration of the coordinate systems
         * @param {Object} overrides the additional values that can be used for an object
         * @returns {Object} updated configuration of coordinate system 
         */
        setOverrides(cs, overrides) {
          overrides = overrides || {};
          const keys = Object.keys(cs);
          for (const key of keys)
            Object.assign(cs[key], overrides[key]);
          return cs;
        },
        /**
         * Generate legend if option -legends- defined as true
         * @memberOf Chart
         * @param {Object} cs configuration of the coordinate system
         */
        generateLegend(cs) {
          if (this.chartData.legends && this.chartData.legends.enabled === true) {
            cs.palette.lineFill = (Array.isArray(cs.palette.lineFill)) ? cs.palette.lineFill : new Array(cs.palette.lineFill); 
            cs.palette.fill = (Array.isArray(cs.palette.fill)) ? cs.palette.fill : new Array(cs.palette.fill); 
            this.metric.forEach( (e, i) => {
              d3.select(`#${this.chartData.selector}`)
              .append('text')
              .attr('font-size', '10')
              .attr('x', this.width - 60)
              .attr('y', this.height * 0.95 - (i * 15))
              .style('text-anchor', 'middle')
              .text(this.metric[i]);

            d3.select(`#${this.chartData.selector}`)
              .append("g")
              .attr("class", "legends")
              .append("rect")
              .attr('x', this.width - 30)
              .attr('y', this.height * 0.95 - (i * 15) - 10)
              .attr("width", 30)
              .attr("height", 10)
              .style("fill", function () {
              const fill = cs.palette.lineFill[i] || cs.palette.fill[i];
                return fill;
              });
            })
          }
        },
        /**
         * Generate Goal 
         * @memberOf Chart
         * @param {Object} cs configuration of the coordinate system
         */

        generateGoal(cs, shiftAxis, padding) {
          d3.select(`#${this.chartData.selector}`).selectAll('line#goal').remove();
          const x1 = shiftAxis ? cs.y.axisWidth: cs.x.scale(this.goal) + padding;
          const x2 = shiftAxis ? this.width : cs.x.scale(this.goal) + padding;
          const y1 = shiftAxis ? cs.y.scale(this.goal) + padding : this.header;
          const y2 = shiftAxis ? cs.y.scale(this.goal) + padding : this.displayHeight - cs.x.axisHeight;
          
          d3.select(`#${this.chartData.selector}`).append('line')
            .attr('x1', x1)
            .attr('x2', x2)
            .attr('y1', y1)
            .attr('y2', y2)
            .attr('id', 'goal')
            .style('stroke', '#708090')
            .style('stroke-width', 1)
        },
        /**
         * Generate Axis Lables
         * @memberOf Chart
         * @param {Object} cs configuration of the coordinate system 
         */
        generateAxisLabels(cs) {
          let footer = (this.chartData.legends) ? .85 : .95;
          if (!this.chartData.label) return;
          d3.select(`#${this.chartData.selector}`).selectAll('text.axisLabel').remove();
          
          if (cs.x && cs.x.label)
            d3.select(`#${this.chartData.selector}`).append('text')
              .attr('font-size', '10')
              .attr('x', this.width / 2)
              .attr('y', this.height * footer)
              .attr('id', 'xAxisLabel')
              .attr('class', 'axisLabel')
              .style('text-anchor', 'middle')
              .text(cs.x.label)

          if (cs.y && cs.y.label)
            d3.select(`#${this.chartData.selector}`).append('text')
              .attr('font-size', '10')
              .attr('x', 10)
              .attr('y', this.height / 2)
              .attr('id', 'xAxisLabel')
              .attr('class', 'axisLabel')
              .style('text-anchor', 'middle')
              .text(cs.y.label)
              .attr('transform', `rotate(-90,10, ${this.height / 2})`)
        },
        /**
         * get the values of a metric as an array
         * @memberOf Chart
         * @returns {Array} metric values
         */
        metricAsArray(metric) {
          metric = this.chartData.data.map(d => d[metric]);
          return metric; 
        },

        ...((typeof barChart !== 'undefined') && { barChart }),
        ...((typeof vBarChart !== 'undefined') && { vBarChart }),
        ...((typeof scatterPlot !== 'undefined') && { scatterPlot }),
        ...((typeof pieChart !== 'undefined') && { pieChart }),
        ...((typeof areaChart !== 'undefined') && { areaChart }),
        ...((typeof lineGraph !== 'undefined') && { lineGraph }),
        ...((typeof bubbleChart !== 'undefined') && { bubbleChart }),
      },
      computed: {
        /**
         * Dataset getter function
         * @memberOf Chart
         * @returns {Object} normalized dataset
         */
        ds() {
          const ds = { metric: [] };
          ds.metric = (Array.isArray(this.chartData.metric)) ? ds.metric = this.chartData.metric : new Array(this.chartData.metric);
          ds.dim = this.chartData.dim;
          ds.data = this.chartData.data;
          return ds.data.map((d) => {
            const td = { metric: [] };
            if (!ds.metric[0])
              td.metric[0] = d || 0;
            else {
              ds.metric.forEach(function(e, i){
                td.metric[i] = d[e] || 0;
              })
            }
            td.dim = this.chartData.dim ? d[this.chartData.dim] : null;
            return td;
          });
        },
        /**
         * Dimension getter function
         * @memberOf Chart
         * @returns {string} dim 
         */
        dim() {
          return this.chartData.dim || "undefined";
        },
        /**
         * Goal getter function
         * @memberOf Chart
         * @returns {number} Goal 
         */
        goal() {
          return this.chartData.goal;
        },
        /**
         * Metric getter function
         * @memberOf Chart
         * @returns {array} Metrics 
         */
        metric() {
          const metric = (Array.isArray(this.chartData.metric)) ? this.chartData.metric : new Array(this.chartData.metric);
          return metric;
        },
        /**
         * Height getter function
         * @memberOf Chart
         * @returns {number} Chart Height
         */
        height() {
          return this.chartData.height - 10 || 190;
        },
        /**
         * Width getter function
         * @memberOf Chart
         * @returns {number} Chart width
         */
        width() {
          return this.chartData.width - 10 || 190;
        },
        /**
         * Grid Tick getter function
         * @memberOf Chart
         * @returns {number} gridTicks 
         */
        gridTicks() {
          if (this.chartData.grid && this.chartData.grid.gridTicks != null) {
            return this.chartData.grid.gridTicks;
          }
          return 100;
        },
        /**
         * Get the maxium value for metric
         * @memberOf Chart
         * @returns {number} Max value for metric
         */
        max() {
          let max = 0;
          var results = []; 
          this.ds.forEach(e => {
            results = results.concat([...e.metric]);
          });
          results.forEach((e) => {
            max = max > e ? max : e;
          });
          return max;
        },
        /**
         * Get the maxium value for triplet
         * @memberOf Chart
         * @returns {Array} Max values for triplet
         */
        maxTriplet() {
          const max = {
            v1: 0,
            v2: 0,
            v3: 0
          };
          this.ds.forEach(e => {
            max.v1 = max.v1 > e.metric[0] ? max.v1 : e.metric[0];
            max.v2 = max.v2 > e.metric[1] ? max.v2 : e.metric[1];
            max.v3 = max.v3 > e.metric[2] ? max.v3 : e.metric[2];
          });
          return max;
        },
        /**
         * Get the minimum value for dataset
         * @memberOf Chart
         * @returns {number} Min value for metric
         */
        min() {
          var results = []; 
          this.ds.forEach(e => {
            results = results.concat([...e.metric]);
          });
          return Math.min(...results.map(o => o));
        },
        /**
         * Get the minimum value for triplet
         * @memberOf Chart
         * @returns {Array} Min values for triplet
         */
        minTriplet() {
          var results = {
            v1: [],
            v2: [],
            v3: []
          };
          this.ds.forEach(e => {
            results.v1.push(e.metric[0])
            results.v2.push(e.metric[1])
            results.v3.push(e.metric[2])
          })
          return {
            v1: (Math.min(...results.v1.map(o => o))),
            v2: (Math.min(...results.v2.map(o => o))),
            v3: (Math.min(...results.v3.map(o => o)))
          };
        },
        /**
         * Gets the height of the dispaly area
         * @memberOf Chart
         * @returns {number} Height of the chart display
         */
        displayHeight() {
          if (this.chartData.legends && this.chartData.legends.enabled === true) {
            return this.height * .80;
          } else {
            return this.height * .90;
          }
        },
        /**
         * Gets the height of the title 
         * @memberOf Chart
         * @returns {number} Height of the chart title
         */
        titleHeight() {
          if (this.chartData.title) return this.chartData.textHeight || 25;
          return 0;
        },
        /**
         * Gets the subtitle height
         * @memberOf Chart
         * @returns {number} Height of chart subtitle
         */
        subtitleHeight() {
          if (this.chartData.subtitle) return this.chartData.textHeight * 0.66 || 25 * 0.66;
          return 0;
        },
        /**
         * Gets the combined height of the title and subtitle
         * @memberOf Chart
         * @returns {number} Total header height
         */
        header() {
          return (this.titleHeight + this.subtitleHeight);
        },
      },
      mounted() {
        this.initalizeChart();
      },
      watch: {
        chartData: {
          handler() {
            this.refreshChart();
          },
          deep: true,
        },
      },
      template:
        '<svg :id=\'this.chartData.selector\' x=\'5\' y=\'5\' :height=\'this.height + 20\' :width=\'this.width + 20\'> </svg>',
    });
  },
};

export default Chart;

if (typeof window !== 'undefined' && window.Vue) {
  window.Vue.use(Chart);
}