diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93cb825266..cc9ee6a22b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,7 +88,7 @@ Feel free to add more content in the body, if you { think } subject is not self- e.g. -``` +```plain fix($compile): [BREAKING_CHANGE] couple of unit tests for IE9 Older IEs serialize html uppercased, but IE9 does not... diff --git a/__tests__/unit/adaptor/pattern-spec.ts b/__tests__/unit/adaptor/pattern-spec.ts new file mode 100644 index 0000000000..bf64bc8bdd --- /dev/null +++ b/__tests__/unit/adaptor/pattern-spec.ts @@ -0,0 +1,69 @@ +import { flow } from '../../../src/utils/flow'; +import { Plot } from '../../../src/core/plot'; +import { pattern } from '../../../src/adaptor/pattern'; +import { theme } from '../../../src/adaptor/common'; +import { createDiv } from '../../utils/dom'; +import { createSquarePattern } from '../../../src/utils/pattern/square'; + +describe('pattern adaptor', () => { + class APlot extends Plot { + type: 'a-plot'; + + getDefaultOptions() { + return {}; + } + + getSchemaAdaptor() { + return (params: any) => flow(theme, pattern('xxx'))(params); + } + } + + const plot = new APlot(createDiv(), { + color: ['red', 'yellow'], + xxx: { + stroke: 'red', + }, + }); + + it('default', () => { + expect(typeof pattern('xxx')).toBe('function'); + let params = pattern('xxx')({ chart: plot.chart, options: plot.options }); + expect(params.options.xxx.fill).toBeUndefined(); + + params = pattern('xxx')({ chart: plot.chart, options: { ...plot.options, pattern: { type: 'dot' } } }); + expect(params.options.xxx).not.toBeUndefined(); + expect(params.options.xxx.call().fill instanceof CanvasPattern).toBe(true); + + // callback + params = pattern('xxx')({ chart: plot.chart, options: { ...plot.options, pattern: () => ({ type: 'line' }) } }); + expect(params.options.xxx.call().fill instanceof CanvasPattern).toBe(true); + + // canvasPattern + const suqarePattern = createSquarePattern({ size: 10, padding: 0, isStagger: true }); + params = pattern('xxx')({ chart: plot.chart, options: { ...plot.options, pattern: suqarePattern } }); + expect(params.options.xxx.call().fill instanceof CanvasPattern).toBe(true); + }); + + it('xxx configuration', () => { + let params = pattern('xxx')({ chart: plot.chart, options: { ...plot.options, pattern: { type: 'dot' } } }); + expect(params.options.xxx.call().stroke).toBe('red'); + + params = pattern('xxx')({ + chart: plot.chart, + options: { ...plot.options, xxx: () => ({ stroke: 'red', fill: 'yellow' }), pattern: null }, + }); + expect(params.options.xxx.call().stroke).toBe('red'); + expect(params.options.xxx.call().fill).toBe('yellow'); + + params = pattern('xxx')({ + chart: plot.chart, + options: { ...plot.options, xxx: () => ({ stroke: 'red', fill: 'yellow' }), pattern: { type: 'dot' } }, + }); + expect(params.options.xxx.call().stroke).toBe('red'); + expect(params.options.xxx.call().fill instanceof CanvasPattern).toBe(true); + }); + + afterAll(() => { + plot.destroy(); + }); +}); diff --git a/__tests__/unit/plots/area/pattern-spec.ts b/__tests__/unit/plots/area/pattern-spec.ts new file mode 100644 index 0000000000..df08289fd6 --- /dev/null +++ b/__tests__/unit/plots/area/pattern-spec.ts @@ -0,0 +1,77 @@ +import { Area } from '../../../../src'; +import { percentData } from '../../../data/area'; +import { createDiv } from '../../../utils/dom'; + +describe('area', () => { + it('pattern: obj', () => { + const area = new Area(createDiv(), { + data: percentData, + width: 400, + height: 300, + xField: 'year', + yField: 'value', + seriesField: 'country', + pattern: { + type: 'line', + }, + }); + + area.render(); + + const geometry = area.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + area.update({ + pattern: { + type: 'dot', + }, + }); + + expect(area.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(area.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(area.chart.geometries[0].elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + area.destroy(); + }); + + it('pattern: function', () => { + const area = new Area(createDiv(), { + data: percentData, + width: 400, + height: 300, + xField: 'year', + yField: 'value', + seriesField: 'country', + pattern: ({ country }) => { + if (country === 'Asia') { + return { + type: 'line', + }; + } + }, + }); + area.render(); + + const geometry = area.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + area.update({ + pattern: ({ country }) => { + if (country === 'Africa') { + return { + type: 'line', + }; + } + }, + }); + expect(area.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(area.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + area.destroy(); + }); +}); diff --git a/__tests__/unit/plots/bar/pattern-spec.ts b/__tests__/unit/plots/bar/pattern-spec.ts new file mode 100644 index 0000000000..806a040bad --- /dev/null +++ b/__tests__/unit/plots/bar/pattern-spec.ts @@ -0,0 +1,73 @@ +import { Bar } from '../../../../src'; +import { salesByArea } from '../../../data/sales'; +import { createDiv } from '../../../utils/dom'; + +describe('bar style', () => { + it('pattern: obj', () => { + const bar = new Bar(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'sales', + yField: 'area', + pattern: { + type: 'line', + }, + }); + + bar.render(); + + const geometry = bar.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + bar.update({ + pattern: { + type: 'dot', + }, + }); + + expect(bar.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(bar.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(bar.chart.geometries[0].elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + bar.destroy(); + }); + + it('pattern: callback', () => { + const bar = new Bar(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'sales', + yField: 'area', + pattern: ({ area }) => { + if (area === '华北') { + return { type: 'dot' }; + } + }, + }); + + bar.render(); + + const geometry = bar.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + bar.update({ + pattern: ({ area }) => { + if (area === '西南') { + return { type: 'dot' }; + } + }, + }); + + expect(bar.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(bar.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + bar.destroy(); + }); +}); diff --git a/__tests__/unit/plots/circle-packing/pattern-spec.ts b/__tests__/unit/plots/circle-packing/pattern-spec.ts new file mode 100644 index 0000000000..754e129e51 --- /dev/null +++ b/__tests__/unit/plots/circle-packing/pattern-spec.ts @@ -0,0 +1,66 @@ +import { CirclePacking } from '../../../../src'; +import { createDiv } from '../../../utils/dom'; +import { DATA } from '../../../data/circle-packing'; + +describe('Circle-Packing', () => { + const div = createDiv(); + it('pattern: obj', () => { + const plot = new CirclePacking(div, { + padding: 0, + data: DATA, + hierarchyConfig: { + sort: (a, b) => b.depth - a.depth, + }, + pattern: { + type: 'line', + }, + }); + plot.render(); + + const geometry = plot.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + plot.update({ + pattern: null, + }); + + expect(plot.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + plot.destroy(); + }); + + it('pattern: callback', () => { + const plot = new CirclePacking(div, { + padding: 0, + data: DATA, + hierarchyConfig: { + sort: (a, b) => b.depth - a.depth, + }, + pattern: ({ depth }) => { + if (depth === 0) { + return { type: 'line' }; + } + }, + }); + plot.render(); + + const geometry = plot.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + plot.update({ + pattern: ({ depth }) => { + if (depth === 1) { + return { type: 'square' }; + } + }, + }); + + expect(plot.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + expect(plot.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + plot.destroy(); + }); +}); diff --git a/__tests__/unit/plots/column/pattern-spec.ts b/__tests__/unit/plots/column/pattern-spec.ts new file mode 100644 index 0000000000..18e5cd76c1 --- /dev/null +++ b/__tests__/unit/plots/column/pattern-spec.ts @@ -0,0 +1,73 @@ +import { Column } from '../../../../src'; +import { salesByArea } from '../../../data/sales'; +import { createDiv } from '../../../utils/dom'; + +describe('column', () => { + it('pattern: obj', () => { + const column = new Column(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + pattern: { + type: 'line', + }, + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + column.update({ + pattern: { + type: 'dot', + }, + }); + + expect(column.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(column.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(column.chart.geometries[0].elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + column.destroy(); + }); + + it('pattern: function', () => { + const column = new Column(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + pattern: ({ area }) => { + if (area === '华北') { + return { type: 'dot' }; + } + }, + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[3].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + column.update({ + pattern: ({ area }) => { + if (area === '中南') { + return { type: 'line' }; + } + }, + }); + + expect(column.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(column.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + column.destroy(); + }); +}); diff --git a/__tests__/unit/plots/heatmap/pattern-spec.ts b/__tests__/unit/plots/heatmap/pattern-spec.ts new file mode 100644 index 0000000000..27096310cb --- /dev/null +++ b/__tests__/unit/plots/heatmap/pattern-spec.ts @@ -0,0 +1,67 @@ +import { Heatmap } from '../../../../src'; +import { semanticBasicHeatmapData } from '../../../data/basic-heatmap'; +import { createDiv } from '../../../utils/dom'; + +describe('heatmap: pattern', () => { + it('pattern: obj', () => { + const heatmap = new Heatmap(createDiv('style'), { + width: 400, + height: 300, + data: semanticBasicHeatmapData, + xField: 'name', + yField: 'day', + colorField: 'sales', + pattern: { + type: 'line', + }, + }); + + heatmap.render(); + + const geometry = heatmap.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + heatmap.update({ + pattern: null, + }); + + expect(heatmap.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + heatmap.destroy(); + }); + + it('pattern: callback', () => { + const heatmap = new Heatmap(createDiv('style'), { + width: 400, + height: 300, + data: semanticBasicHeatmapData, + xField: 'name', + yField: 'day', + colorField: 'sales', + pattern: ({ sales }) => { + if (sales === 10) { + return { type: 'dot' }; + } + }, + }); + + heatmap.render(); + + const geometry = heatmap.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + heatmap.update({ + pattern: ({ sales }) => { + if (sales === 19) { + return { type: 'dot' }; + } + }, + }); + + expect(heatmap.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + heatmap.destroy(); + }); +}); diff --git a/__tests__/unit/plots/histogram/pattern-spec.ts b/__tests__/unit/plots/histogram/pattern-spec.ts new file mode 100644 index 0000000000..4ee518d31b --- /dev/null +++ b/__tests__/unit/plots/histogram/pattern-spec.ts @@ -0,0 +1,81 @@ +import { Histogram } from '../../../../src'; +import { histogramData } from '../../../data/histogram-data'; +import { createDiv } from '../../../utils/dom'; + +describe('Histogram: pattern', () => { + it('pattern: obj', () => { + const histogram = new Histogram(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data: histogramData, + binField: 'value', + binWidth: 2, + columnStyle: { + stroke: 'black', + lineWidth: 2, + }, + pattern: { + type: 'line', + }, + }); + histogram.render(); + + const geometry = histogram.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + histogram.update({ + pattern: { + type: 'dot', + }, + }); + + expect(histogram.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(histogram.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(histogram.chart.geometries[0].elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + histogram.destroy(); + }); + + it('pattern: callback', () => { + const histogram = new Histogram(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data: histogramData, + binField: 'value', + binWidth: 2, + pattern: (d) => { + if (d.count > 7) { + return { type: 'dot' }; + } + }, + }); + + histogram.render(); + + const geometry = histogram.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[3].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + expect(elements[4].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[5].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[6].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + histogram.update({ + pattern: (d) => { + if (d.count > 9) { + return { type: 'dot' }; + } + }, + }); + + expect(histogram.chart.geometries[0].elements[4].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + expect(histogram.chart.geometries[0].elements[5].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(histogram.chart.geometries[0].elements[6].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + histogram.destroy(); + }); +}); diff --git a/__tests__/unit/plots/liquid/pattern-spec.ts b/__tests__/unit/plots/liquid/pattern-spec.ts new file mode 100644 index 0000000000..23a6257992 --- /dev/null +++ b/__tests__/unit/plots/liquid/pattern-spec.ts @@ -0,0 +1,34 @@ +import { Liquid } from '../../../../src'; +import { createDiv } from '../../../utils/dom'; + +describe('liquid: pattern', () => { + const liquid = new Liquid(createDiv(), { + width: 600, + height: 300, + percent: 0.25, + color: 'blue', + }); + + liquid.render(); + + const getWaveColor = (liquid) => + liquid.chart.middleGroup.getChildren()[0].getChildren()[0].getChildren()[0].attr('fill'); + + it('pattern: obj', () => { + liquid.update({ + pattern: { + type: 'dot', + }, + }); + expect(getWaveColor(liquid) instanceof CanvasPattern).toEqual(true); // wave path + + liquid.update({ + pattern: null, + }); + expect(getWaveColor(liquid) instanceof CanvasPattern).toEqual(false); // wave path + }); + + afterAll(() => { + liquid.destroy(); + }); +}); diff --git a/__tests__/unit/plots/pie/pattern-spec.ts b/__tests__/unit/plots/pie/pattern-spec.ts new file mode 100644 index 0000000000..26d968d547 --- /dev/null +++ b/__tests__/unit/plots/pie/pattern-spec.ts @@ -0,0 +1,87 @@ +import { Pie } from '../../../../src'; +import { POSITIVE_NEGATIVE_DATA } from '../../../data/common'; +import { createDiv } from '../../../utils/dom'; + +describe('pie', () => { + const data = POSITIVE_NEGATIVE_DATA.filter((o) => o.value > 0).map((d, idx) => + idx === 1 ? { ...d, type: 'item1' } : d + ); + + it('pattern: obj', () => { + const pie = new Pie(createDiv(), { + width: 400, + height: 300, + data, + angleField: 'value', + colorField: 'type', + color: ['yellow', 'lightgreen', 'lightblue', 'pink'], + radius: 0.8, + innerRadius: 0.5, + pattern: { + type: 'line', + }, + }); + + pie.render(); + + const geometry = pie.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + pie.update({ + pattern: { + type: 'dot', + }, + }); + + expect(pie.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(pie.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(pie.chart.geometries[0].elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + pie.destroy(); + }); + + it('pattern: callback', () => { + const pie = new Pie(createDiv(), { + width: 400, + height: 300, + data, + angleField: 'value', + colorField: 'type', + color: ['green', 'lightgreen', 'lightblue', 'pink'], + radius: 0.8, + innerRadius: 0.5, + pattern: (d) => { + if (d.type === 'pc') { + return { type: 'line' }; + } else if (d.type === 'pa') { + return { type: 'dot' }; + } + }, + }); + + pie.render(); + + const geometry = pie.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + expect(elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + pie.update({ + pattern: (d) => { + if (d.type === 'item1') { + return { type: 'line' }; + } + }, + }); + + expect(pie.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + expect(pie.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(pie.chart.geometries[0].elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + pie.destroy(); + }); +}); diff --git a/__tests__/unit/plots/radial-bar/pattern-spec.ts b/__tests__/unit/plots/radial-bar/pattern-spec.ts new file mode 100644 index 0000000000..becd0bcba3 --- /dev/null +++ b/__tests__/unit/plots/radial-bar/pattern-spec.ts @@ -0,0 +1,71 @@ +import { RadialBar } from '../../../../src'; +import { createDiv } from '../../../utils/dom'; +import { antvStar } from '../../../data/antv-star'; + +const xField = 'name'; +const yField = 'star'; + +describe('radial-bar pattern', () => { + it('pattern: obj', () => { + const bar = new RadialBar(createDiv(), { + width: 400, + height: 300, + data: antvStar, + xField, + yField, + pattern: { + type: 'line', + }, + }); + bar.render(); + + const geometry = bar.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + bar.update({ + pattern: null, + }); + + expect(bar.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + bar.destroy(); + }); + + it('pattern: callback', () => { + const bar = new RadialBar(createDiv(), { + width: 400, + height: 300, + data: antvStar, + xField, + yField, + pattern: (d) => { + if (d.star > 7346) { + return { type: 'dot' }; + } + }, + }); + bar.render(); + + const geometry = bar.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[7].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + bar.update({ + pattern: (d) => { + if (d.star > 7100) { + return { type: 'dot' }; + } + }, + }); + + expect(bar.chart.geometries[0].elements[7].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(bar.chart.geometries[0].elements[6].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + expect(bar.chart.geometries[0].elements[5].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + bar.destroy(); + }); +}); diff --git a/__tests__/unit/plots/rose/pattern-spec.ts b/__tests__/unit/plots/rose/pattern-spec.ts new file mode 100644 index 0000000000..5b33e1a034 --- /dev/null +++ b/__tests__/unit/plots/rose/pattern-spec.ts @@ -0,0 +1,61 @@ +import { Rose } from '../../../../src'; +import { salesByArea } from '../../../data/sales'; +import { createDiv } from '../../../utils/dom'; + +describe('rose: pattern', () => { + const rose = new Rose(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + meta: { + sales: { + nice: true, + formatter: (v) => `${Math.floor(v / 10000)}万`, + }, + }, + }); + + rose.render(); + + it('pattern: obj', () => { + rose.update({ + pattern: { + type: 'line', + }, + }); + + const geometry = rose.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + rose.update({ + pattern: null, + }); + + expect(rose.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + expect(rose.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + expect(rose.chart.geometries[0].elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + }); + + it('pattern: callback', () => { + rose.update({ + pattern: ({ area }) => { + if (area === '中南') { + return { type: 'dot' }; + } + }, + }); + + expect(rose.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + expect(rose.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(rose.chart.geometries[0].elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + }); + + afterAll(() => { + rose.destroy(); + }); +}); diff --git a/__tests__/unit/plots/sunburst/pattern-spec.ts b/__tests__/unit/plots/sunburst/pattern-spec.ts new file mode 100644 index 0000000000..84642d944d --- /dev/null +++ b/__tests__/unit/plots/sunburst/pattern-spec.ts @@ -0,0 +1,50 @@ +import { Sunburst } from '../../../../src'; +import { createDiv } from '../../../utils/dom'; +import { SIMPLE_SUNBURST_DATA } from '../../../data/sunburst'; + +describe('sunburst: pattern', () => { + const div = createDiv(); + const plot = new Sunburst(div, { + data: SIMPLE_SUNBURST_DATA, + }); + plot.render(); + + it('pattern: obj', () => { + plot.update({ + pattern: { + type: 'line', + }, + }); + + const geometry = plot.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + plot.update({ + pattern: null, + }); + + expect(plot.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + expect(plot.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + expect(plot.chart.geometries[0].elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + }); + + it('pattern: callback', () => { + plot.update({ + pattern: ({ name }) => { + if (name === '中南美洲') { + return { type: 'line' }; + } + }, + }); + + expect(plot.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(plot.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + }); + + afterAll(() => { + plot.destroy(); + }); +}); diff --git a/__tests__/unit/plots/tiny-area/pattern-spec.ts b/__tests__/unit/plots/tiny-area/pattern-spec.ts new file mode 100644 index 0000000000..ea27a11335 --- /dev/null +++ b/__tests__/unit/plots/tiny-area/pattern-spec.ts @@ -0,0 +1,43 @@ +import { TooltipCfg } from '@antv/g2/lib/interface'; +import { TinyArea } from '../../../../src'; +import { DEFAULT_OPTIONS } from '../../../../src/plots/tiny-area/constants'; +import { partySupport } from '../../../data/party-support'; +import { createDiv } from '../../../utils/dom'; + +describe('tiny-area: pattern', () => { + const tinyArea = new TinyArea(createDiv(), { + width: 200, + height: 100, + data: partySupport + .filter((o) => o.type === 'FF') + .map((item) => { + return item.value; + }), + line: {}, + autoFit: false, + }); + + tinyArea.render(); + + it('pattern: obj', () => { + tinyArea.update({ + pattern: { + type: 'line', + }, + }); + + const geometry = tinyArea.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + tinyArea.update({ + pattern: null, + }); + + expect(tinyArea.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + }); + + afterAll(() => { + tinyArea.destroy(); + }); +}); diff --git a/__tests__/unit/plots/tiny-column/pattern-spec.ts b/__tests__/unit/plots/tiny-column/pattern-spec.ts new file mode 100644 index 0000000000..185a50a400 --- /dev/null +++ b/__tests__/unit/plots/tiny-column/pattern-spec.ts @@ -0,0 +1,71 @@ +import { TinyColumn } from '../../../../src'; +import { partySupport } from '../../../data/party-support'; +import { createDiv } from '../../../utils/dom'; + +describe('tiny-column pattern', () => { + it('pattern: obj', () => { + const tinyColumn = new TinyColumn(createDiv(), { + width: 200, + height: 100, + data: partySupport + .filter((o) => o.type === 'FF') + .map((item) => { + return item.value; + }), + pattern: { + type: 'line', + }, + }); + + tinyColumn.render(); + + const geometry = tinyColumn.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + tinyColumn.update({ + pattern: null, + }); + expect(tinyColumn.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + tinyColumn.destroy(); + }); + + it('pattern: callback', () => { + const tinyColumn = new TinyColumn(createDiv(), { + width: 200, + height: 100, + data: partySupport + .filter((o) => o.type === 'FF') + .map((item) => { + return item.value; + }), + pattern: ({ y }) => { + if (y > 4600) { + return { type: 'dot' }; + } + }, + }); + + tinyColumn.render(); + + const geometry = tinyColumn.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[9].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[10].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[11].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + tinyColumn.update({ + pattern: ({ y }) => { + if (y === 3800) { + return { type: 'dot' }; + } + }, + }); + + expect(tinyColumn.chart.geometries[0].elements[8].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(tinyColumn.chart.geometries[0].elements[9].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + tinyColumn.destroy(); + }); +}); diff --git a/__tests__/unit/plots/treemap/pattern-spec.ts b/__tests__/unit/plots/treemap/pattern-spec.ts new file mode 100644 index 0000000000..e8e7001304 --- /dev/null +++ b/__tests__/unit/plots/treemap/pattern-spec.ts @@ -0,0 +1,93 @@ +import { Treemap } from '../../../../src'; +import { createDiv } from '../../../utils/dom'; +import { TREEMAP } from '../../../data/treemap'; + +const color = [ + '#5B8FF9', + '#CDDDFD', + '#5AD8A6', + '#CDF3E4', + '#5D7092', + '#CED4DE', + '#F6BD16', + '#FCEBB9', + '#6F5EF9', + '#D3CEFD', + '#6DC8EC', + '#D3EEF9', + '#945FB9', + '#DECFEA', + '#FF9845', + '#FFE0C7', + '#1E9493', + '#BBDEDE', + '#FF99C3', + '#FFE0ED', +]; + +describe('treemap: pattern', () => { + it('pattern: obj', () => { + const treemapPlot = new Treemap(createDiv(), { + data: TREEMAP, + colorField: 'name', + color, + pattern: { + type: 'line', + }, + }); + + treemapPlot.render(); + + const geometry = treemapPlot.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + treemapPlot.update({ + pattern: { + type: 'dot', + }, + }); + + expect(treemapPlot.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(treemapPlot.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(treemapPlot.chart.geometries[0].elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + + treemapPlot.destroy(); + }); + + it('pattern: callback', () => { + const treemapPlot = new Treemap(createDiv(), { + data: TREEMAP, + colorField: 'name', + color, + pattern: ({ value }) => { + if (value > 600) { + return { type: 'line' }; + } + }, + }); + + treemapPlot.render(); + + const geometry = treemapPlot.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + treemapPlot.update({ + pattern: ({ value }) => { + if (value > 400) { + return { type: 'line' }; + } + }, + }); + + expect(treemapPlot.chart.geometries[0].elements[0].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(treemapPlot.chart.geometries[0].elements[1].shape.attr('fill') instanceof CanvasPattern).toEqual(true); + expect(treemapPlot.chart.geometries[0].elements[2].shape.attr('fill') instanceof CanvasPattern).toEqual(false); + + treemapPlot.destroy(); + }); +}); diff --git a/__tests__/unit/utils/pattern/dot-spec.ts b/__tests__/unit/utils/pattern/dot-spec.ts new file mode 100644 index 0000000000..d1b3de0f08 --- /dev/null +++ b/__tests__/unit/utils/pattern/dot-spec.ts @@ -0,0 +1,75 @@ +import { initCanvas } from '../../../../src/utils/pattern/util'; +import { drawDot, defaultDotPatternCfg, createDotPattern } from '../../../../src/utils/pattern/dot'; +import { DotPatternCfg } from '../../../../src/types/pattern'; +import { getPixelColor } from '../../../utils/getPixelColor'; +import { deepAssign } from '../../../../src/utils'; + +describe('utils: dot pattern', () => { + const width = 30, + height = 30; + const canvas = initCanvas(width, height); + const ctx = canvas.getContext('2d'); + document.body.appendChild(canvas); + + it('createDotPattern with defaultCfg', () => { + const pattern = createDotPattern(defaultDotPatternCfg as DotPatternCfg); + expect(pattern.toString()).toEqual('[object CanvasPattern]'); + }); + + it('dotUnitPattern with fill', () => { + const cfg = deepAssign(defaultDotPatternCfg, { + // size: 4, // 默认 + fill: '#898989', + }) as DotPatternCfg; + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawDot(ctx, cfg as DotPatternCfg, width / 2, height / 2); + // 传入的是呈现的位置 + expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#898989'); + expect(getPixelColor(canvas, width / 2 + cfg.size / 2, height / 2 + cfg.size / 2).hex).toEqual('#000000'); // 超出圆范围, 像素点在右下方 + }); + + it('dotUnitPattern with fillOpacity', () => { + const cfg = deepAssign(defaultDotPatternCfg, { + // size: 4, // 默认 + fill: '#898989', + fillOpacity: 0.5, + }); + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawDot(ctx, cfg as DotPatternCfg, width / 2, height / 2); + // 传入的是呈现的位置 + expect(getPixelColor(canvas, width / 2, height / 2).alpha.toPrecision(1)).toEqual(`${0.5}`); + expect(getPixelColor(canvas, width / 2 + cfg.size / 2, height / 2 + cfg.size / 2).alpha.toPrecision(1)).toEqual( + `${0}` + ); // 透明度为0 + }); + + it('dotUnitPattern with size', () => { + const size = 30; + const cfg = deepAssign(defaultDotPatternCfg, { + fill: '#ff0000', + fillOpacity: 1, + size, + }); + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawDot(ctx, cfg as DotPatternCfg, width / 2, height / 2); + // 传入的是呈现的位置 + expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#ff0000'); + expect(getPixelColor(canvas, width / 2 + size / 2 - 1, height / 2).hex).toEqual('#ff0000'); // 圆的边界 + expect(getPixelColor(canvas, width / 2 + size / 2, height / 2).hex).toEqual('#000000'); + }); + + it('dotUnitPattern with stroke and lineWidth', () => { + const cfg = deepAssign(defaultDotPatternCfg, { + size: 20, + fill: '#ff0000', + stroke: '#00ff00', + lineWidth: 2, + }); + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawDot(ctx, cfg as DotPatternCfg, width / 2, height / 2); + // 传入的是呈现的位置 + expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#ff0000'); + expect(getPixelColor(canvas, width / 2 + cfg.size / 2, height / 2).hex).toEqual('#00ff00'); + expect(getPixelColor(canvas, width / 2 + cfg.size / 2, height / 2 + cfg.size / 2).hex).toEqual('#000000'); + }); +}); diff --git a/__tests__/unit/utils/pattern/index-spec.ts b/__tests__/unit/utils/pattern/index-spec.ts new file mode 100644 index 0000000000..9c746d4c7e --- /dev/null +++ b/__tests__/unit/utils/pattern/index-spec.ts @@ -0,0 +1,69 @@ +import { getCanvasPattern, PatternOption } from '../../../../src/utils/pattern'; + +describe('getCanvasPattern', () => { + it('dot-pattern without cfg', () => { + const pattern = getCanvasPattern({ type: 'dot' }); + expect(pattern.toString()).toEqual('[object CanvasPattern]'); + }); + + it('dot-pattern with cfg', () => { + const patternOption = { + type: 'dot', + cfg: { + radius: 4, + padding: 6, + }, + } as PatternOption; + const pattern = getCanvasPattern(patternOption); + expect(pattern.toString()).toEqual('[object CanvasPattern]'); + }); + + it('line-pattern without cfg', () => { + const pattern = getCanvasPattern({ type: 'line' }); + expect(pattern.toString()).toEqual('[object CanvasPattern]'); + }); + + it('line-pattern with cfg', () => { + const patternOption = { + type: 'dot', + cfg: { + rotation: 0, + spacing: 12, + stroke: '#FFF', + }, + } as PatternOption; + const pattern = getCanvasPattern(patternOption); + expect(pattern.toString()).toEqual('[object CanvasPattern]'); + }); + + it('square-pattern without cfg', () => { + const pattern = getCanvasPattern({ type: 'square' }); + expect(pattern.toString()).toEqual('[object CanvasPattern]'); + }); + + it('square-pattern with cfg', () => { + const patternOption = { + type: 'dot', + cfg: { + size: 4, + padding: 10, + backgroundColor: 'transparent', + fill: 'transparent', + }, + } as PatternOption; + const pattern = getCanvasPattern(patternOption); + expect(pattern.toString()).toEqual('[object CanvasPattern]'); + }); + + it('pattern without option', () => { + //@ts-ignore + const pattern = getCanvasPattern({}); + expect(pattern).toEqual(undefined); + }); + + it('pattern with error type', () => { + //@ts-ignore + const pattern = getCanvasPattern({ type: 'xxx' }); + expect(pattern).toEqual(undefined); + }); +}); diff --git a/__tests__/unit/utils/pattern/line-spec.ts b/__tests__/unit/utils/pattern/line-spec.ts new file mode 100644 index 0000000000..d28137a6b1 --- /dev/null +++ b/__tests__/unit/utils/pattern/line-spec.ts @@ -0,0 +1,52 @@ +import { initCanvas } from '../../../../src/utils/pattern/util'; +import { defaultLinePatternCfg, createLinePattern, drawLine } from '../../../../src/utils/pattern/line'; +import { LinePatternCfg } from '../../../../src/types/pattern'; +import { getPixelColor } from '../../../utils/getPixelColor'; +import { deepAssign } from '../../../../src/utils'; + +describe('utils: line pattern', () => { + const width = 30, + height = 30; + const canvas = initCanvas(width, height); + const ctx = canvas.getContext('2d'); + document.body.appendChild(canvas); + + it('createLinePattern with defaultCfg', () => { + const pattern = createLinePattern(defaultLinePatternCfg as LinePatternCfg); + expect(pattern.toString()).toEqual('[object CanvasPattern]'); + }); + + it('lineUnitPattern with stroke and strokeWidth', () => { + const cfg = deepAssign(defaultLinePatternCfg, { + stroke: '#ff0000', + lineWidth: 2, + }); + const d = ` + M 0 0 L ${width} 0 + M 0 ${height} L ${width} ${height} + `; + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawLine(ctx, cfg as LinePatternCfg, d); + // 传入的是呈现的位置 + expect(getPixelColor(canvas, 0, 0).hex).toEqual('#ff0000'); + expect(getPixelColor(canvas, 0, height - 1).hex).toEqual('#ff0000'); + }); + + it('lineUnitPattern with strokeOpacity', () => { + const cfg = deepAssign(defaultLinePatternCfg, { + stroke: '#ff0000', + lineWidth: 2, + strokeOpacity: 0.5, + }); + const d = ` + M 0 0 L ${width} 0 + M 0 ${height} L ${width} ${height} + `; + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawLine(ctx, cfg as LinePatternCfg, d); + // 传入的是呈现的位置 + expect(getPixelColor(canvas, 0, 0).alpha.toPrecision(1)).toEqual(`${0.5}`); + expect(getPixelColor(canvas, 0, height - 1).alpha.toPrecision(1)).toEqual(`${0.5}`); + expect(getPixelColor(canvas, width / 2, height / 2).alpha.toPrecision(1)).toEqual(`${0}`); + }); +}); diff --git a/__tests__/unit/utils/pattern/square-spec.ts b/__tests__/unit/utils/pattern/square-spec.ts new file mode 100644 index 0000000000..4c97667946 --- /dev/null +++ b/__tests__/unit/utils/pattern/square-spec.ts @@ -0,0 +1,73 @@ +import { initCanvas } from '../../../../src/utils/pattern/util'; +import { drawSquare, defaultSquarePatternCfg, createSquarePattern } from '../../../../src/utils/pattern/square'; +import { SquarePatternCfg } from '../../../../src/types/pattern'; +import { getPixelColor } from '../../../utils/getPixelColor'; +import { deepAssign } from '../../../../src/utils'; + +describe('utils: square pattern', () => { + const width = 30, + height = 30; + const canvas = initCanvas(width, height); + const ctx = canvas.getContext('2d'); + document.body.appendChild(canvas); + + it('createSquarePattern with defaultCfg', () => { + const pattern = createSquarePattern(defaultSquarePatternCfg as SquarePatternCfg); + expect(pattern.toString()).toEqual('[object CanvasPattern]'); + }); + + it('squareUnitPattern with fill', () => { + const cfg = deepAssign(defaultSquarePatternCfg, { + fill: '#898989', + }); + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawSquare(ctx, cfg as SquarePatternCfg, width / 2, height / 2); + // 传入的是呈现的位置 + expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#898989'); + expect(getPixelColor(canvas, width / 2 + cfg.size + 5, height / 2).hex).toEqual('#000000'); // 超出范围 + }); + + it('squareUnitPattern with fillOpacity', () => { + const cfg = deepAssign(defaultSquarePatternCfg, { + // radius: 4, // 默认 + fill: '#898989', + fillOpacity: 0.5, + }); + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawSquare(ctx, cfg as SquarePatternCfg, width / 2, height / 2); + // 传入的是呈现的位置 + expect(getPixelColor(canvas, width / 2, height / 2).alpha.toPrecision(1)).toEqual(`${0.5}`); + expect(getPixelColor(canvas, width / 2 + cfg.size + 5, height / 2).alpha.toPrecision(1)).toEqual(`${0}`); // 透明度为0 + }); + + it('squareUnitPattern with size', () => { + const size = 15; + const cfg = deepAssign(defaultSquarePatternCfg, { + fill: '#898989', + fillOpacity: 1, + size, + }); + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawSquare(ctx, cfg as SquarePatternCfg, width / 2, height / 2); + // 传入的是呈现的位置 + expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#898989'); + expect(getPixelColor(canvas, width / 2 + size / 2 - 1, height / 2).hex).toEqual('#898989'); // 边界 + expect(getPixelColor(canvas, width / 2 + size / 2 + 5, height / 2).hex).toEqual('#000000'); // 超出边界 + }); + + it('squareUnitPattern with stroke and lineWidth', () => { + const cfg = deepAssign(defaultSquarePatternCfg, { + size: 15, + fill: '#ff0000', + stroke: '#00ff00', + lineWidth: 4, + }); + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawSquare(ctx, cfg as SquarePatternCfg, width / 2, height / 2); + // 传入的是呈现的位置 + expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#ff0000'); + // 描边 + expect(getPixelColor(canvas, width / 2 + cfg.size / 2 + 1, height / 2).hex).toEqual('#00ff00'); + expect(getPixelColor(canvas, width / 2 + cfg.size / 2 + 5, height / 2 + cfg.size / 2).hex).toEqual('#000000'); + }); +}); diff --git a/__tests__/unit/utils/pattern/util-spec.ts b/__tests__/unit/utils/pattern/util-spec.ts new file mode 100644 index 0000000000..6af1f75c55 --- /dev/null +++ b/__tests__/unit/utils/pattern/util-spec.ts @@ -0,0 +1,74 @@ +import { + initCanvas, + getUnitPatternSize, + getSymbolsPosition, + drawBackground, + transformMatrix, +} from '../../../../src/utils/pattern/util'; +import { DotPatternCfg } from '../../../../src/types/pattern'; +import { getPixelColor } from '../../../utils/getPixelColor'; + +describe('utils', () => { + const width = 30, + height = 30; + let canvas = null; + + it('initCanvas', () => { + canvas = initCanvas(width, height); + document.body.appendChild(canvas); + + const dpr = window?.devicePixelRatio || 1; + expect(canvas.width).toBe(dpr * 30); + expect(canvas.height).toBe(dpr * 30); + }); + + it('getPixelColor', () => { + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#989898'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#989898'); + expect(getPixelColor(canvas, width + 1, height + 1).hex).toEqual('#000000'); // 超出范围,黑色 + }); + + it('drawBackground', () => { + const defaultDotPatternCfg = { + backgroundColor: '#eee', // 为了测试背景色填充 + }; + const ctx = canvas.getContext('2d'); + drawBackground(ctx, defaultDotPatternCfg as DotPatternCfg, width, height); + const color = getPixelColor(canvas, width / 2, height / 2).hex; + expect(color).toEqual('#eeeeee'); + }); + + it('getUnitPatternSize', () => { + expect(getUnitPatternSize(4, 6, false)).toBe(10); + expect(getUnitPatternSize(4, 6, true)).toBe(20); + }); + + it('getSymbolsPosition', () => { + expect(getSymbolsPosition(12, false)).toEqual([[6, 6]]); + expect(getSymbolsPosition(12, true)).toEqual([ + [3, 3], + [9, 9], + ]); + }); + + it('transformMatrix', () => { + expect(transformMatrix(2, 45)).toEqual({ + a: 0.3535533905932738, + b: 0.35355339059327373, + c: -0.35355339059327373, + d: 0.3535533905932738, + e: 0, + f: 0, + }); + expect(transformMatrix(1, 45)).toEqual({ + a: 0.7071067811865476, + b: 0.7071067811865475, + c: -0.7071067811865475, + d: 0.7071067811865476, + e: 0, + f: 0, + }); + }); +}); diff --git a/__tests__/utils/getPixelColor.ts b/__tests__/utils/getPixelColor.ts new file mode 100644 index 0000000000..31e9a4d046 --- /dev/null +++ b/__tests__/utils/getPixelColor.ts @@ -0,0 +1,26 @@ +/** + * @param canvas + * @param x和y 呈现的坐标位置 + */ +export function getPixelColor(canvas: HTMLCanvasElement, x: number, y: number) { + const dpr = window.devicePixelRatio || 2; + const ctx = canvas.getContext('2d'); + // ctx 获取的是实际渲染的图形的位置,所以需要:呈现的坐标位置 * dpr + const pixel = ctx.getImageData(x * dpr, y * dpr, 1, 1); + const data = pixel.data; // [r, g, b, a] 暂不处理透明度情况 + + // 如果 x, y 超出图像边界,则是透明黑色(全为零,即 rgba(0,0,0,0)) + // 详见:https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas + const rHex = fixTwoDigit(data[0].toString(16)); + const gHex = fixTwoDigit(data[1].toString(16)); + const bHex = fixTwoDigit(data[2].toString(16)); + const hex = `#${rHex}${gHex}${bHex}`; + + const alpha = data[3] / 255; + + return { hex, alpha }; +} + +function fixTwoDigit(str) { + return str.length < 2 ? `0${str}` : `${str}`; +} diff --git a/docs/api/pattern.en.md b/docs/api/pattern.en.md new file mode 100644 index 0000000000..205d0ef520 --- /dev/null +++ b/docs/api/pattern.en.md @@ -0,0 +1,6 @@ +--- +title: Pattern +order: 12 +--- + +`markdown:docs/api/pattern.zh.md` diff --git a/docs/api/pattern.zh.md b/docs/api/pattern.zh.md new file mode 100644 index 0000000000..8fbe6c8cff --- /dev/null +++ b/docs/api/pattern.zh.md @@ -0,0 +1,35 @@ +--- +title: 贴图图案 +order: 12 +--- + +### 介绍 + +使用 Pattern 可以很有用地对相似的项目进行分组,例如,假设您想要构建一个显示各种食物的饼图,您可以使用颜色比例尺为每种食物指定一种唯一的颜色,然后您可以对蔬菜/水果/肉类/进行分组,为每组使用类似的 pattern(同时保持颜色)。 + +### 在 G2Plot 中使用 pattern + +G2Plot 内置了 `'dot' | 'line' | 'square'` 等若干贴图, 图案颜色默认从当前 element 继承。 + + + +一些场景使用: + +- [Demo1](/zh/examples/plugin/pattern#legend-marker-with-pattern): 图例(legend) marker 使用 pattern +- [Demo2](/zh/examples/plugin/pattern#bar-pattern): 通过回调设置不同的 pattern + + + + +### API 说明 + +`markdown:docs/common/pattern.zh.md` + +### 注意事项 + +请注意 pattern 的使用,目前有一些限制: + +1. `svg` 的渲染方式下,暂不支持 pattern 图案填充 +2. pattern 默认继承元素(element)的填充色,但不支持 pattern 填充色为渐变色,即元素(element)为渐变色时,pattern 背景色无法继承,需要手动指定。参考:[Demo](/zh/examples/tiny/tiny-area#pattern) +3. Tooltip, Legend 的 marker 使用的是依旧是纯颜色(plain color). 对于 Legend marker 可以考虑使用回调的方式来设置,参考:[Demo](/zh/examples/plugin/pattern#pie-pattern-callback) + diff --git a/docs/api/plots/area.en.md b/docs/api/plots/area.en.md index 4b001abb8c..f4af39f560 100644 --- a/docs/api/plots/area.en.md +++ b/docs/api/plots/area.en.md @@ -87,6 +87,12 @@ Point graphic style in the Area. `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + #### state **optional** _object_ diff --git a/docs/api/plots/area.zh.md b/docs/api/plots/area.zh.md index 612796461c..db1d7cf00b 100644 --- a/docs/api/plots/area.zh.md +++ b/docs/api/plots/area.zh.md @@ -87,6 +87,12 @@ order: 1 `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + #### state **可选** _object_ diff --git a/docs/api/plots/bar.en.md b/docs/api/plots/bar.en.md index 2837eecc43..cb505433e2 100644 --- a/docs/api/plots/bar.en.md +++ b/docs/api/plots/bar.en.md @@ -61,6 +61,12 @@ Whether the plot is Percent Bar. When isPercent is `true`, isStack must be `true `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + `markdown:docs/common/bar-style.en.md` #### barWidthRatio diff --git a/docs/api/plots/bar.zh.md b/docs/api/plots/bar.zh.md index dff6ffbfcd..0355370ce5 100644 --- a/docs/api/plots/bar.zh.md +++ b/docs/api/plots/bar.zh.md @@ -61,6 +61,12 @@ order: 3 `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + `markdown:docs/common/bar-style.en.md` #### barWidthRatio diff --git a/docs/api/plots/circle-packing.en.md b/docs/api/plots/circle-packing.en.md index 69e0045ba6..f5f4b327ac 100644 --- a/docs/api/plots/circle-packing.en.md +++ b/docs/api/plots/circle-packing.en.md @@ -105,6 +105,12 @@ Inner radius, 0~1 of the value. --> `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + #### pointStyle **optional** _object_ diff --git a/docs/api/plots/circle-packing.zh.md b/docs/api/plots/circle-packing.zh.md index 188b6cd95a..619797f17d 100644 --- a/docs/api/plots/circle-packing.zh.md +++ b/docs/api/plots/circle-packing.zh.md @@ -105,6 +105,12 @@ meta: { `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + #### pointStyle **optional** _object_ diff --git a/docs/api/plots/column.en.md b/docs/api/plots/column.en.md index 392998816c..5ffa3241fc 100644 --- a/docs/api/plots/column.en.md +++ b/docs/api/plots/column.en.md @@ -61,6 +61,12 @@ Whether to percent columns, if isPercent is true, isStack also needs to be true. `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + `markdown:docs/common/column-style.en.md` #### columnWidthRatio diff --git a/docs/api/plots/column.zh.md b/docs/api/plots/column.zh.md index 137e5a40c9..ebee783207 100644 --- a/docs/api/plots/column.zh.md +++ b/docs/api/plots/column.zh.md @@ -63,6 +63,12 @@ order: 2 `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + `markdown:docs/common/column-style.zh.md` #### columnWidthRatio diff --git a/docs/api/plots/heatmap.en.md b/docs/api/plots/heatmap.en.md index ef64424c24..efe1210343 100644 --- a/docs/api/plots/heatmap.en.md +++ b/docs/api/plots/heatmap.en.md @@ -49,6 +49,12 @@ Axis mapping. `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + #### shape **optional** _rect | square | circle_ diff --git a/docs/api/plots/heatmap.zh.md b/docs/api/plots/heatmap.zh.md index eda4569f65..eb9ed8b287 100644 --- a/docs/api/plots/heatmap.zh.md +++ b/docs/api/plots/heatmap.zh.md @@ -49,6 +49,12 @@ order: 23 `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + #### shape **可选** _rect | square | circle_ diff --git a/docs/api/plots/histogram.en.md b/docs/api/plots/histogram.en.md index 968e298f5d..2e2c18e2b8 100644 --- a/docs/api/plots/histogram.en.md +++ b/docs/api/plots/histogram.en.md @@ -66,6 +66,12 @@ Column style configuration. `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + #### state **optional** _object_ diff --git a/docs/api/plots/histogram.zh.md b/docs/api/plots/histogram.zh.md index 5bba03650a..a7fc8e6cea 100644 --- a/docs/api/plots/histogram.zh.md +++ b/docs/api/plots/histogram.zh.md @@ -66,6 +66,12 @@ order: 11 `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + #### state **可选** _object_ diff --git a/docs/api/plots/liquid.en.md b/docs/api/plots/liquid.en.md index 5db11e630b..6642b8e14c 100644 --- a/docs/api/plots/liquid.en.md +++ b/docs/api/plots/liquid.en.md @@ -62,6 +62,12 @@ function shape(x: number, y: number, width: number, height: number) { `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + #### outline **optional** _Outline_ diff --git a/docs/api/plots/liquid.zh.md b/docs/api/plots/liquid.zh.md index 944511bb39..142de1d0ed 100644 --- a/docs/api/plots/liquid.zh.md +++ b/docs/api/plots/liquid.zh.md @@ -62,6 +62,12 @@ function shape(x: number, y: number, width: number, height: number) { `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + #### outline **optional** _Outline_ diff --git a/docs/api/plots/pie.en.md b/docs/api/plots/pie.en.md index 6800aa11b1..47c2a964dc 100644 --- a/docs/api/plots/pie.en.md +++ b/docs/api/plots/pie.en.md @@ -91,6 +91,12 @@ Configure the end Angle of the coordinate system. `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + #### pieStyle **optional** _object_ diff --git a/docs/api/plots/pie.zh.md b/docs/api/plots/pie.zh.md index 6ef2b23f9f..c26812fb1f 100644 --- a/docs/api/plots/pie.zh.md +++ b/docs/api/plots/pie.zh.md @@ -90,6 +90,12 @@ piePlot.render(); `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + #### statistic ✨ **optional** _object_ diff --git a/docs/api/plots/radial-bar.en.md b/docs/api/plots/radial-bar.en.md index dbb38e2d85..2c1bebb695 100644 --- a/docs/api/plots/radial-bar.en.md +++ b/docs/api/plots/radial-bar.en.md @@ -65,6 +65,12 @@ Display type of plot. You can specify `type: 'line'` to display a `Radial-Line` `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + ### Plot Components `markdown:docs/common/component.en.md` diff --git a/docs/api/plots/radial-bar.zh.md b/docs/api/plots/radial-bar.zh.md index 09f7441184..3d8080fe12 100644 --- a/docs/api/plots/radial-bar.zh.md +++ b/docs/api/plots/radial-bar.zh.md @@ -66,6 +66,12 @@ order: 25 `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + ### 图表组件 `markdown:docs/common/component.zh.md` diff --git a/docs/api/plots/rose.en.md b/docs/api/plots/rose.en.md index 482f8bd99c..68558e12e3 100644 --- a/docs/api/plots/rose.en.md +++ b/docs/api/plots/rose.en.md @@ -115,6 +115,12 @@ The termination Angle of the disk. `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + #### sectorStyle **optional** _object | Function_ diff --git a/docs/api/plots/rose.zh.md b/docs/api/plots/rose.zh.md index 2349b06201..6468602874 100644 --- a/docs/api/plots/rose.zh.md +++ b/docs/api/plots/rose.zh.md @@ -115,6 +115,12 @@ piePlot.render(); `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + #### sectorStyle **optional** _object | Function_ diff --git a/docs/api/plots/sunburst.en.md b/docs/api/plots/sunburst.en.md index 5f85cb36f1..c03be2f2fe 100644 --- a/docs/api/plots/sunburst.en.md +++ b/docs/api/plots/sunburst.en.md @@ -115,6 +115,12 @@ Inner radius, 0~1 of the value. `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + #### sunburstStyle **optional** _object_ diff --git a/docs/api/plots/sunburst.zh.md b/docs/api/plots/sunburst.zh.md index 1ae52ece5e..9e06caa872 100644 --- a/docs/api/plots/sunburst.zh.md +++ b/docs/api/plots/sunburst.zh.md @@ -111,6 +111,12 @@ meta: { `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + #### sunburstStyle **optional** _object | Function_ diff --git a/docs/api/plots/tiny-area.en.md b/docs/api/plots/tiny-area.en.md index 9b83808771..07b4169e49 100644 --- a/docs/api/plots/tiny-area.en.md +++ b/docs/api/plots/tiny-area.en.md @@ -57,6 +57,12 @@ Area Chart Data Point Graph Style. `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + ### Plot Component `markdown:docs/common/component-tiny.en.md` diff --git a/docs/api/plots/tiny-area.zh.md b/docs/api/plots/tiny-area.zh.md index b073b04eb3..9742255c29 100644 --- a/docs/api/plots/tiny-area.zh.md +++ b/docs/api/plots/tiny-area.zh.md @@ -57,6 +57,13 @@ order: 15 `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + + ### 图表组件 `markdown:docs/common/component-tiny.zh.md` diff --git a/docs/api/plots/tiny-column.en.md b/docs/api/plots/tiny-column.en.md index f00ff7dfe3..5fdd1563c7 100644 --- a/docs/api/plots/tiny-column.en.md +++ b/docs/api/plots/tiny-column.en.md @@ -37,6 +37,12 @@ Bar chart graphic styles. `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + ### Plot Component `markdown:docs/common/component-tiny.en.md` diff --git a/docs/api/plots/tiny-column.zh.md b/docs/api/plots/tiny-column.zh.md index 5375dd3b34..4f04cd182d 100644 --- a/docs/api/plots/tiny-column.zh.md +++ b/docs/api/plots/tiny-column.zh.md @@ -37,6 +37,12 @@ order: 16 `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + ### 图表组件 `markdown:docs/common/component-tiny.zh.md` diff --git a/docs/api/plots/treemap.en.md b/docs/api/plots/treemap.en.md index 44e4c79a93..b3eb13e22d 100644 --- a/docs/api/plots/treemap.en.md +++ b/docs/api/plots/treemap.en.md @@ -57,6 +57,12 @@ Extra original fields. Once configured, you can retrieve additional raw data in `markdown:docs/common/color.en.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.en.md` + #### rectStyle **optional** _object_ diff --git a/docs/api/plots/treemap.zh.md b/docs/api/plots/treemap.zh.md index 5571921919..25585c5a52 100644 --- a/docs/api/plots/treemap.zh.md +++ b/docs/api/plots/treemap.zh.md @@ -58,6 +58,12 @@ G2 plot 会根据 data 生成以下数据结构: `markdown:docs/common/color.zh.md` +#### pattern ✨ + +**optional** _object | Function_ + +`markdown:docs/common/pattern.zh.md` + #### rectStyle **optional** _object_ diff --git a/docs/common/legend-cfg.zh.md b/docs/common/legend-cfg.zh.md index dc374b2ad0..957bab91d8 100644 --- a/docs/common/legend-cfg.zh.md +++ b/docs/common/legend-cfg.zh.md @@ -225,12 +225,20 @@ type Marker = { ##### marker -**optional** _MarkerCfg_ +**optional** _MarkerCfg \| MarkerCfgCallback_ 适用于 分类图例,图例项的 marker 图标的配置。 `markdown:docs/common/marker.zh.md` + +```sign +type LegendItem = { name: string; value: string; } & MarkerCfg; + +type MarkerCfgCallback = (name: string, index: number, item: LegendItem) => MarkerCfg; +``` + + ##### maxItemWidth _number_ **optional** diff --git a/docs/common/marker.zh.md b/docs/common/marker.zh.md index 940e313f92..7729bd3d0f 100644 --- a/docs/common/marker.zh.md +++ b/docs/common/marker.zh.md @@ -1,8 +1,17 @@ -| 参数名 | 类型 | 默认值 | 描述 | -| ------- | ---------------------------- | ------ | -------------------------------- | -| symbol | _Marker_ \| _MarkerCallback_ | - | 配置图例 marker 的 symbol 形状 | -| style | _ShapeAttrs_ | - | 图例项 marker 的配置项 | -| spacing | _number_ | - | 图例项 marker 同后面 name 的间距 | - -_Marker_ 为支持的标记类型有: _circle | square | line | diamond | triangle | triangle-down | hexagon | bowtie | cross | tick | plus | hyphen_; -_MarkerCallback_ 为 `(x: number, y: number, r: number) => PathCommand`; + + +| 参数名 | 类型 | 默认值 | 描述 | +| ------- | --------------------- | ------ | ------------------------------------------------------------------------ | +| symbol | _string \| MarkerSymbolCallback_  | - | 配置图例 marker 的 symbol 形状 | +| style | _ShapeAttrs \| ((style: ShapeAttrs) => ShapeAttrs)_ | - | 图例项 marker 的配置项 | +| spacing | number | - | 图例项 marker 同后面 name 的间距 | + +**_MarkerSymbolCallback_** 类型定义如下: + +除了内置一些 symbol 类型,可以指定具体的标记类型,也可以通过回调的方式返回 symbol 绘制的 path 命令 + +内置支持的标记类型有:`"circle" | "square" | "line" | "diamond" | "triangle" | "triangle-down" | "hexagon" | "bowtie" | "cross" | "tick" | "plus" | "hyphen"` + +回调的方式为:`(x: number, y: number, r: number) => PathCommand`; + + diff --git a/docs/common/pattern.en.md b/docs/common/pattern.en.md new file mode 100644 index 0000000000..678ecb8dbe --- /dev/null +++ b/docs/common/pattern.en.md @@ -0,0 +1,90 @@ +Set the pattern style of the geometries. +- PatternOption: consists of `type` and `cfg`, `type` includes: `dot`, `line`, `square`, `cfg` is optional. +- Features: pattern will override the `style` of geometry (such as pieStyle, columnStyle, etc.). +- Usage: You can set a uniform pattern style for all geometries of the chart by using configuration (`PatternOption`) or `CanvasPattern` object, and a `callback` is provided to set the pattern for each geometry. +In addition, we provide `getCanvasPattern` function, pass in the PatternOption to create the pattern to modify the Legend, Tooltip Marker styles[Demo](/zh/examples/plugin/pattern#legend-marker-with-pattern) + +The type of pattern is defined as follows: +```plain +PatternAttr = + | CanvasPattern + | PatternOption + | ((datum: Datum, color: string /** inherit color */) => PatternOption | CanvasPattern); +``` + +Usage: +```ts +// set a uniform pattern style for all geometries +{ + pattern: { + type: 'dot', + cfg: { + size: 4, + padding: 4, + rotation: 0, + fill: '#FFF', + isStagger: true, + }, + }, +} +// set the pattern for each geometry +{ + pattern: ({type}, color) =>{ + if(type ==='分类一') { + return { + type: 'dot', + cfg: { + backgroundColor: color, // inherit color + } + } + } else if(type ==='分类二') { + return { + type: 'square', + cfg: { + backgroundColor: 'pink', // custom color + } + } + } else if(type ==='分类三') { + return { + type: 'line' + } + } + }, +} +``` + + +Common configuration(cfg) for all types of pattern: + +| Attribute | Type | Description | +| ------------- | --------------- | ---------------- | +| backgroundColor | _string_ | Background color of the pattern | +| fill | _string_ | Fill color of the symbol in pattern | +| fillOpacity | _number_ | Transparency of the symbol in pattern | +| stroke | _string_ | Stroke color of the symbol in pattern | +| strokeOpacity | _number_ | Stroke opacity of the symbol in pattern | +| lineWidth | _number_ | The thickness of the symbol's stroke | +| opacity | _number_ | Overall transparency of the pattern | +| rotation | _number_ | Rotation angle of the pattern | + +Additional configuration for dotPattern + +| Attribute | Type | Description | +| ------------- | --------------- | ---------------- | +| size | _number_ | The size of the dot, default: `4` | +| padding | _number_ | The distance between dots, default: `2` | +| isStagger | _boolean_ | Staggered dots. default: `true` | + +Additional configuration for linePattern + +| Attribute | Type | Description | +| ------------- | --------------- | ---------------- | +| spacing | _number_ | The distance between the two lines, default: `4` | + +Additional configuration for squarePattern + +| Attribute | Type | Description | +| ------------- | --------------- | ---------------- | +| size | _number_ | The size of the square, default: `5` | +| padding | _number_ | The distance between squares, default:`0` | +| isStagger | _boolean_ | Staggered squares. default:`true` | \ No newline at end of file diff --git a/docs/common/pattern.zh.md b/docs/common/pattern.zh.md new file mode 100644 index 0000000000..66591b286e --- /dev/null +++ b/docs/common/pattern.zh.md @@ -0,0 +1,92 @@ +设置图形的贴图样式。 + +- 配置项:由`type`和`cfg`组成,`type`目前包括三种类型:`dot`、`line`、`square`,`cfg`为可选项。 +- 特点:`pattern`会覆盖当前图形设置的`style`样式(如 pieStyle、columnStyle 等)。 +- 使用方式:可通过 配置项(PatternOption) 或传入 CanvasPattern 对象 的方式给图表的所有图形设置统一的贴图样式,还提供了 callback 的方式给对应的图形设置样式。此外,提供了 getCanvasPattern 方法传入 PatternOption 配置来创建 pattern,以修改 Legend、Tooltip Marker 样式[Demo](/zh/examples/plugin/pattern#legend-marker-with-pattern) + +pattern 的类型定义如下: + +```plain +PatternAttr = + | CanvasPattern + | PatternOption + | ((datum: Datum, color: string /** inherit color */) => PatternOption | CanvasPattern); +``` + +具体用法: + +```ts +// 给图形设置统一贴图 +{ + pattern: { + type: 'dot', + cfg: { + size: 4, + padding: 4, + rotation: 0, + fill: '#FFF', + isStagger: true, + }, + }, +} +// 给图形分别设置贴图 +{ + pattern: ({type}, color) =>{ + if(type ==='分类一') { + return { + type: 'dot', + cfg: { + backgroundColor: color, // 继承主题颜色 + } + } + } else if(type ==='分类二') { + return { + type: 'square', + cfg: { + backgroundColor: 'pink', // 自定义颜色 + } + } + } else if(type ==='分类三') { + return { + type: 'line' + } + } + }, +} +``` + + +pattern 共有的 cfg 配置项 + +| 属性名 | 类型 | 介绍 | +| ------------- | --------------- | ---------------- | +| backgroundColor | _string_ | 贴图的背景色 | +| fill | _string_ | 贴图元素的填充色 | +| fillOpacity | _number_ | 贴图元素填充的透明度 | +| stroke | _string_ | 贴图元素的描边色 | +| strokeOpacity | _number_ | 贴图元素的描边透明度色 | +| lineWidth | _number_ | 贴图元素的描边粗细 | +| opacity | _number_ | 贴图整体的透明度 | +| rotation | _number_ | 贴图整体的旋转角度 | + +dotPattern 额外的 cfg 配置项 + +| 属性名 | 类型 | 介绍 | +| ------------- | --------------- | ---------------- | +| size | _number_ | 圆点的大小,默认为`4` | +| padding | _number_ | 圆点之间的间隔,默认为`2` | +| isStagger | _boolean_ | 圆点之间是否交错,默认为`true` | + +linePattern 额外的 cfg 配置项 + +| 属性名 | 类型 | 介绍 | +| ------------- | --------------- | ---------------- | +| spacing | _number_ | 两条线之间的距离,默认为`4` | + +squarePattern 额外的 cfg 配置项 + +| 属性名 | 类型 | 介绍 | +| ------------- | --------------- | ---------------- | +| size | _number_ | 矩形的大小,默认为`5` | +| padding | _number_ | 矩形之间的间隔,默认为`0` | +| isStagger | _boolean_ | 矩形之间是否交错,默认为`true` | \ No newline at end of file diff --git a/examples/area/percent/demo/meta.json b/examples/area/percent/demo/meta.json index e38cd05b62..5ab83843f3 100644 --- a/examples/area/percent/demo/meta.json +++ b/examples/area/percent/demo/meta.json @@ -11,6 +11,15 @@ "en": "Percent Stacked Area" }, "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*vr8gQJiyNmQAAAAAAAAAAAAAARQnAQ" + }, + { + "filename": "pattern.ts", + "title": { + "zh": "带贴图百分比堆叠面积图", + "en": "Percent Stacked Area with pattern" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/jJsc4f2dCu/dcbe08bf-8f48-4a22-90e3-e3dd60369d0d.png" } ] } diff --git a/examples/area/percent/demo/pattern.ts b/examples/area/percent/demo/pattern.ts new file mode 100644 index 0000000000..d5c7c57331 --- /dev/null +++ b/examples/area/percent/demo/pattern.ts @@ -0,0 +1,29 @@ +import { Area } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/bmw-prod/67ef5751-b228-417c-810a-962f978af3e7.json') + .then((res) => res.json()) + .then((data) => { + const area = new Area('container', { + data, + xField: 'year', + yField: 'value', + seriesField: 'country', + color: ['#82d1de', '#cb302d', '#e3ca8c'], + areaStyle: { + fillOpacity: 0.7, + }, + appendPadding: 10, + isPercent: true, + yAxis: { + label: { + formatter: (value) => { + return value * 100; + }, + }, + }, + pattern: { + type: 'line', + }, + }); + area.render(); + }); diff --git a/examples/component/legend/demo/meta.json b/examples/component/legend/demo/meta.json index 697119d63e..226afcb2f9 100644 --- a/examples/component/legend/demo/meta.json +++ b/examples/component/legend/demo/meta.json @@ -26,7 +26,6 @@ "zh": "图例项配置", "en": "Configuration of legend item" }, - "new": true, "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/Bn1qWqQlkG/2d3c80e3-acde-4d13-afc5-83063f684ade.png" }, { @@ -43,7 +42,6 @@ "zh": "图例项分页设置", "en": "Legend flip-page configuration" }, - "new": "true", "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/W6n7yMd5lw/0a28a2bd-e5f3-4756-8fda-d8e2faf79076.png" } ] diff --git a/examples/dual-axes/column-line/demo/pattern.ts b/examples/dual-axes/column-line/demo/pattern.ts new file mode 100644 index 0000000000..6ad6face6b --- /dev/null +++ b/examples/dual-axes/column-line/demo/pattern.ts @@ -0,0 +1,29 @@ +import { DualAxes } from '@antv/g2plot'; + +const data = [ + { time: '2019-03', value: 350, count: 800 }, + { time: '2019-04', value: 900, count: 600 }, + { time: '2019-05', value: 300, count: 400 }, + { time: '2019-06', value: 450, count: 380 }, + { time: '2019-07', value: 470, count: 220 }, +]; + +const dualAxes = new DualAxes('container', { + data: [data, data], + xField: 'time', + yField: ['value', 'count'], + geometryOptions: [ + { + geometry: 'column', + pattern: { type: 'line' }, + }, + { + geometry: 'line', + lineStyle: { + lineWidth: 2, + }, + }, + ], +}); + +dualAxes.render(); diff --git a/examples/dual-axes/grouped-column-line/demo/meta.json b/examples/dual-axes/grouped-column-line/demo/meta.json index 104a66f1c0..d598172588 100644 --- a/examples/dual-axes/grouped-column-line/demo/meta.json +++ b/examples/dual-axes/grouped-column-line/demo/meta.json @@ -20,6 +20,15 @@ }, "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*Ogg7R6trDvgAAAAAAAAAAAAAARQnAQ" }, + { + "filename": "pattern-with-callback.ts", + "title": { + "zh": "带纹理图案的分组柱线图表", + "en": "Grouped column line with pattern" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/SL3QbumEbL/6131d19d-56b8-42bb-9609-8d0640a77d4a.png" + }, { "filename": "grouped-column-multi-line.ts", "title": { diff --git a/examples/dual-axes/grouped-column-line/demo/pattern-with-callback.ts b/examples/dual-axes/grouped-column-line/demo/pattern-with-callback.ts new file mode 100644 index 0000000000..f6c7dd77eb --- /dev/null +++ b/examples/dual-axes/grouped-column-line/demo/pattern-with-callback.ts @@ -0,0 +1,68 @@ +import { DualAxes } from '@antv/g2plot'; + +const uvBillData = [ + { time: '2019-03', value: 350, type: 'uv' }, + { time: '2019-04', value: 900, type: 'uv' }, + { time: '2019-05', value: 300, type: 'uv' }, + { time: '2019-06', value: 450, type: 'uv' }, + { time: '2019-07', value: 470, type: 'uv' }, + { time: '2019-03', value: 220, type: 'bill' }, + { time: '2019-04', value: 300, type: 'bill' }, + { time: '2019-05', value: 250, type: 'bill' }, + { time: '2019-06', value: 220, type: 'bill' }, + { time: '2019-07', value: 362, type: 'bill' }, +]; + +const transformData = [ + { time: '2019-03', count: 800 }, + { time: '2019-04', count: 600 }, + { time: '2019-05', count: 400 }, + { time: '2019-06', count: 380 }, + { time: '2019-07', count: 220 }, +]; + +const dualAxes = new DualAxes('container', { + data: [uvBillData, transformData], + xField: 'time', + yField: ['value', 'count'], + geometryOptions: [ + { + geometry: 'column', + isGroup: true, + seriesField: 'type', + columnWidthRatio: 0.4, + label: {}, + color: ['#5B8FF9', '#5D7092'], + pattern: ({ type }) => { + return { type: type === 'bill' ? 'square' : 'line' }; + }, + }, + { + geometry: 'line', + color: '#5AD8A6', + }, + ], + legend: { + custom: true, + position: 'bottom', + items: [ + { + value: 'uv', + name: 'uv', + marker: { symbol: 'square', style: { fill: '#5B8FF9', r: 5 } }, + }, + { + value: 'bill', + name: '账单', + marker: { symbol: 'square', style: { fill: '#5D7092', r: 5 } }, + }, + { + value: 'count', + name: '数值', + marker: { symbol: 'square', style: { fill: '#5AD8A6', r: 5 } }, + }, + ], + }, +}); + +dualAxes.render(); diff --git a/examples/heatmap/basic/demo/meta.json b/examples/heatmap/basic/demo/meta.json index e5acb0a371..2ebc802adf 100644 --- a/examples/heatmap/basic/demo/meta.json +++ b/examples/heatmap/basic/demo/meta.json @@ -12,6 +12,15 @@ }, "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*55KOQI2EhVUAAAAAAAAAAAAAARQnAQ" }, + { + "filename": "pattern.ts", + "title": { + "zh": "带贴图热力图", + "en": "Basic heatmap plot with pattern" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/JitXZA%26D%26S/30a463c4-05ee-4a6b-a69c-c2a7b44a9584.png" + }, { "filename": "shape.ts", "title": { diff --git a/examples/heatmap/basic/demo/pattern.ts b/examples/heatmap/basic/demo/pattern.ts new file mode 100644 index 0000000000..ba5690a911 --- /dev/null +++ b/examples/heatmap/basic/demo/pattern.ts @@ -0,0 +1,31 @@ +import { Heatmap } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/basement_prod/a719cd4e-bd40-4878-a4b4-df8a6b531dfe.json') + .then((res) => res.json()) + .then((data) => { + const heatmapPlot = new Heatmap(document.getElementById('container'), { + width: 650, + height: 500, + autoFit: false, + data, + xField: 'Month of Year', + yField: 'District', + colorField: 'AQHI', + color: ['#174c83', '#7eb6d4', '#efefeb', '#efa759', '#9b4d16'], + meta: { + 'Month of Year': { + type: 'cat', + }, + }, + tooltip: { + showMarkers: false, + }, + pattern: { + type: 'square', + cfg: { + isStagger: true, + }, + }, + }); + heatmapPlot.render(); + }); diff --git a/examples/more-plots/sunburst/demo/meta.json b/examples/more-plots/sunburst/demo/meta.json index edb5844d81..0be2424d1a 100644 --- a/examples/more-plots/sunburst/demo/meta.json +++ b/examples/more-plots/sunburst/demo/meta.json @@ -52,6 +52,15 @@ }, "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/hWIcBgVYMI/d265c8b0-2184-48aa-8610-173cb0b065a8.png" }, + { + "filename": "pattern.ts", + "title": { + "zh": "带纹理图案的旭日图", + "en": "Sunburst plot with pattern" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/UUU%24Gu8byX/5caede2e-eef7-4512-9ac5-d8ae4f3fa5cd.png" + }, { "filename": "tooltip-fields.ts", "title": { diff --git a/examples/more-plots/sunburst/demo/pattern.ts b/examples/more-plots/sunburst/demo/pattern.ts new file mode 100644 index 0000000000..64065ff29b --- /dev/null +++ b/examples/more-plots/sunburst/demo/pattern.ts @@ -0,0 +1,23 @@ +import { Sunburst } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/sunburst.json') + .then((res) => res.json()) + .then((data) => { + const plot = new Sunburst('container', { + data, + innerRadius: 0.3, + interactions: [{ type: 'element-active' }], + hierarchyConfig: { + field: 'sum', + }, + drilldown: { + breadCrumb: { + rootText: '起始', + }, + }, + pattern: { + type: 'line', + }, + }); + plot.render(); + }); diff --git a/examples/pie/basic/demo/basic.ts b/examples/pie/basic/demo/basic.ts index 27b300511a..cecd59650a 100644 --- a/examples/pie/basic/demo/basic.ts +++ b/examples/pie/basic/demo/basic.ts @@ -24,6 +24,15 @@ const piePlot = new Pie('container', { textAlign: 'center', }, }, + pattern: { + type: 'dot', + cfg: { + radius: 2, + padding: 10, + isStagger: true, + mode: 'repeat', + }, + }, interactions: [{ type: 'element-active' }], }); diff --git a/examples/plugin/pattern/API.en.md b/examples/plugin/pattern/API.en.md new file mode 100644 index 0000000000..c0e8507701 --- /dev/null +++ b/examples/plugin/pattern/API.en.md @@ -0,0 +1 @@ +`markdown:docs/api/pattern.en.md` diff --git a/examples/plugin/pattern/API.zh.md b/examples/plugin/pattern/API.zh.md new file mode 100644 index 0000000000..0ad99d158f --- /dev/null +++ b/examples/plugin/pattern/API.zh.md @@ -0,0 +1 @@ +`markdown:docs/api/pattern.zh.md` diff --git a/examples/plugin/pattern/demo/bar-pattern.ts b/examples/plugin/pattern/demo/bar-pattern.ts new file mode 100644 index 0000000000..c84e006540 --- /dev/null +++ b/examples/plugin/pattern/demo/bar-pattern.ts @@ -0,0 +1,63 @@ +import { Bar, getCanvasPattern } from '@antv/g2plot'; +import { deepMix } from '@antv/util'; + +const data = [ + { type: '分类一', value: 27 }, + { type: '分类二', value: 25 }, + { type: '分类三', value: 18 }, + { type: '分类四', value: 15 }, + { type: '分类五', value: 10 }, +]; +const PATTERN_MAP = { + 分类一: { + type: 'dot', + }, + 分类二: { + type: 'square', + }, + 分类三: { + type: 'line', + }, + 分类四: { + type: 'square', + cfg: { + size: 4, + padding: 1, + rotation: 45, + isStagger: false, + }, + }, + 分类五: { + type: 'line', + cfg: { + spacing: 3, + lineWidth: 2, + rotation: 90, + }, + }, +}; +const pattern = ({ type }, color) => + getCanvasPattern(deepMix({}, PATTERN_MAP[type], { cfg: { backgroundColor: color } })); + +const plot = new Bar('container', { + data, + xField: 'value', + yField: 'type', + // 可不设置 + seriesField: 'type', + legend: { + marker: (text, index, item) => { + const color = item.style.fill; + return { + style: { + fill: pattern({ type: text }, color), + }, + }; + }, + }, + pattern: ({ type }) => { + return PATTERN_MAP[type] || { type: 'dot' }; + }, +}); + +plot.render(); diff --git a/examples/plugin/pattern/demo/column-pattern.ts b/examples/plugin/pattern/demo/column-pattern.ts new file mode 100644 index 0000000000..870d7c57d5 --- /dev/null +++ b/examples/plugin/pattern/demo/column-pattern.ts @@ -0,0 +1,27 @@ +import { Column } from '@antv/g2plot'; + +const data = [ + { type: '分类一', value: 27 }, + { type: '分类二', value: 25 }, + { type: '分类三', value: 18 }, + { type: '分类四', value: 15 }, + { type: '分类五', value: 10 }, + { type: '其他', value: 5 }, +]; + +const plot = new Column('container', { + data, + yField: 'value', + xField: 'type', + pattern: { + type: 'line', + cfg: { + spacing: 6, + lineWidth: 2, + strokeOpacity: 0.5, + rotation: 45, + }, + }, +}); + +plot.render(); diff --git a/examples/plugin/pattern/demo/group-column-pattern.ts b/examples/plugin/pattern/demo/group-column-pattern.ts new file mode 100644 index 0000000000..27006ccb4a --- /dev/null +++ b/examples/plugin/pattern/demo/group-column-pattern.ts @@ -0,0 +1,41 @@ +import { Column } from '@antv/g2plot'; + +const data = [ + { name: 'London', 月份: 'Jan.', 月均降雨量: 18.9 }, + { name: 'London', 月份: 'Feb.', 月均降雨量: 28.8 }, + { name: 'London', 月份: 'Mar.', 月均降雨量: 39.3 }, + { name: 'London', 月份: 'Apr.', 月均降雨量: 81.4 }, + { name: 'London', 月份: 'May', 月均降雨量: 47 }, + { name: 'London', 月份: 'Jun.', 月均降雨量: 20.3 }, + { name: 'London', 月份: 'Jul.', 月均降雨量: 24 }, + { name: 'London', 月份: 'Aug.', 月均降雨量: 35.6 }, + { name: 'Berlin', 月份: 'Jan.', 月均降雨量: 12.4 }, + { name: 'Berlin', 月份: 'Feb.', 月均降雨量: 23.2 }, + { name: 'Berlin', 月份: 'Mar.', 月均降雨量: 34.5 }, + { name: 'Berlin', 月份: 'Apr.', 月均降雨量: 99.7 }, + { name: 'Berlin', 月份: 'May', 月均降雨量: 52.6 }, + { name: 'Berlin', 月份: 'Jun.', 月均降雨量: 35.5 }, + { name: 'Berlin', 月份: 'Jul.', 月均降雨量: 37.4 }, + { name: 'Berlin', 月份: 'Aug.', 月均降雨量: 42.4 }, +]; + +const plot = new Column('container', { + data, + yField: '月均降雨量', + xField: '月份', + seriesField: 'name', + isGroup: true, + pattern: ({ name }) => { + if (name === 'London') { + return { + type: 'line', + }; + } else { + return { + type: 'dot', + }; + } + }, +}); + +plot.render(); diff --git a/examples/plugin/pattern/demo/legend-marker-with-pattern.ts b/examples/plugin/pattern/demo/legend-marker-with-pattern.ts new file mode 100644 index 0000000000..813f5c82c0 --- /dev/null +++ b/examples/plugin/pattern/demo/legend-marker-with-pattern.ts @@ -0,0 +1,51 @@ +import { Pie, getCanvasPattern } from '@antv/g2plot'; + +const data = [ + { type: '分类一', value: 27 }, + { type: '分类二', value: 25 }, + { type: '分类三', value: 18 }, + { type: '分类四', value: 15 }, + { type: '分类五', value: 10 }, + { type: '其他', value: 5 }, +]; + +const pattern = (datum, color) => + getCanvasPattern({ + type: datum.type === '其他' ? 'dot' : 'line', + cfg: { + backgroundColor: datum.type === '其他' ? '#014c63' : color, + }, + }); + +const plot = new Pie('container', { + appendPadding: 10, + data, + angleField: 'value', + colorField: 'type', + radius: 0.6, + label: { + type: 'outer', + offset: '20%', + content: ({ percent }) => `${(percent * 100).toFixed(0)}%`, + style: { + fontSize: 12, + }, + }, + pieStyle: { + lineWidth: 1, + }, + legend: { + marker: (text, index, item) => { + const color = item.style.fill; + return { + style: { + fill: pattern({ type: text }, color), + }, + }; + }, + }, + pattern, + interactions: [{ type: 'element-active' }], +}); + +plot.render(); diff --git a/examples/plugin/pattern/demo/meta.json b/examples/plugin/pattern/demo/meta.json new file mode 100644 index 0000000000..a08f94eb59 --- /dev/null +++ b/examples/plugin/pattern/demo/meta.json @@ -0,0 +1,62 @@ +{ + "title": { + "zh": "中文分类", + "en": "Category" + }, + "demos": [ + { + "filename": "pie-pattern.ts", + "title": { + "zh": "带贴图图案的饼图", + "en": "Pie plot with pattern" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/q74g6X7aF3/d9238359-5228-402e-a265-f3e2d25aa802.png" + }, + { + "filename": "legend-marker-with-pattern.ts", + "title": { + "zh": "图例 marker 设置贴图图案", + "en": "Custom legend marker with pattern" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/%241UK9BTcGM/c8446d1c-3180-4f95-8d9b-487ad04b87fc.png" + }, + { + "filename": "column-pattern.ts", + "title": { + "zh": "带贴图图案的柱状图", + "en": "Column plot with pattern" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/1%26kZLf2KMW/68f576f5-1729-45f8-a48c-9e12e809ade1.png" + }, + { + "filename": "group-column-pattern.ts", + "title": { + "zh": "带贴图图案的分组柱状图", + "en": "Grouped column plot with pattern" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/sAXTkW5p2C/93012937-dd60-4a52-9b4b-cb0ea01fd078.png" + }, + { + "filename": "bar-pattern.ts", + "title": { + "zh": "带贴图图案的条形图", + "en": "Bar plot with pattern" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/5Q%24sV5H6BG/cc8ebaba-0c13-4f3e-bf24-0de4e20f6c2d.png" + }, + { + "filename": "radial-bar-pattern.ts", + "title": { + "zh": "带贴图图案的玉珏图", + "en": "Radial-bar plot with pattern" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/WKAcXIU4px/a20a3082-5c77-4787-87e6-4d4b072470a7.png" + } + ] +} diff --git a/examples/plugin/pattern/demo/pie-pattern.ts b/examples/plugin/pattern/demo/pie-pattern.ts new file mode 100644 index 0000000000..568316c5c0 --- /dev/null +++ b/examples/plugin/pattern/demo/pie-pattern.ts @@ -0,0 +1,29 @@ +import { Pie } from '@antv/g2plot'; + +const data = [ + { type: '分类一', value: 27 }, + { type: '分类二', value: 25 }, + { type: '分类三', value: 18 }, + { type: '分类四', value: 15 }, + { type: '分类五', value: 10 }, + { type: '其他', value: 5 }, +]; + +const plot = new Pie('container', { + appendPadding: 10, + data, + angleField: 'value', + colorField: 'type', + radius: 0.6, + label: false, + pattern: { + type: 'dot', + cfg: { + size: 4, + padding: 5, + }, + }, + interactions: [{ type: 'element-active' }], +}); + +plot.render(); diff --git a/examples/plugin/pattern/demo/radial-bar-pattern.ts b/examples/plugin/pattern/demo/radial-bar-pattern.ts new file mode 100644 index 0000000000..44cfea2712 --- /dev/null +++ b/examples/plugin/pattern/demo/radial-bar-pattern.ts @@ -0,0 +1,38 @@ +import { RadialBar } from '@antv/g2plot'; + +const data = [ + { name: 'X6', star: 297 }, + { name: 'G', star: 506 }, + { name: 'AVA', star: 805 }, + { name: 'G2Plot', star: 1478 }, + { name: 'L7', star: 2029 }, + { name: 'G6', star: 7100 }, + { name: 'F2', star: 7346 }, + { name: 'G2', star: 10178 }, +]; + +const plot = new RadialBar('container', { + data, + xField: 'name', + yField: 'star', + // color 字段,可不设置 + colorField: 'name', + radius: 0.8, + innerRadius: 0.2, + tooltip: { + formatter: (datum) => { + return { name: 'star数', value: datum.star }; + }, + }, + pattern: { + type: 'dot', + cfg: { + size: 4, + padding: 2, + // dot 不进行交错 + isStagger: true, + }, + }, +}); + +plot.render(); diff --git a/examples/plugin/pattern/index.en.md b/examples/plugin/pattern/index.en.md new file mode 100644 index 0000000000..57300062e6 --- /dev/null +++ b/examples/plugin/pattern/index.en.md @@ -0,0 +1,4 @@ +--- +title: Pattern +order: 4 +--- diff --git a/examples/plugin/pattern/index.zh.md b/examples/plugin/pattern/index.zh.md new file mode 100644 index 0000000000..aee77f7a40 --- /dev/null +++ b/examples/plugin/pattern/index.zh.md @@ -0,0 +1,4 @@ +--- +title: 图案 - Pattern +order: 4 +--- diff --git a/examples/progress-plots/liquid/demo/meta.json b/examples/progress-plots/liquid/demo/meta.json index 3e5ca9058b..8cfaeaa35f 100644 --- a/examples/progress-plots/liquid/demo/meta.json +++ b/examples/progress-plots/liquid/demo/meta.json @@ -28,6 +28,15 @@ }, "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*IzMHQL12utQAAAAAAAAAAAAAARQnAQ" }, + { + "filename": "pattern.ts", + "title": { + "zh": "带纹理图案的水波图", + "en": "Liquid plot with pattern" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/6uQnXA6lU2/4916bed6-3aeb-427b-ad86-51e3db07c0a1.png" + }, { "filename": "outline-style.ts", "title": { diff --git a/examples/progress-plots/liquid/demo/pattern.ts b/examples/progress-plots/liquid/demo/pattern.ts new file mode 100644 index 0000000000..646f501620 --- /dev/null +++ b/examples/progress-plots/liquid/demo/pattern.ts @@ -0,0 +1,16 @@ +import { Liquid } from '@antv/g2plot'; + +const plot = new Liquid('container', { + percent: 0.65, + shape: 'diamond', + outline: { + border: 4, + distance: 8, + }, + wave: { + length: 128, + }, + pattern: { type: 'line' }, +}); + +plot.render(); diff --git a/examples/relation-plots/sankey/demo/meta.json b/examples/relation-plots/sankey/demo/meta.json index 4b5df3a200..827ad1be8a 100644 --- a/examples/relation-plots/sankey/demo/meta.json +++ b/examples/relation-plots/sankey/demo/meta.json @@ -18,7 +18,6 @@ "zh": "桑基图状态交互", "en": "Sankey with state" }, - "new": true, "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/rAuJKL%24Y6n/7a301a5e-4864-44f3-acb7-ebefb9c2402b.png" }, { diff --git a/examples/relation-plots/sankey/demo/set-state.ts b/examples/relation-plots/sankey/demo/set-state.ts index 97d37f981a..02e9c26c1b 100644 --- a/examples/relation-plots/sankey/demo/set-state.ts +++ b/examples/relation-plots/sankey/demo/set-state.ts @@ -27,14 +27,14 @@ const sankey = new Sankey('container', { nodeState: { active: { style: { - linewidth: 1.5 - } - } + linewidth: 1.5, + }, + }, }, - tooltip: { showTitle: true } + tooltip: { showTitle: true }, }); sankey.render(); sankey.setState('active', (datum) => { - return datum.isNode && datum.name === '首次打开' + return datum.isNode && datum.name === '首次打开'; }); diff --git a/examples/rose/grouped/demo/basic.ts b/examples/rose/grouped/demo/basic.ts index 2c4f724899..8b07fb7070 100644 --- a/examples/rose/grouped/demo/basic.ts +++ b/examples/rose/grouped/demo/basic.ts @@ -1,85 +1,26 @@ import { Rose } from '@antv/g2plot'; -const data = [ - { - type: '分类一', - value: 27, - user: '用户一', - }, - { - type: '分类二', - value: 25, - user: '用户一', - }, - { - type: '分类三', - value: 18, - user: '用户一', - }, - { - type: '分类四', - value: 15, - user: '用户一', - }, - { - type: '分类五', - value: 10, - user: '用户一', - }, - { - type: '其它', - value: 5, - user: '用户一', - }, - { - type: '分类一', - value: 7, - user: '用户二', - }, - { - type: '分类二', - value: 5, - user: '用户二', - }, - { - type: '分类三', - value: 38, - user: '用户二', - }, - { - type: '分类四', - value: 5, - user: '用户二', - }, - { - type: '分类五', - value: 20, - user: '用户二', - }, - { - type: '其它', - value: 15, - user: '用户二', - }, -]; +fetch('https://gw.alipayobjects.com/os/antfincdn/XcLAPaQeeP/rose-data.json') + .then((data) => data.json()) + .then((data) => { + // 分组玫瑰图 + const rosePlot = new Rose('container', { + data, + xField: 'type', + yField: 'value', + isGroup: true, + // 当 isGroup 为 true 时,该值为必填 + seriesField: 'user', + radius: 0.9, + label: { + offset: -15, + }, + interactions: [ + { + type: 'element-active', + }, + ], + }); -// 分组玫瑰图 -const rosePlot = new Rose('container', { - data, - xField: 'type', - yField: 'value', - isGroup: true, - // 当 isGroup 为 true 时,该值为必填 - seriesField: 'user', - radius: 0.9, - label: { - offset: -15, - }, - interactions: [ - { - type: 'element-active', - }, - ], -}); - -rosePlot.render(); + rosePlot.render(); + }); diff --git a/examples/rose/grouped/demo/meta.json b/examples/rose/grouped/demo/meta.json index dfce295358..8d36ca91ed 100644 --- a/examples/rose/grouped/demo/meta.json +++ b/examples/rose/grouped/demo/meta.json @@ -11,6 +11,15 @@ "en": "Basic grouped rose plot" }, "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*EUesQ4eJ1fcAAAAAAAAAAAAAARQnAQ" + }, + { + "filename": "pattern.ts", + "title": { + "zh": "带纹理图案的分组玫瑰图", + "en": "Grouped rose plot with pattern" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/bVfROtpcku/a1cf2fa2-35ef-4a3a-b2cc-ade53b537e11.png" } ] } diff --git a/examples/rose/grouped/demo/pattern.ts b/examples/rose/grouped/demo/pattern.ts new file mode 100644 index 0000000000..aa60e0ee83 --- /dev/null +++ b/examples/rose/grouped/demo/pattern.ts @@ -0,0 +1,29 @@ +import { Rose } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/antfincdn/XcLAPaQeeP/rose-data.json') + .then((data) => data.json()) + .then((data) => { + // 分组玫瑰图 + const rosePlot = new Rose('container', { + data, + xField: 'type', + yField: 'value', + isGroup: true, + // 当 isGroup 为 true 时,该值为必填 + seriesField: 'user', + radius: 0.9, + label: { + offset: -15, + }, + pattern: { + type: 'dot', + }, + interactions: [ + { + type: 'element-active', + }, + ], + }); + + rosePlot.render(); + }); diff --git a/examples/tiny/tiny-area/demo/meta.json b/examples/tiny/tiny-area/demo/meta.json index a4a5451ed2..2d77b8ae5a 100644 --- a/examples/tiny/tiny-area/demo/meta.json +++ b/examples/tiny/tiny-area/demo/meta.json @@ -12,6 +12,15 @@ }, "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*1dr5RKX8gWIAAAAAAAAAAAAAARQnAQ" }, + { + "filename": "pattern.ts", + "title": { + "zh": "带纹理图案的迷你面积图", + "en": "Tiny area plot with pattern" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/DZ4mKJ9Y%240/aeae5577-f5fc-49e1-a4aa-eeb91d76610d.png" + }, { "filename": "area-annotation.ts", "title": { diff --git a/examples/tiny/tiny-area/demo/pattern.ts b/examples/tiny/tiny-area/demo/pattern.ts new file mode 100644 index 0000000000..640eb955ba --- /dev/null +++ b/examples/tiny/tiny-area/demo/pattern.ts @@ -0,0 +1,16 @@ +import { TinyArea } from '@antv/g2plot'; + +const data = [ + 264, 417, 438, 887, 309, 397, 550, 575, 563, 430, 525, 592, 492, 467, 513, 546, 983, 340, 539, 243, 226, 192, +]; + +const tinyArea = new TinyArea('container', { + height: 60, + autoFit: false, + data, + smooth: true, + color: '#E5EDFE', + pattern: { type: 'line', cfg: { stroke: '#5B8FF9' } }, +}); + +tinyArea.render(); diff --git a/examples/tiny/tiny-column/demo/meta.json b/examples/tiny/tiny-column/demo/meta.json index 7b6a602274..192286053c 100644 --- a/examples/tiny/tiny-column/demo/meta.json +++ b/examples/tiny/tiny-column/demo/meta.json @@ -8,7 +8,15 @@ "filename": "basic-column.ts", "title": { "zh": "基础迷你柱形图", - "en": "Basic column plot" + "en": "Tiny column plot" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*tLNqQpXbZWgAAAAAAAAAAAAAARQnAQ" + }, + { + "filename": "tiny-column-pattern.ts", + "title": { + "zh": "带贴图迷你柱形图", + "en": "Tiny column plot with pattern" }, "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*tLNqQpXbZWgAAAAAAAAAAAAAARQnAQ" }, diff --git a/examples/tiny/tiny-column/demo/tiny-column-pattern.ts b/examples/tiny/tiny-column/demo/tiny-column-pattern.ts new file mode 100644 index 0000000000..ffe92587d0 --- /dev/null +++ b/examples/tiny/tiny-column/demo/tiny-column-pattern.ts @@ -0,0 +1,17 @@ +import { TinyColumn } from '@antv/g2plot'; + +const data = [274, 337, 81, 497, 666, 219, 269]; + +const tinyColumn = new TinyColumn('container', { + height: 64, + autoFit: false, + data, + tooltip: { + customContent: function (x, data) { + return `NO.${x}: ${data[0]?.data?.y.toFixed(2)}`; + }, + }, + pattern: { type: 'line' }, +}); + +tinyColumn.render(); diff --git a/gatsby-config.js b/gatsby-config.js index 97038c7e48..f6c0e428c7 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -26,6 +26,7 @@ module.exports = { galleryMenuCloseAll: true, showChartResize: true, // 是否在demo页展示图表视图切换 showAPIDoc: true, // 是否在demo页展示API文档 + showExampleDemoTitle: true, // demo 页截图展示 title themeSwitcher: 'g2plot', playground: { extraLib: '', diff --git a/package.json b/package.json index eda1887ec1..b63e8449c9 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "lint": "eslint --ext .ts ./src ./__tests__ && prettier --check ./src ./__tests__ && lint-md ./examples ./docs", "lint-staged": "lint-staged", "test": "jest", - "test-live": "cross-env DEBUG_MODE=1 jest --watch ./__tests__", + "test-live": "cross-env DEBUG_MODE=1 jest --watch ./__tests__/", "coverage": "jest -w 16 --coverage", "ci": "run-s lint coverage build", "changelog": "generate-changelog", @@ -56,7 +56,7 @@ }, "dependencies": { "@antv/event-emitter": "^0.1.2", - "@antv/g2": "^4.1.19", + "@antv/g2": "^4.1.23", "d3-hierarchy": "^2.0.0", "d3-regression": "^1.3.5", "pdfast": "^0.2.0", diff --git a/src/adaptor/common.ts b/src/adaptor/common.ts index 19a53a4215..a24910dc52 100644 --- a/src/adaptor/common.ts +++ b/src/adaptor/common.ts @@ -206,3 +206,5 @@ export function limitInPlot(params: Params): Params { return params; } + +export { pattern } from './pattern'; diff --git a/src/adaptor/pattern.ts b/src/adaptor/pattern.ts new file mode 100644 index 0000000000..3f25121f4b --- /dev/null +++ b/src/adaptor/pattern.ts @@ -0,0 +1,66 @@ +import { Util } from '@antv/g2'; +import { get } from '@antv/util'; +import { getCanvasPattern } from '../utils/pattern'; +import { Params } from '../core/adaptor'; +import { Datum, Options, StyleAttr } from '../types'; +import { deepAssign } from '../utils'; + +/** + * 使用 Pattern 通道的 options,要求有 colorField/seriesField/stackField 作为分类字段(进行颜色映射) + */ +type OptionsRequiredInPattern = Omit; + +/** + * Pattern 通道,处理图案填充 + * 🚀 目前支持图表类型:饼图、柱状图、条形图、玉珏图等(不支持在多 view 图表中,后续按需扩展) + * + * @param key key of style property + * @returns + */ +export function pattern(key: string) { + return (params: Params): Params => { + const { options, chart } = params; + const { pattern: patternOption } = options; + + // 没有 pattern 配置,则直接返回 + if (!patternOption) { + return params; + } + + /** ~~~~~~~ 进行贴图图案处理 ~~~~~~~ */ + + const style: StyleAttr = (datum?: Datum, ...args: any[]) => { + const { defaultColor } = chart.getTheme(); + let color = defaultColor; + + const colorAttribute = chart.geometries?.[0]?.getAttribute('color'); + if (colorAttribute) { + const colorField = colorAttribute.getFields()[0]; + const seriesValue = get(datum, colorField); + color = Util.getMappingValue(colorAttribute, seriesValue, colorAttribute.values?.[0] || defaultColor); + } + + let pattern: CanvasPattern = patternOption as CanvasPattern; + + // 1. 如果 patternOption 是一个回调,则获取回调结果。`(datum: Datum, color: string) => CanvasPattern` + if (typeof patternOption === 'function') { + pattern = patternOption.call(this, datum, color); + } + + // 2. 如果 pattern 不是 CanvasPattern,则进一步处理,否则直接赋予给 fill + if (pattern instanceof CanvasPattern === false) { + // 通过 createPattern(PatternStyle) 转换为 CanvasPattern + pattern = getCanvasPattern(deepAssign({}, { cfg: { backgroundColor: color } }, pattern)); + } + + const styleOption = options[key] as StyleAttr; + + return { + ...(typeof styleOption === 'function' ? styleOption.call(this, datum, ...args) : styleOption || {}), + fill: pattern || color, + }; + }; + + return deepAssign({}, params, { options: { [key]: style } }); + }; +} diff --git a/src/index.ts b/src/index.ts index 344786a023..6b7bcb2a22 100644 --- a/src/index.ts +++ b/src/index.ts @@ -158,6 +158,9 @@ export type { CirclePackingOptions } from './plots/circle-packing'; /** 所有开放图表都使用 G2Plot.P 作为入口开发,理论上官方的所有图表都可以走 G2Plot.P 的入口(暂时不处理) */ export { P } from './plugin'; +/** 开放 getCanvasPatterng 方法 */ +export { getCanvasPattern } from './utils/pattern'; + // 已经废弃,更名为 Mix export { Mix as MultiView } from './plots/mix'; export type { MixOptions as MultiViewOptions } from './plots/mix'; diff --git a/src/plots/area/adaptor.ts b/src/plots/area/adaptor.ts index 25e0b6ad9d..f200d2c2dc 100644 --- a/src/plots/area/adaptor.ts +++ b/src/plots/area/adaptor.ts @@ -1,6 +1,6 @@ import { Geometry } from '@antv/g2'; import { each } from '@antv/util'; -import { tooltip, slider, interaction, animation, theme, annotation, limitInPlot } from '../../adaptor/common'; +import { tooltip, slider, interaction, animation, theme, annotation, limitInPlot, pattern } from '../../adaptor/common'; import { findGeometry } from '../../utils'; import { Params } from '../../core/adaptor'; import { area, point, line } from '../../adaptor/geometries'; @@ -134,10 +134,11 @@ function adjust(params: Params): Params { export function adaptor(params: Params) { // flow 的方式处理所有的配置到 G2 API return flow( + theme, + pattern('areaStyle'), geometry, meta, adjust, - theme, axis, legend, tooltip, diff --git a/src/plots/circle-packing/adaptor.ts b/src/plots/circle-packing/adaptor.ts index baeeac7b7e..25a8cc74b0 100644 --- a/src/plots/circle-packing/adaptor.ts +++ b/src/plots/circle-packing/adaptor.ts @@ -2,7 +2,15 @@ import { get } from '@antv/util'; import { Types } from '@antv/g2'; import { point } from '../../adaptor/geometries/point'; import { Params } from '../../core/adaptor'; -import { interaction as baseInteraction, animation, theme, legend, annotation, scale } from '../../adaptor/common'; +import { + interaction as baseInteraction, + animation, + theme, + legend, + annotation, + scale, + pattern, +} from '../../adaptor/common'; import { flow, deepAssign } from '../../utils'; import { getAdjustAppendPadding } from '../../utils/padding'; import { transformData, resolvePaddingForCircle, resolveAllPadding } from './utils'; @@ -206,6 +214,7 @@ function interaction(params: Params): Params) { return flow( + pattern('pointStyle'), defaultOptions, padding, theme, diff --git a/src/plots/column/adaptor.ts b/src/plots/column/adaptor.ts index 741a01fe94..7eb487f1ce 100644 --- a/src/plots/column/adaptor.ts +++ b/src/plots/column/adaptor.ts @@ -15,10 +15,11 @@ import { import { conversionTag } from '../../adaptor/conversion-tag'; import { connectedArea } from '../../adaptor/connected-area'; import { interval } from '../../adaptor/geometries'; +import { pattern } from '../../adaptor/pattern'; +import { brushInteraction } from '../../adaptor/brush'; import { flow, transformLabel, deepAssign, findGeometry, adjustYMetaByZero, pick } from '../../utils'; import { getDataWhetherPecentage, getDeepPercent } from '../../utils/transform/percent'; import { Datum } from '../../types'; -import { brushInteraction } from '../../adaptor/brush'; import { ColumnOptions } from './types'; /** @@ -287,6 +288,7 @@ export function adaptor(params: Params, isBar = false) { return flow( defaultOptions, // 处理默认配置 theme, // theme 需要在 geometry 之前 + pattern('columnStyle'), state, geometry, meta, diff --git a/src/plots/dual-axes/util/geometry.ts b/src/plots/dual-axes/util/geometry.ts index faa9380ab2..103b8400eb 100644 --- a/src/plots/dual-axes/util/geometry.ts +++ b/src/plots/dual-axes/util/geometry.ts @@ -1,8 +1,9 @@ import { each } from '@antv/util'; import { Geometry } from '@antv/g2'; import { Params } from '../../../core/adaptor'; -import { point, line, interval } from '../../../adaptor/geometries'; +import { point, line } from '../../../adaptor/geometries'; import { pick, deepAssign } from '../../../utils'; +import { adaptor as columnAdaptor } from '../../column/adaptor'; import { GeometryOption } from '../types'; import { isLine, isColumn } from './option'; @@ -68,7 +69,7 @@ export function drawSingleGeometry) { // flow 的方式处理所有的配置到 G2 API return flow( theme, + pattern('heatmapStyle'), meta, coordinate, geometry, diff --git a/src/plots/heatmap/types.ts b/src/plots/heatmap/types.ts index 0dd85a1ca2..f4c399c616 100644 --- a/src/plots/heatmap/types.ts +++ b/src/plots/heatmap/types.ts @@ -16,10 +16,14 @@ export interface HeatmapOptions extends Options { readonly shape?: string; /** 热力格子中图形的尺寸比例,可选,只有当 shape 和 sizeField 至少指定一项后才生效 */ readonly sizeRatio?: number; - /** 热力图形样式 */ - readonly heatmapStyle?: StyleAttr; /** 坐标轴映射 */ readonly reflect?: 'x' | 'y'; /** 极坐标属性 */ readonly coordinate?: Types.CoordinateOption; + + // 样式相关 + /** 热力图形样式 */ + readonly heatmapStyle?: StyleAttr; + /** 贴图图案, 在 type="density" 时不支持 */ + readonly pattern?: Options['pattern']; } diff --git a/src/plots/histogram/adaptor.ts b/src/plots/histogram/adaptor.ts index 2b9b2de604..1d25373742 100644 --- a/src/plots/histogram/adaptor.ts +++ b/src/plots/histogram/adaptor.ts @@ -1,5 +1,6 @@ import { Params } from '../../core/adaptor'; import { tooltip, interaction, animation, theme, scale, state } from '../../adaptor/common'; +import { pattern } from '../../adaptor/pattern'; import { findGeometry, deepAssign } from '../../utils'; import { flow, transformLabel } from '../../utils'; import { interval } from '../../adaptor/geometries'; @@ -114,5 +115,16 @@ function label(params: Params): Params { */ export function adaptor(params: Params) { // flow 的方式处理所有的配置到 G2 API - return flow(geometry, meta, axis, theme, state, label, tooltip, interaction, animation)(params); + return flow( + theme, + pattern('columnStyle'), + geometry, + meta, + axis, + state, + label, + tooltip, + interaction, + animation + )(params); } diff --git a/src/plots/liquid/adaptor.ts b/src/plots/liquid/adaptor.ts index a93db5883e..1c5af5c7e6 100644 --- a/src/plots/liquid/adaptor.ts +++ b/src/plots/liquid/adaptor.ts @@ -1,6 +1,6 @@ import { Geometry } from '@antv/g2'; import { get, isNil } from '@antv/util'; -import { interaction, animation, theme, scale } from '../../adaptor/common'; +import { interaction, animation, theme, scale, pattern } from '../../adaptor/common'; import { Params } from '../../core/adaptor'; import { flow, deepAssign, renderStatistic } from '../../utils'; import { interval } from '../../adaptor/geometries'; @@ -97,5 +97,5 @@ export function statistic(params: Params, updated?: boolean): Par */ export function adaptor(params: Params) { // flow 的方式处理所有的配置到 G2 API (主题前置,会影响绘制的取色) - return flow(theme, geometry, statistic, scale({}), animation, interaction)(params); + return flow(theme, pattern('liquidStyle'), geometry, statistic, scale({}), animation, interaction)(params); } diff --git a/src/plots/pie/adaptor.ts b/src/plots/pie/adaptor.ts index 8f1aa51d84..d751e01390 100644 --- a/src/plots/pie/adaptor.ts +++ b/src/plots/pie/adaptor.ts @@ -3,6 +3,7 @@ import { Params } from '../../core/adaptor'; import { legend, animation, theme, state, annotation } from '../../adaptor/common'; import { getMappingFunction } from '../../adaptor/geometries/base'; import { interval } from '../../adaptor/geometries'; +import { pattern } from '../../adaptor/pattern'; import { getLocale } from '../../core/locale'; import { Interaction } from '../../types/interaction'; import { flow, template, transformLabel, deepAssign, renderStatistic, processIllegalData } from '../../utils'; @@ -315,6 +316,7 @@ export function interaction(params: Params): Params { export function adaptor(params: Params) { // flow 的方式处理所有的配置到 G2 API return flow>( + pattern('pieStyle'), geometry, meta, theme, diff --git a/src/plots/radial-bar/adaptor.ts b/src/plots/radial-bar/adaptor.ts index 281e856d8d..cd392ad6a2 100644 --- a/src/plots/radial-bar/adaptor.ts +++ b/src/plots/radial-bar/adaptor.ts @@ -2,6 +2,7 @@ import { interaction, animation, theme, scale, tooltip, legend, annotation } fro import { Params } from '../../core/adaptor'; import { flow, deepAssign, findGeometry, transformLabel } from '../../utils'; import { interval, point } from '../../adaptor/geometries'; +import { pattern } from '../../adaptor/pattern'; import { processIllegalData } from '../../utils'; import { RadialBarOptions } from './types'; import { getScaleMax, getStackedData } from './utils'; @@ -131,6 +132,7 @@ function label(params: Params): Params { */ export function adaptor(params: Params) { return flow( + pattern('barStyle'), geometry, meta, axis, diff --git a/src/plots/rose/adaptor.ts b/src/plots/rose/adaptor.ts index 63448ee822..8098c75402 100644 --- a/src/plots/rose/adaptor.ts +++ b/src/plots/rose/adaptor.ts @@ -1,7 +1,7 @@ import { filter, isObject, isArray } from '@antv/util'; import { Params } from '../../core/adaptor'; import { flow, findGeometry, log, LEVEL, transformLabel, deepAssign } from '../../utils'; -import { tooltip, interaction, animation, theme, scale, annotation, state } from '../../adaptor/common'; +import { tooltip, interaction, animation, theme, scale, annotation, state, pattern } from '../../adaptor/common'; import { interval } from '../../adaptor/geometries'; import { RoseOptions } from './types'; @@ -155,6 +155,7 @@ function axis(params: Params): Params { export function adaptor(params: Params) { // flow 的方式处理所有的配置到 G2 API flow( + pattern('sectorStyle'), geometry, meta, label, diff --git a/src/plots/sunburst/adaptor.ts b/src/plots/sunburst/adaptor.ts index d5bef8d033..abf419958d 100644 --- a/src/plots/sunburst/adaptor.ts +++ b/src/plots/sunburst/adaptor.ts @@ -2,7 +2,7 @@ import { isFunction, get, uniq } from '@antv/util'; import { Types } from '@antv/g2'; import { Params } from '../../core/adaptor'; import { polygon as polygonAdaptor } from '../../adaptor/geometries'; -import { interaction as baseInteraction, animation, theme, annotation, scale } from '../../adaptor/common'; +import { interaction as baseInteraction, animation, theme, annotation, scale, pattern } from '../../adaptor/common'; import { flow, findGeometry, transformLabel, deepAssign } from '../../utils'; import { getAdjustAppendPadding } from '../../utils/padding'; import { Datum } from '../../types'; @@ -227,6 +227,7 @@ export function adaptor(params: Params) { // flow 的方式处理所有的配置到 G2 API return flow( theme, + pattern('sunburstStyle'), geometry, axis, meta, diff --git a/src/plots/tiny-area/adaptor.ts b/src/plots/tiny-area/adaptor.ts index 9010abc225..b66d1894d1 100644 --- a/src/plots/tiny-area/adaptor.ts +++ b/src/plots/tiny-area/adaptor.ts @@ -1,4 +1,4 @@ -import { theme, scale, animation, annotation, tooltip } from '../../adaptor/common'; +import { theme, scale, animation, annotation, tooltip, pattern } from '../../adaptor/common'; import { Params } from '../../core/adaptor'; import { flow, deepAssign } from '../../utils'; import { area, line, point } from '../../adaptor/geometries'; @@ -74,5 +74,5 @@ export function meta(params: Params): Params { * @param options */ export function adaptor(params: Params) { - return flow(geometry, meta, tooltip, theme, animation, annotation())(params); + return flow(pattern('areaStyle'), geometry, meta, tooltip, theme, animation, annotation())(params); } diff --git a/src/plots/tiny-column/adaptor.ts b/src/plots/tiny-column/adaptor.ts index 55b5428c1c..d88d4fd83c 100644 --- a/src/plots/tiny-column/adaptor.ts +++ b/src/plots/tiny-column/adaptor.ts @@ -1,4 +1,4 @@ -import { theme, animation, annotation, tooltip } from '../../adaptor/common'; +import { theme, animation, annotation, tooltip, pattern } from '../../adaptor/common'; import { Params } from '../../core/adaptor'; import { flow, deepAssign } from '../../utils'; import { interval } from '../../adaptor/geometries'; @@ -46,5 +46,5 @@ function geometry(params: Params): Params * @param options */ export function adaptor(params: Params) { - return flow(geometry, meta, tooltip, theme, animation, annotation())(params); + return flow(theme, pattern('columnStyle'), geometry, meta, tooltip, animation, annotation())(params); } diff --git a/src/plots/treemap/adaptor.ts b/src/plots/treemap/adaptor.ts index bcd1326c50..5de68879e8 100644 --- a/src/plots/treemap/adaptor.ts +++ b/src/plots/treemap/adaptor.ts @@ -2,6 +2,7 @@ import { get } from '@antv/util'; import { polygon as basePolygon } from '../../adaptor/geometries/polygon'; import { Params } from '../../core/adaptor'; import { interaction as commonInteraction, animation, theme, legend, annotation, tooltip } from '../../adaptor/common'; +import { pattern } from '../../adaptor/pattern'; import { flow, deepAssign } from '../../utils'; import { getAdjustAppendPadding } from '../../utils/padding'; import { transformData, findInteraction, enableDrillInteraction } from './utils'; @@ -145,5 +146,16 @@ export function interaction(params: Params): Params) { - return flow(defaultOptions, theme, geometry, axis, legend, tooltip, interaction, animation, annotation())(params); + return flow( + defaultOptions, + theme, + pattern('rectStyle'), + geometry, + axis, + legend, + tooltip, + interaction, + animation, + annotation() + )(params); } diff --git a/src/types/attr.ts b/src/types/attr.ts index 7b39406932..ec98bb4be3 100644 --- a/src/types/attr.ts +++ b/src/types/attr.ts @@ -1,11 +1,17 @@ import { ShapeAttrs } from '@antv/g-base'; +import { PatternOption } from '../utils/pattern/index'; import { Datum } from './common'; /** 图形属性 */ export type ShapeStyle = ShapeAttrs; /** 颜色映射 */ -export type ColorAttr = string | string[] | ((datum: Datum) => string); +export type ColorAttr = string | string[] | ((datum: Datum) => string) | object; +/** pattern 映射*/ +export type PatternAttr = + | CanvasPattern + | PatternOption + | ((datum: Datum, color: string /** inherit color */) => PatternOption | CanvasPattern); /** 尺寸大小映射 */ export type SizeAttr = number | [number, number] | ((datum: Datum) => number); /** 图形 shape 映射 */ diff --git a/src/types/common.ts b/src/types/common.ts index 6765711b74..9361a979e7 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -9,7 +9,7 @@ import { Annotation } from './annotation'; import { State } from './state'; import { Slider } from './slider'; import { Scrollbar } from './scrollbar'; -import { ColorAttr } from './attr'; +import { ColorAttr, PatternAttr } from './attr'; import { Meta } from './meta'; /** annotation position */ @@ -109,6 +109,8 @@ export type Options = { readonly theme?: string | object; /** 颜色色板 */ readonly color?: ColorAttr; + /** pattern 配置 */ + readonly pattern?: PatternAttr; /** xAxis 的配置项 */ readonly xAxis?: Axis; /** yAxis 的配置项 */ diff --git a/src/types/pattern.ts b/src/types/pattern.ts new file mode 100644 index 0000000000..6ee685fcb4 --- /dev/null +++ b/src/types/pattern.ts @@ -0,0 +1,50 @@ +export type PatternCfg = { + /** pattern background color. Default: inherit (默认: 继承图形元素颜色) */ + backgroundColor?: string; + /** pattern fill color. 贴图图案填充色 */ + fill?: string; + /** 填充透明度 */ + fillOpacity?: number; + /** pattern stroke color. 贴图图案描边色 */ + stroke?: string; + /** 描边透明度 */ + strokeOpacity?: number; + /** lines thickness. 描边粗细 */ + lineWidth?: number; + /** 整个pattern 透明度 */ + opacity?: number; + /** 整个pattern 的旋转角度 */ + rotation?: number; +}; + +/** + * dot pattern + */ +export type DotPatternCfg = PatternCfg & { + /** 点的大小, 默认: 4 */ + size?: number; + /** padding between dots, 默认: 4 */ + padding?: number; + /** 是否交错,默认: true. 即 staggered dots. */ + isStagger?: boolean; +}; + +/** + * line pattern + */ +export type LinePatternCfg = PatternCfg & { + /** pacing between lines. 线之间的距离 */ + spacing?: number; +}; + +/** + * square pattern + */ +export type SquarePatternCfg = PatternCfg & { + /** 矩形的大小 */ + size?: number; + /** 矩形之间的间隔 */ + padding?: number; + /** 是否交错,默认: true. 即 staggered squares. */ + isStagger?: boolean; +}; diff --git a/src/utils/pattern/dot.ts b/src/utils/pattern/dot.ts new file mode 100644 index 0000000000..9f4b7e3355 --- /dev/null +++ b/src/utils/pattern/dot.ts @@ -0,0 +1,86 @@ +import { DotPatternCfg } from '../../types/pattern'; +import { deepAssign } from '../../utils'; +import { + getUnitPatternSize, + initCanvas, + drawBackground, + getSymbolsPosition, + transformMatrix, + getPixelRatio, +} from './util'; + +/** + * dotPattern的默认配置 + */ +export const defaultDotPatternCfg = { + size: 4, + padding: 2, + backgroundColor: 'transparent', + opacity: 1, + rotation: 0, + fill: '#FFF', + fillOpacity: 0.5, + stroke: 'transparent', + lineWidth: 0, + isStagger: true, +}; + +/** + * 绘制圆点 + * + * @param context + * @param cfg + * @param x 圆点中心坐标x + * @param y 圆点中心坐标y + */ +export function drawDot(context: CanvasRenderingContext2D, cfg: DotPatternCfg, x: number, y: number) { + const { size, fill, lineWidth, stroke, fillOpacity } = cfg; + + context.beginPath(); + context.globalAlpha = fillOpacity; + context.fillStyle = fill; + context.strokeStyle = stroke; + context.lineWidth = lineWidth; + context.arc(x, y, size / 2, 0, 2 * Math.PI, false); + context.fill(); + if (lineWidth) { + context.stroke(); + } + context.closePath(); +} + +/** + * 创建 dot pattern,返回 HTMLCanvasElement + * + * @param cfg + * @returns HTMLCanvasElement + */ +export function createDotPattern(cfg?: DotPatternCfg): CanvasPattern { + const dotCfg = deepAssign({}, defaultDotPatternCfg, cfg); + + const { size, padding, isStagger, rotation } = dotCfg; + + // 计算 画布大小,dots的位置 + const unitSize = getUnitPatternSize(size, padding, isStagger); + const dots = getSymbolsPosition(unitSize, isStagger); + + // 初始化 patternCanvas + const canvas = initCanvas(unitSize, unitSize); + const ctx = canvas.getContext('2d'); + + // 绘制 background,dots + drawBackground(ctx, dotCfg, unitSize); + for (const [x, y] of dots) { + drawDot(ctx, dotCfg, x, y); + } + + const pattern = ctx.createPattern(canvas, 'repeat'); + + if (pattern) { + const dpr = getPixelRatio(); + const matrix = transformMatrix(dpr, rotation); + pattern.setTransform(matrix); + } + + return pattern; +} diff --git a/src/utils/pattern/index.ts b/src/utils/pattern/index.ts new file mode 100644 index 0000000000..9e0819e77c --- /dev/null +++ b/src/utils/pattern/index.ts @@ -0,0 +1,45 @@ +import { DotPatternCfg, LinePatternCfg, SquarePatternCfg } from '../../types/pattern'; +import { createDotPattern } from './dot'; +import { createLinePattern } from './line'; +import { createSquarePattern } from './square'; + +export type PatternOption = + | { + type: 'dot'; + cfg?: DotPatternCfg; + } + | { + type: 'line'; + cfg?: LinePatternCfg; + } + | { + type: 'square'; + cfg?: SquarePatternCfg; + }; + +/** + * 获取内置的 CanvasPattern 方法 + * @param options + * @returns + */ +export function getCanvasPattern(options: PatternOption): CanvasPattern | undefined { + const { type, cfg } = options; + + let pattern; + + switch (type) { + case 'dot': + pattern = createDotPattern(cfg); + break; + case 'line': + pattern = createLinePattern(cfg); + break; + case 'square': + pattern = createSquarePattern(cfg); + break; + default: + break; + } + + return pattern; +} diff --git a/src/utils/pattern/line.ts b/src/utils/pattern/line.ts new file mode 100644 index 0000000000..dc124fd047 --- /dev/null +++ b/src/utils/pattern/line.ts @@ -0,0 +1,70 @@ +import { LinePatternCfg } from '../../types/pattern'; +import { deepAssign } from '../../utils'; +import { initCanvas, drawBackground, transformMatrix, getPixelRatio } from './util'; + +/** + * linePattern 的 默认配置 + */ +export const defaultLinePatternCfg = { + rotation: 45, + spacing: 4, + opacity: 1, + backgroundColor: 'transparent', + strokeOpacity: 0.5, + stroke: '#FFF', + lineWidth: 1, +}; + +/** + * 绘制line + * + * @param context canvasContext + * @param cfg linePattern 的配置 + * @param d 绘制 path 所需的 d + */ +export function drawLine(context: CanvasRenderingContext2D, cfg: LinePatternCfg, d: string) { + const { stroke, lineWidth, strokeOpacity } = cfg; + const path = new Path2D(d); + + context.globalAlpha = strokeOpacity; + context.lineCap = 'square'; + context.strokeStyle = lineWidth ? stroke : 'transparent'; + context.lineWidth = lineWidth; + context.stroke(path); +} + +/** + * 创建 linePattern + */ +export function createLinePattern(cfg?: LinePatternCfg): CanvasPattern { + const lineCfg = deepAssign({}, defaultLinePatternCfg, cfg); + + const { spacing, rotation, lineWidth } = lineCfg; + + // 计算 pattern 画布的大小, path 所需的 d + const width = spacing + lineWidth || 1; + const height = spacing + lineWidth || 1; + const d = ` + M 0 0 L ${width} 0 + M 0 ${height} L ${width} ${height} + `; + + // 初始化 patternCanvas + const canvas = initCanvas(width, height); + const ctx = canvas.getContext('2d'); + + // 绘制 background,line + drawBackground(ctx, lineCfg, width, height); + drawLine(ctx, lineCfg, d); + + const pattern = ctx.createPattern(canvas, 'repeat'); + + if (pattern) { + const dpr = getPixelRatio(); + const matrix = transformMatrix(dpr, rotation); + pattern.setTransform(matrix); + } + + // 返回 Pattern 对象 + return pattern; +} diff --git a/src/utils/pattern/square.ts b/src/utils/pattern/square.ts new file mode 100644 index 0000000000..1b1df0bc59 --- /dev/null +++ b/src/utils/pattern/square.ts @@ -0,0 +1,78 @@ +import { SquarePatternCfg } from '../../types/pattern'; +import { deepAssign } from '../../utils'; +import { + getUnitPatternSize, + initCanvas, + drawBackground, + getSymbolsPosition, + transformMatrix, + getPixelRatio, +} from './util'; + +/** + * squarePattern 的 默认配置 + */ +export const defaultSquarePatternCfg = { + size: 5, + padding: 0, + isStagger: true, + backgroundColor: 'transparent', + opacity: 1, + rotation: 0, + fill: '#FFF', + fillOpacity: 0.5, + stroke: 'transparent', + lineWidth: 0, +}; + +/** + * 绘制square + * + * @param context canvasContext + * @param cfg squarePattern 的配置 + * @param x和y square的中心位置 + */ +export function drawSquare(context: CanvasRenderingContext2D, cfg: SquarePatternCfg, x: number, y: number) { + const { stroke, size, fill, lineWidth, fillOpacity } = cfg; + + context.globalAlpha = fillOpacity; + context.strokeStyle = stroke; + context.lineWidth = lineWidth; + context.fillStyle = fill; + // 因为正方形绘制从左上角开始,所以x,y做个偏移 + context.strokeRect(x - size / 2, y - size / 2, size, size); + context.fillRect(x - size / 2, y - size / 2, size, size); +} + +/** + * 创建 squarePattern + */ +export function createSquarePattern(cfg?: SquarePatternCfg): CanvasPattern { + const squareCfg = deepAssign({}, defaultSquarePatternCfg, cfg); + + const { size, padding, isStagger, rotation } = squareCfg; + + // 计算 画布大小,squares的位置 + const unitSize = getUnitPatternSize(size, padding, isStagger); + const squares = getSymbolsPosition(unitSize, isStagger); // 计算方法与 dots 一样 + + // 初始化 patternCanvas + const canvas = initCanvas(unitSize, unitSize); + const ctx = canvas.getContext('2d'); + + // 绘制 background,squares + drawBackground(ctx, squareCfg, unitSize); + for (const [x, y] of squares) { + drawSquare(ctx, squareCfg, x, y); + } + + const pattern = ctx.createPattern(canvas, 'repeat'); + + if (pattern) { + const dpr = getPixelRatio(); + const matrix = transformMatrix(dpr, rotation); + pattern.setTransform(matrix); + } + + return pattern; +} diff --git a/src/utils/pattern/util.ts b/src/utils/pattern/util.ts new file mode 100644 index 0000000000..9c5aef17e4 --- /dev/null +++ b/src/utils/pattern/util.ts @@ -0,0 +1,104 @@ +import { PatternCfg } from '../../types/pattern'; + +/** + * 获取设备像素比 + */ +export function getPixelRatio(): number { + return typeof window === 'object' ? window?.devicePixelRatio : 2; +} + +/** + * 初始化 cavnas,设置宽高等 + */ +export function initCanvas(width: number, height: number = width): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + + const pixelRatio = getPixelRatio(); + // 画布尺寸 + canvas.width = width * pixelRatio; + canvas.height = height * pixelRatio; + // 显示尺寸 + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + + const ctx = canvas.getContext('2d'); + ctx.scale(pixelRatio, pixelRatio); + + return canvas; +} + +/** + * 绘制背景 + * + * @param context + * @param cfg + * @param width + * @param height + */ +export function drawBackground( + context: CanvasRenderingContext2D, + cfg: PatternCfg, + width: number, + height: number = width +) { + const { backgroundColor, opacity } = cfg; + + context.globalAlpha = opacity; + context.fillStyle = backgroundColor; + + context.beginPath(); + context.fillRect(0, 0, width, height); + context.closePath(); +} + +/** + * 计算贴图单元大小 + * + * @param size 元素大小 + * @param padding 圆点间隔 + * @param isStagger 是否交错 + * @reutrn 返回贴图单元大小 + */ +export function getUnitPatternSize(size: number, padding: number, isStagger: boolean): number { + // 如果交错, unitSize 放大两倍 + const unitSize = size + padding; + return isStagger ? unitSize * 2 : unitSize; +} + +/** + * 计算有交错情况的元素坐标 + * + * @param unitSize 贴图单元大小 + * @param isStagger 是否交错 + * @reutrn 元素中心坐标 x,y 数组集合 + */ +export function getSymbolsPosition(unitSize: number, isStagger: boolean): number[][] { + // 如果交错, 交错绘制 dot + const symbolsPos = isStagger + ? [ + [unitSize * (1 / 4), unitSize * (1 / 4)], + [unitSize * (3 / 4), unitSize * (3 / 4)], + ] + : [[unitSize * (1 / 2), unitSize * (1 / 2)]]; + return symbolsPos; +} + +/** + * 给整个 pattern贴图 做变换, 目前支持旋转 + * + * @param pattern 整个贴图 + * @param dpr 设备像素比 + * @param rotation 旋转角度 + */ +export function transformMatrix(dpr: number, rotation: number) { + const radian = (rotation * Math.PI) / 180; + const matrix = { + a: Math.cos(radian) * (1 / dpr), + b: Math.sin(radian) * (1 / dpr), + c: -Math.sin(radian) * (1 / dpr), + d: Math.cos(radian) * (1 / dpr), + e: 0, + f: 0, + }; + return matrix; +}