UNPKG

@huangjs888/d3-chart

Version:

Implement some charts based on d3 library.

824 lines (822 loc) 31.6 kB
const _excluded = ["data", "tooltip", "legend", "scale"], _excluded2 = ["heat"], _excluded3 = ["width", "height", "padding"], _excluded4 = ["heat"], _excluded5 = ["z"], _excluded6 = ["z"]; 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; } 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); } // @ts-nocheck /* * @Author: Huangjs * @Date: 2021-12-07 15:02:48 * @LastEditors: Huangjs * @LastEditTime: 2023-10-24 14:21:19 * @Description: 按需生成HeatMap构造器 */ import * as d3 from 'd3'; import BaseChart from '../BaseChart'; import LineGraph from '../LineGraph'; import * as util from '../util'; const iconSize = 18; const prefixSIFormat = d3.format('~s'); // 折半查找指定像素val在数组arr的哪个像素范围内 const searchValIndex = (val, arr) => { let n1 = 0; let n2 = arr.length; let c = 0; const binSize = n2 > 1 ? (arr[n2 - 1] - arr[0]) / (n2 - 1) : 1; let n; let test; if (binSize >= 0) { test = (a, b) => a <= b; } else { test = (a, b) => a > b; } // don't trust floating point equality - fraction of bin size to call const v = val + binSize * 1e-9 * (binSize >= 0 ? 1 : -1); // c is just to avoid infinite loops if there's an error while (n1 < n2 && c < 100) { c += 1; n = Math.floor((n1 + n2) / 2); if (test(arr[n], v)) n1 = n + 1;else n2 = n; } if (c > 90) window.console.log('Long binary search...'); return Math.min(Math.max(n1 - 1, 0), arr.length - 1); }; // 计算两值之间任意值占比以及前后值 const computeFactor = (val, val0, val1, bin0, bin1) => { const factor = (val - val0) / (val1 - val0) || 0; if (factor <= 0) { return { factor: 0, bin0, bin1: bin0 }; } if (factor > 0.5) { return { factor: 1 - factor, bin0: bin1, bin1: bin0 }; } return { factor, bin0, bin1 }; }; // 计算每个像素对应值的插值计算比例 const getInterpFactor = (pixel, valPixs) => { const index0 = searchValIndex(pixel, valPixs); const index1 = index0 + 1; return computeFactor(pixel, valPixs[index0], valPixs[index1], index0, index1); }; // 根据四个点计算中间点的平均值 const matrixAverage = (xFactor, val00, val01, yFactor, val10, val11) => { let val = 0; if (val00 !== undefined) { const dx = val01 - val00 || 0; const dy = val10 - val00 || 0; let dxy = 0; if (val01 === undefined) { if (val11 === undefined) dxy = 0;else if (val10 === undefined) dxy = 2 * (val11 - val00);else dxy = (2 * val11 - val10 - val00) * 2 / 3; } else if (val11 === undefined) { if (val10 === undefined) dxy = 0;else dxy = (2 * val00 - val01 - val10) * 2 / 3; } else if (val10 === undefined) dxy = (2 * val11 - val01 - val00) * 2 / 3;else dxy = val11 + val00 - val01 - val10; val = val00 + (xFactor || 0) * dx + (yFactor || 0) * (dy + (xFactor || 0) * dxy); } return val; }; // 将给定值数组中的值映射到给定长度的每一个像素点上 // 若值数组长度比像素点多,则会均匀抽稀映射 // 若值数组长度比像素点少,则会平滑插值映射 const valueForPixel = (value, pixel, reverse) => { const valPixs = []; const len = value.length; const minVal = value[0]; const maxVal = value[len - 1]; for (let i = 0; i < len; i += 1) { valPixs[reverse ? len - 1 - i : i] = Math.round(Math.round(100 * pixel * ((value[i] - minVal) / (maxVal - minVal))) / 100); } return valPixs; }; // 矩阵插值上色(根据矩形四个点值,计算矩形内部所有像素点的值,并渲染颜色) const matrixInterp = ({ w, h, d }, { x, y, z }, c) => { const [[x0, y0], [x1, y1]] = d; const cw = x1 - x0; const ch = y1 - y0; const xValPixs = valueForPixel(x, w); const yValPixs = valueForPixel(y, h, true); let pixels; let index = 0; try { pixels = new Uint8Array(cw * ch * 4); } catch (e) { pixels = new Array(cw * ch * 4); } const xInterpArray = []; for (let j = y0; j < y1; j += 1) { const yInterp = getInterpFactor(j, yValPixs); const val0 = z[yInterp.bin0] || []; const val1 = z[yInterp.bin1] || []; // 获取y轴(横向)范围内无效索引集合 const yInvalidIndex = y.invalid || []; // 判断当前横向一整条像素的上bin0下bin1都是无效索引,则无效 const yInvalid = yInvalidIndex.findIndex(o => o === yInterp.bin0 || o === yInterp.bin1) !== -1; for (let i = x0; i < x1; i += 1) { let rgba = { r: 0, g: 0, b: 0, opacity: 0 }; // 有效才计算颜色 if (!yInvalid) { let xInterp = xInterpArray[i]; if (!xInterp) { xInterp = getInterpFactor(i, xValPixs); xInterpArray[i] = xInterp; } // 获取x轴(竖向)范围内无效索引集合 const xInvalidIndex = x.invalid || []; // 判断当前像素点的左bin0右bin1都是无效索引,则无效 const xInvalid = xInvalidIndex.findIndex(o => o === xInterp.bin0 || o === xInterp.bin1) !== -1; // 有效才计算颜色 if (!xInvalid) { rgba = c(matrixAverage(xInterp.factor, val0[xInterp.bin0], val0[xInterp.bin1], yInterp.factor, val1[xInterp.bin0], val1[xInterp.bin1])); } } pixels[index] = rgba.r; pixels[index + 1] = rgba.g; pixels[index + 2] = rgba.b; pixels[index + 3] = rgba.opacity * 255; // 透明度0-1需要转换成0-255 index += 4; } } return pixels; }; function drawend(zContext, { xScale, yScale }) { zContext.save(); // 先清空画布 zContext.clearRect(0, 0, this.width$, this.height$); if (this.showHeat$) { const zScale = this.zScale$; let viewRange = null; const { x, y, z } = this.data.heat; // 原始范围 // 根据xy轴的刻度变换函数分别计算出原始数据图的上下左右坐标位置 const xMinPx = xScale(x[0] || 0); const xMaxPx = xScale(x[x.length - 1] || 0); // 因为y坐标是反转的(domain和range大小反转对应) const yMinPx = yScale(y[y.length - 1] || 0); const yMaxPx = yScale(y[0] || 0); // 原始数据的实际宽高 const width = Math.round(xMaxPx - xMinPx); const height = Math.round(yMaxPx - yMinPx); // 如果实际的宽高小于或等于0则表示压根没有数据 if (width > 0 && height > 0) { // 画布范围 // 画布上下左右坐标位置,this.width$, this.height$是画布的宽高 const xcMinPx = 0; const xcMaxPx = this.width$ + xcMinPx; const ycMinPx = 0; const ycMaxPx = this.height$ + ycMinPx; // 真实渲染的数据,是原始数据的范围,配置中筛选的范围以及画布的范围最终确定 // 根据以上三个范围计算确定真正需要渲染的范围,要考虑此范围的两个问题: // 1,对于原始数据段转换的像素范围,应该选择哪一段像素去渲染? // 2,对于视图,这一段数据应该渲染在画布上哪一个范围(在画布内部的位置)? // 以下计算需要渲染的像素段在原始数据像素范围内(宽和高)起点和终点的索引 const xMin = Math.max(xcMinPx, xMinPx); // 取x最小范围中的最大值 const xMax = Math.min(xcMaxPx, xMaxPx); // 取x最大范围中的最小值 const yMin = Math.max(ycMinPx, yMinPx); // 取y最小范围中的最大值 const yMax = Math.min(ycMaxPx, yMaxPx); // 取y最大范围中的最小值 // 实际被裁减后的宽高(可能等于画布宽高,也可能小于,但是不会大于因为被裁掉了) const dataRange = [[Math.round(xMin - xMinPx), Math.round(yMin - yMinPx)], // 筛选后数据在原数据的起点index [Math.round(xMax - xMinPx), Math.round(yMax - yMinPx)] // 筛选后数据在原数据的终点index ]; const cWidth = dataRange[1][0] - dataRange[0][0]; const cHeight = dataRange[1][1] - dataRange[0][1]; // 如果裁剪后的宽高小于或等于0则表示没有可以渲染的数据了 if (cWidth > 0 && cHeight > 0) { // 计算出渲染图片的每个像素rgba值 const pixelData = matrixInterp({ w: width, h: height, d: dataRange }, { x, y, z }, v => { // 老版本d3得到的rgba是字符串,新版本d3是对象,这里做个兼容 const rgba = zScale(v); return typeof rgba === 'string' ? d3.color(rgba) : rgba; }); // 创建一个空的图片数据(使用裁剪后的宽高) const imageData = zContext.createImageData(cWidth, cHeight); // 将像素数据设置到图片数据内 try { imageData.data.set(pixelData); } catch (e) { const cdata = imageData.data; const len = cdata.length; for (let i = 0; i < len; i += 1) { cdata[i] = pixelData[i]; } } // 以下计在算在画布上需要渲染的范围起点和终点的位置(相对于画布) viewRange = [[xMin - xcMinPx, yMin - ycMinPx], [xMax - xcMinPx, yMax - ycMinPx]]; // 将计算好的数据放到画布 zContext.putImageData(imageData, Math.round(viewRange[0][0]), Math.round(viewRange[0][1])); const { canvas } = this.tempCanvas$; if (canvas) { // 存储渲染后的裁剪数据和图片数据 canvas.width = cWidth; canvas.height = cHeight; canvas.getContext('2d').putImageData(imageData, 0, 0); } } } this.tempCanvas$.range = !viewRange ? null : viewRange.map(r => r.map((v, i) => (i === 0 ? xScale : yScale).invert(v))); } zContext.restore(); } function drawing(zContext, { xScale, yScale }) { if (this.showHeat$) { const lineMarkX = this.lineMark$[0]; if (lineMarkX.node()) { const xp = xScale(lineMarkX.datum()) || (!this.scale.x ? this.width$ : 0); lineMarkX.style('left', `${xp}px`).style('display', xp < 0 || xp >= this.width$ ? 'none' : 'block'); } const lineMarkY = this.lineMark$[1]; if (lineMarkY.node()) { const yp = yScale(lineMarkY.datum()) || (!this.scale.y ? 0 : this.height$); lineMarkY.style('top', `${yp}px`).style('display', yp < 0 || yp >= this.height$ ? 'none' : 'block'); } const { canvas, range } = this.tempCanvas$; if (range) { // 将上一次变换后的图,经过本次变换的变形重新绘制到画布上 const [[x0, y0], [x1, y1]] = range.map(r => r.map((v, i) => (i === 0 ? xScale : yScale)(v))); zContext.save(); zContext.clearRect(0, 0, this.width$, this.height$); zContext.drawImage(canvas, x0, y0, x1 - x0, y1 - y0); zContext.restore(); } } } function tipCompute(prevRes, point, scaleAxis) { const scaleOpt = this.scale; const { cross, average } = this.tooltip; const { point: newPoint, value } = cross === 'xy' ? this.getPointData(point, scaleAxis, average) : { point, value: [] }; if (value.length > 0) { // 去掉第一个因为第一个是标题 const prevValue = (prevRes.data || []).slice(1); const ndata = [...value.map((v, i) => { const k = i === 0 ? 'x' : i === 1 ? 'y' : 'z'; return _extends({}, scaleOpt[scaleAxis[k] ? k : `${k}${k != 'z' ? '2' : ''}`], { value: v }); }), ...prevValue]; const result = selection => { selection.selectAll('div').data(ndata).join('div').attr('style', 'white-space: nowrap;').html((d, i) => i === 0 ? `${d.label ? `${d.label}: ` : ''}${d.format(d.value)}${d.unit || ''}` : `${!d.color ? '' : `<span style="background: ${d.color}; width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 8px;"></span>`}<span>${d.label ? `${d.label}: ` : ''}</span><span style="display: inline-block; ${!d.color ? '' : 'margin-left: 30px;'}">${d.format(d.value)}${d.unit || ''}</span>`); }; return { x0: newPoint[0], y0: newPoint[1], data: ndata, result }; } return _extends({}, prevRes); } function updateScale() { if (this.scale.z) { const zFormat = this.scale.z.format || (v => v); let [opacity, range, domain] = this.scale.z.domain || []; opacity = opacity || 1; range = range || ['#000', '#fff']; domain = domain || [0, 1]; this.zScale$.range(range.map(c => d3.color(c).copy({ opacity }))).domain(domain).clamp(true); // 设置true可以卡住所给不在domain中的参数生成的数据仍然在range范围内 if (this.legend) { const linearGradient = this.rootSelection$.select('linearGradient'); const heatLegend = this.rootSelection$.select('.heatLegend'); const height = this.height$; const startDomain = domain[0]; const deltDomain = domain[domain.length - 1] - startDomain; const width = this.legend.width; linearGradient.selectAll('stop').data(domain).join('stop').attr('offset', v => `${100 * (v - startDomain) / deltDomain}%`).attr('stop-color', (_, i) => range[i]); heatLegend.selectAll('g').data(domain).join(enter => { const tick = enter.append('g').attr('class', 'tick').attr('transform', v => `translate(${width},${height * (1 - (v - startDomain) / deltDomain)})`); tick.append('path').attr('d', 'M0,0 L4,4 L4,-4 Z'); tick.append('text').attr('x', 8).attr('dy', '0.32em').attr('text-anchor', 'start').text(v => zFormat(v)); return tick; }, update => { update.attr('transform', v => `translate(${width},${height * (1 - (v - startDomain) / deltDomain)})`); update.select('text').text(v => zFormat(v)); return update; }); } } } function doubleClick(point, { xScale, yScale }) { let result = {}; if (this.showHeat$) { const heatData = this.data.heat; let [x0, y0] = point; let [xval, yval] = []; const lineMarkX = this.lineMark$[0]; if (lineMarkX.node()) { xval = Math.max(Math.min(+xScale.invert(x0), heatData.x[heatData.x.length - 1]), heatData.x[0]); x0 = xScale(xval); lineMarkX.style('left', `${x0}px`).style('display', 'block'); } const lineMarkY = this.lineMark$[1]; if (lineMarkY.node()) { yval = Math.max(Math.min(+yScale.invert(y0), heatData.y[heatData.y.length - 1]), heatData.y[0]); y0 = yScale(yval); lineMarkY.style('top', `${y0}px`).style('display', 'block'); } this.setLineMark([xval, yval], res => { result = res; }); } return result; } export default function generateHeatMap(superName) { class HeatMap extends (superName === 'LineGraph' ? LineGraph : BaseChart) { constructor(...params) { const _ref = params[0] || {}, { data, tooltip, legend, scale } = _ref, restOptions = _objectWithoutPropertiesLoose(_ref, _excluded); const _ref2 = data || {}, { heat } = _ref2, restData = _objectWithoutPropertiesLoose(_ref2, _excluded2); let rWidth = restOptions.rWidth; if (legend) { legend.show = !!legend.show; legend.left = util.isNumber(legend.left) ? legend.left : 0; legend.width = util.isNumber(legend.width) ? legend.width : 0; legend.right = util.isNumber(legend.right) ? legend.right : 0; if (legend.show) { rWidth = (util.isNumber(rWidth) ? rWidth : 0) + legend.left + legend.width + legend.right; } } super(_extends({}, restOptions, { rWidth, data: _extends({ heat: !heat ? { x: [], y: [], z: [] } : { x: heat.x || [], y: heat.y || [], z: heat.z || [] } }, restData), tooltip: !tooltip ? false : _extends({ cross: 'xy', select: '', average: true }, tooltip, { compute: (res, ...args) => { let result = tipCompute.call(this, res, ...args); if (typeof tooltip.compute === 'function') { result = tooltip.compute.call(this, result, ...args); } return result; } }), scale: _extends({ /* z: { type: 'linear', // 坐标类型 ticks: 5, // 坐标刻度数目 format: (v) => v, // 坐标值格式化函数 domain: [0, ['#fff', '#fff'], [0, 0]], // 值的色域范围和透明度 label: '', // 坐标名称 unit: '', // 坐标值单位 }, */ z: { format: prefixSIFormat } }, scale) })); this.legend = legend; this.showHeat$ = true; const tempText = `${this.scale.z.label || ''}${this.scale.z.subLabel ? ` ( ${this.scale.z.subLabel} )` : ''}${this.scale.z.unit ? ` ( ${this.scale.z.unit} )` : ''}`; const baselineDelt = this.fontSize + 2; const heatLabel = this.rootSelection$.select('g.group').append('g').attr('class', 'heatLabel').attr('fill', 'currentColor').attr('transform', `translate(${this.scale.y ? 10 : this.width$ - 10},${this.scale.x ? baselineDelt - this.padding[0] : this.height$ + this.padding[2] - iconSize + baselineDelt})`); heatLabel.append('text').attr('dx', (this.scale.y ? 1 : -1) * util.measureSvgText(tempText, this.fontSize) / 2).text(tempText); if (this.tooltip) { const { select } = this.tooltip; (select || '').split('').forEach(key => { if (key) { this.zoomSelection$.append('div').attr('class', `${key}-linemark`).style('background', '#fa9305').style('display', 'none').style('position', 'absolute').style('top', !this.scale.y ? 0 : this.height$ - 1).style('left', !this.scale.x ? this.width$ - 1 : 0).style('width', key === 'x' ? '1px' : '100%').style('height', key === 'x' ? '100%' : '1px'); } }); } const lineMark = [this.zoomSelection$.select('.x-linemark'), this.zoomSelection$.select('.y-linemark')]; this.lineMark$ = lineMark; const zCanvasParent = this.rootSelection$.insert('div', 'svg').style('position', 'absolute').style('width', `${this.width$}px`).style('height', `${this.height$}px`).style('top', `${this.padding[0]}px`).style('left', `${this.padding[3]}px`); const zCanvas = zCanvasParent.append('canvas').style('width', '100%').style('height', '100%').attr('width', this.width$).attr('height', this.height$); if (legend) { const gradientId = util.guid('gradient'); this.rootSelection$.select('svg').select('defs').append('linearGradient').attr('id', gradientId).attr('x1', '0%').attr('y1', '100%').attr('x2', '0%').attr('y2', '0%'); this.rootSelection$.select('svg').select('.group').append('g').attr('class', 'heatLegend').style('display', legend.show ? 'block' : 'none').attr('fill', 'currentColor').attr('transform', `translate(${this.width$ + this.padding[1] + legend.left},0)`).append('rect').attr('x', 0).attr('y', 0).attr('width', legend.width).attr('height', this.height$).attr('fill', `url(#${gradientId})`); } const zContext = zCanvas.node().getContext('2d'); this.zScale$ = d3.scaleLinear(); updateScale.call(this); this.tempCanvas$ = { canvas: document.createElement('canvas'), range: null }; const dblclick$$ = this.dblclick$; this.dblclick$ = (e, ...args) => { dblclick$$.call(null, e, ...args); const { x, x2, y, y2 } = e.scaleAxis; const xScale = (x || x2).scale; const yScale = (y || y2).scale; return doubleClick.call(this, e.sourceEvent ? d3.pointer(e.sourceEvent) : [0, 0], { xScale, yScale }); }; const click$$ = this.click$; this.click$ = (e, ...args) => { click$$.call(null, e, ...args); lineMark.forEach(lm => lm.node() && lm.style('display', 'none')); }; const contextmenu$$ = this.contextmenu$; this.contextmenu$ = (e, ...args) => { contextmenu$$.call(null, e, ...args); lineMark.forEach(lm => lm.node() && lm.style('display', 'none')); }; const zoomstart$$ = this.zoomstart$; this.zoomstart$ = (e, ...args) => { zoomstart$$.call(null, e, ...args); if (this.debounceDrawend$) { // 每次开始前把上一次结束时需要调用的取消掉,防止调用两次有闪动 this.debounceDrawend$.cancel(); } }; const zooming$$ = this.zooming$; this.zooming$ = (e, ...args) => { zooming$$.call(null, e, ...args); const { x, x2, y, y2 } = e.scaleAxis; const xScale = (x || x2).scale; const yScale = (y || y2).scale; drawing.call(this, zContext, { xScale, yScale }); }; const zoomend$$ = this.zoomend$; this.debounceDrawend$ = util.debounce(drawend, 300, { leading: false, trailing: true }); this.zoomend$ = (e, ...args) => { zoomend$$.call(null, e, ...args); const { x, x2, y, y2 } = e.scaleAxis; const xScale = (x || x2).scale; const yScale = (y || y2).scale; this.debounceDrawend$.call(this, zContext, { xScale, yScale }); // 用户自己调用渲染的时候,可以在下一事件循环立即调用 if (!e.sourceEvent || e.sourceEvent.type === 'call') { util.delay(() => { this.debounceDrawend$.flush(); }, 1); } }; const resize$$ = this.resize$; this.resize$ = (e, _ref3, ...args) => { let { width, height, padding } = _ref3, rest = _objectWithoutPropertiesLoose(_ref3, _excluded3); resize$$.call(null, e, _extends({ width, height, padding }, rest), ...args); zCanvasParent.style('width', `${width}px`).style('height', `${height}px`).style('top', `${padding[0]}px`).style('left', `${padding[3]}px`); zCanvas.attr('width', width).attr('height', height); heatLabel.attr('transform', `translate(${this.scale.y ? 10 : width - 10},${this.scale.x ? baselineDelt - padding[0] : height + padding[2] - iconSize + baselineDelt})`); if (legend) { const heatLegend = this.rootSelection$.select('.heatLegend'); heatLegend.attr('transform', `translate(${width + padding[1] + legend.left},0)`); heatLegend.select('rect').attr('height', height); const ticks = heatLegend.selectAll('.tick'); const domain = ticks.data(); const startDomain = domain[0]; const deltDomain = domain[domain.length - 1] - startDomain; ticks.attr('transform', v => `translate(${legend.width},${height * (1 - (v - startDomain) / deltDomain)})`); } }; heatLabel.on('click', () => { if (this.destroyed) return; this.showHeat$ = !this.showHeat$; lineMark.forEach(lm => lm.node() && lm.style('display', this.showHeat$ ? 'block' : 'none')); heatLabel.attr('fill', !this.showHeat$ ? '#aaa' : 'currentColor'); if (this.rendered) { this.render(); } }); } getPointData(point, scaleAxis, avg) { let currentPoint = point; if (!Array.isArray(point)) { currentPoint = d3.pointer(point, this.zoomSelection$.node()); } let [x0, y0] = currentPoint; const data = this.data.heat; const value = []; if (this.showHeat$) { const xScale = (scaleAxis.x || scaleAxis.x2).scale; const yScale = (scaleAxis.y || scaleAxis.y2).scale; let xval = xScale.invert(x0); let yval = yScale.invert(y0); let zval = 0; const [xi0, xi1] = util.findNearIndex(+xval, data.x); const [yi0, yi1] = util.findNearIndex(+yval, data.y); // 确保point在图层内 if (xi0 >= 0 && xi1 >= 0 && yi0 >= 0 && yi1 >= 0) { const xval0 = +data.x[xi0]; const xval1 = +data.x[xi1]; const yval0 = +data.y[yi0]; const yval1 = +data.y[yi1]; if (xval0 === xval1 && yval0 === yval1) { // 正好移动到了xy交叉数据点上 zval = (data.z[yi0] || [])[xi0]; } else if (avg) { const xInterp = computeFactor(+xval, xval0, xval1, xi0, xi1); const yInterp = computeFactor(+yval, yval0, yval1, yi0, yi1); const val00 = (data.z[yInterp.bin0] || [])[xInterp.bin0]; const val01 = (data.z[yInterp.bin0] || [])[xInterp.bin1]; const val10 = (data.z[yInterp.bin1] || [])[xInterp.bin0]; const val11 = (data.z[yInterp.bin1] || [])[xInterp.bin1]; zval = matrixAverage(xInterp.factor, val00, val01, yInterp.factor, val10, val11); } else { const xi = Math.abs(xval - xval0) > Math.abs(xval - xval1) ? xi1 : xi0; const yi = Math.abs(yval - yval0) > Math.abs(yval - yval1) ? yi1 : yi0; xval = +data.x[xi]; yval = +data.y[yi]; zval = (data.z[yi] || [])[xi]; x0 = xScale(xval); y0 = yScale(yval); } value.push(xval, yval, zval); } } return { value, point: [x0, y0] }; } getLineMark() { const result = []; const lineMarkX = this.lineMark$[0]; if (lineMarkX.node()) { const xval = lineMarkX.datum(); result[0] = xval; } const lineMarkY = this.lineMark$[1]; if (lineMarkY.node()) { const yval = lineMarkY.datum(); result[1] = yval; } return result; } setLineMark(lm, cb) { const lineMark = lm || []; const { average } = this.tooltip; const heatData = this.data.heat; let xSelect = null; let ySelect = null; const lineMarkX = this.lineMark$[0]; if (lineMarkX.node()) { const xval = !lineMark[0] && lineMark[0] !== 0 ? heatData.x[0] : lineMark[0]; lineMarkX.datum(xval); let zval = []; // xval在数据范围之内才可计算出zval if (xval >= heatData.x[0] && xval <= heatData.x[heatData.x.length - 1]) { let xi = util.findNearIndex(xval, heatData.x, !average); if (!average) xi = [xi, xi]; let xBin = 0; if (xi[0] !== xi[1]) xBin = (xval - heatData.x[xi[0]]) / (heatData.x[xi[1]] - heatData.x[xi[0]]); zval = heatData.z.map(v => { const vv = v || []; return xBin * ((vv[xi[1]] || 0) - (vv[xi[0]] || 0)) + (vv[xi[0]] || 0); }); } xSelect = { x: xval, y: heatData.y, z: zval }; } const lineMarkY = this.lineMark$[1]; if (lineMarkY.node()) { const yval = !lineMark[1] && lineMark[1] !== 0 ? heatData.y[0] : lineMark[1]; lineMarkY.datum(yval); let zval = []; // yval在数据范围之内才可计算出zval if (yval >= heatData.y[0] && yval <= heatData.y[heatData.y.length - 1]) { let yi = util.findNearIndex(yval, heatData.y, !average); if (!average) yi = [yi, yi]; let yBin = 0; if (yi[0] !== yi[1]) yBin = (yval - heatData.y[yi[0]]) / (heatData.y[yi[1]] - heatData.y[yi[0]]); zval = (heatData.z[yi[0]] || []).map((v0, i) => { const v1 = (heatData.z[yi[1]] || [])[i] || 0; return yBin * (v1 - v0) + v0; }); } ySelect = { x: heatData.x, y: yval, z: zval }; } if (typeof cb === 'function') { cb({ xSelect, ySelect }); } return this; } setData(data, render, computeDomain) { if (!data) return this; const _ref4 = data || {}, { heat } = _ref4, restData = _objectWithoutPropertiesLoose(_ref4, _excluded4); super.setData(_extends({ heat: !heat ? { x: [], y: [], z: [] } : { x: heat.x || [], y: heat.y || [], z: heat.z || [] } }, restData), false, !heat ? computeDomain : ({ heat: heatData }, needDomain) => { const domains = {}; needDomain.forEach(key => { if (heatData[key] && heatData[key].length > 0) { domains[key] = d3.extent([...heatData[key]]); } }); return domains; }); // 将linemark归位到起点 this.setLineMark(); if (render) { this.render(); } return this; } setDomain(domain, render) { if (!domain) return this; const { z } = domain, rest = _objectWithoutPropertiesLoose(domain, _excluded5); super.setDomain(rest); if (z && this.scale.z) { this.scale.z.domain = z; updateScale.call(this); if (render) { this.render(); } } return this; } setLabel(label, render) { if (!label) return this; const { z } = label, rest = _objectWithoutPropertiesLoose(label, _excluded6); super.setLabel(rest); if (z && this.scale.z) { this.scale.z.label = z.label; this.scale.z.subLabel = z.subLabel; this.scale.z.unit = z.unit; const tempText = `${this.scale.z.label || ''}${this.scale.z.subLabel ? ` ( ${this.scale.z.subLabel} )` : ''}${this.scale.z.unit ? ` ( ${this.scale.z.unit} )` : ''}`; this.rootSelection$.select('.heatLabel').select('text').attr('dx', (this.scale.y ? 1 : -1) * util.measureSvgText(tempText, this.fontSize) / 2).text(tempText); if (render) { this.render(); } } return this; } downloadImage() { const zCanvas = this.rootSelection$.select('canvas').node(); const svgDiv = this.rootSelection$.select('.actions'); const left = window.parseInt(svgDiv.style('left')); const top = window.parseInt(svgDiv.style('top')); super.downloadImage((this.scale.z || {}).label, { image: zCanvas, x: left, y: top }); } destroy() { if (this.debounceDrawend$) { this.debounceDrawend$.cancel(); this.debounceDrawend$ = null; } this.zScale$ = null; this.tempCanvas$ = 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; super.destroy(); return this; } } return HeatMap; }