Skip to content

Commit

Permalink
feat: 渲染流程
Browse files Browse the repository at this point in the history
  • Loading branch information
manyyuri committed May 27, 2024
1 parent 4b4f7c1 commit a1f30d7
Show file tree
Hide file tree
Showing 11 changed files with 1,802 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .eslintcache

Large diffs are not rendered by default.

418 changes: 418 additions & 0 deletions __tests__/plot/data.js

Large diffs are not rendered by default.

778 changes: 778 additions & 0 deletions __tests__/plot/plot.spec.js

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions src/plot/create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
interval, line, area, text, link, cell, rect, point, path,
} from '../geometry';
import {
createBand,
createIdentity,
createLinear,
createLog,
createOrdinal,
createPoint,
createQuantile,
createQuantize,
createThreshold,
createTime,
} from '../scale';
import {
axisX, axisY, legendRamp, legendSwatches,
} from '../guide';
import { cartesian, transpose, polar } from '../coordinate';
import {
createBinX, createNormalizeY, createSymmetryY, createStackY,
} from '../statistic';

export function create(options) {
if (typeof options === 'function') return options;
const { type, ...rest } = options;

// geometries
if (type === 'interval') return interval;
if (type === 'line') return line;
if (type === 'area') return area;
if (type === 'text') return text;
if (type === 'link') return link;
if (type === 'cell') return cell;
if (type === 'rect') return rect;
if (type === 'point') return point;
if (type === 'path') return path;

// facet
if (type === 'facet') {
const facet = () => {};
facet.channels = () => ({
x: { name: 'x', optional: true },
y: { name: 'y', optional: true },
});
return facet;
}

// statistics
if (type === 'stackY') return createStackY(rest);
if (type === 'normalizeY') return createNormalizeY(rest);
if (type === 'symmetryY') return createSymmetryY(rest);
if (type === 'binX') return createBinX(rest);

// coordinates
if (type === 'cartesian') return cartesian(rest);
if (type === 'transpose') return transpose(rest);
if (type === 'polar') return polar(rest);

// scales
if (type === 'band') return createBand(rest);
if (type === 'linear') return createScaleQ(createLinear, rest);
if (type === 'time') return createScaleQ(createTime, rest);
if (type === 'log') return createScaleQ(createLog, rest);
if (type === 'identity') return createIdentity(rest);
if (type === 'ordinal') return createOrdinal(rest);
if (type === 'dot') return createPoint(rest);
if (type === 'quantile') return createQuantile(rest);
if (type === 'quantize') return createQuantize(rest);
if (type === 'threshold') return createThreshold(rest);

// guides
if (type === 'axisX') return createGuide(axisX, rest);
if (type === 'axisY') return createGuide(axisY, rest);
if (type === 'legendSwatches') return createGuide(legendSwatches, rest);
if (type === 'legendRamp') return createGuide(legendRamp, rest);

throw new Error(`Unknown node type: ${options.type}`);
}

function createGuide(guide, options) {
return (renderer, scale, coordinate) => guide(renderer, scale, coordinate, options);
}

function createScaleQ(ctor, options) {
const { nice = true, tickCount = 10 } = options;
const scale = ctor(options);
if (nice) scale.nice(tickCount);
return scale;
}
100 changes: 100 additions & 0 deletions src/plot/encoding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { firstOf, map } from '../utils';
import { categoricalColors } from './theme';

export function inferEncodings(type, data, encodings) {
const typedEncodings = map(encodings, (encoding, key) => ({
type: inferType(data, encoding, key),
value: encoding,
}));

switch (type) {
case 'interval':
return maybeFill(maybeZeroX(maybeZeroY1(typedEncodings)));
case 'line':
return maybeStroke(maybeGroup(typedEncodings));
case 'area':
return maybeFill(maybeIdentityX(maybeZeroY1(maybeGroup(typedEncodings))));
case 'link':
return maybeStroke(maybeIdentityX(typedEncodings));
case 'point':
return maybeZeroY(maybeStroke(typedEncodings));
case 'rect':
return maybeFill(maybeZeroX1(maybeZeroY1(typedEncodings)));
case 'cell':
return maybeFill(typedEncodings);
default:
break;
}

return typedEncodings;
}

