Skip to content

Commit

Permalink
feat: add rect mark (#4186)
Browse files Browse the repository at this point in the history
* feat: rect mark

* feat: add inset in rect shape in cartesian coordinate (#4176)
  • Loading branch information
pepper-nice authored and hustcc committed May 16, 2023
1 parent 3055272 commit f006c15
Show file tree
Hide file tree
Showing 15 changed files with 289 additions and 6 deletions.
1 change: 1 addition & 0 deletions .genjirc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"标记": {
"概述": "mark-overview",
"Interval": "mark-interval",
"Rect": "mark-rect",
"Point": "mark-point",
"Line": "mark-line",
"Area": "mark-area",
Expand Down
3 changes: 3 additions & 0 deletions __tests__/unit/api/chart.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
Range,
RangeX,
RangeY,
Rect,
Connector,
} from '../../../src/api/mark/mark';
import { createDiv } from '../../utils/dom';
Expand Down Expand Up @@ -103,6 +104,7 @@ describe('Chart', () => {
it('chart.nodeName() should return expected node ', () => {
const chart = new Chart();
expect(chart.interval()).toBeInstanceOf(Interval);
expect(chart.rect()).toBeInstanceOf(Rect);
expect(chart.point()).toBeInstanceOf(Point);
expect(chart.area()).toBeInstanceOf(Area);
expect(chart.line()).toBeInstanceOf(Line);
Expand All @@ -121,6 +123,7 @@ describe('Chart', () => {
expect(chart.connector()).toBeInstanceOf(Connector);
expect(chart.options().children).toEqual([
{ type: 'interval' },
{ type: 'rect' },
{ type: 'point' },
{ type: 'area' },
{ type: 'line' },
Expand Down
2 changes: 2 additions & 0 deletions __tests__/unit/api/composition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ import {
Range,
RangeX,
RangeY,
Rect,
Text,
Connector,
} from '../../../src/api/mark/mark';

function expectToCreateMarks(node) {
expect(node.interval()).toBeInstanceOf(Interval);
expect(node.rect()).toBeInstanceOf(Rect);
expect(node.point()).toBeInstanceOf(Point);
expect(node.area()).toBeInstanceOf(Area);
expect(node.line()).toBeInstanceOf(Line);
Expand Down
62 changes: 62 additions & 0 deletions __tests__/unit/mark/rect.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Rect } from '../../../src/mark';
import { plot } from './helper';

describe('Rect', () => {
it('Rect has expected props', () => {
expect(Rect.props).toEqual({
defaultShape: 'rect',
defaultLabelShape: 'label',
channels: [
{ name: 'color' },
{ name: 'shape' },
{ name: 'enterType' },
{ name: 'enterDelay', scaleName: 'enter' },
{ name: 'enterDuration', scaleName: 'enter' },
{ name: 'enterEasing' },
{ name: 'key', scale: 'identity' },
{ name: 'groupKey', scale: 'identity' },
{ name: 'label', scale: 'identity' },
{ name: 'title', scale: 'identity' },
{ name: 'tooltip', scale: 'identity', independent: true },
{ name: 'x', required: true },
{ name: 'y', required: true },
],
preInference: [{ type: 'maybeZeroY1' }],
postInference: [
{ type: 'maybeKey' },
{ type: 'maybeTitleX' },
{ type: 'maybeTooltipY' },
],
shapes: ['rect', 'hollow'],
});
});
});

it('Rect should draw basic rect', () => {
const [I, P] = plot({
mark: Rect({}),
index: [0, 1],
channel: {
x: [0.1, 0.2],
x1: [0.5, 0.8],
y: [0.1, 0.2],
y1: [0.9, 0.5],
},
});

expect(I).toEqual([0, 1]);
expect(P).toEqual([
[
[60, 40],
[300, 40],
[300, 360],
[60, 360],
],
[
[120, 80],
[480, 80],
[480, 200],
[120, 200],
],
]);
});
48 changes: 48 additions & 0 deletions __tests__/unit/runtime/rect.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { hierarchy, treemap } from 'd3-hierarchy';
import { G2Spec, render } from '../../../src';

import { createDiv, mount } from '../../utils/dom';

describe('rect', () => {
it.skip('render({...}) should render basic rect', () => {
const width = 640;
const height = 480;
const padding = 3;

const treeMapLayout = (data) => {
const root = hierarchy(data);
root.count();
// @ts-ignore
treemap().size([width, height]).padding(padding)(root);
return root
.descendants()
.map((d) =>
Object.assign(d, {
x: [d.x0, d.x1],
y: [d.y0, d.y1],
}),
)
.filter((d) => d.height === 0);
};

const chart = render<G2Spec>({
width: 640,
height: 480,
padding: 3,
type: 'rect',
data: {
type: 'fetch',
value:
'https://gw.alipayobjects.com/os/bmw-prod/5155ef81-db23-49f3-b72b-d436a219d289.json',
transform: [{ type: 'custom', callback: treeMapLayout }],
},
encode: {
x: 'x',
y: 'y',
size: 'r',
color: (d) => d.parent.data.name,
},
});
mount(createDiv(), chart);
});
});
4 changes: 4 additions & 0 deletions __tests__/unit/stdlib/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { Constant, Field, Transform, Column } from '../../../src/encode';
import {
Interval,
Rect,
Line,
Point as PointGeometry,
Text as TextGeometry,
Expand Down Expand Up @@ -278,6 +279,7 @@ describe('stdlib', () => {
'encode.transform': Transform,
'encode.column': Column,
'mark.interval': Interval,
'mark.rect': Rect,
'mark.line': Line,
'mark.point': PointGeometry,
'mark.text': TextGeometry,
Expand Down Expand Up @@ -331,6 +333,8 @@ describe('stdlib', () => {
'shape.interval.hollow': HollowRect,
'shape.interval.funnel': Funnel,
'shape.interval.pyramid': Pyramid,
'shape.rect.rect': RectShape,
'shape.rect.hollow': HollowRect,
'shape.grid.grid': RectShape,
'shape.grid.hollow': HollowRect,
'shape.line.line': LineShape,
Expand Down
92 changes: 92 additions & 0 deletions docs/mark-rect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Rect

常用于直方图、矩阵树图、聚合热力图等。


## 开始


```js
(() => {
const width = 640;
const height = 480;
const padding = 3;
const layout = (data) => {
const root = d3.hierarchy(data);
root.count();
d3.treemap().size([width, height]).paddingLeft(padding)(root);
return root
.descendants()
.map((d) =>
Object.assign(d, {
x: [d.x0, d.x1,],
y: [d.y0, d.y1],
}),
)
.filter((d) => d.height === 0);
};
const name = (d) => {
const { name } = d.data;
return name.length > 5 ? name.slice(0, 4) + '...' : name;
};
const chart = new G2.Chart({
width,
height,
paddingLeft: padding,
paddingBottom: padding,
paddingRight: padding,
});

chart.data({
type: 'fetch',
value:
'https://gw.alipayobjects.com/os/bmw-prod/5155ef81-db23-49f3-b72b-d436a219d289.json',
transform: [{ type: 'custom', callback: layout }],
});

chart
.rect()
.encode('x', 'x')
.encode('y', 'y')
.encode('size', 'r')
.encode('color', (d) => d.parent.data.name)
.encode('tooltip', (d) => d.parent.data.name)
.encode('title', '')
.scale('x', { domain: [0, width], guide: null })
.scale('y', { domain: [0, height], guide: null, range: [0, 1] })
.scale('color', {
field: '学派',
guide: { size: 72, autoWrap: true, maxRows: 3, cols: 6 },
})
.scale('size', { type: 'identity' })
.scale('tooltip', { field: '流派' });

chart
.text()
.data({
transform: [
{ type: 'filterBy', fields: ['height'], callback: (d) => d === 0 },
],
})
.encode('x', (d) => d.x[0])
.encode('y', (d) => d.y[0])
.encode('text', name)
.style('dy', '15px')
.style('dx', '5px')
.style('fill', 'black')
.style('textAnchor', 'start')
.style('fontSize', 12);

return chart.render().node();
})();
```

## Dependance

```js | dom "pin: false"
d3 = (async () => {
const { voronoi } = await genji.require('d3-voronoi');
const { hierarchy, treemap } = await genji.require('d3-hierarchy');
return { voronoi, hierarchy, treemap };
})();
```
3 changes: 3 additions & 0 deletions src/api/mark/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Interval,
Rect,
Point,
Area,
Line,
Expand All @@ -20,6 +21,7 @@ import {

export interface Mark {
interval(): Interval;
rect(): Rect;
point(): Point;
area(): Area;
line(): Line;
Expand All @@ -40,6 +42,7 @@ export interface Mark {

export const mark = {
interval: Interval,
rect: Rect,
point: Point,
area: Area,
line: Line,
Expand Down
11 changes: 11 additions & 0 deletions src/api/mark/mark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export interface Interval extends API<Concrete<IntervalGeometry>, Interval> {
type: 'interval';
}

export interface Rect extends API<Concrete<IntervalGeometry>, Rect> {
type: 'rect';
}

export interface Point extends API<Concrete<PointGeometry>, Point> {
type: 'point';
}
Expand Down Expand Up @@ -118,6 +122,13 @@ export class Interval extends Node<IntervalGeometry> {
}
}

@defineProps(props)
export class Rect extends Node<IntervalGeometry> {
constructor() {
super({}, 'rect');
}
}

@defineProps(props)
export class Point extends Node<PointGeometry> {
constructor() {
Expand Down
2 changes: 2 additions & 0 deletions src/mark/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { Interval } from './interval';
export { Rect } from './rect';
export { Line } from './line';
export { Point } from './point';
export { Text } from './text';
Expand All @@ -17,6 +18,7 @@ export { RangeX } from './rangeX';
export { RangeY } from './rangeY';

export type { IntervalOptions } from './interval';
export type { RectOptions } from './rect';
export type { LineOptions } from './line';
export type { PointOptions } from './point';
export type { TextOptions } from './text';
Expand Down
43 changes: 43 additions & 0 deletions src/mark/rect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { MarkComponent as MC, Vector2 } from '../runtime';
import { RectGeometry } from '../spec';
import {
baseGeometryChannels,
basePostInference,
basePreInference,
} from './utils';

export type RectOptions = Omit<RectGeometry, 'type'>;

export const Rect: MC<RectOptions> = () => {
return (index, scale, value, coordinate) => {
const { x: X, x1: X1, y: Y, y1: Y1 } = value;

const P = Array.from(index, (i) => {
const p1 = [+X[i], +Y[i]];
const p2 = [+X1[i], +Y[i]];
const p3 = [+X1[i], +Y1[i]];
const p4 = [+X[i], +Y1[i]];

return [p1, p2, p3, p4].map((d) => coordinate.map(d)) as Vector2[];
});

return [index, P];
};
};

Rect.props = {
defaultShape: 'rect',
defaultLabelShape: 'label',
channels: [
...baseGeometryChannels(),
{ name: 'x', required: true },
{ name: 'y', required: true },
],
preInference: [...basePreInference(), { type: 'maybeZeroY1' }],
postInference: [
...basePostInference(),
{ type: 'maybeTitleX' },
{ type: 'maybeTooltipY' },
],
shapes: ['rect', 'hollow'],
};
1 change: 1 addition & 0 deletions src/runtime/types/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type ElementStyle = {
};

type MarkTheme = NestUnion<'interval', ['rect', 'hollowRect'], ElementStyle> &
NestUnion<'rect', ['rect', 'hollowRect'], ElementStyle> &
NestUnion<'line', ['line'], ElementStyle> &
NestUnion<'point', ['point', 'hollowPoint'], ElementStyle> &
NestUnion<'text', ['text'], ElementStyle> &
Expand Down

0 comments on commit f006c15

Please sign in to comment.