UNPKG

@dboxjs/core

Version:

A library to create easy reusable charts

1,737 lines (1,576 loc) 58.8 kB
import Helper from './helper.js'; import * as d3 from 'd3'; import * as _ from 'lodash'; /** * Dbox Chart core */ /** * Creates a chart type object * @constructor * @param {Object} config - Object with params to set chart size, xAxis and yAxis properties */ export default function Chart(config) { var Chart = {}; // Internet Explorer 6-11 var isIE; try { isIE = /*@cc_on!@*/ false || (document && !!document.documentMode); } catch (err) { isIE = false; } // Edge 20+ var isEdge; try { isEdge = !isIE && window && !!window.StyleMedia; } catch (err) { isEdge = false; } /** * Initialize chart object configuration * @param {Object} config - Object with params to configure the chart (developer's configure) */ Chart.init = function (config) { var vm = this; /** * Set default size and default margins * for chart container (SVG) */ var defaultConfig = { size: { width: 800, height: 600, margin: { left: 0, right: 0, top: 0, bottom: 0, }, }, }; var defaultStyle = { chart: { backgroundColor: { color: 'transparent', // linearGradient : { x1: 0, y1: 0, x2: 1, y2: 1 }, // stops: [ [0, '#FCFCFC'], [1, '#F3F2F2'] ] }, }, title: { textColor: '#E6B537', fontSize: '30px', fontWeight: 600, textAlign: 'center', hr: { enabled: true, borderWidth: '1px', borderColor: '#fff', }, }, legend: { position: 'bottom', figure: 'circle', text: { textColor: '#fff', fontSize: '12px', fontWeight: 600, }, }, tooltip: { backgroundColor: '#757575', opacity: 0.9, text: { textColor: '#fff', fontSize: '12px', fontWeight: 600, textAlign: 'center', fontFamily: 'sans-serif', padding: '0.8em', }, border: { color: '#5A5C5D', radius: '5px', width: '1px', }, }, yAxis: { enabled: true, axis: { strokeWidth: 3, strokeColor: '#5F6C6C', strokeOpacity: 0, paddingTick: 0, }, ticks: { strokeWidth: 3, strokeColor: '#929D9E', grid: 'dashed', gridDashed: '3, 5', opacity: 0.6, }, labels: { fontSize: 12, fontWeight: 600, textColor: '#fff', textAnchor: 'end', }, title: { fontSize: 17, fontWeight: 600, textColor: '#fff', textAnchor: 'middle', // rotation // text align or position }, }, xAxis: { enabled: true, axis: { strokeWidth: 3, strokeColor: '#5F6C6C', strokeOpacity: 1, //??? paddingTick: 5, }, ticks: { strokeWidth: 1, strokeColor: '#5F6C6C', // ticksize }, labels: { fontSize: 12, fontWeight: 400, textColor: '#fff', textAnchor: 'middle', }, title: { fontSize: 17, fontWeight: 600, textColor: '#fff', textAnchor: 'middle', // text align }, }, }; /** * Clone recursively the config object if it has content. * Keep default configuration in other case. */ vm._config = config ? _.cloneDeep(config) : defaultConfig; if (vm._config.xAxis) { vm._config.xAxis.axis = 'xAxis'; /** * @deprecated Allow to use general config if axis-based is not especified */ if ( vm._config.xAxis.decimals === undefined && vm._config.decimals !== undefined ) { vm._config.xAxis.decimals = vm._config.decimals; } if ( vm._config.xAxis.formatPreffix === undefined && vm._config.formatPreffix !== undefined ) { vm._config.xAxis.formatPreffix = vm._config.formatPreffix; } if ( vm._config.xAxis.formatSuffix === undefined && vm._config.formatSuffix !== undefined ) { vm._config.xAxis.formatSuffix = vm._config.formatSuffix; } } if (vm._config.yAxis) { vm._config.yAxis.axis = 'yAxis'; /** * @deprecated Allow to use general config if axis-based is not especified */ if ( vm._config.yAxis.decimals === undefined && vm._config.decimals !== undefined ) { vm._config.yAxis.decimals = vm._config.decimals; } if ( vm._config.yAxis.formatPreffix === undefined && vm._config.formatPreffix !== undefined ) { vm._config.yAxis.formatPreffix = vm._config.formatPreffix; } if ( vm._config.yAxis.formatSuffix === undefined && vm._config.formatSuffix !== undefined ) { vm._config.yAxis.formatSuffix = vm._config.formatSuffix; } } // Initialize data array vm._data = []; // Define margin sizes and styles vm._margin = vm._config.size.margin; vm._addStyle = vm._config.addStyle ? vm._config.addStyle : defaultStyle; // Define width and height vm._width = vm._config.size.width - vm._margin.left - vm._margin.right; vm._height = vm._config.size.height - vm._margin.top - vm._margin.bottom; vm._svg = ''; vm._scales = {}; vm._axes = {}; // Public vm.layers = []; // Helper data/functions for layers to use vm.helper = Helper.bind(vm)(); }; //------------------------ // User /** * User can set config object using this method * @param {Object} config - Objec with params to configure the chart (user's configure) */ Chart.config = function (config) { var vm = this; vm._config = _.cloneDeep(config); return vm; }; /** * User can set size of the chart using this method * @param {object} sizeObj - Size object * @param {number} [sizeObj.height] - Height of the chart * @param {number} [sizeObj.width] - Width of the chart * @param {object} [sizeObj.margin] - Margins of the chart * @param {number} [sizeObj.margin.top] - Margin top of the chart * @param {number} [sizeObj.margin.right] - Margin right of the chart * @param {number} [sizeObj.margin.bottom] - Margin bottom of the chart * @param {number} [sizeObj.margin.left] - Margin left of the chart */ Chart.size = function (sizeObj) { var vm = this; if (sizeObj) { if (sizeObj.margin) { if (!Number.isNaN(+sizeObj.margin.left)) { vm._config.size.margin.left = sizeObj.margin.left; vm._margin.left = sizeObj.margin.left; } if (!Number.isNaN(+sizeObj.margin.right)) { vm._config.size.margin.right = sizeObj.margin.right; vm._margin.right = sizeObj.margin.right; } if (!Number.isNaN(+sizeObj.margin.top)) { vm._config.size.margin.top = sizeObj.margin.top; vm._margin.top = sizeObj.margin.top; } if (!Number.isNaN(+sizeObj.margin.bottom)) { vm._config.size.margin.bottom = sizeObj.margin.bottom; vm._margin.bottom = sizeObj.margin.bottom; } } if (!Number.isNaN(+sizeObj.width)) { vm._config.size.width = sizeObj.width; vm._width = sizeObj.width; } if (!Number.isNaN(+sizeObj.height)) { vm._config.size.height = sizeObj.height; vm._height = sizeObj.height; } } return vm; }; /** * User can set a personalized style object * @param {object} stylesObj */ Chart.addStyle = function (stylesObj) { var vm = this; if (stylesObj) { vm._addStyle = stylesObj; } return vm; }; /** * Set if background grid is enabled and should be drawn * @param {boolean} enabled - Grid status */ Chart.grid = function (enabled) { var vm = this; vm._config.grid = enabled ? true : false; return vm; }; /** * Set selector of HTML node to embed the whole chart * @param {string} selector - HTML node selector */ Chart.bindTo = function (selector) { var vm = this; vm._config.bindTo = selector; return vm; }; /** * Set data to be used in chart * @param {Object[]} data - Data structure to be used */ Chart.data = function (data) { var vm = this; if (vm._config.data !== undefined) { vm._config.data = Object.assign({}, vm._config.data, data); } else { vm._config.data = data; } return vm; }; /** * Set configuration for legend * @param {Object} legend - Legend configuration */ Chart.legend = function (legend) { var vm = this; vm._config.legend = legend; return vm; }; Chart.legendType = function (legendType) { var vm = this; vm._config.legendType = legendType; return vm; }; Chart.legendTitle = function (legendTitle) { var vm = this; vm._config.legendTitle = legendTitle; return vm; }; Chart.layer = function (_layer, _config) { var vm = this; var layer; var config = _config ? _config : vm._config; if (_layer === undefined && _layer === null) { /** @todo Throw Error */ } else { layer = _layer(config, vm.helper); vm.layers.push(layer); return layer; } }; Chart.getLayer = function (layerIndex) { var vm = this; return vm.layers[layerIndex]; }; Chart.draw = function () { var vm = this, q; vm._scales = vm.scales(); // vm._axes = vm.axes(); // CALL THE AXES AFTER DATA LOADING IN ORDER TO UPDATE THE DOMAINS OF THE SCALES q = vm.loadData(); q.awaitAll(function (error, results) { if (error) throw error; if (Array.isArray(results) && results.length === 1) { vm._data = results[0]; } else { vm._data = results; } vm.initLayers(); vm.drawSVG(); /** @todo ONE MAIN AXES THEN ADD THE POSSIBILITY FOR THE LAYER TO OVERRIDE */ vm._axes = vm.axes(); vm.drawAxes(); // Draw layers after axes vm.drawLayers(); // Trigger load chart event if (vm._config.events && vm._config.events.load) { vm.dispatch.on('load.chart', vm._config.events.load(vm)); } }); return vm; }; Chart.addStyle = function (theme) { var vm = this; vm._addStyle = theme; return vm; }; //---------------------- // Helper functions Chart.scales = function () { var vm = this; var scales = {}; // xAxis scale if (vm._config.xAxis && vm._config.xAxis.scale) { switch (vm._config.xAxis.scale) { case 'linear': scales.x = d3.scaleLinear().range([0, vm._width]); break; case 'time': scales.x = d3.scaleTime().range([0, vm._width]); break; case 'ordinal': scales.x = d3.scaleOrdinal().range([0, vm._width], 0.1); break; case 'band': scales.x = d3.scaleBand().rangeRound([0, vm._width]).padding(0.1); break; case 'quantile': scales.x = d3.scaleOrdinal().range([0, vm._width], 0.1); scales.q = d3 .scaleQuantile() .range(d3.range(vm._config.xAxis.buckets)); break; default: scales.x = d3.scaleLinear().range([0, vm._width]); break; } } else { scales.x = d3.scaleLinear().range([0, vm._width]); } // yAxis scale if (vm._config.yAxis && vm._config.yAxis.scale) { switch (vm._config.yAxis.scale) { case 'linear': scales.y = d3.scaleLinear().range([vm._height, 0]); break; case 'time': scales.y = d3.scaleTime().range([vm._height, 0]); break; case 'ordinal': scales.y = d3.scaleOrdinal().range([vm._height, 0], 0.1); break; case 'band': scales.y = d3.scaleBand().rangeRound([vm._height, 0]).padding(0.1); break; case 'quantile': scales.y = d3.scaleOrdinal().range([0, vm._width], 0.1); scales.q = d3 .scaleQuantile() .range(d3.range(vm._config.yAxis.buckets)); break; default: scales.y = d3.scaleLinear().range([vm._height, 0]); break; } } else { scales.y = d3.scaleLinear().range([vm._height, 0]); } scales.color = d3.scaleOrdinal(d3.schemeCategory10); if (vm._config.legend && vm._config.legend.length > 0) { scales.color.domain(vm._config.legend.map((o) => o.name)); } return scales; }; Chart.setScales = function () {}; Chart.axes = function () { var vm = this, axes = {}; axes.x = d3.axisBottom(vm._scales.x); axes.y = d3.axisLeft(vm._scales.y); // Remove corners in axis line axes.x.tickSizeOuter(0); axes.y.tickSizeOuter(0); // Replaced with *addStyle -check if ( vm._config.xAxis && vm._config.xAxis.ticks && vm._config.xAxis.ticks.enabled === true && vm._config.xAxis.ticks.style ) { switch (vm._config.xAxis.ticks.style) { case 'straightLine': axes.x.tickSize(-vm._height, 0); break; case 'dashLine': axes.x.tickSize(-vm._width, 0); break; } } // addStyle if (vm._addStyle.xAxis.ticks.grid) { switch (vm._addStyle.xAxis.ticks.grid) { case 'straight': axes.y.tickSize(-vm._width, 0); break; case 'dashed': axes.y.tickSize(-vm._width, 0); break; } } if ( vm._config.xAxis && vm._config.xAxis.ticks && vm._config.xAxis.ticks.ticks ) { axes.x.ticks(vm._config.xAxis.ticks.ticks); } if ( vm._config.xAxis && vm._config.xAxis.ticks && vm._config.xAxis.ticks.values ) { axes.x.tickValues(vm._config.xAxis.ticks.values); } if ( vm._config.xAxis && vm._config.xAxis.ticks && vm._config.xAxis.scale === 'linear' ) { vm._scales.x.domain()[1]; axes.x.tickFormat(vm.helper.utils.format(vm._config.xAxis, true)); } if ( vm._config.xAxis && vm._config.xAxis.ticks && vm._config.xAxis.ticks.format ) { axes.x.tickFormat(vm._config.xAxis.ticks.format); } // Replaced with *addStyle -check if ( vm._config.yAxis && vm._config.yAxis.ticks && vm._config.yAxis.ticks.enabled === true && vm._config.yAxis.ticks.style ) { switch (vm._config.yAxis.ticks.style) { case 'straightLine': axes.y.tickSize(-vm._width, 0); break; case 'dashLine': axes.y.tickSize(-vm._width, 0); break; } } // addStyle if (vm._addStyle.yAxis.ticks.grid) { switch (vm._addStyle.yAxis.ticks.grid) { case 'straight': axes.y.tickSize(-vm._width, 0); break; case 'dashed': axes.y.tickSize(-vm._width, 0); break; } } if ( vm._config.yAxis && vm._config.yAxis.ticks && vm._config.yAxis.scale === 'linear' ) { axes.y.tickFormat(vm.helper.utils.format(vm._config.yAxis, true)); } if ( vm._config.yAxis && vm._config.yAxis.ticks && vm._config.yAxis.ticks.format ) { axes.y.tickFormat(vm._config.yAxis.ticks.format); } if ( vm._config.yAxis && vm._config.yAxis.ticks && vm._config.yAxis.ticks.ticks ) { axes.y.ticks(vm._config.yAxis.ticks.ticks); } return axes; }; Chart.loadData = function () { var vm = this; var q; if (vm._config.data.tsv) { q = d3.queue().defer(d3.tsv, vm._config.data.tsv); } if (vm._config.data.json) { q = d3.queue().defer(d3.json, vm._config.data.json); } if (vm._config.data.csv) { q = d3.queue().defer(d3.csv, vm._config.data.csv); } if (vm._config.data.raw) { q = d3.queue().defer(vm.mapData, vm._config.data.raw); } if ( vm._config.map && vm._config.map.topojson && vm._config.map.topojson.url ) { q.defer(d3.json, vm._config.map.topojson.url); } return q; }; Chart.initLayers = function () { var vm = this; vm.layers.forEach(function (ly) { ly.data(vm._data).scales(); /** @todo validate domains from multiple layers */ vm._scales = ly._scales; }); }; Chart.drawSVG = function () { var vm = this; // Remove any previous svg d3.select(vm._config.bindTo).select('svg').remove(); d3.select(vm._config.bindTo).html(''); // Add the css template class if (vm._config.template) { d3.select(vm._config.bindTo).classed(vm._config.template, true); } // Add title to the chart if (vm._config && vm._config.title) { d3.select(vm._config.bindTo) .append('div') .attr('class', 'chart-title') .html(vm._config.title) .style('display', 'flex') .style('justify-content', 'center') .style('align-items', 'center') .style('font-size', vm._addStyle.title.fontSize) .style('font-weight', vm._addStyle.title.fontWeight) .style('color', vm._addStyle.title.textColor) .style('text-align', vm._addStyle.title.textAlign); if (vm._addStyle.title.hr.enabled) { d3.select(vm._config.bindTo) .append('hr') .attr('class', 'hr-title') .style('width', '80%') .style('margin-left', '10%') .style('margin-top', '0.5em') .style('border-width', vm._addStyle.title.hr.borderWidth) .style('border-color', vm._addStyle.title.hr.borderColor); } } // Add Legend to the chart /** @todo PASS THE STYLES TO DBOX.CSS */ /** @todo ALLOW DIFFERENT POSSITIONS FOR THE LEGEND */ if ( vm._config.legend && vm._config.legend.enabled === true && vm._config.legend.position === 'top' ) { var legend = d3 .select(vm._config.bindTo) .append('div') .attr('class', 'chart-legend-top'); var html = ''; html += '<div style="background-color:#E2E2E1;text-align:center;height: 40px;margin: 0px 15px">'; vm._config.legend.categories.forEach(function (c) { html += '<div class="dbox-legend-category-title" style="margin:0 20px;"><span class="dbox-legend-category-color" style="background-color:' + c.color + ';"> </span><span style="height: 10px;float: left;margin: 10px 5px 5px 5px;border-radius: 50%;">' + c.title + '</span></div>'; }); html += '</div>'; legend.html(html); } const width = vm._width + vm._margin.left + vm._margin.right; const height = vm._height + vm._margin.top + vm._margin.bottom; // Create the svg vm._fullSvg = d3 .select(vm._config.bindTo) .append('svg') .style( 'font-size', vm._config.chart ? vm._config.chart['font-size'] ? vm._config.chart['font-size'] : '12px' : '12px' ) .attr('width', width) .attr('height', height) .attr('viewBox', `0 0 ${width} ${height}`); vm._svg = vm._fullSvg .append('g') .attr( 'transform', 'translate(' + vm._margin.left + ',' + vm._margin.top + ')' ); //Apply background color d3.select(vm._config.bindTo + ' svg').style( 'background-color', vm._addStyle.chart.backgroundColor.color ); // Legend for average lines /* d3.select(vm._config.bindTo).append('div') .attr('class', 'chart-legend-bottom'); if (vm._config.plotOptions && vm._config.plotOptions.bars && vm._config.plotOptions.bars.averageLines && Array.isArray(vm._config.plotOptions.bars.averageLines) && vm._config.plotOptions.bars.averageLines.length >0 ){ d3.select(vm._config.bindTo).append('div') .attr('class', 'container-average-lines') .append('div') .attr('class', 'legend-average-lines') .html('Average Lines Controller') } */ if ( vm._config.hasOwnProperty('legend') && vm._config.legendEnabled === true ) { vm.drawLegend(); } }; Chart.drawLayers = function () { var vm = this; vm.layers.forEach(function (ly) { ly.draw(); }); }; /** * Draw chart legends section */ Chart.drawLegend = function () { // Reference to chart object var vm = this, legendBox, virtualScroller, legendX = vm._config.size.width - vm._config.size.margin.right + 10, marginTop = vm._config.size.margin.top + 10; // Set color domain if (!vm._scales.color && vm._config.colors) { vm._scales.color = d3.scaleOrdinal(vm._config.colors); } if (vm._config.legend && vm._config.legend.length > 0) { vm._scales.color.domain(vm._config.legend.map((o) => o.name)); } // Create information tips for each legend const legendTip = vm.helper.utils.d3.tip().html((d) => { return '<div class="title-tip">' + (d.name || d) + '</div>'; }); vm._fullSvg.call(legendTip); // Draw legend, defaults to right legendBox = vm._fullSvg .append('g') .attr('class', 'legendBox') .attr('transform', 'translate(' + legendX + ', ' + marginTop + ')') .attr('width', vm._width) .attr('height', vm._height); // Add legends title (on top of legendBox) if (vm._config.legendTitle) { legendBox .append('g') .attr('width', '150px') .append('text') .attr('class', 'legend-title') .attr('x', 5) .style('font-weight', 'bold') .text(vm._config.legendTitle); // Wrap legend title if text size exceeds 70% of container let lWidth = vm._config.size.margin.right; var text = d3.selectAll('.legend-title'), words = text.text().split(/\s+/).reverse(), word, line = [], lineNumber = 1, lineHeight = 1.1, // ems y = text.attr('y'), dy = 0, tspan = text .text(null) .append('tspan') .attr('x', 0) .attr('y', y) .attr('dy', dy + 'em'); while ((word = words.pop())) { line.push(word); tspan.text(line.join(' ')); if (tspan.node().getComputedTextLength() > lWidth) { line.pop(); tspan.text(line.join(' ')); line = [word]; ++lineNumber; tspan = text .append('tspan') .attr('x', -6) .attr('y', y) .attr('dy', lineHeight + 'em') .text(word); } } } // Label width let lbWidth = vm._config.size.margin.right * 0.8; if (vm._config.legendType === 'checkbox') { // Size and position of every checkbox let size = 18, x = 0, y = 0, rx = 2, ry = 2, markStrokeWidth = 3; vm._scales.color.domain(vm._config.legend.map((o) => o.name)); var legendEnter = function (legendCheck) { legendCheck .append('rect') .attr('width', size) .attr('height', size) .attr('x', x) .attr('rx', rx) .attr('ry', ry) .attr('fill-opacity', 1); let coordinates = [ { x: x + size / 8, y: y + size / 2, }, { x: x + size / 2.2, y: y + size - size / 4, }, { x: x + size - size / 8, y: y + size / 10, }, ]; let line = d3 .line() .x(function (d) { return d.x; }) .y(function (d) { return d.y; }); // Mark (inside checkbox) legendCheck .append('path') .attr('d', line(coordinates)) .attr('stroke-width', markStrokeWidth) .attr('stroke', 'white') .attr('fill', 'none') .attr('class', 'mark') .property('checked', function (d) { // External function call. It must be after all the internal code; allowing the user to overide return d.active; }) .attr('opacity', function () { if (d3.select(this).property('checked')) { return 1; } else { return 0; } }); legendCheck .append('text') .attr('class', 'labelText') .attr('x', 20) .attr('y', 9) .attr('dy', '.35em') .attr('text-anchor', 'start'); legendCheck.select('rect').attr('fill', function (d) { return vm._scales.color(d.name); }); legendCheck.select('text').text(function (d) { // External function call. It must be after all the internal code; allowing the user to overide // External function call. It must be after all the internal code; allowing the user to overide // External function call. It must be after all the internal code; allowing the user to overide return d.name; }); // Cut label text if text size exceeds 80% of container legendCheck.selectAll('text').each(function (d) { if (typeof d.name === 'string') { // getComputedTextLenght: Returns a float representing the computed length for the text within the element. if (this.getComputedTextLength() > lbWidth) { d3.select(this) .attr('title', d.name) .on('mouseover', legendTip.show) .on('mouseout', legendTip.hide); let i = 1; while (this.getComputedTextLength() > lbWidth) { d3.select(this).text(function (d) { return d['name'].slice(0, -i) + '...'; }); ++i; } } else { return d; } } }); }; // legendEnter ends // Let scroll legendBox from anywhere inside g container legendBox .append('rect') .attr('class', 'legendBoxBackground') .attr('width', '160px') .attr('height', '90%') .attr('transform', 'translate(0,0)') .style('fill', 'transparent'); var legendUpdate = function (legendCheck) { legendCheck.select('rect').attr('fill', function (d) { return vm._scales.color(d.name); }); legendCheck.select('text').text(function (d) { // External function call. It must be after all the internal code; allowing the user to overide // External function call. It must be after all the internal code; allowing the user to overide // External function call. It must be after all the internal code; allowing the user to overide return d.name; }); legendCheck.selectAll('text').each(function (d) { if (typeof d.name === 'string') { // getComputedTextLenght: Returns a float representing the computed length for the text within the element. if (this.getComputedTextLength() > lbWidth) { d3.select(this) .attr('title', d.name) .on('mouseover', legendTip.show) .on('mouseout', legendTip.hide); let i = 1; while (this.getComputedTextLength() > lbWidth) { d3.select(this).text(function (d) { return d['name'].slice(0, -i) + '...'; }); ++i; } } else { return d; } } }); }; var legendExit = function (legendCheck) {}; virtualScroller = vm.helper.utils .VirtualScroller() .rowHeight(size) .enter(legendEnter) .update(legendUpdate) .exit(legendExit) .svg(vm._fullSvg) .totalRows(vm._config.legend.length) .viewport(d3.select('.empty-chart')) .lineNumber(lineNumber); virtualScroller.data(vm._config.legend, function (d) { return d.name; }); legendBox.call(virtualScroller); // End of checkbox case } else { var legend; if (vm._config.styles && vm._addStyle.legend.position === 'bottom') { legend = legendBox .selectAll('.legend') .data(vm._config.legend) .enter() .append('g') .attr('class', 'legend') .attr('width', vm._width / (vm._config.legend.length + 1)) .attr('transform', function (d, i) { // Horizontal position // What if there are too many legends? return ( 'translate(' + (vm._width / (vm._config.legend.length + 1)) * i + ',' + (vm._config.legendTitle && lineNumber > 1 ? lineNumber * lineHeight : 0) * 19 + ')' ); }); } else { legend = legendBox .selectAll('.legend') .data(vm._config.legend) .enter() .append('g') .attr('class', 'legend') .attr('transform', function (d, i) { return ( 'translate(' + 5 + ',' + (vm._config.legendTitle && lineNumber > 1 ? lineNumber * lineHeight + i : 1 + i) * 19 + ')' ); }); } if (vm._addStyle.legend.figure === 'circle') { legend .append('circle') .attr('cx', 2) .attr('cy', 9) .attr('r', 7) .attr('stroke-width', 2) .attr('stroke', function (d) { return vm._scales.color(d.name); }) .attr('fill', function (d) { return vm._scales.color(d.name); }) .attr('fill-opacity', 0.8); } else { legend .append('rect') .attr('x', 0) .attr('width', 18) .attr('height', 18) .attr('fill', function (d) { return vm._scales.color(d.name); }); } legend .append('text') .attr('x', 20) .attr('y', 9) .attr('dy', '.35em') .attr('text-anchor', 'start') .text(function (d) { // External function call. It must be after all the internal code; allowing the user to overide return d.name; }); // Cut label text if text size exceeds 80% of container let lbWidth = vm._config.size.margin.right - 19; legend.selectAll('text').each(function (d) { if (typeof d.name === 'string') { if (this.getComputedTextLength() > lbWidth) { d3.select(this) .attr('title', d.name) .on('mouseover', legendTip.show) .on('mouseout', legendTip.hide); let i = 1; while (this.getComputedTextLength() > lbWidth) { d3.select(this).text(function (d) { return d['name'].slice(0, -i) + '...'; }); ++i; } } else { return d; } } }); } /** * Give some extra style */ legendBox .selectAll('.legend text') .attr('fill', vm._addStyle.legend.text.textColor) .attr('font-size', vm._addStyle.legend.text.fontSize) .attr('font-weight', vm._addStyle.legend.text.fontWeight); // Prevent default scrolling of all elements inside legendBox let isFirefox = typeof InstallTrigger !== 'undefined'; let support = 'onwheel' in d3.select('.legendBox') ? 'wheel' // Modern browsers support 'wheel' : document.onmousewheel !== undefined ? 'mousewheel' // Webkit and IE support at least 'mousewheel' : 'DOMMouseScroll'; // let's assume that remaining browsers are older Firefox if (isFirefox) { // Newer Firefox versions support wheel events. // DOMMouseScroll is not supported yet. support = 'wheel'; } d3.select('.legendBoxBackground').on(support, function () { var evt = d3.event; evt.preventDefault(); }); d3.select('.scroll-legend').on(support, function () { var evt = d3.event; evt.preventDefault(); }); legendBox.selectAll('text').on(support, function () { var evt = d3.event; evt.preventDefault(); }); legendBox.selectAll('rect').on(support, function () { var evt = d3.event; evt.preventDefault(); }); legendBox.selectAll('path').on(support, function () { var evt = d3.event; evt.preventDefault(); }); }; Chart.drawGrid = function () { var vm = this; return vm; }; Chart.drawAxes = function () { var vm = this; var yAxis; /** * Draw axes depends on axes config and styles. * Let's start with config. */ //Axes tooltip const axesTip = vm.helper.utils.d3.tip().html((d) => { return '<div class="title-tip">' + d + '</div>'; }); if ( (!vm._config.yAxis || (vm._config.yAxis && vm._config.yAxis.enabled !== false)) && (!vm._config.xAxis || (vm._config.xAxis && vm._config.xAxis.enabled !== false)) ) { vm._svg.call(axesTip); } /** * Config axes */ // y if ( !vm._config.yAxis || (vm._config.yAxis && vm._config.yAxis.enabled !== false) ) { yAxis = vm._svg.append('g').attr('class', 'y axis').call(vm._axes.y); if (vm._config.yAxis && vm._config.yAxis.text) { yAxis .append('text') .attr('class', 'axis-title') .attr('y', -vm._margin.left * 0.7) .attr('x', -vm._height / 2) .attr('transform', 'rotate(-90)') .attr('text-anchor', 'middle') .text(vm._config.yAxis.text); } if (vm._config.yAxis && vm._config.yAxis.scaleText) { yAxis .append('text') .attr('class', 'axis-scale-title') .attr('y', -12) .attr('x', -12) .attr('font-weight', 'bold') .attr('text-anchor', 'middle') .text(vm._config.yAxis.scaleText); } } if ( vm._config.yAxis.domain && vm._config.yAxis.domain.hasOwnProperty('enabled') ) { if (vm._config.yAxis.ticks) { if ( vm._config.yAxis.domain.enabled === false && vm._config.yAxis.ticks.enabled === false ) { d3.select('g.y.axis .domain').remove(); d3.selectAll('g.y.axis .tick').remove(); } else if ( vm._config.yAxis.domain.enabled === false && vm._config.yAxis.ticks.enabled === true ) { d3.select('g.y.axis .domain').remove(); // d3.selectAll('g.y.axis .tick text').remove(); } } else { if (vm._config.yAxis.domain.enabled === false) { d3.select('g.y.axis .domain').remove(); d3.selectAll('g.y.axis .tick').remove(); } } } if ( !vm._config.yAxis || (vm._config.yAxis && vm._config.yAxis.enabled !== false) ) { // Add ellipsis to cut label text when too long // for yAxis on the left yAxis.selectAll('text').each(function (d) { let element = this; if (typeof d === 'string') { if (this.getComputedTextLength() > 0.8 * vm._margin.left) { d3.select(element) .on('mouseover', axesTip.show) .on('mouseout', axesTip.hide); let i = 1; while (this.getComputedTextLength() > 0.8 * vm._margin.left) { d3.select(this) .text(function (d) { return d.slice(0, -i) + '...'; }) .attr('title', d); ++i; } } else { return d; } } }); } // Dropdown Y axis if ( vm._config.yAxis && vm._config.yAxis.dropdown && vm._config.yAxis.dropdown.enabled === true ) { var yAxisDropDown = d3 .select(vm._config.bindTo) .append('div') .attr('class', 'dbox-yAxis-select') .append('select') .on('change', function () { vm.updateAxis('y', this.value); }); /* .attr('style', function(){ var x = -1*d3.select(vm._config.bindTo).node().getBoundingClientRect().width/2+ vm._chart._margin.left/4; var y = -1*d3.select(vm._config.bindTo).node().getBoundingClientRect().height/2; return 'transform: translate('+x+'px,'+y+'px) rotate(-90deg);' }) */ var x = (-1 * d3.select(vm._config.bindTo).node().getBoundingClientRect().width) / 2 + vm._margin.left / 2.5; var y = (-1 * d3.select(vm._config.bindTo).node().getBoundingClientRect().height) / 1.5; if (vm._config.yAxis.dropdown.styles) { var styles = vm._config.yAxis.dropdown.styles; styles.display = 'block'; styles.transform = 'translate(' + x + 'px,' + y + 'px) rotate(-90deg)'; styles.margin = 'auto'; styles['text-align'] = 'center'; styles['text-align-last'] = 'center'; d3.select('.dbox-yAxis-select select').styles(styles); d3.select('.dbox-yAxis-select select option').styles({ 'text-align': 'left', }); } else { d3.select('.dbox-yAxis-select select').styles({ display: 'block', transform: 'translate(' + x + 'px,' + y + 'px) rotate(-90deg)', margin: 'auto', 'text-align': 'center', 'text-align-last': 'center', }); d3.select('.dbox-yAxis-select select option').styles({ 'text-align': 'left', }); } if (vm._config.yAxis.dropdown.options) { yAxisDropDown .selectAll('option') .data(vm._config.yAxis.dropdown.options) .enter() .append('option') .attr('class', function (d) { return d.value; }) .attr('value', function (d) { return d.value; }) .text(function (d) { return d.title; }) .property('selected', function (d) { return d.selected; }); } else { console.log('No options present in config'); } } // y ///////////////////////////// // x if ( !vm._config.xAxis || (vm._config.xAxis && vm._config.xAxis.enabled !== false) ) { vm._xAxis = vm._svg .append('g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + vm._height + ')') .call(vm._axes.x); // Move axis domain line if (vm._config.yAxis.scale === 'linear' && vm._scales.y.domain()[0] < 0) { vm._xAxis .select('.domain') .attr( 'transform', 'translate(0,-' + Math.abs(vm._scales.y.range()[0] - vm._scales.y(0)) + ')' ); } // Do not show line if axis is disabled if (vm._config.xAxis.line && vm._config.xAxis.line.enabled === false) { vm._xAxis.selectAll('path').style('display', 'none'); } // Set custom position for ticks if (vm._config.xAxis.ticks && vm._config.xAxis.ticks.x) { vm._xAxis.selectAll('text').attr('dx', vm._config.xAxis.ticks.x); } if (vm._config.xAxis.ticks && vm._config.xAxis.ticks.y) { vm._xAxis.selectAll('text').attr('dy', vm._config.xAxis.ticks.y); } // Disable ticks when set to false if ( vm._config.xAxis.ticks && vm._config.xAxis.ticks.line && vm._config.xAxis.ticks.line.enabled === false ) { vm._xAxis.selectAll('line').style('display', 'none'); } } if (vm._config.xAxis && vm._config.xAxis.text) { vm._svg .select('.x.axis') .append('text') .attr('class', 'axis-title') .attr('x', vm._width / 2) .attr('y', 40) .style('text-anchor', 'middle') .text(vm._config.xAxis.text); } if (vm._config.xAxis && vm._config.xAxis.scaleText && vm._xAxis) { vm._xAxis .append('text') .attr('class', 'axis-scale-title') .attr('x', vm._width) .attr('y', 15) .style('text-anchor', 'start') .text(vm._config.xAxis.scaleText); } if ( vm._config.xAxis.domain !== undefined && vm._config.xAxis.domain.hasOwnProperty('enabled') ) { if (vm._config.xAxis.ticks) { if ( vm._config.xAxis.domain.enabled === false && vm._config.xAxis.ticks.enabled === false ) { d3.select(vm._config.bindTo + ' g.x.axis .domain').remove(); d3.selectAll('g.x.axis .tick').remove(); } else if ( vm._config.xAxis.domain.enabled === false && vm._config.xAxis.ticks.enabled === true ) { d3.select(vm._config.bindTo + ' g.x.axis .domain').remove(); // d3.selectAll('g.x.axis .tick text').remove(); } } else { if (vm._config.xAxis.domain.enabled === false) { d3.select(vm._config.bindTo + ' g.x.axis .domain').remove(); d3.selectAll(vm._config.bindTo + ' g.x.axis .tick').remove(); } } } // Dropdown X axis if ( vm._config.xAxis && vm._config.xAxis.dropdown && vm._config.xAxis.dropdown.enabled === true ) { var xAxisDropDown = d3 .select(vm._config.bindTo) .append('div') .attr('class', 'dbox-xAxis-select') .append('select') .on('change', function () { vm.updateAxis('x', this.value); }); if (vm._config.xAxis.dropdown.styles) { var styles = vm._config.xAxis.dropdown.styles; styles.display = 'block'; styles.margin = 'auto'; styles['text-align'] = 'center'; styles['text-align-last'] = 'center'; d3.select('.dbox-xAxis-select select').styles(styles); d3.select('.dbox-xAxis-select select option').styles({ 'text-align': 'left', }); } else { d3.select('.dbox-xAxis-select select').styles({ display: 'block', margin: 'auto', 'text-align': 'center', 'text-align-last': 'center', }); d3.select('.dbox-xAxis-select select option').styles({ 'text-align': 'left', }); } if (vm._config.xAxis.dropdown.options) { xAxisDropDown .selectAll('option') .data(vm._config.xAxis.dropdown.options) .enter() .append('option') .attr('class', function (d) { return d.value; }) .attr('value', function (d) { return d.value; }) .text(function (d) { return d.title; }) .property('selected', function (d) { return d.selected; }); } else { console.log('No options present in config'); } } /** * Let's style axes */ // Style Y axis if (vm._config.yAxis.enabled !== false && vm._addStyle.yAxis.enabled) { // Axis line yAxis .selectAll('.domain') .attr('stroke-linecap', 'round') .attr('stroke-width', vm._addStyle.yAxis.axis.strokeWidth) .attr('stroke', vm._addStyle.yAxis.axis.strokeColor) .attr('opacity', vm._addStyle.yAxis.axis.strokeOpacity); //axis title yAxis .selectAll('.axis-title') .attr('font-size', vm._addStyle.yAxis.title.fontSize) .attr('font-weight', vm._addStyle.yAxis.title.fontWeight) .attr('fill', vm._addStyle.yAxis.title.textColor) .attr('text-anchor', vm._addStyle.yAxis.title.textAnchor); // Tick lines yAxis .selectAll('.tick line') .attr('stroke-width', vm._addStyle.yAxis.ticks.strokeWidth) .attr('stroke', vm._addStyle.yAxis.ticks.strokeColor) .attr('stroke-opacity', vm._addStyle.yAxis.ticks.opacity) .attr('width', vm._addStyle.yAxis.ticks.tickWidth) // Condition gridline .attr('stroke-dasharray', vm._addStyle.yAxis.ticks.gridDashed) .attr( 'transform', 'translate(-' + vm._addStyle.yAxis.axis.paddingTick + ', 0)' ); // Don't draw first tick when styled as grid if (vm._addStyle.yAxis.ticks.grid) { yAxis .selectAll('.tick:first-of-type line:first-of-type') .attr('stroke', 'none'); } // Tick text yAxis .selectAll('.tick text') .attr('font-size', vm._addStyle.yAxis.labels.fontSize) .attr('font-weight', vm._addStyle.yAxis.labels.fontWeight) .attr('fill', vm._addStyle.yAxis.labels.textColor) .attr('text-anchor', vm._addStyle.yAxis.labels.textAnchor) .attr( 'transform', 'translate(-' + vm._addStyle.yAxis.axis.paddingTick + ', 0)' ); } // Style X axis if (vm._config.xAxis.enabled !== false && vm._addStyle.xAxis.enabled) { // Axis line vm._xAxis .selectAll('.domain') .attr('stroke-linecap', 'round') .attr('stroke-width', vm._addStyle.xAxis.axis.strokeWidth) .attr('stroke', vm._addStyle.xAxis.axis.strokeColor) .attr('opacity', vm._addStyle.xAxis.axis.strokeOpacity); // Tick lines vm._xAxis .selectAll('.tick line') .attr('stroke-width', vm._addStyle.xAxis.ticks.strokeWidth) .attr('stroke', vm._addStyle.xAxis.ticks.strokeColor) .attr( 'transform', 'translate(0, ' + vm._addStyle.xAxis.axis.paddingTick + ')' ); // Don't draw first tick when styled as grid if (vm._addStyle.xAxis.ticks.grid) { vm._xAxis .selectAll('.tick:first-of-type line:first-of-type') .attr('stroke', 'none'); } // Tick text vm._xAxis .selectAll('.tick text') .attr('font-size', vm._addStyle.xAxis.labels.fontSize) .attr('font-weight', vm._addStyle.xAxis.labels.fontWeight) .attr('fill', vm._addStyle.xAxis.labels.textColor) .attr('text-anchor', vm._addStyle.xAxis.labels.textAnchor) .attr( 'transform', vm._addStyle.xAxis.labels.rotate ? 'translate(0,55) rotate(' + vm._addStyle.xAxis.axis.labels.rotate + ')' : 'translate(0, ' + vm._addStyle.xAxis.axis.paddingTick + ')' ); } const biggestLabelWidth = d3.max( d3 .select('.x.axis') .selectAll('text') .nodes() .map((o) => o.getComputedTextLength()) ); if ( !vm._config.xAxis || (vm._config.xAxis && vm._config.xAxis.enabled !== false) ) { // Add ellipsis to cut label text // when it is too long // Biggest label computed text length let xBandWidth = (vm._scales.x.bandwidth ? vm._scales.x.bandwidth() : (vm._config.size.width - (vm._config.size.margin.left + vm._config.size.margin.right)) / vm._scales.x.ticks()) - 5; if (biggestLabelWidth > xBandWidth) { vm._addStyle.xAxis.labels.rotate = true; // Biggest label doesn't fit vm._xAxis.selectAll('text').each(function (d) { if (typeof d === 'string') { // Vertical labels d3.select(this) .attr('text-anchor', 'end') .attr('dy', 0) .attr('transform', 'translate(-6,12)rotate(-90)'); // Still doesn't fit! if ( this.getComputedTextLength() > 0.8 * vm._config.size.margin.bottom ) { d3.select(this) .on('mouseover', axesTip.show) .on('mouseout', axesTip.hide); let i = 1; while ( this.getComputedTextLength() > 0.8 * vm._config.size.margin.bottom ) { d3.select(this) .text(function (d) { return d.slice(0, -i) + '...'; }) .attr('title', d); ++i; } } else { return d; } } }); } } if (vm._config.xAxis.enabled !== false && vm._addStyle.xAxis.enabled) { // Axis title vm._xAxis .selectAll('.axis-title') .attr('font-size', vm._addStyle.xAxis.title.fontSize) .attr('font-weight', vm._addStyle.xAxis.title.fontWeight) .attr('fill', vm._addStyle.xAxis.title.textColor) .attr('text-anchor', vm._addStyle.xAxis.title.textAnchor) .attr( 'transform', 'translate(0, ' + (vm._addStyle.xAxis.labels.rotate ? d3.min([vm._config.size.margin.bottom * 0.7, biggestLabelWidth]) : vm._addStyle.xAxis.axis.paddingTick) + ')' ); } /** * Already replaced with addStyle */ /*if (vm._config.yAxis && vm._config.yAxis.enabled !== false) { if (vm._config.yAxis && vm._config.yAxis.text) { yAxis.append('text') .att