export function valueOf(data, { type, value }) {
if (type === 'transform') return data.map(value);
if (type === 'value') return data.map(() => value);
return data.map((d) => d[value]);
}

function inferType(data, encoding, name) {
if (typeof encoding === 'function') return 'transform';
if (typeof encoding === 'string') {
if (data.length && firstOf(data)[encoding] !== undefined) return 'field';
if (isStyle(name)) return 'constant';
}
return 'value';
}

function isStyle(type) {
return type === 'fill' || type === 'stroke';
}

function maybeFill({ fill = color(), ...rest }) {
return { fill, ...rest };
}

function maybeStroke({ stroke = color(), ...rest }) {
return { stroke, ...rest };
}

function maybeZeroY1({ y1 = zero(), ...rest }) {
return { y1, ...rest };
}

function maybeZeroX1({ x1 = zero(), ...rest }) {
return { x1, ...rest };
}

function maybeZeroY({ y = zero(), ...rest }) {
return { y, ...rest };
}

function maybeZeroX({ x = zero(), ...rest }) {
return { x, ...rest };
}

function maybeIdentityX({ x, x1 = x, ...rest }) {
return { x, x1, ...rest };
}

function maybeGroup({
fill, stroke, z, ...rest
}) {
if (z === undefined) z = maybeField(fill);
if (z === undefined) z = maybeField(stroke);
return {
fill, stroke, z, ...rest,
};
}

function maybeField(encoding) {
if (encoding === undefined || encoding.type !== 'field') return undefined;
return encoding;
}

function zero() {
return { type: 'value', value: 0 };
}

function color() {
return { type: 'constant', value: categoricalColors[0] };
}
59 changes: 59 additions & 0 deletions src/plot/geometry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { compose, indexOf } from '../utils';
import { inferEncodings, valueOf } from './encoding';
import { create } from './create';

export function initialize({
data,
type,
encodings: E = {},
statistics: statisticsOptions = [],
transforms: transformsOptions = [],
styles,
}) {
// apply transform
const transform = compose(...transformsOptions.map(create));
const transformedData = transform(data);
const index = indexOf(transformedData);

// apply valueOf
const encodings = inferEncodings(type, transformedData, E);
const constants = {};
const values = {};
for (const [key, e] of Object.entries(encodings)) {
if (e) {
const { type, value } = e;
if (type === 'constant') constants[key] = value;
else values[key] = valueOf(transformedData, e);
}
}

// apply statistics
const statistic = compose(...statisticsOptions.map(create));
const { values: transformedValues, index: I } = statistic({ index, values });

// create channels
const geometry = create({ type });
const channels = {};
for (const [key, channel] of Object.entries(geometry.channels())) {
const values = transformedValues[key];
const { optional } = channel;
if (values) {
channels[key] = createChannel(channel, values, encodings[key]);
} else if (!optional) {
throw new Error(`Missing values for channel: ${key}`);
}
}

return {
index: I, geometry, channels, styles: { ...styles, ...constants },
};
}

function createChannel(channel, values, encoding = {}) {
const { type, value } = encoding;
return {
...channel,
...(type === 'field' && { field: value }),
values,
};
}
37 changes: 37 additions & 0 deletions src/plot/guide.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export function inferGuides(scales, dimensions, options) {
const { x: xScale, y: yScale, color: colorScale } = scales;
const { x = {}, y = {}, color = {} } = options;
const { display: dx = true } = x;
const { display: dy = true } = y;
const { display: dc = true } = color;

return {
...(dx && xScale && { x: { ...merge(x, xScale), type: 'axisX' } }),
...(dy && yScale && { y: { ...merge(y, yScale), type: 'axisY' } }),
...(dc && colorScale && {
color: {
...merge(color, colorScale),
...inferPosition(dimensions),
type: inferLegendType(colorScale),
},
}),
};
}

function merge(options, { domain, label }) {
return { ...options, domain, label };
}

function inferLegendType({ type }) {
switch (type) {
case 'linear': case 'log': case 'time':
case 'threshold': case 'quantile': case 'quantize':
return 'legendRamp';
default:
return 'legendSwatches';
}
}

function inferPosition({ x, y, paddingLeft }) {
return { x: x + paddingLeft, y };
}
1 change: 1 addition & 0 deletions src/plot/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { plot } from './plot';
Loading

0 comments on commit a1f30d7

Please sign in to comment.