import raf from '../utils/raf' import isUndef from '../utils/isUndef' import isFn from '../utils/isFn' import { getDomPos, range, toPixel, isFunction } from '../utils' import { DEFAULT_CHART_STYLES } from '../consts'
export default function chartCommon(target) {
return class extends target { constructor(...args) { super(...args) this.raf = raf } init() { this.offLabels = [] this.layers = [] if (isFunction(super.init)) { super.init() } } get firstLayer() { return this.layers[0] } addLayer() { const { dom } = this const canvas = document.createElement('canvas') canvas.style.position = 'absolute' canvas.style.top = 0 canvas.style.left = 0 canvas.style.right = 0 canvas.style.bottom = 0 const ctx = canvas.getContext('2d') this.setCanvasSize(canvas) this.layers.push({ canvas, ctx }) dom.style.position = 'relative' dom.appendChild(canvas) } bindMedia() { if (this.media) { return } this.media = window.matchMedia(`(resolution: ${this.dpr}dppx)`) this._handleDprChange = this.handleDprChange.bind(this) this.media.addListener(this._handleDprChange) } clear() { const { ctx } = this ctx.fillStyle = this.bg ctx.fillRect(0, 0, this.width, this.height) } getHypotenuse(x1, y1, x2, y2) { return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)) } fillCircle(ctx, x, y, radius, style, alpha) { ctx.save() ctx.beginPath() ctx.arc(x, y, radius, 0, 2 * Math.PI) ctx.fillStyle = style ctx.globalAlpha = alpha || 1 ctx.fill() ctx.closePath() ctx.restore() } fillArc(ctx, x, y, radius, startAngle = 0, endAngle = 2 * Math.PI, options = {}) { ctx.save() ctx.beginPath() ctx.arc(x, y, radius, startAngle, endAngle) ctx.fillStyle = options.style || '#555' ctx.globalAlpha = options.alpha || 1 ctx.lineTo(x, y) ctx.fill() ctx.closePath() ctx.restore() } getAutoStep(firstValue, lastValue, pointsLength) { return (lastValue - firstValue) / (pointsLength - 1) } getHighestCanvas() { const { layers, canvas } = this if (layers.length === 0) { return canvas } return layers[layers.length - 1].canvas } // real position in window including scrolling distance getMousePos(canvasMousePos) { const domPos = getDomPos(this.dom) return { x: domPos.x + canvasMousePos.x, y: domPos.y + canvasMousePos.y } } getMousePosInCanvas(event) { const rect = this.canvas.getBoundingClientRect() return { x: event.clientX - rect.left, y: event.clientY - rect.top } } getLengthTotalData(gap, gutter, values, measureLength, toLabel) { const valueCount = values.length const marked = {} // mark the first and last marked[0] = true marked[valueCount - 1] = true // Check whether a value can be marked next // For example, gap is 2 // // values: 1 2 3 4 5 6 7 // marked: v v // // 4 will only be marked because it has enough gap on left and right side. const hasGap = index => { return range(index - gap, index).every(i => isUndef(marked[i])) && range(index + 1, index + gap + 1).every(i => isUndef(marked[i])) } return values.reduce((res, value, i) => { if (i === 0) { const label = toLabel(value) const length = measureLength(label) const lengthTotal = res.lengthTotal + length + gutter const rows = res.rows.slice() rows.push({ label, length, value }) return { lengthTotal, rows } } if (i === (valueCount - 1)) { const label = toLabel(value) const length = measureLength(label) const lengthTotal = res.lengthTotal + length const rows = res.rows.slice() rows.push({ label, length, value }) return { lengthTotal, rows } } if (hasGap(i)) { const label = toLabel(value) marked[i] = true const length = measureLength(label) const lengthTotal = res.lengthTotal + length + gutter const rows = res.rows.slice() rows.push({ label, length, value }) return { lengthTotal, rows } } return res }, { lengthTotal: 0, rows: [] }) } getStepStartEnd(step, firstValue, lastValue) { const stepStart = parseInt(firstValue / step, 10) * step let stepEnd = parseInt(lastValue / step, 10) * step if (stepEnd < lastValue) { stepEnd += step } return [stepStart, stepEnd] } measureWidth(value) { return this.ctx.measureText(value).width } removeAllLayers() { const { dom } = this this.layers.forEach(layer => { const { canvas } = layer if (dom.contains(canvas)) { dom.removeChild(canvas) } }) } setCanvas() { const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') this.canvas = canvas this.ctx = ctx this.setCanvasFontSize(this.canvas, this.fontSize) this.setCanvasSize(canvas) this.dom.appendChild(canvas) } setLabelBox() { const box = document.createElement('div') box.className = 'chart-box' this.labelBox = box this.dom.appendChild(box) } drawLabels(labels, styles = DEFAULT_CHART_STYLES) { if (labels.length <= 0) { return } const { labelBox, handleLabelMouseOver, handleLabelMouseLeave } = this this.dom.style.backgroundColor = this.bg this.offLabels.forEach(off => off()) labelBox.innerHTML = '' this.offLabels.length = 0 labels.forEach((label, i) => { const div = document.createElement('div') div.className = 'chart-box-item' const square = document.createElement('div') square.className = 'chart-box-square' square.style.backgroundColor = styles[i] div.appendChild(square) const span = document.createElement('span') span.textContent = label div.appendChild(span) labelBox.appendChild(div) if (isFn(handleLabelMouseOver)) { const off = this.addEvent(div, 'mouseover', event => this.handleLabelMouseOver(event, i)) this.offLabels.push(off) } if (isFn(handleLabelMouseLeave)) { const off = this.addEvent(div, 'mouseleave', event => this.handleLabelMouseLeave(event, i)) this.offLabels.push(off) } }) } setCanvasFontSize(canvas, fontSize) { const ctx = canvas.getContext('2d') const args = ctx.font.split(' ') ctx.font = fontSize + 'px ' + args[args.length - 1] } setCanvasSize(canvas) { const { dpr, width, height } = this // https://coderwall.com/p/vmkk6a/how-to-make-the-canvas-not-look-like-crap-on-retina canvas.width = width * dpr canvas.height = height * dpr canvas.style.width = toPixel(width) canvas.style.height = toPixel(height) canvas.getContext('2d').scale(dpr, dpr) } clearCanvasSize(canvas) { canvas.width = 0 canvas.height = 0 canvas.style.width = 0 canvas.style.height = 0 } setDomSizeIfNeeded() { if (isUndef(this.options.width)) { this.width = this.dom.offsetWidth } if (isUndef(this.options.height)) { this.height = this.dom.offsetHeight } } setDpr() { this.dpr = window.devicePixelRatio || 1 } unbindMedia() { this.media.removeListener(this._handleDprChange) } }
}