UNPKG

dygraphs

Version:

dygraphs is a fast, flexible open source JavaScript charting library.

310 lines (266 loc) 9.6 kB
/** * @license * Copyright 2012 Dan Vanderkam (danvdk@gmail.com) * MIT-licenced: https://opensource.org/licenses/MIT */ /*global Dygraph:false */ 'use strict'; /* Bits of jankiness: - Direct layout access - Direct area access - Should include calculation of ticks, not just the drawing. Options left to make axis-friendly. ('drawAxesAtZero') ('xAxisHeight') */ import * as utils from '../dygraph-utils'; /** * Draws the axes. This includes the labels on the x- and y-axes, as well * as the tick marks on the axes. * It does _not_ draw the grid lines which span the entire chart. */ var axes = function() { this.xlabels_ = []; this.ylabels_ = []; }; axes.prototype.toString = function() { return 'Axes Plugin'; }; axes.prototype.activate = function(g) { return { layout: this.layout, clearChart: this.clearChart, willDrawChart: this.willDrawChart }; }; axes.prototype.layout = function(e) { var g = e.dygraph; if (g.getOptionForAxis('drawAxis', 'y')) { var w = g.getOptionForAxis('axisLabelWidth', 'y') + 2 * g.getOptionForAxis('axisTickSize', 'y'); e.reserveSpaceLeft(w); } if (g.getOptionForAxis('drawAxis', 'x')) { var h; // NOTE: I think this is probably broken now, since g.getOption() now // hits the dictionary. (That is, g.getOption('xAxisHeight') now always // has a value.) if (g.getOption('xAxisHeight')) { h = g.getOption('xAxisHeight'); } else { h = g.getOptionForAxis('axisLabelFontSize', 'x') + 2 * g.getOptionForAxis('axisTickSize', 'x'); } e.reserveSpaceBottom(h); } if (g.numAxes() == 2) { if (g.getOptionForAxis('drawAxis', 'y2')) { var w = g.getOptionForAxis('axisLabelWidth', 'y2') + 2 * g.getOptionForAxis('axisTickSize', 'y2'); e.reserveSpaceRight(w); } } else if (g.numAxes() > 2) { g.error('Only two y-axes are supported at this time. (Trying ' + 'to use ' + g.numAxes() + ')'); } }; axes.prototype.detachLabels = function() { function removeArray(ary) { for (var i = 0; i < ary.length; i++) { var el = ary[i]; if (el.parentNode) el.parentNode.removeChild(el); } } removeArray(this.xlabels_); removeArray(this.ylabels_); this.xlabels_ = []; this.ylabels_ = []; }; axes.prototype.clearChart = function(e) { this.detachLabels(); }; axes.prototype.willDrawChart = function(e) { var g = e.dygraph; if (!g.getOptionForAxis('drawAxis', 'x') && !g.getOptionForAxis('drawAxis', 'y') && !g.getOptionForAxis('drawAxis', 'y2')) { return; } // Round pixels to half-integer boundaries for crisper drawing. function halfUp(x) { return Math.round(x) + 0.5; } function halfDown(y){ return Math.round(y) - 0.5; } var context = e.drawingContext; var containerDiv = e.canvas.parentNode; var canvasWidth = g.width_; // e.canvas.width is affected by pixel ratio. var canvasHeight = g.height_; var label, x, y, tick, i; var makeLabelStyle = function(axis) { return { position: 'absolute', fontSize: g.getOptionForAxis('axisLabelFontSize', axis) + 'px', width: g.getOptionForAxis('axisLabelWidth', axis) + 'px', }; }; var labelStyles = { x: makeLabelStyle('x'), y: makeLabelStyle('y'), y2: makeLabelStyle('y2') }; var makeDiv = function(txt, axis, prec_axis) { /* * This seems to be called with the following three sets of axis/prec_axis: * x: undefined * y: y1 * y: y2 */ var div = document.createElement('div'); var labelStyle = labelStyles[prec_axis == 'y2' ? 'y2' : axis]; utils.update(div.style, labelStyle); // TODO: combine outer & inner divs var inner_div = document.createElement('div'); inner_div.className = 'dygraph-axis-label' + ' dygraph-axis-label-' + axis + (prec_axis ? ' dygraph-axis-label-' + prec_axis : ''); inner_div.innerHTML = txt; div.appendChild(inner_div); return div; }; // axis lines context.save(); var layout = g.layout_; var area = e.dygraph.plotter_.area; // Helper for repeated axis-option accesses. var makeOptionGetter = function(axis) { return function(option) { return g.getOptionForAxis(option, axis); }; }; const that = this; if (g.getOptionForAxis('drawAxis', 'y') || (g.numAxes() == 2 && g.getOptionForAxis('drawAxis', 'y2'))) { if (layout.yticks && layout.yticks.length > 0) { var num_axes = g.numAxes(); var getOptions = [makeOptionGetter('y'), makeOptionGetter('y2')]; layout.yticks.forEach(function (tick) { if (tick.label === undefined) return; // this tick only has a grid line. x = area.x; var sgn = 1; var prec_axis = 'y1'; var getAxisOption = getOptions[0]; if (tick.axis == 1) { // right-side y-axis x = area.x + area.w; sgn = -1; prec_axis = 'y2'; getAxisOption = getOptions[1]; } if (!getAxisOption('drawAxis')) return; var fontSize = getAxisOption('axisLabelFontSize'); y = area.y + tick.pos * area.h; /* Tick marks are currently clipped, so don't bother drawing them. context.beginPath(); context.moveTo(halfUp(x), halfDown(y)); context.lineTo(halfUp(x - sgn * that.attr_('axisTickSize')), halfDown(y)); context.closePath(); context.stroke(); */ label = makeDiv(tick.label, 'y', num_axes == 2 ? prec_axis : null); var top = (y - fontSize / 2); if (top < 0) top = 0; if (top + fontSize + 3 > canvasHeight) { label.style.bottom = '0'; } else { // The lowest tick on the y-axis often overlaps with the leftmost // tick on the x-axis. Shift the bottom tick up a little bit to // compensate if necessary. label.style.top = Math.min(top, canvasHeight - (2 * fontSize)) + 'px'; } // TODO: replace these with css classes? if (tick.axis === 0) { label.style.left = (area.x - getAxisOption('axisLabelWidth') - getAxisOption('axisTickSize')) + 'px'; label.style.textAlign = 'right'; } else if (tick.axis == 1) { label.style.left = (area.x + area.w + getAxisOption('axisTickSize')) + 'px'; label.style.textAlign = 'left'; } label.style.width = getAxisOption('axisLabelWidth') + 'px'; containerDiv.appendChild(label); that.ylabels_.push(label); }); } // draw a vertical line on the left to separate the chart from the labels. var axisX; if (g.getOption('drawAxesAtZero')) { var r = g.toPercentXCoord(0); if (r > 1 || r < 0 || isNaN(r)) r = 0; axisX = halfUp(area.x + r * area.w); } else { axisX = halfUp(area.x); } context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y'); context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y'); context.beginPath(); context.moveTo(axisX, halfDown(area.y)); context.lineTo(axisX, halfDown(area.y + area.h)); context.closePath(); context.stroke(); // if there's a secondary y-axis, draw a vertical line for that, too. if (g.numAxes() == 2 && g.getOptionForAxis('drawAxis', 'y2')) { context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y2'); context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y2'); context.beginPath(); context.moveTo(halfDown(area.x + area.w), halfDown(area.y)); context.lineTo(halfDown(area.x + area.w), halfDown(area.y + area.h)); context.closePath(); context.stroke(); } } if (g.getOptionForAxis('drawAxis', 'x')) { if (layout.xticks) { var getAxisOption = makeOptionGetter('x'); layout.xticks.forEach(function (tick) { if (tick.label === undefined) return; // this tick only has a grid line. x = area.x + tick.pos * area.w; y = area.y + area.h; /* Tick marks are currently clipped, so don't bother drawing them. context.beginPath(); context.moveTo(halfUp(x), halfDown(y)); context.lineTo(halfUp(x), halfDown(y + that.attr_('axisTickSize'))); context.closePath(); context.stroke(); */ label = makeDiv(tick.label, 'x'); label.style.textAlign = 'center'; label.style.top = (y + getAxisOption('axisTickSize')) + 'px'; var left = (x - getAxisOption('axisLabelWidth')/2); if (left + getAxisOption('axisLabelWidth') > canvasWidth) { left = canvasWidth - getAxisOption('axisLabelWidth'); label.style.textAlign = 'right'; } if (left < 0) { left = 0; label.style.textAlign = 'left'; } label.style.left = left + 'px'; label.style.width = getAxisOption('axisLabelWidth') + 'px'; containerDiv.appendChild(label); that.xlabels_.push(label); }); } context.strokeStyle = g.getOptionForAxis('axisLineColor', 'x'); context.lineWidth = g.getOptionForAxis('axisLineWidth', 'x'); context.beginPath(); var axisY; if (g.getOption('drawAxesAtZero')) { var r = g.toPercentYCoord(0, 0); if (r > 1 || r < 0) r = 1; axisY = halfDown(area.y + r * area.h); } else { axisY = halfDown(area.y + area.h); } context.moveTo(halfUp(area.x), axisY); context.lineTo(halfUp(area.x + area.w), axisY); context.closePath(); context.stroke(); } context.restore(); }; export default axes;