diff --git a/site/.dumi/global.ts b/site/.dumi/global.ts index d4497f206e..e9f813a2de 100644 --- a/site/.dumi/global.ts +++ b/site/.dumi/global.ts @@ -24,6 +24,8 @@ if (window) { (window as any).gWebgl = require('@antv/g-webgl'); (window as any).fecha = require('fecha'); (window as any).React = require('react'); + (window as any).dataSet = require('@antv/data-set'); + (window as any).lodash = require('lodash'); } if ( diff --git a/site/examples/general/heatmap/demo/heatmap-density.ts b/site/examples/general/heatmap/demo/heatmap-density.ts new file mode 100644 index 0000000000..e42749739c --- /dev/null +++ b/site/examples/general/heatmap/demo/heatmap-density.ts @@ -0,0 +1,54 @@ +import DataSet from '@antv/data-set'; +import { Chart } from '@antv/g2'; + +const chart = new Chart({ + container: 'container', + theme: 'classic', + autoFit: true, +}); + +chart.data({ + type: 'fetch', + value: 'https://assets.antv.antgroup.com/g2/diamond.json', +}); + +chart.scale('x', { nice: true, domainMin: -0.5 }); +chart.scale('y', { nice: true, domainMin: -2000 }); +chart.scale('color', { nice: true }); + +chart + .heatmap() + .data({ + transform: [ + { + type: 'custom', + callback: (data) => { + const dv = new DataSet.View().source(data); + dv.transform({ + type: 'kernel-smooth.density', + fields: ['carat', 'price'], + as: ['carat', 'price', 'density'], + }); + return dv.rows; + }, + }, + ], + }) + .encode('x', 'carat') + .encode('y', 'price') + .encode('color', 'density') + .style({ + opacity: 0.3, + gradient: { + '0': 'white', + '0.2': 'blue', + '0.4': 'cyan', + '0.6': 'lime', + '0.8': 'yellow', + '0.9': 'red', + }, + }); + +chart.point().encode('x', 'carat').encode('y', 'price'); + +chart.render(); diff --git a/site/examples/general/heatmap/demo/heatmap.ts b/site/examples/general/heatmap/demo/heatmap.ts new file mode 100644 index 0000000000..b6c5d0027a --- /dev/null +++ b/site/examples/general/heatmap/demo/heatmap.ts @@ -0,0 +1,37 @@ +import { Chart } from '@antv/g2'; + +const chart = new Chart({ + container: 'container', + theme: 'classic', + autoFit: true, + padding: 0, +}); + +chart.axis(false); + +chart + .image() + .data([0]) + .encode( + 'src', + 'https://gw.alipayobjects.com/zos/rmsportal/NeUTMwKtPcPxIFNTWZOZ.png', + ) + .style('x', '50%') + .style('y', '50%') + .style('width', '100%') + .style('height', '100%') + .tooltip(false); + +chart + .heatmap() + .data({ + type: 'fetch', + value: 'https://assets.antv.antgroup.com/g2/heatmap.json', + }) + .encode('x', 'g') + .encode('y', 'l') + .encode('color', 'tmp') + .style('opacity', 0) + .tooltip(false); + +chart.render(); diff --git a/site/examples/general/heatmap/demo/meta.json b/site/examples/general/heatmap/demo/meta.json new file mode 100644 index 0000000000..0b3c45728d --- /dev/null +++ b/site/examples/general/heatmap/demo/meta.json @@ -0,0 +1,32 @@ +{ + "title": { + "zh": "中文分类", + "en": "Category" + }, + "demos": [ + { + "filename": "heatmap.ts", + "title": { + "zh": "密度热力图", + "en": "Heatmap" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ze7gSYylw_QAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "heatmap-density.ts", + "title": { + "zh": "散点分布热力", + "en": "Heatmap Density" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*0AfVQpGgcsoAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "mouse-heatmap.ts", + "title": { + "zh": "鼠标热力图", + "en": "Mouse Heatmap" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*kC58R7TEdd0AAAAAAAAAAAAADmJ7AQ/original" + } + ] +} diff --git a/site/examples/general/heatmap/demo/mouse-heatmap.ts b/site/examples/general/heatmap/demo/mouse-heatmap.ts new file mode 100644 index 0000000000..c15df8265c --- /dev/null +++ b/site/examples/general/heatmap/demo/mouse-heatmap.ts @@ -0,0 +1,59 @@ +import { Chart } from '@antv/g2'; +import { throttle } from 'lodash'; + +const data = {}; + +const chart = new Chart({ + container: 'container', + theme: 'classic', + width: 640, + height: 480, + padding: 0, +}); + +chart.style({ + viewFill: '#4e79a7', +}); + +chart.data([]); +chart.axis(false); + +chart + .heatmap() + .encode('x', 'x') + .encode('y', 'y') + .encode('color', 'v') + .scale('x', { domain: [0, 640] }) + .scale('y', { domain: [0, 480], range: [0, 1] }) + .style('opacity', 0); + +chart.render(); + +chart.on( + 'plot:pointermove', + throttle((e) => { + const { x, y } = e; + + const kx = Math.floor(x - (x % 8)); + const ky = Math.floor(y - (y % 8)); + + if (!data[kx]) data[kx] = {}; + if (!data[kx][ky]) data[kx][ky] = 0; + + data[kx][ky] += 1; + + const d = transform(data); + + chart.changeData(d); + }), +); + +function transform(dataMap) { + const arr = []; + Object.keys(dataMap).forEach((x) => { + Object.keys(dataMap[x]).forEach((y) => { + arr.push({ x, y, v: dataMap[x][y] }); + }); + }); + return arr; +} diff --git a/site/examples/general/heatmap/index.en.md b/site/examples/general/heatmap/index.en.md new file mode 100644 index 0000000000..b8931f628b --- /dev/null +++ b/site/examples/general/heatmap/index.en.md @@ -0,0 +1,4 @@ +--- +title: Heatmap +order: 20 +--- \ No newline at end of file diff --git a/site/examples/general/heatmap/index.zh.md b/site/examples/general/heatmap/index.zh.md new file mode 100644 index 0000000000..be5ee203ba --- /dev/null +++ b/site/examples/general/heatmap/index.zh.md @@ -0,0 +1,4 @@ +--- +title: 热力图 +order: 20 +--- \ No newline at end of file diff --git a/site/package.json b/site/package.json index 3c2bec02d1..3ca8258a01 100644 --- a/site/package.json +++ b/site/package.json @@ -8,6 +8,7 @@ "preview": "dumi preview" }, "dependencies": { + "@antv/data-set": "^0.11.8", "@antv/dumi-theme-antv": "^0.3.0", "@antv/g-lottie-player": "^0.0.13", "@antv/g-pattern": "^0.0.3", @@ -26,6 +27,7 @@ "d3-voronoi": "^1.1.4", "dumi": "^2.0.0", "fecha": "^4.2.3", + "lodash": "^4.17.21", "topojson": "^3.0.2", "webfontloader": "1.6.28" }, diff --git a/src/api/mark/index.ts b/src/api/mark/index.ts index 724630296c..c826cb41c8 100644 --- a/src/api/mark/index.ts +++ b/src/api/mark/index.ts @@ -22,6 +22,7 @@ import { Treemap, Boxplot, Density, + Heatmap, Shape, Pack, ForceGraph, @@ -55,6 +56,7 @@ export interface Mark { treemap(): Treemap; boxplot(): Boxplot; density(): Density; + heatmap(): Heatmap; shape(): Shape; pack(): Pack; forceGraph(): ForceGraph; @@ -89,6 +91,7 @@ export const mark = { treemap: Treemap, boxplot: Boxplot, density: Density, + heatmap: Heatmap, shape: Shape, pack: Pack, forceGraph: ForceGraph, diff --git a/src/api/mark/mark.ts b/src/api/mark/mark.ts index 5416b09d9f..5af176a643 100644 --- a/src/api/mark/mark.ts +++ b/src/api/mark/mark.ts @@ -20,6 +20,7 @@ import { SankeyMark, BoxPlotMark, DensityMark, + HeatmapMark, ShapeMark, TreemapMark, ForceGraphMark, @@ -118,6 +119,10 @@ export interface Density extends API, Density> { type: 'density'; } +export interface Heatmap extends API, Heatmap> { + type: 'heatmap'; +} + export interface Shape extends API, Shape> { type: 'shape'; } @@ -335,6 +340,13 @@ export class Density extends MarkNode { } } +@defineProps(props) +export class Heatmap extends MarkNode { + constructor() { + super({}, 'heatmap'); + } +} + @defineProps([...props, { name: 'layout', type: 'value' }]) export class Pack extends MarkNode { constructor() { diff --git a/src/shape/heatmap/heatmap.ts b/src/shape/heatmap/heatmap.ts index d215e22c25..d6ebdbf4d0 100644 --- a/src/shape/heatmap/heatmap.ts +++ b/src/shape/heatmap/heatmap.ts @@ -51,15 +51,18 @@ export const Heatmap: SC = (options) => { blur, useGradientOpacity, }; - const ctx = HeatmapRenderer( - width, - height, - min, - max, - data, - deleteKey(options, (v) => v === undefined), - createCanvas, - ); + const ctx = + width && height + ? HeatmapRenderer( + width, + height, + min, + max, + data, + deleteKey(options, (v) => v === undefined), + createCanvas, + ) + : { canvas: null }; return select(new GImage()) .call(applyStyle, shapeTheme)