UNPKG

@progress/kendo-charts

Version:

Kendo UI platform-independent Charts library

391 lines (315 loc) 12.1 kB
import Axis from './axis'; import AxisLabel from './axis-label'; import Box from './box'; import createAxisTick from './utils/create-axis-tick'; import createAxisGridLine from './utils/create-axis-grid-line'; import limitCoordinate from './utils/limit-coordinate'; import { DEFAULT_PRECISION, BLACK } from '../common/constants'; import { deepExtend, defined, inArray, limitValue, round, setDefaultOptions, valueOrDefault } from '../common'; const DEFAULT_MAJOR_UNIT = 10; const MIN_VALUE_RANGE = 1e-6; class LogarithmicAxis extends Axis { constructor(seriesMin, seriesMax, options, chartService) { const axisOptions = deepExtend({ majorUnit: DEFAULT_MAJOR_UNIT, min: seriesMin, max: seriesMax }, options); const base = axisOptions.majorUnit; const autoMax = autoAxisMax(seriesMax, base); const autoMin = autoAxisMin(seriesMin, seriesMax, axisOptions); const range = initRange(autoMin, autoMax, axisOptions, options); axisOptions.max = range.max; axisOptions.min = range.min; axisOptions.minorUnit = options.minorUnit || round(base - 1, DEFAULT_PRECISION); super(axisOptions, chartService); this.totalMin = defined(options.min) ? Math.min(autoMin, options.min) : autoMin; this.totalMax = defined(options.max) ? Math.max(autoMax, options.max) : autoMax; this.logMin = round(log(range.min, base), DEFAULT_PRECISION); this.logMax = round(log(range.max, base), DEFAULT_PRECISION); this.seriesMin = seriesMin; this.seriesMax = seriesMax; this.createLabels(); } clone() { return new LogarithmicAxis( this.seriesMin, this.seriesMax, Object.assign({}, this.options), this.chartService ); } startValue() { return this.options.min; } getSlot(a, b, limit) { const { options, logMin, logMax } = this; const { majorUnit: base, min, max } = options; const { axis, axisDir, lineBox, lineSize, lineStart } = this.lineInfo(); const step = axisDir * (lineSize / (logMax - logMin)); let start = valueOrDefault(a, b || 1); let end = valueOrDefault(b, a || 1); if (start <= 0 || end <= 0) { return null; } if (limit) { start = limitValue(start, min, max); end = limitValue(end, min, max); } start = log(start, base); end = log(end, base); const p1 = Math.min(start, end) - logMin; const p2 = Math.max(start, end) - logMin; const slotBox = new Box(lineBox.x1, lineBox.y1, lineBox.x1, lineBox.y1); slotBox[axis + 1] = limitCoordinate(lineStart + step * (axisDir > 0 ? p1 : p2)); slotBox[axis + 2] = limitCoordinate(lineStart + step * (axisDir > 0 ? p2 : p1)); return slotBox; } getValue(point) { const { options, logMin, logMax } = this; const { majorUnit: base } = options; const { axis, axisDir, lineStart, lineSize } = this.lineInfo(); const step = ((logMax - logMin) / lineSize); const offset = axisDir * (point[axis] - lineStart); const valueOffset = offset * step; if (offset < 0 || offset > lineSize) { return null; } const value = logMin + valueOffset; return round(Math.pow(base, value), DEFAULT_PRECISION); } range() { const options = this.options; return { min: options.min, max: options.max }; } translateRange(delta) { const { options, logMin, logMax } = this; const { reverse, vertical, majorUnit: base } = options; const lineBox = this.lineBox(); const size = vertical ? lineBox.height() : lineBox.width(); const scale = size / (logMax - logMin); let offset = round(delta / scale, DEFAULT_PRECISION); if ((vertical || reverse) && !(vertical && reverse )) { offset = -offset; } return { min: Math.pow(base, logMin + offset), max: Math.pow(base, logMax + offset), offset: offset }; } labelsCount() { const floorMax = Math.floor(this.logMax); const count = Math.floor(floorMax - this.logMin) + 1; return count; } getMajorTickPositions() { const ticks = []; this.traverseMajorTicksPositions((position) => { ticks.push(position); }, { step: 1, skip: 0 }); return ticks; } createTicks(lineGroup) { const options = this.options; const { majorTicks, minorTicks, vertical } = options; const mirror = options.labels.mirror; const lineBox = this.lineBox(); const ticks = []; const tickLineOptions = { // TODO // _alignLines: options._alignLines, vertical: vertical }; function render(tickPosition, tickOptions) { tickLineOptions.tickX = mirror ? lineBox.x2 : lineBox.x2 - tickOptions.size; tickLineOptions.tickY = mirror ? lineBox.y1 - tickOptions.size : lineBox.y1; tickLineOptions.position = tickPosition; lineGroup.append(createAxisTick(tickLineOptions, tickOptions)); } if (majorTicks.visible) { this.traverseMajorTicksPositions(render, majorTicks); } if (minorTicks.visible) { this.traverseMinorTicksPositions(render, minorTicks); } return ticks; } createGridLines(altAxis) { const options = this.options; const { minorGridLines, majorGridLines, vertical } = options; const lineBox = altAxis.lineBox(); const lineOptions = { lineStart: lineBox[vertical ? "x1" : "y1"], lineEnd: lineBox[vertical ? "x2" : "y2"], vertical: vertical }; const majorTicks = []; const container = this.gridLinesVisual(); function render(tickPosition, gridLine) { if (!inArray(tickPosition, majorTicks)) { lineOptions.position = tickPosition; container.append(createAxisGridLine(lineOptions, gridLine)); majorTicks.push(tickPosition); } } if (majorGridLines.visible) { this.traverseMajorTicksPositions(render, majorGridLines); } if (minorGridLines.visible) { this.traverseMinorTicksPositions(render, minorGridLines); } return container.children; } traverseMajorTicksPositions(callback, tickOptions) { const { lineStart, step } = this.lineInfo(); const { logMin, logMax } = this; for (let power = Math.ceil(logMin) + tickOptions.skip; power <= logMax; power += tickOptions.step) { let position = round(lineStart + step * (power - logMin), DEFAULT_PRECISION); callback(position, tickOptions); } } traverseMinorTicksPositions(callback, tickOptions) { const { min, max, minorUnit, majorUnit: base } = this.options; const { lineStart, step } = this.lineInfo(); const { logMin, logMax } = this; const start = Math.floor(logMin); for (let power = start; power < logMax; power++) { const minorOptions = this._minorIntervalOptions(power); for (let idx = tickOptions.skip; idx < minorUnit; idx += tickOptions.step) { const value = minorOptions.value + idx * minorOptions.minorStep; if (value > max) { break; } if (value >= min) { const position = round(lineStart + step * (log(value, base) - logMin), DEFAULT_PRECISION); callback(position, tickOptions); } } } } createAxisLabel(index, labelOptions, labelContext) { const power = Math.ceil(this.logMin + index); const value = Math.pow(this.options.majorUnit, power); const text = this.axisLabelText(value, labelOptions, labelContext); return new AxisLabel(value, text, index, null, labelOptions); } shouldRenderNote(value) { const range = this.range(); return range.min <= value && value <= range.max; } pan(delta) { const range = this.translateRange(delta); return this.limitRange(range.min, range.max, this.totalMin, this.totalMax, range.offset); } pointsRange(start, end) { const startValue = this.getValue(start); const endValue = this.getValue(end); const min = Math.min(startValue, endValue); const max = Math.max(startValue, endValue); return { min: min, max: max }; } scaleRange(scale, cursor) { const { majorUnit: base } = this.options; const logMin = log(this.options.min, base); const logMax = log(this.options.max, base); const position = Math.abs(this.pointOffset(cursor)); const range = logMax - logMin; const delta = this.scaleToDelta(scale, range); const min = Math.pow(base, logMin + position * delta); let max = Math.pow(base, logMax - (1 - position) * delta); if (max - min < MIN_VALUE_RANGE) { max = min + MIN_VALUE_RANGE; } return { min: min, max: max }; } zoomRange(scale, cursor) { const range = this.scaleRange(scale, cursor); const { totalMin, totalMax } = this; return { min: limitValue(range.min, totalMin, totalMax), max: limitValue(range.max, totalMin, totalMax) }; } _minorIntervalOptions(power) { const { minorUnit, majorUnit: base } = this.options; const value = Math.pow(base, power); const nextValue = Math.pow(base, power + 1); const difference = nextValue - value; const minorStep = difference / minorUnit; return { value: value, minorStep: minorStep }; } lineInfo() { const info = super.lineInfo(); info.step = info.axisDir * (info.lineSize / (this.logMax - this.logMin)); return info; } } function initRange(autoMin, autoMax, axisOptions, options) { let { min, max } = axisOptions; if (defined(axisOptions.axisCrossingValue) && axisOptions.axisCrossingValue <= 0) { throwNegativeValuesError(); } if (!defined(options.max)) { max = autoMax; } else if (options.max <= 0) { throwNegativeValuesError(); } if (!defined(options.min)) { min = autoMin; } else if (options.min <= 0) { throwNegativeValuesError(); } return { min: min, max: max }; } function autoAxisMin(min, max, options) { const base = options.majorUnit; let autoMin = min; if (min <= 0) { autoMin = max <= 1 ? Math.pow(base, -2) : 1; } else if (!options.narrowRange) { autoMin = Math.pow(base, Math.floor(log(min, base))); } return autoMin; } function autoAxisMax(max, base) { const logMaxRemainder = round(log(max, base), DEFAULT_PRECISION) % 1; let autoMax; if (max <= 0) { autoMax = base; } else if (logMaxRemainder !== 0 && (logMaxRemainder < 0.3 || logMaxRemainder > 0.9)) { autoMax = Math.pow(base, log(max, base) + 0.2); } else { autoMax = Math.pow(base, Math.ceil(log(max, base))); } return autoMax; } function throwNegativeValuesError() { throw new Error("Non positive values cannot be used for a logarithmic axis"); } function log(x, base) { return Math.log(x) / Math.log(base); } setDefaultOptions(LogarithmicAxis, { type: "log", majorUnit: DEFAULT_MAJOR_UNIT, minorUnit: 1, axisCrossingValue: 1, vertical: true, majorGridLines: { visible: true, width: 1, color: BLACK }, zIndex: 1, _deferLabels: true }); export default LogarithmicAxis;