UNPKG

@huangjs888/d3-chart

Version:

Implement some charts based on d3 library.

1,212 lines (1,211 loc) 43.7 kB
const _excluded = ["transform", "sourceEvent"], _excluded2 = ["transform", "sourceEvent"], _excluded3 = ["transform", "sourceEvent"]; function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } // @ts-nocheck /* * @Author: Huangjs * @Date: 2021-03-17 16:23:00 * @LastEditors: Huangjs * @LastEditTime: 2023-05-12 14:11:32 * @Description: 基础图表构造器 */ import * as d3 from 'd3'; import * as util from '../util'; const iconSize = 18; const downloadPath = 'M505.7 661a8 8 0 0012.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z'; const resetPath = 'M483.031 380.989v-61.004l-162.969 92.891 162.969 99.24v-64.04c128.025 0.122 157.093 64.052 157.093 94.107 0 33.863-29.068 97.771-157.093 97.771l-98.957 0.011v63.919l98.957-0.011c157.093 0.011 221.105-63.907 221.105-159.017 0.001-96.647-64.012-163.856-221.105-163.867 M511.372 64.548c-247.628 0-448.369 200.319-448.369 447.426S263.744 959.4 511.372 959.4s448.369-200.32 448.369-447.426S758.999 64.548 511.372 64.548z m-0.203 823.564c-207.318 0-375.382-167.71-375.382-374.592s168.064-374.592 375.382-374.592 375.382 167.71 375.382 374.592-168.064 374.592-375.382 374.592z'; const lockPath = 'M650 432.6v-99.4c0-76.3-61.8-138.1-138.1-138.1S373.8 258 373.8 334.3v99.4H650z m-382.9 1.1l-0.1-3.1V322.5c0-59.9 23.8-117.4 66.2-159.8C375.6 120.3 433 96.5 493 96.5h37.9c124.8 0 225.9 101.2 225.9 225.9v111.3c58.2 8.1 101.5 57.9 101.5 116.7v258.3c0 65.1-52.7 117.8-117.8 117.8H283.3c-65.1 0-117.8-52.7-117.8-117.8V550.5c0-58.8 43.3-108.6 101.6-116.8z m217.6 266v77c0 15 12.2 27.2 27.2 27.2s27.2-12.2 27.2-27.2v-77c33.5-13.1 53-48.2 46.3-83.5-6.7-35.4-37.5-61-73.5-61s-66.9 25.6-73.5 61c-6.7 35.3 12.8 70.4 46.3 83.5z'; const unlockPath = 'M650 432.6v-99.4c0-76.3-61.8-138.1-138.1-138.1S373.8 258 373.8 334.3v99.4H650z m-382.9 1.1l-0.1-3.1V322.5c0-59.9 23.8-117.4 66.2-159.8C375.6 120.3 433 96.5 493 96.5h37.9c124.8 0 225.9 101.2 225.9 225.9v111.3c58.2 8.1 101.5 57.9 101.5 116.7v258.3c0 65.1-52.7 117.8-117.8 117.8H283.3c-65.1 0-117.8-52.7-117.8-117.8V550.5c0-58.8 43.3-108.6 101.6-116.8z m217.6 266v77c0 15 12.2 27.2 27.2 27.2s27.2-12.2 27.2-27.2v-77c33.5-13.1 53-48.2 46.3-83.5-6.7-35.4-37.5-61-73.5-61s-66.9 25.6-73.5 61c-6.7 35.3 12.8 70.4 46.3 83.5zm272 -369h-106.5v103h106.5z'; const actionButtons = { download: { title: '下载视图', path: downloadPath, className: 'download' }, reset: { title: '重置视图', path: resetPath, className: 'reset' }, xlock: { title: 'X轴缩放开关', path: unlockPath, className: 'xlock' }, ylock: { title: 'Y轴缩放开关', path: unlockPath, className: 'ylock' } }; const axisType = ['x', 'x2', 'y', 'y2']; const scaleType = { cat: () => d3.scaleBand(), timeCat: () => d3.scaleBand(), time: () => d3.scaleTime(), linear: () => d3.scaleLinear(), log: () => d3.scaleLog(), sqrt: () => d3.scaleSqrt(), pow: () => d3.scalePow(), // sequential: () => d3.scaleSequential(), quantize: () => d3.scaleQuantize(), quantile: () => d3.scaleQuantile(), identity: () => d3.scaleIdentity() }; const timeFormat = d3.timeFormat('%Y-%m-%d %H:%M:%S'); const formatMillisecond = d3.timeFormat('.%L'); const formatSecond = d3.timeFormat(':%S'); const formatMinute = d3.timeFormat('%H:%M'); const formatHour = d3.timeFormat('%H:00'); const formatDay = d3.timeFormat('%m-%d'); const formatMonth = d3.timeFormat('%m月'); const formatYear = d3.timeFormat('%Y年'); const prefixSIFormat = d3.format('.4~s'); const exponentFormat = d3.format('.4~g'); const roundFormat = d3.format('.4~r'); const getDefaultFormat = type => { if (type === 'time') { return (date, ax) => { if (!ax) { return timeFormat(date); } if (d3.timeSecond(date) < date) { return formatMillisecond(date); } if (d3.timeMinute(date) < date) { return formatSecond(date); } if (d3.timeHour(date) < date) { return formatMinute(date); } if (d3.timeDay(date) < date) { return formatHour(date); } if (d3.timeMonth(date) < date) { return formatDay(date); } if (d3.timeYear(date) < date) { return formatMonth(date); } return formatYear(date); }; } if (type === 'log') { return exponentFormat; } if (type === 'sqrt') { return roundFormat; } if (type === 'linear' || type === 'pow') { return prefixSIFormat; } return v => v; }; const computeSize = (element, { width, height, padding, rWidth, rHeight }) => { let w = width; let h = height; const style = getComputedStyle(element); if (!(util.isNumber(width) && width > 1)) { w = (element.clientWidth || parseInt(style.width, 10)) - parseInt(style.paddingLeft, 10) - parseInt(style.paddingRight, 10); } if (!(util.isNumber(height) && height > 1)) { h = (element.clientHeight || parseInt(style.height, 10)) - parseInt(style.paddingTop, 10) - parseInt(style.paddingBottom, 10); } let [top, right, bottom, left] = []; const pad = Array.isArray(padding) ? padding : []; let [pad0, pad1, pad2, pad3] = pad; pad0 = !util.isNumber(pad0) || pad0 < 0 ? 0 : pad0; pad1 = !util.isNumber(pad1) || pad1 < 0 ? 0 : pad1; pad2 = !util.isNumber(pad2) || pad2 < 0 ? 0 : pad2; pad3 = !util.isNumber(pad3) || pad3 < 0 ? 0 : pad3; if (!pad.length) { top = 0; right = top; bottom = top; left = top; } else if (pad.length === 1) { top = pad0; right = top; bottom = top; left = top; } else if (pad.length === 2) { top = pad0; right = pad1; bottom = top; left = right; } else if (pad.length === 3) { top = pad0; right = pad1; bottom = pad2; left = right; } else { top = pad0; right = pad1; bottom = pad2; left = pad3; } return { width: Math.max(util.isNumber(w) ? w : 1, 1), height: Math.max(util.isNumber(h) ? h : 1, 1), padding: [top, right, bottom, left], rWidth: util.isNumber(rWidth) ? rWidth : 0, rHeight: util.isNumber(rHeight) ? rHeight : 0 }; }; const scaleLable = (labelSel, [scale, domain]) => { let { type, format, label, unit } = scale; type = type || 'linear'; label = label || ''; unit = !label || !unit ? '' : ` ( ${unit} )`; format = format || getDefaultFormat(type); let [min, max] = domain; min = min || 0; max = max || 0; labelSel.select('text').selectAll('tspan').data([0, 1]).join('tspan').attr('dx', (_, i) => { if (!label) return 0; return i === 0 ? -12 : 12; }).text((_, i) => i === 0 ? `${label}${unit}` : `${format(min)} - ${format(max)}`); }; function getScaleAxis() { const [xTransform, yTransform] = this.zoomSelection$.datum(); const scaleAxis = {}; axisType.forEach(key => { if (this.scale[key]) { const { scale, axis } = this.rescale$(key === 'x' || key === 'x2' ? xTransform : yTransform, key); scaleAxis[key] = { axis, scale }; } }); return scaleAxis; } function updateScale() { axisType.forEach(key => { if (this.axisScale$[key]) { const [minDomain, maxDomain] = this.scale[key].domain || this.dataDomains$[key] || [0, 1]; this.axisScale$[key].scale.range(key === 'x' || key === 'x2' ? [0, this.width$] : [this.height$, 0]).domain([minDomain || 0, maxDomain || 1]); if (this.scale[key].nice) { this.axisScale$[key].scale.nice(); const niceDomain = this.axisScale$[key].scale.domain(); if (this.scale[key].domain) { this.scale[key].domain = niceDomain; } if (this.dataDomains$[key]) { this.dataDomains$[key] = niceDomain; } } } }); } function createScale() { const axisScale = {}; axisType.forEach(key => { if (this.scale[key]) { const { type = 'linear', ticks = 5, tickSize, format } = this.scale[key]; const scale = scaleType[type](); let axis = null; if (key === 'x2') { axis = d3.axisTop(scale); } else if (key === 'y') { axis = d3.axisLeft(scale); } else if (key === 'y2') { axis = d3.axisRight(scale); } else { axis = d3.axisBottom(scale); } if (ticks) { axis = axis.ticks(ticks); } if (tickSize) { axis = axis.tickSize(tickSize); } const defaultFormat = getDefaultFormat(type); if (!format) { this.scale[key].format = defaultFormat; } axis = axis.tickFormat(format || (val => defaultFormat(val, true))); axisScale[key] = { scale, axis }; } }); this.axisScale$ = axisScale; updateScale.call(this); this.rescale$ = (transform, key) => { const as = this.axisScale$[key]; if (as) { const { scale, axis } = as; const recale = key === 'x' || key === 'x2' ? transform.rescaleX(scale) : transform.rescaleY(scale); return { scale: recale, axis: axis.scale(recale) }; } return as; }; } function updateZoom() { const viewExtent = [[0, 0], [this.width$, this.height$]]; let scaleExtent = [[1, 1], [1, 1]]; let translateExtent = [[0, 0], [0, 0]]; const { x: xzoom, y: yzoom } = this.zoom; if (xzoom || yzoom) { const [xdomain, ydomain] = [xzoom, yzoom].map((zoom, i) => { const key = zoom.domain || (i === 0 ? 'x' : 'y'); let domain = [0, 1]; if (this.scale[key]) { domain = this.scale[key].domain || this.dataDomains$[key] || [0, 1]; } return domain; }); const [xMinDomain, xMaxDomain] = xdomain; const [yMinDomain, yMaxDomain] = ydomain; const xDomainEx = (xMaxDomain || 1) - (xMinDomain || 0); const yDomainEx = (yMaxDomain || 1) - (yMinDomain || 0); const [xMinTranslate = 0, xMaxTranslate = 0] = xzoom.translate || [-Infinity, Infinity]; const [yMinTranslate = 0, yMaxTranslate = 0] = yzoom.translate || [-Infinity, Infinity]; const [xMinPrecision = xDomainEx / 10, xMaxPrecision = xDomainEx * 10] = xzoom.precision || [xDomainEx / 10, xDomainEx * 10]; const [yMinPrecision = yDomainEx / 10, yMaxPrecision = yDomainEx * 10] = yzoom.precision || [yDomainEx / 10, yDomainEx * 10]; const xRate = this.width$ / xDomainEx; const yRate = this.height$ / yDomainEx; scaleExtent = [[xDomainEx / xMaxPrecision || 1, yDomainEx / yMaxPrecision || 1], [xDomainEx / xMinPrecision || 1, yDomainEx / yMinPrecision || 1]]; translateExtent = [[(xMinTranslate - (xMinDomain || 0)) * xRate || xMinDomain || 0, ((yMaxDomain || 1) - yMaxTranslate) * yRate || yMinDomain || 0], [(xMaxTranslate - (xMinDomain || 0)) * xRate || xMaxDomain || 1, ((yMaxDomain || 1) - yMinTranslate) * yRate || yMaxDomain || 1]]; } this.zoomer$.extent(viewExtent).scaleExtent([(scaleExtent[0][0] * scaleExtent[0][1]) ** 2, (scaleExtent[1][0] * scaleExtent[1][1]) ** 2]).translateExtent(translateExtent); this.zoomExtent$ = { viewExtent, scaleExtent, translateExtent }; } function createZoom() { this.zoomExtent$ = null; updateZoom.call(this); this.transform$ = (axis, tf, t, p, l) => { let transform = tf; if (this.zoomExtent$) { const { viewExtent, translateExtent, scaleExtent } = this.zoomExtent$; const tk = Math.max(axis === 'x' ? scaleExtent[0][0] : scaleExtent[0][1], Math.min(axis === 'x' ? scaleExtent[1][0] : scaleExtent[1][1], transform.k * t)); const tx = p[0] - tk * l[0]; const ty = p[1] - tk * l[1]; if (tk !== transform.k || tx !== transform.x || ty !== transform.y) { transform = this.zoomer$.constrain()(d3.zoomIdentity.translate(tx, ty).scale(tk), viewExtent, translateExtent); } } return transform; }; } function updateElement(size) { const { width, height, padding, rWidth, rHeight } = computeSize(this.rootSelection$.node(), size); let zw = width - rWidth - padding[3] - padding[1]; let zh = height - rHeight - padding[0] - padding[2]; if (zw < 1) zw = 1; if (zh < 1) zh = 1; const group = this.rootSelection$.select('svg').attr('width', width).attr('height', height).select('g.group').attr('transform', `translate(${padding[3]},${padding[0]})`); this.rootSelection$.select('clipPath').select('rect').attr('width', zw).attr('height', zh); const baselineDelt = this.fontSize + 2; if (this.scale.x) { group.select('.xAxis').attr('transform', `translate(0,${zh})`); if (this.scale.x.showRange || typeof this.scale.x.showRange === 'undefined') { group.select('.xLabel').attr('transform', `translate(${zw / 2} ${zh + padding[2] - iconSize + baselineDelt}) rotate(0)`); } } if (this.scale.x2 && (this.scale.x2.showRange || typeof this.scale.x2.showRange === 'undefined')) { group.select('.x2Label').attr('transform', `translate(${zw / 2} ${baselineDelt - padding[0]}) rotate(0)`); } if (this.scale.y && (this.scale.y.showRange || typeof this.scale.y.showRange === 'undefined')) { group.select('.yLabel').attr('transform', `translate(${baselineDelt - padding[3]} ${zh / 2}) rotate(-90)`); } if (this.scale.y2) { group.select('.y2Axis').attr('transform', `translate(${zw},0)`); if (this.scale.y2.showRange || typeof this.scale.y2.showRange === 'undefined') { group.select('.y2Label').attr('transform', `translate(${zw + padding[1] - iconSize + baselineDelt} ${zh / 2}) rotate(-90)`); } } const svgDiv = this.rootSelection$.select('div.actions').style('width', `${zw}px`).style('height', `${zh}px`).style('top', `${padding[0]}px`).style('left', `${padding[3]}px`); if (this.zoom.x) { svgDiv.select('.xlock').style('top', `${this.scale.x ? zh + padding[2] - iconSize : -padding[0]}px`).style('left', `${this.scale.y ? zw - iconSize / 2 : -iconSize / 2}px`); } if (this.zoom.y) { svgDiv.select('.ylock').style('top', `${this.scale.x ? -iconSize / 2 : zh - iconSize / 2}px`).style('left', `${this.scale.y ? -padding[3] : zw + padding[1] - iconSize}px`); } let offset = 5; if (this.zoom.x || this.zoom.y) { svgDiv.select('.reset').style('top', `${this.scale.x ? -padding[0] : zh + padding[2] - iconSize}px`).style('left', `${this.scale.y ? zw - offset - iconSize : offset}px`); offset += 23; } if (this.download) { svgDiv.select('.download').style('top', `${this.scale.x ? -padding[0] : zh + padding[2] - iconSize}px`).style('left', `${this.scale.y ? zw - offset - iconSize : offset}px`); offset += 23; } this.actions.forEach((a, i) => { if (!a) return; svgDiv.select(`.action-${i}`).style('top', `${this.scale.x ? -padding[0] : zh + padding[2] - iconSize}px`).style('left', `${this.scale.y ? zw - offset - iconSize : offset}px`); offset += 23; }); group.select('.zLabel').attr('transform', `translate(${this.scale.y ? zw - offset : offset} ${(this.scale.x ? -padding[0] : zh + padding[2] - iconSize) + baselineDelt})`); this.width = width; this.height = height; this.padding = padding; this.width$ = zw; this.height$ = zh; } function createElement(container, size) { const svgXmlns = 'http://www.w3.org/2000/svg'; const rootSelection = d3.select(container || document.createElement('div')).append('div').attr('class', 'd3chart').style('position', 'relative') // .style('overflow', 'hidden') .style('width', '100%').style('height', '100%'); const svgSelection = rootSelection.append('svg').attr('xmlns', svgXmlns).attr('width', 1) // 清除默认宽高 .attr('height', 1) // 清除默认宽高 .attr('fill', 'none').attr('text-anchor', 'middle').attr('font-size', this.fontSize).attr('stroke', 'none').attr('stroke-width', 1).attr('stroke-linejoin', 'round').attr('stroke-linecap', 'round').style('position', 'absolute'); const pathClipId = util.guid('clip'); svgSelection.append('defs').append('svg:clipPath').attr('id', pathClipId).append('rect'); const groupSelection = svgSelection.append('g').attr('class', 'group'); axisType.forEach(key => { if (this.scale[key]) { groupSelection.append('g').attr('class', `${key}Axis`); if (this.scale[key].showRange || typeof this.scale[key].showRange === 'undefined') { groupSelection.append('g').attr('class', `${key}Label`).attr('fill', 'currentColor').append('text'); } } }); groupSelection.append('g').attr('class', 'zAxis').attr('clip-path', `url(#${pathClipId})`); groupSelection.append('g').attr('class', 'zLabel').attr('fill', 'currentColor'); const divSelection = rootSelection.append('div').attr('class', 'actions').style('position', 'absolute').style('top', 0).style('left', 0); const zoomSelection = divSelection.append('div').attr('class', 'xyzoom').style('position', 'absolute') // .style('overflow', 'hidden') .style('background', 'rgba(0,0,0,0)') // IE9,10的层级(z-index)不起作用,需要加个背景使鼠标事件生效 .style('width', '100%').style('height', '100%').style('top', 0).style('left', 0); const actions = []; if (this.zoom) { if (this.zoom.x) { actions.push(actionButtons.xlock); } if (this.zoom.y) { actions.push(actionButtons.ylock); } if (this.zoom.x || this.zoom.y) { actions.push(actionButtons.reset); } } if (this.download) { actions.push(actionButtons.download); } [...actions, ...this.actions].forEach((action, i) => { const index = i - actions.length; if (!action) return; const { className, title, path, text, src, menus } = action; const actionSelection = divSelection.append('div').attr('class', index < 0 ? className : `action-${index}`).style('position', 'absolute').attr('title', title || '').style('display', 'inline-flex').style('font-size', `${path ? iconSize : 12}px`); if (path) { actionSelection.append('svg').attr('xmlns', svgXmlns).attr('fill', 'currentColor').attr('viewBox', '0 0 1024 1024').attr('width', '1em').attr('height', '1em').append('path').attr('d', path); } else if (text) { actionSelection.append('span').text(text); } else if (src) { actionSelection.append('img').attr('src', src).attr('width', iconSize).attr('height', iconSize); } if (Array.isArray(menus)) { const menusSelection = actionSelection.append('div').attr('class', `action-menu action-${index}-menu`).style('position', 'absolute').style('top', '24px').style('font-size', '12px').style('padding', '6px 0px'); let maxWidth = 0; menus.forEach((menu, j) => { if (!menu) return; const { text } = menu; maxWidth = Math.max(maxWidth, util.measureSvgText(text, 12)); menusSelection.append('div').attr('class', `action-${index}-menu-${j}`).style('padding', '6px 12px').text(text); }); menusSelection.style('width', `${maxWidth + 24}px`).style('left', `${-(maxWidth + 24) / 2}px`).style('display', 'none'); } }); if (this.tooltip) { const { cross } = this.tooltip; zoomSelection.append('div').attr('class', 'tooltip').style('display', 'none').style('z-index', '999').style('border-radius', '8px').style('transition', 'none').style('padding', '10px 14px').style('font-size', `${this.fontSize}px`).style('line-height', '22px').style('position', 'absolute').style('top', 0).style('left', 0); cross.split('').forEach(key => { if (key) { zoomSelection.append('div').attr('class', `${key}-cross`).style('display', 'none').style('position', 'absolute').style('transition', 'none').style('width', key === 'x' ? '1px' : '100%').style('height', key === 'x' ? '100%' : '1px').style('top', 0).style('left', 0); } }); } this.rootSelection$ = rootSelection; this.zoomSelection$ = zoomSelection; updateElement.call(this, size); if (!(util.isNumber(size.width) && size.width > 1 && util.isNumber(size.height) && size.height > 1)) { const resize = util.debounce(e => { if (this.destroyed) return; updateElement.call(this, size); updateScale.call(this); updateZoom.call(this); if (this.rendered) { this.reset(); } this.resize$.call(null, { sourceEvent: e || null, target: this.rootSelection$, transform: [d3.zoomTransform(this.zoomSelection$.node()), ...this.zoomSelection$.datum()], scaleAxis: getScaleAxis.call(this), type: 'resize' }, { width: this.width$, height: this.height$, padding: this.padding }); }, 250, { leading: false, trailing: true }); window.addEventListener('resize', resize); this.unBindResize$ = () => { window.removeEventListener('resize', resize); }; } } function bindEvents() { let point = null; let transform0 = null; let longPress = 0; const showTooltip = point => { if (this.tooltip) { const { cross, compute } = this.tooltip; const crossX = cross.indexOf('x') !== -1; const crossY = cross.indexOf('y') !== -1; if (crossX || crossY) { const [x0, y0] = point; let { x0: x00, y0: y00, result } = compute([x0, y0], getScaleAxis.call(this)); x00 = x00 || x00 === 0 ? x00 : x0; y00 = y00 || y00 === 0 ? y00 : y0; let display = 'none'; if (typeof result !== 'function') { result = util.noop; } else { display = 'block'; } if (crossX) { selection.select('.x-cross').style('left', `${x00}px`).style('display', display); } if (crossY) { selection.select('.y-cross').style('top', `${y00}px`).style('display', display); } const tooltip = selection.select('.tooltip'); tooltip.style('display', display).call(result); if (display !== 'none') { const width = tooltip.node().clientWidth; const height = tooltip.node().clientHeight; let left = x0 + 10; let top = y0 - height - 10; if (this.width$ - left < width) { left = x0 - 10 - width; } left = left <= -60 ? -60 : left; top = top <= -20 ? -20 : top; tooltip.style('left', `${left}px`).style('top', `${top}px`); } } } }; const hideTooltip = target => { if (this.tooltip) { const { cross } = this.tooltip; const zNode = element; if (!target || target !== zNode && target.parentNode !== zNode) { if (cross.indexOf('x') !== -1) { selection.select('.x-cross').style('display', 'none'); } if (cross.indexOf('y') !== -1) { selection.select('.y-cross').style('display', 'none'); } selection.select('.tooltip').style('display', 'none'); } } }; const selection = this.zoomSelection$; const element = selection.call(this.zoomer$.on('start', (_ref, [xTransform, yTransform]) => { let { transform, sourceEvent } = _ref, restEvent = _objectWithoutPropertiesLoose(_ref, _excluded); if (this.destroyed || !this.rendered) return; const { type } = sourceEvent; // 移动端双击放大的时候,因为是模拟的双击,所以event实际传过来的是touchend的事件对象 const touches = type === 'touchend' ? sourceEvent.changedTouches : sourceEvent.touches; if (type !== 'call' && (this.xCanZoom$ || this.yCanZoom$)) { let p = null; if (touches) { // 表示移动端触摸事件 point = []; for (let i = 0, len = touches.length; i < len; i += 1) { const touch = touches.item(i); p = d3.pointer(touch, element); p = [p, [xTransform.invert(p), yTransform.invert(p)], touch.identifier]; if (!point[0]) point[0] = p;else if (!point[1] && point[0][2] !== p[2]) point[1] = p; } } else { p = d3.pointer(sourceEvent, element); p = [p, [xTransform.invert(p), yTransform.invert(p)]]; point = p; } transform0 = transform; } if (touches) { longPress = setTimeout(() => { showTooltip(d3.pointer(touches.item(0), element)); longPress = 0; }, 500); } this.zoomstart$.call(null, _extends({}, restEvent, { sourceEvent, scaleAxis: getScaleAxis.call(this), transform: [transform, xTransform, yTransform] })); }).on('zoom', (_ref2, [xTransform, yTransform]) => { let { transform, sourceEvent } = _ref2, restEvent = _objectWithoutPropertiesLoose(_ref2, _excluded2); if (this.destroyed || !this.rendered) return; let [newXTransform, newYTransform] = [xTransform, yTransform]; const { type, changedTouches: touches, transform: eventTransform } = sourceEvent; // 表示事用户主动触发,如滚轮和移动,触摸缩放 if (type !== 'call') { if (this.xCanZoom$ || this.yCanZoom$) { const t = transform.k / transform0.k; let p = null; let lx = null; let ly = null; if (touches) { // 表示移动端触摸事件 let np = null; let np1 = null; for (let i = 0, len = touches.length; i < len; i += 1) { const touch = touches.item(i); const pt = d3.pointer(touch, element); if (point[0] && point[0][2] === touch.identifier) np = pt;else if (point[1] && point[1][2] === touch.identifier) np1 = pt; } if (point[0]) { p = np || point[0][0]; lx = this.xCanZoom$ && point[0][1][0]; ly = this.yCanZoom$ && point[0][1][1]; if (point[1]) { const p1 = np1 || point[1][0]; const [lx1, ly1] = point[1][1]; p = [(p[0] + p1[0]) / 2, (p[1] + p1[1]) / 2]; lx = this.xCanZoom$ && [(lx[0] + lx1[0]) / 2, (lx[1] + lx1[1]) / 2]; ly = this.yCanZoom$ && [(ly[0] + ly1[0]) / 2, (ly[1] + ly1[1]) / 2]; } } } else { p = d3.pointer(sourceEvent, element); const changed = p[0] !== point[0][0] || p[1] !== point[0][1]; // zoom的时候点(鼠标)是否移动过 const wheel = type === 'wheel'; // 是否是滚轮 if (wheel && !changed) p = point[0]; lx = this.xCanZoom$ && (wheel && changed ? xTransform.invert(p) : point[1][0]); ly = this.yCanZoom$ && (wheel && changed ? yTransform.invert(p) : point[1][1]); } if (p) { if (lx) { newXTransform = this.transform$('x', xTransform, t, p, lx); } if (ly) { newYTransform = this.transform$('y', yTransform, t, p, ly); } } transform0 = transform; } } else if (eventTransform) { // 如果存在xy缩放,则会有补间调用,万一补间一半,触发了滚轮或移动,则补间会停止,造成无法到达指定缩放位置,所以xy锁定的时候应该忽略补间 // transform和eventTransform原本是一样的,但是动画补间的每一次调用时transform会变化,直到最后一次才会和eventTransform一样 // eventTransform是不变的,是最终的指定的变换结果 let xtf = transform; let ytf = transform; if (Array.isArray(eventTransform)) { xtf = eventTransform[1]; ytf = eventTransform[2]; } else { if (!this.xCanZoom$) xtf = eventTransform; if (!this.yCanZoom$) ytf = eventTransform; } if (sourceEvent.zoomX) { newXTransform = xtf; } if (sourceEvent.zoomY) { newYTransform = ytf; } } selection.datum([newXTransform, newYTransform]); const scaleAxis = getScaleAxis.call(this); Object.keys(scaleAxis).forEach(key => { const { axis, scale } = scaleAxis[key]; this.rootSelection$.select(`.${key}Axis`).call(axis); this.rootSelection$.select(`.${key}Label`).call(scaleLable, [this.scale[key], scale.domain()]); }); if (touches) { if (longPress != 0) { clearTimeout(longPress); longPress = 0; } hideTooltip(); } this.zooming$.call(null, _extends({}, restEvent, { sourceEvent, transform: [transform, newXTransform, newYTransform], scaleAxis })); }).on('end', (_ref3, [xTransform, yTransform]) => { let { transform, sourceEvent } = _ref3, restEvent = _objectWithoutPropertiesLoose(_ref3, _excluded3); if (this.destroyed || !this.rendered) return; if (sourceEvent.type !== 'call' && (this.xCanZoom$ || this.yCanZoom$)) { point = null; transform0 = null; } if (sourceEvent.changedTouches) { if (longPress != 0) { clearTimeout(longPress); longPress = 0; hideTooltip(); } } this.zoomend$.call(null, _extends({}, restEvent, { sourceEvent, transform: [transform, xTransform, yTransform], scaleAxis: getScaleAxis.call(this) })); })).on('click', e => { if (this.destroyed) return; this.click$.call(null, { sourceEvent: e, target: selection, transform: [d3.zoomTransform(element), ...selection.datum()], scaleAxis: getScaleAxis.call(this), type: 'click' }); }).on('dblclick', e => { if (this.destroyed) return; this.dblclick$.call(null, { sourceEvent: e, target: selection, transform: [d3.zoomTransform(element), ...selection.datum()], scaleAxis: getScaleAxis.call(this), type: 'dblclick' }); }).on('contextmenu', e => { if (this.destroyed) return; this.contextmenu$.call(null, { sourceEvent: e, target: selection, transform: [d3.zoomTransform(element), ...selection.datum()], scaleAxis: getScaleAxis.call(this), type: 'contextmenu' }); e.preventDefault(); }).datum([d3.zoomIdentity, d3.zoomIdentity]).node(); selection.on('mouseout', e => { if (this.destroyed || !this.rendered) return; hideTooltip(e.relatedTarget); }).on('mousemove', e => { if (this.destroyed || !this.rendered) return; showTooltip(d3.pointer(e)); }); if (!this.zoom.doubleZoom) { // 取消双击放大 selection.on('dblclick.zoom', null); } if (this.zoom.x) { const xlock = this.rootSelection$.select('.xlock'); xlock.on('click', () => { if (this.destroyed) return; this.setCanZoom('x', !this.xCanZoom$); this.canZoom$('x', this.xCanZoom$); }); } if (this.zoom.y) { const ylock = this.rootSelection$.select('.ylock'); ylock.on('click', () => { if (this.destroyed) return; this.setCanZoom('y', !this.yCanZoom$); this.canZoom$('y', this.yCanZoom$); }); } if (this.zoom.x || this.zoom.y) { this.rootSelection$.select('.reset').on('click', () => { if (this.destroyed) return; this.reset(); this.reset$(); }); } if (this.download) { this.rootSelection$.select('.download').on('click', () => { if (this.destroyed) return; if (this.download.action) { this.download.action(() => { this.downloadImage(); }, this.rootSelection$); } else { this.downloadImage(); } }); } this.actions.forEach((a, i) => { if (!a) return; const { action, menus } = a; const am = this.rootSelection$.select(`.action-${i}-menu`); const at = this.rootSelection$.select(`.action-${i}`); at.on('click', e => { e.stopPropagation(); if (e.currentTarget === at.node()) { if (this.destroyed) return; if (action) { action(this.rootSelection$, () => {}); } const display = am.style('display'); am.style('display', display === 'none' ? 'block' : 'none'); } }); if (Array.isArray(menus)) { menus.forEach((menu, j) => { if (!menu) return; this.rootSelection$.select(`.action-${i}-menu-${j}`).on('click', e => { e.stopPropagation(); if (this.destroyed) return; if (menu.action) { menu.action(this.rootSelection$, () => {}); } am.style('display', 'none'); }); }); } }); if (this.actions.length) { d3.select('body').on('click', () => { this.rootSelection$.selectAll('.action-menu').style('display', 'none'); }); } } class BaseChart { constructor(...params) { const { container, width, height, padding = [0, 0, 0, 0], rWidth, rHeight, fontSize = 12, download, actions, tooltip, zoom, scale, data } = params[0] || {}; this.destroyed = false; this.rendered = false; if (typeof download === 'function') { this.download = { action: download }; } else if (typeof download === 'string') { this.download = { ext: download }; } else { this.download = download || false; } this.tooltip = !tooltip ? false : _extends({ cross: '' }, tooltip, { compute: (...args) => { let result = {}; if (typeof tooltip.compute === 'function') { result = tooltip.compute.call(this, result, ...args); } return result; } }); this.zoom = _extends({ /* x: { // 一旦domain设置了x或x2的某一类型,则translate和precision都要按照这一类型数据设置 domain: 'x', // 坐标缩放为1,位移为0时的坐标范围(会默认取scale的x或x2的domain) translate: [-Infinity, Infinity], // 坐标标尺可以拖动的范围值 precision: [(1 - 0) / 10, (1 - 0) * 10], // 坐标标尺缩到最小和最大的跨度值 }, */ x: null, y: null, doubleZoom: false }, zoom); this.scale = _extends({ /* x: { type: 'linear', // 坐标类型 ticks: 5, // 坐标刻度数目 format: (v) => v, // 坐标值格式化函数 domain: [0, 1], // 横坐标缩放为1,位移为0时的坐标范围 label: '', // 坐标名称 unit: '', // 坐标值单位 }, */ x: null, x2: null, y: null, y2: null }, scale); this.actions = Array.isArray(actions) ? actions : []; this.fontSize = fontSize; this.data = data || {}; this.dataDomains$ = {}; this.zoomer$ = d3.zoom(); this.xCanZoom$ = !!this.zoom.x; this.yCanZoom$ = !!this.zoom.y; createElement.call(this, container, { width, height, padding, rWidth, rHeight }); createScale.call(this); createZoom.call(this); bindEvents.call(this); this.click$ = util.noop; this.dblclick$ = util.noop; this.contextmenu$ = util.noop; this.zoomstart$ = util.noop; this.zooming$ = util.noop; this.zoomend$ = util.noop; this.resize$ = util.noop; this.reset$ = util.noop; this.canZoom$ = util.noop; } reset(ta) { this.render(d3.zoomIdentity, 'xy', ta); return this; } render(tf, ax = 'xy', ta = true) { this.rendered = true; const transform = (Array.isArray(tf) ? tf[0] : tf) || d3.zoomTransform(this.zoomSelection$.node()); // 对this.zoomSelection$调用this.zoomer$.transform函数变换到指定的transform // 变换过程中用240ms及ease函数进行transition (!ta ? this.zoomSelection$ : this.zoomSelection$.transition().duration(240).ease(d3.easeLinear)).call(this.zoomer$.transform, transform, null, { type: 'call', transform: tf, zoomX: ax.indexOf('x') !== -1, zoomY: ax.indexOf('y') !== -1 }); return this; } getCanZoom() { return [this.xCanZoom$, this.yCanZoom$]; } getTransform() { return [d3.zoomTransform(this.zoomSelection$.node()), ...this.zoomSelection$.datum()]; } setCanZoom(ax = 'xy', canZoom = true) { let xCanZoom = canZoom; let yCanZoom = canZoom; if (Array.isArray(canZoom)) { [xCanZoom = true, yCanZoom = true] = canZoom; } if (ax.indexOf('x') !== -1) { this.xCanZoom$ = xCanZoom; this.rootSelection$.select('.xlock').select('path').attr('d', xCanZoom ? unlockPath : lockPath); } if (ax.indexOf('y') !== -1) { this.yCanZoom$ = yCanZoom; this.rootSelection$.select('.ylock').select('path').attr('d', yCanZoom ? unlockPath : lockPath); } return this; } setEvent(type, handler) { if (typeof handler === 'function') { const oldHandler = this[`${type}$`]; this[`${type}$`] = (...args) => { handler.call(null, ...args, oldHandler.call(null, ...args)); }; } } runEvent(type, data = {}) { if (this.destroyed) return; const { sourceEvent, transform, scaleAxis, target } = data; this[`${type}$`].call(null, { type, sourceEvent: sourceEvent || null, target: target || this.zoomSelection$, transform: transform || [d3.zoomTransform(this.zoomSelection$.node()), ...this.zoomSelection$.datum()], scaleAxis: scaleAxis || getScaleAxis.call(this) }); } getData() { return this.data; } setData(data, render, computeDomain = util.noop) { if (!data) return this; this.rendered = false; this.data = data; // 根据data计算domain const needCompute = axisType.filter(key => this.scale[key] && !this.scale[key].domain); if (needCompute.length > 0) { this.dataDomains$ = computeDomain(this.data, needCompute) || {}; updateScale.call(this); updateZoom.call(this); } if (render) { this.render(); } return this; } setDomain(domain, render) { if (!domain) return this; let isScale = false; axisType.forEach(key => { if (this.scale[key] && domain[key]) { this.scale[key].domain = domain[key]; isScale = true; } }); if (isScale) { this.rendered = false; updateScale.call(this); let isZoom = false; ['x', 'y'].forEach(key => { isZoom = this.zoom[key] && (this.scale[key] && domain[key] || this.scale[`${key}2`] && domain[`${key}2`]); }); if (isZoom) { updateZoom.call(this); } if (render) { this.render(); } } return this; } setTranslate(translate, render) { if (!translate) return this; let isZoom = false; ['x', 'y'].forEach(key => { if (this.zoom[key]) { this.zoom[key].translate = translate[key]; isZoom = true; } }); if (isZoom) { this.rendered = false; updateZoom.call(this); if (render) { this.render(); } } return this; } setPrecision(precision, render) { if (!precision) return this; let isZoom = false; ['x', 'y'].forEach(key => { if (this.zoom[key]) { this.zoom[key].precision = precision[key]; isZoom = true; } }); if (isZoom) { this.rendered = false; updateZoom.call(this); if (render) { this.render(); } } return this; } setLabel(label, render) { if (!label) return this; let isScale = false; axisType.forEach(key => { if (this.scale[key] && label[key]) { this.scale[key].label = label[key].label; this.scale[key].unit = label[key].unit; isScale = true; } }); if (isScale) { this.rendered = false; if (render) { this.render(); } } return this; } destroy() { this.destroyed = true; this.rendered = false; this.data = null; if (this.zoomer$) { this.zoomer$.on('start', null).on('zoom', null).on('end', null); this.zoomer$ = null; } if (this.zoomSelection$) { this.zoomSelection$.on('click', null).on('dblclick', null).on('contextmenu', null).on('mouseout', null).on('mousemove', null); this.zoomSelection$ = null; } if (this.rootSelection$) { this.rootSelection$.select('.xlock').on('click', null); this.rootSelection$.select('.ylock').on('click', null); this.rootSelection$.select('.reset').on('click', null); if (this.actions && this.actions.length) { this.actions.forEach((a, i) => { if (!a) return; const { menus } = a; this.rootSelection$.select(`.action-${i}`).on('click', null); if (Array.isArray(menus)) { menus.forEach((menu, j) => { if (!menu) return; this.rootSelection$.select(`.action-${i}-menu-${j}`).on('click', null); }); } }); d3.select('body').on('click', null); } this.rootSelection$.remove(); this.rootSelection$ = null; } if (this.unBindResize$) { this.unBindResize$(); this.unBindResize$ = null; } this.click$ = null; this.dblclick$ = null; this.contextmenu$ = null; this.zoomstart$ = null; this.zooming$ = null; this.zoomend$ = null; this.resize$ = null; this.reset$ = null; this.canZoom$ = null; } resize() { if (document.createEvent) { const event = document.createEvent('Event'); event.initEvent('resize', true, true); window.dispatchEvent(event); } else if (document.createEventObject) { const event = document.createEventObject(); event.type = 'onresize'; window.fireEvent('onresize', event); } return this; } downloadImage(name, content) { const { ext, color, background } = this.download; const svgSel = this.rootSelection$.select('svg'); const width = +svgSel.attr('width'); const height = +svgSel.attr('height'); const svgNode = svgSel.node(); const tempCanvas = document.createElement('canvas'); tempCanvas.width = width; tempCanvas.height = height; const tempCxt = tempCanvas.getContext('2d'); tempCxt.clearRect(0, 0, width, height); if (background && background !== 'transparent') { tempCxt.fillStyle = background; tempCxt.fillRect(0, 0, width, height); } let tmpColor = svgNode.style.color; if (color) { svgNode.style.color = color; } const tempImage = new Image(); tempImage.src = `data:image/svg+xml;base64,${window.btoa(window.unescape(window.encodeURIComponent(`<?xml version="1.0" standalone="no"?> ${new XMLSerializer().serializeToString(svgNode)}`)))}`; svgNode.style.color = tmpColor; tempImage.onload = () => { tempCxt.drawImage(tempImage, 0, 0); if (content) { tempCxt.drawImage(content.image, content.x, content.y); } const a = document.createElement('a'); a.download = `${name || 'basechart'}.${ext || 'png'}`; a.href = tempCanvas.toDataURL(`image/${ext || 'png'}`); a.click(); }; } } export default BaseChart;