dashtaste 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +124 -0
- package/dist/color.d.ts +4 -0
- package/dist/color.js +43 -0
- package/dist/engine.d.ts +11 -0
- package/dist/engine.js +39 -0
- package/dist/extract-in-page.d.ts +3 -0
- package/dist/extract-in-page.js +149 -0
- package/dist/extract.d.ts +3 -0
- package/dist/extract.js +39 -0
- package/dist/format.d.ts +18 -0
- package/dist/format.js +65 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/packs/cn.d.ts +2 -0
- package/dist/packs/cn.js +56 -0
- package/dist/packs/global.d.ts +2 -0
- package/dist/packs/global.js +45 -0
- package/dist/packs/index.d.ts +53 -0
- package/dist/packs/index.js +13 -0
- package/dist/rules/dt001-default-palette.d.ts +3 -0
- package/dist/rules/dt001-default-palette.js +11 -0
- package/dist/rules/dt002-rainbow-palette.d.ts +3 -0
- package/dist/rules/dt002-rainbow-palette.js +11 -0
- package/dist/rules/dt003-smooth-line.d.ts +3 -0
- package/dist/rules/dt003-smooth-line.js +9 -0
- package/dist/rules/dt004-symbols-everywhere.d.ts +3 -0
- package/dist/rules/dt004-symbols-everywhere.js +21 -0
- package/dist/rules/dt005-pie-slices.d.ts +3 -0
- package/dist/rules/dt005-pie-slices.js +13 -0
- package/dist/rules/dt006-pie-overuse.d.ts +3 -0
- package/dist/rules/dt006-pie-overuse.js +14 -0
- package/dist/rules/dt007-three-d.d.ts +3 -0
- package/dist/rules/dt007-three-d.js +16 -0
- package/dist/rules/dt008-dual-axis.d.ts +3 -0
- package/dist/rules/dt008-dual-axis.js +10 -0
- package/dist/rules/dt009-missing-unit.d.ts +3 -0
- package/dist/rules/dt009-missing-unit.js +8 -0
- package/dist/rules/dt010-bar-baseline.d.ts +3 -0
- package/dist/rules/dt010-bar-baseline.js +11 -0
- package/dist/rules/dt011-raw-large-numbers.d.ts +3 -0
- package/dist/rules/dt011-raw-large-numbers.js +21 -0
- package/dist/rules/dt012-unit-system.d.ts +3 -0
- package/dist/rules/dt012-unit-system.js +12 -0
- package/dist/rules/dt013-delta-convention.d.ts +3 -0
- package/dist/rules/dt013-delta-convention.js +21 -0
- package/dist/rules/dt014-delta-colour-only.d.ts +3 -0
- package/dist/rules/dt014-delta-colour-only.js +16 -0
- package/dist/rules/dt015-low-contrast-text.d.ts +3 -0
- package/dist/rules/dt015-low-contrast-text.js +28 -0
- package/dist/rules/dt016-thin-line-contrast.d.ts +3 -0
- package/dist/rules/dt016-thin-line-contrast.js +12 -0
- package/dist/rules/dt017-gradient-decoration.d.ts +3 -0
- package/dist/rules/dt017-gradient-decoration.js +15 -0
- package/dist/rules/dt018-glow.d.ts +3 -0
- package/dist/rules/dt018-glow.js +27 -0
- package/dist/rules/dt019-kpi-overload.d.ts +3 -0
- package/dist/rules/dt019-kpi-overload.js +7 -0
- package/dist/rules/dt020-kpi-rainbow.d.ts +3 -0
- package/dist/rules/dt020-kpi-rainbow.js +27 -0
- package/dist/rules/dt021-emoji-icons.d.ts +3 -0
- package/dist/rules/dt021-emoji-icons.js +10 -0
- package/dist/rules/dt022-cjk-min-size.d.ts +3 -0
- package/dist/rules/dt022-cjk-min-size.js +9 -0
- package/dist/rules/dt023-legend-single-series.d.ts +3 -0
- package/dist/rules/dt023-legend-single-series.js +8 -0
- package/dist/rules/dt024-bar-width.d.ts +1 -0
- package/dist/rules/dt024-bar-width.js +14 -0
- package/dist/rules/dt025-legend-not-direct-label.d.ts +1 -0
- package/dist/rules/dt025-legend-not-direct-label.js +11 -0
- package/dist/rules/dt026-line-weight.d.ts +1 -0
- package/dist/rules/dt026-line-weight.js +8 -0
- package/dist/rules/dt027-tabular-numerals.d.ts +1 -0
- package/dist/rules/dt027-tabular-numerals.js +16 -0
- package/dist/rules/dt028-hero-metric.d.ts +1 -0
- package/dist/rules/dt028-hero-metric.js +14 -0
- package/dist/rules/helpers.d.ts +32 -0
- package/dist/rules/helpers.js +161 -0
- package/dist/rules/index.d.ts +2 -0
- package/dist/rules/index.js +58 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.js +1 -0
- package/docs/rules.md +735 -0
- package/docs/usage.md +126 -0
- package/package.json +74 -4
- package/skills/dashtaste-cn/SKILL.md +51 -0
- package/skills/dashtaste-cn/references/avoid.md +135 -0
- package/skills/dashtaste-cn/references/charts.md +122 -0
- package/skills/dashtaste-cn/references/layout.md +78 -0
- package/skills/dashtaste-cn/references/numbers.md +93 -0
- package/skills/dashtaste-cn/references/tokens.md +72 -0
- package/skills/dashtaste-global/SKILL.md +43 -0
- package/skills/dashtaste-global/references/avoid.md +135 -0
- package/skills/dashtaste-global/references/charts.md +122 -0
- package/skills/dashtaste-global/references/layout.md +78 -0
- package/skills/dashtaste-global/references/numbers.md +83 -0
- package/skills/dashtaste-global/references/tokens.md +65 -0
package/dist/format.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { deltaSwapped, getTokens, } from './packs/index.js';
|
|
2
|
+
export function formatCompact(value, pack, options = {}) {
|
|
3
|
+
if (!Number.isFinite(value))
|
|
4
|
+
return '–';
|
|
5
|
+
const units = pack === 'cn'
|
|
6
|
+
? [
|
|
7
|
+
[1e8, '亿'],
|
|
8
|
+
[1e4, '万'],
|
|
9
|
+
]
|
|
10
|
+
: [
|
|
11
|
+
[1e9, 'B'],
|
|
12
|
+
[1e6, 'M'],
|
|
13
|
+
[1e3, 'K'],
|
|
14
|
+
];
|
|
15
|
+
const unit = units.find(([n]) => Math.abs(value) >= Math.max(n, options.compactFrom ?? 10000));
|
|
16
|
+
const minimumFractionDigits = options.minimumFractionDigits ?? 0;
|
|
17
|
+
return unit
|
|
18
|
+
? `${(value / unit[0]).toLocaleString('en-US', { minimumFractionDigits, maximumFractionDigits: Math.max(minimumFractionDigits, pack === 'cn' ? 2 : 1), useGrouping: false })}${unit[1]}`
|
|
19
|
+
: options.compactFrom !== undefined || options.minimumFractionDigits !== undefined
|
|
20
|
+
? value.toLocaleString('en-US', {
|
|
21
|
+
minimumFractionDigits,
|
|
22
|
+
maximumFractionDigits: Math.max(minimumFractionDigits, 2),
|
|
23
|
+
})
|
|
24
|
+
: value.toLocaleString('en-US', { maximumFractionDigits: 2 });
|
|
25
|
+
}
|
|
26
|
+
export function formatPercent(value, digits = 1) {
|
|
27
|
+
return Number.isFinite(value) ? `${(value * 100).toFixed(digits)}%` : '–';
|
|
28
|
+
}
|
|
29
|
+
export function formatDelta(current, previous, pack, options = {}) {
|
|
30
|
+
const t = getTokens(pack, options.mode);
|
|
31
|
+
const delta = (current - previous) / Math.abs(previous);
|
|
32
|
+
const direction = !Number.isFinite(current) || !Number.isFinite(previous) || previous === 0
|
|
33
|
+
? 'invalid'
|
|
34
|
+
: delta === 0
|
|
35
|
+
? 'flat'
|
|
36
|
+
: delta > 0
|
|
37
|
+
? 'up'
|
|
38
|
+
: 'down';
|
|
39
|
+
const swapped = deltaSwapped(pack, options.deltaColors, options.polarity);
|
|
40
|
+
const color = direction === 'up'
|
|
41
|
+
? swapped
|
|
42
|
+
? t.down
|
|
43
|
+
: t.up
|
|
44
|
+
: direction === 'down'
|
|
45
|
+
? swapped
|
|
46
|
+
? t.up
|
|
47
|
+
: t.down
|
|
48
|
+
: t.muted;
|
|
49
|
+
const text = direction === 'invalid'
|
|
50
|
+
? '–'
|
|
51
|
+
: `${direction === 'up' ? '▲ ' : direction === 'down' ? '▼ ' : ''}${formatPercent(Math.abs(delta))}`;
|
|
52
|
+
return {
|
|
53
|
+
text: options.label && direction !== 'invalid' ? `${options.label} ${text}` : text,
|
|
54
|
+
direction,
|
|
55
|
+
color,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Uses local calendar fields; no locale-dependent date ordering. */
|
|
59
|
+
export function formatDate(value, pack = 'cn') {
|
|
60
|
+
const d = new Date(value);
|
|
61
|
+
if (!Number.isFinite(d.getTime()))
|
|
62
|
+
return '–';
|
|
63
|
+
const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
64
|
+
return pack === 'cn' ? iso : iso;
|
|
65
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/packs/cn.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const cn = {
|
|
2
|
+
name: 'cn',
|
|
3
|
+
numberSystem: 'wan-yi',
|
|
4
|
+
limits: { maxPieSlices: 5, maxRoseSlices: 8, maxKpis: 8, cjkMinFontSize: 12 },
|
|
5
|
+
deltaColors: 'red-up',
|
|
6
|
+
typography: {
|
|
7
|
+
monoFontFamily: 'Geist Mono',
|
|
8
|
+
axisSize: 12,
|
|
9
|
+
tracking: '0.06em',
|
|
10
|
+
body: 14,
|
|
11
|
+
minimum: 12,
|
|
12
|
+
fontFamily: 'Instrument Sans',
|
|
13
|
+
fontFeatureSettings: '"tnum"',
|
|
14
|
+
headingWeight: 500,
|
|
15
|
+
bodyWeight: 400,
|
|
16
|
+
},
|
|
17
|
+
spacing: [4, 8, 12, 16, 24, 32],
|
|
18
|
+
screen: { width: 1920, height: 1080, baseFontSize: 16, kpiMinFontSize: 32, kpiMaxFontSize: 128 },
|
|
19
|
+
light: {
|
|
20
|
+
page: '#F2F4F2',
|
|
21
|
+
card: '#FFFFFF',
|
|
22
|
+
sunken: '#F7F8F7',
|
|
23
|
+
ink: '#141414',
|
|
24
|
+
muted: '#636261',
|
|
25
|
+
subtle: '#7B7B76',
|
|
26
|
+
hairline: '#E6E6E6',
|
|
27
|
+
strong: '#DDDDDD',
|
|
28
|
+
primary: '#141414',
|
|
29
|
+
brand: '#3E6A88',
|
|
30
|
+
accent: '#C68A1C',
|
|
31
|
+
up: '#C8372F',
|
|
32
|
+
down: '#167D4F',
|
|
33
|
+
flat: '#636261',
|
|
34
|
+
other: '#BCBBBA',
|
|
35
|
+
categorical: ['#3E6A88', '#20293A', '#8080F7', '#B4A85D', '#77C4D6'],
|
|
36
|
+
sequential: ['#EEF2F5', '#DDE4EA', '#B3CADB', '#74ABD0', '#3E6A88'],
|
|
37
|
+
},
|
|
38
|
+
dark: {
|
|
39
|
+
bg: '#121212',
|
|
40
|
+
panel: '#171716',
|
|
41
|
+
sunken: '#0A0B0A',
|
|
42
|
+
ink: '#EEEEEB',
|
|
43
|
+
muted: '#959590',
|
|
44
|
+
subtle: '#7B7B76',
|
|
45
|
+
hairline: '#2A2926',
|
|
46
|
+
strong: '#484843',
|
|
47
|
+
brand: '#74ABD0',
|
|
48
|
+
accent: '#E8B657',
|
|
49
|
+
up: '#DB655C',
|
|
50
|
+
down: '#73B78E',
|
|
51
|
+
flat: '#959590',
|
|
52
|
+
other: '#61615C',
|
|
53
|
+
categorical: ['#74ABD0', '#E5B357', '#9B7EF0', '#466376', '#4B8C9C'],
|
|
54
|
+
sequential: ['#1F303B', '#2A4456', '#3E6A88', '#74ABD0', '#B3CADB'],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const global = {
|
|
2
|
+
name: 'global',
|
|
3
|
+
numberSystem: 'k-m-b',
|
|
4
|
+
limits: { maxPieSlices: 5, maxRoseSlices: 5, maxKpis: 6, cjkMinFontSize: 12 },
|
|
5
|
+
deltaColors: 'green-up',
|
|
6
|
+
typography: {
|
|
7
|
+
monoFontFamily: 'Geist Mono',
|
|
8
|
+
axisSize: 11,
|
|
9
|
+
tracking: '0.06em',
|
|
10
|
+
body: 14,
|
|
11
|
+
minimum: 12,
|
|
12
|
+
fontFamily: 'Inter',
|
|
13
|
+
fontFeatureSettings: '"tnum", "cv11"',
|
|
14
|
+
headingWeight: 500,
|
|
15
|
+
bodyWeight: 400,
|
|
16
|
+
},
|
|
17
|
+
spacing: [4, 8, 12, 16, 24, 32],
|
|
18
|
+
light: {
|
|
19
|
+
paper: '#F4F2EE',
|
|
20
|
+
ink: '#1A1A18',
|
|
21
|
+
muted: '#5F5F67',
|
|
22
|
+
subtle: '#8A8A93',
|
|
23
|
+
hairline: '#DEDCD6',
|
|
24
|
+
grid: '#E8E6E0',
|
|
25
|
+
brand: '#B65A08',
|
|
26
|
+
other: '#C4C4CC',
|
|
27
|
+
up: '#107451',
|
|
28
|
+
down: '#9B1745',
|
|
29
|
+
categorical: ['#B65A08', '#544D61', '#9B896D', '#243635', '#B8BBC6'],
|
|
30
|
+
sequential: ['#E8E6E0', '#DDE1F0', '#B4C0F0', '#778CEC', '#B65A08'],
|
|
31
|
+
},
|
|
32
|
+
dark: {
|
|
33
|
+
paper: '#141412',
|
|
34
|
+
panel: '#191917',
|
|
35
|
+
ink: '#ECEAE4',
|
|
36
|
+
muted: '#A1A1AA',
|
|
37
|
+
hairline: '#26262A',
|
|
38
|
+
grid: '#242420',
|
|
39
|
+
brand: '#E6A24B',
|
|
40
|
+
other: '#4A4A52',
|
|
41
|
+
up: '#4CC38A',
|
|
42
|
+
down: '#D65A83',
|
|
43
|
+
categorical: ['#E6A24B', '#DCD1B5', '#9E849F', '#527371', '#52525E'],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { global } from './global.js';
|
|
2
|
+
import { cn } from './cn.js';
|
|
3
|
+
export type PackName = 'global' | 'cn';
|
|
4
|
+
export type Mode = 'light' | 'dark';
|
|
5
|
+
export type DeltaColors = 'red-up' | 'green-up' | 'by-polarity';
|
|
6
|
+
export type Polarity = 'higher-is-better' | 'lower-is-better';
|
|
7
|
+
export declare function deltaSwapped(pack: PackName, convention?: DeltaColors, polarity?: Polarity): boolean;
|
|
8
|
+
export interface Tokens {
|
|
9
|
+
[key: string]: string | readonly string[];
|
|
10
|
+
ink: string;
|
|
11
|
+
muted: string;
|
|
12
|
+
hairline: string;
|
|
13
|
+
brand: string;
|
|
14
|
+
other: string;
|
|
15
|
+
up: string;
|
|
16
|
+
down: string;
|
|
17
|
+
categorical: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
export interface Pack {
|
|
20
|
+
name: PackName;
|
|
21
|
+
deltaColors: DeltaColors;
|
|
22
|
+
numberSystem: 'k-m-b' | 'wan-yi';
|
|
23
|
+
limits: {
|
|
24
|
+
maxPieSlices: number;
|
|
25
|
+
maxRoseSlices: number;
|
|
26
|
+
maxKpis: number;
|
|
27
|
+
cjkMinFontSize: number;
|
|
28
|
+
};
|
|
29
|
+
screen?: {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
baseFontSize: number;
|
|
33
|
+
kpiMinFontSize: number;
|
|
34
|
+
kpiMaxFontSize: number;
|
|
35
|
+
};
|
|
36
|
+
light: Tokens;
|
|
37
|
+
dark: Tokens;
|
|
38
|
+
typography: {
|
|
39
|
+
monoFontFamily: string;
|
|
40
|
+
axisSize: number;
|
|
41
|
+
tracking: string;
|
|
42
|
+
fontFamily: string;
|
|
43
|
+
fontFeatureSettings: string;
|
|
44
|
+
headingWeight: number;
|
|
45
|
+
bodyWeight: number;
|
|
46
|
+
body: number;
|
|
47
|
+
minimum: number;
|
|
48
|
+
};
|
|
49
|
+
spacing: readonly number[];
|
|
50
|
+
}
|
|
51
|
+
export declare const packs: Record<PackName, Pack>;
|
|
52
|
+
export { global, cn };
|
|
53
|
+
export declare function getTokens(pack: PackName, mode?: Mode): Tokens;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { global } from './global.js';
|
|
2
|
+
import { cn } from './cn.js';
|
|
3
|
+
export function deltaSwapped(pack, convention, polarity) {
|
|
4
|
+
const byPolarity = convention === 'by-polarity' || (pack === 'global' && convention === undefined);
|
|
5
|
+
if (byPolarity)
|
|
6
|
+
return (pack === 'cn') !== (polarity === 'lower-is-better');
|
|
7
|
+
return (convention ?? packs[pack].deltaColors) !== packs[pack].deltaColors;
|
|
8
|
+
}
|
|
9
|
+
export const packs = { global, cn };
|
|
10
|
+
export { global, cn };
|
|
11
|
+
export function getTokens(pack, mode = 'light') {
|
|
12
|
+
return packs[pack][mode];
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { asArray, series, allColors, defaultColors, rule, charts } from './helpers.js';
|
|
2
|
+
/** Multiple series need explicit colours. Any default palette colour is flagged. */
|
|
3
|
+
export const dt001 = rule(1, 'default-palette', 'error', 'Multiple series need explicit colours. Any default palette colour is flagged.', 'set option.color to the pack palette (dashtaste tokens --pack <pack>)', function check(facts, context, emit) {
|
|
4
|
+
charts(facts, (option, path) => {
|
|
5
|
+
if ((series(option).length > 1 &&
|
|
6
|
+
!asArray(option.color).length &&
|
|
7
|
+
series(option).some((item) => !item.itemStyle?.color && !item.lineStyle?.color)) ||
|
|
8
|
+
allColors(option).some((entry) => defaultColors.has(entry.toLowerCase())))
|
|
9
|
+
emit(path, 'default ECharts palette');
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { allColors, rule, charts } from './helpers.js';
|
|
2
|
+
import { hueBucket } from '../color.js';
|
|
3
|
+
/** More than 6 distinct categorical colours or more than 5 saturated 60-degree hue buckets. */
|
|
4
|
+
export const dt002 = rule(2, 'rainbow-palette', 'warn', 'More than 6 distinct categorical colours or more than 5 saturated 60-degree hue buckets.', 'use the pack palette and group remaining categories as Other', function check(facts, context, emit) {
|
|
5
|
+
charts(facts, (option, path) => {
|
|
6
|
+
const colors = allColors(option);
|
|
7
|
+
if (new Set(colors.map((entry) => entry.toLowerCase())).size > 6 ||
|
|
8
|
+
new Set(colors.map(hueBucket).filter((entry) => entry !== undefined)).size > 5)
|
|
9
|
+
emit(path);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { series, rule, charts } from './helpers.js';
|
|
2
|
+
/** Line smoothing greater than zero draws values that do not exist. */
|
|
3
|
+
export const dt003 = rule(3, 'smooth-line', 'warn', 'Line smoothing greater than zero draws values that do not exist.', 'use straight segments: smooth: false', function check(facts, context, emit) {
|
|
4
|
+
charts(facts, (option, path) => series(option).forEach((item, index) => {
|
|
5
|
+
if (item.type === 'line' &&
|
|
6
|
+
(item.smooth === true || (typeof item.smooth === 'number' && item.smooth > 0)))
|
|
7
|
+
emit(`${path}.series[${index}]`);
|
|
8
|
+
}));
|
|
9
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { asArray, series, rule, charts } from './helpers.js';
|
|
2
|
+
/** Use small explicit dots only on short series; avoid default symbols everywhere. */
|
|
3
|
+
export const dt004 = rule(4, 'symbols-everywhere', 'warn', 'Use small explicit dots only on short series; avoid default symbols everywhere.', 'use dots at most 5px on at most 31 points, or use dots at most 5px on at most 31 points, or set showSymbol: false', function check(facts, context, emit) {
|
|
4
|
+
charts(facts, (option, path) => series(option).forEach((item, index) => {
|
|
5
|
+
if (item.type === 'line' &&
|
|
6
|
+
item.showSymbol !== false &&
|
|
7
|
+
item.symbol !== 'none' &&
|
|
8
|
+
asArray(item.data).length > 0 &&
|
|
9
|
+
!(asArray(item.data).length <= 31 &&
|
|
10
|
+
item.symbol === 'circle' &&
|
|
11
|
+
typeof item.symbolSize === 'number' &&
|
|
12
|
+
item.symbolSize <= 5 &&
|
|
13
|
+
series(option).filter((s) => s.type === 'line' && s.showSymbol !== false && s.symbol === 'circle').length <= 1) &&
|
|
14
|
+
!(asArray(item.data).length <= 31 &&
|
|
15
|
+
item.symbol === 'emptyCircle' &&
|
|
16
|
+
typeof item.symbolSize === 'number' &&
|
|
17
|
+
item.symbolSize <= 5 &&
|
|
18
|
+
item.lineStyle?.type === 'dashed'))
|
|
19
|
+
emit(`${path}.series[${index}]`);
|
|
20
|
+
}));
|
|
21
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { packs } from '../packs/index.js';
|
|
2
|
+
import { asArray, series, isPie, rule, charts } from './helpers.js';
|
|
3
|
+
/** Maximum 5 slices; cn rose charts allow 8. */
|
|
4
|
+
export const dt005 = rule(5, 'pie-slices', 'warn', 'Maximum 5 slices; cn rose charts allow 8.', 'use horizontal bars or combine small parts into Other', function check(facts, context, emit) {
|
|
5
|
+
charts(facts, (option, path) => series(option).forEach((item, index) => {
|
|
6
|
+
if (isPie(item) &&
|
|
7
|
+
asArray(item.data).length >
|
|
8
|
+
(item.roseType
|
|
9
|
+
? packs[context.pack].limits.maxRoseSlices
|
|
10
|
+
: packs[context.pack].limits.maxPieSlices))
|
|
11
|
+
emit(`${path}.series[${index}]`);
|
|
12
|
+
}));
|
|
13
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { series, isPie, pieRadii, rule, charts } from './helpers.js';
|
|
2
|
+
/** More than one pie per page or radius ratio above 0.75 warns. A global pie is informational. */
|
|
3
|
+
export const dt006 = rule(6, 'pie-overuse', 'warn', 'More than one pie per page or radius ratio above 0.75 warns. A global pie is informational.', 'prefer horizontal bars; keep donut radius ratio at most 0.75', function check(facts, context, emit) {
|
|
4
|
+
const count = facts.charts.reduce((n, entry) => n + series(entry.option).filter(isPie).length, 0);
|
|
5
|
+
charts(facts, (option, path) => series(option).forEach((item, index) => {
|
|
6
|
+
if (!isPie(item))
|
|
7
|
+
return;
|
|
8
|
+
const channels = pieRadii(item);
|
|
9
|
+
if (count > 1 || (channels.length === 2 && channels[0] / channels[1] > 0.75))
|
|
10
|
+
emit(`${path}.series[${index}]`);
|
|
11
|
+
else if (context.pack === 'global')
|
|
12
|
+
emit(`${path}.series[${index}]`, 'prefer bars for part comparison', 'info');
|
|
13
|
+
}));
|
|
14
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { walk, rule, charts } from './helpers.js';
|
|
2
|
+
/** 3D series, axes, coordinates, and pseudo-3D pie decoration distort comparisons. */
|
|
3
|
+
export const dt007 = rule(7, 'three-d', 'error', '3D series, axes, coordinates, and pseudo-3D pie decoration distort comparisons.', 'use flat 2D charts', function check(facts, context, emit) {
|
|
4
|
+
charts(facts, (option, path) => {
|
|
5
|
+
let bad = false;
|
|
6
|
+
walk(option, (key, value) => {
|
|
7
|
+
if ((/3d|globe/i.test(key) && value !== false && value != null) ||
|
|
8
|
+
(typeof value === 'string' &&
|
|
9
|
+
(/3d|globe/i.test(value) || value === 'surface') &&
|
|
10
|
+
['type', 'coordinateSystem'].includes(key)))
|
|
11
|
+
bad = true;
|
|
12
|
+
});
|
|
13
|
+
if (bad)
|
|
14
|
+
emit(path);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { valueAxes, rule, charts } from './helpers.js';
|
|
2
|
+
/** Multiple value axes obscure comparisons, including mixed bar and line charts. */
|
|
3
|
+
export const dt008 = rule(8, 'dual-axis', 'warn', 'Multiple value axes obscure comparisons, including mixed bar and line charts.', 'use one value axis or aligned small multiples', function check(facts, context, emit) {
|
|
4
|
+
charts(facts, (option, path) => {
|
|
5
|
+
const axes = valueAxes(option);
|
|
6
|
+
if (axes.length < 2)
|
|
7
|
+
return;
|
|
8
|
+
emit(path);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { valueAxes, hasUnit, rule, charts } from './helpers.js';
|
|
2
|
+
/** Value axes need a name, formatter, unit metadata, or units in rendered ticks. */
|
|
3
|
+
export const dt009 = rule(9, 'missing-unit', 'warn', 'Value axes need a name, formatter, unit metadata, or units in rendered ticks.', 'put units in axis names, rendered ticks, or formatter metadata', function check(facts, context, emit) {
|
|
4
|
+
charts(facts, (option, path) => valueAxes(option).forEach((axis) => {
|
|
5
|
+
if (!hasUnit(axis))
|
|
6
|
+
emit(`${path}.${axis._path}`);
|
|
7
|
+
}));
|
|
8
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { series, valueAxes, rule, charts } from './helpers.js';
|
|
2
|
+
/** Bar value axes must start at zero; positive min and scale: true distort lengths. */
|
|
3
|
+
export const dt010 = rule(10, 'bar-baseline', 'error', 'Bar value axes must start at zero; positive min and scale: true distort lengths.', 'set min: 0 and scale: false', function check(facts, context, emit) {
|
|
4
|
+
charts(facts, (option, path) => {
|
|
5
|
+
valueAxes(option).forEach((axis) => {
|
|
6
|
+
const bars = series(option).filter((item) => item.type === 'bar' && (item[`${axis._dimension}AxisIndex`] ?? 0) === axis._index);
|
|
7
|
+
if (bars.length && ((typeof axis.min === 'number' && axis.min > 0) || axis.scale === true))
|
|
8
|
+
emit(`${path}.${axis._path}`);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { series, valueAxes, seriesValues, rule, charts } from './helpers.js';
|
|
2
|
+
/** At data maximum magnitude 10000 or above, axis, visible data label, and tooltip channels need formatters. */
|
|
3
|
+
export const dt011 = rule(11, 'raw-large-numbers', 'warn', 'At data maximum magnitude 10000 or above, axis, visible data label, and tooltip channels need formatters.', 'format compact units: cn 万/亿; global K/M', function check(facts, context, emit) {
|
|
4
|
+
charts(facts, (option, path) => {
|
|
5
|
+
if (!series(option).some((item) => seriesValues(item).some((value) => Math.abs(value) >= 10000)))
|
|
6
|
+
return;
|
|
7
|
+
valueAxes(option).forEach((axis) => {
|
|
8
|
+
if (!axis.axisLabel?.formatter)
|
|
9
|
+
emit(`${path}.${axis._path}.axisLabel`);
|
|
10
|
+
});
|
|
11
|
+
series(option).forEach((item, index) => {
|
|
12
|
+
if (item.label?.show && !item.label.formatter)
|
|
13
|
+
emit(`${path}.series[${index}].label`);
|
|
14
|
+
});
|
|
15
|
+
if (option.tooltip?.show !== false &&
|
|
16
|
+
(option.tooltip || series(option).some((item) => item.tooltip)) &&
|
|
17
|
+
!option.tooltip?.formatter &&
|
|
18
|
+
!option.tooltip?.valueFormatter)
|
|
19
|
+
emit(`${path}.tooltip`);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { walk, rule, charts } from './helpers.js';
|
|
2
|
+
/** String formatter templates must use the selected compact unit system. */
|
|
3
|
+
export const dt012 = rule(12, 'unit-system', 'warn', 'String formatter templates must use the selected compact unit system.', 'use cn 万/亿 or global K/M/B consistently', function check(facts, context, emit) {
|
|
4
|
+
charts(facts, (option, path) => {
|
|
5
|
+
walk(option, (key, value) => {
|
|
6
|
+
if (/formatter/i.test(key) &&
|
|
7
|
+
typeof value === 'string' &&
|
|
8
|
+
(context.pack === 'cn' ? /(?:\}|\d|\s)[KMB]\b/.test(value) : /[万亿]/.test(value)))
|
|
9
|
+
emit(path, 'formatter uses another unit system');
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { rule, sameColor, deltaDirection, conventionColor } from './helpers.js';
|
|
2
|
+
import { deltaSwapped, getTokens } from '../packs/index.js';
|
|
3
|
+
/** Directional deltas must follow the page convention and metric polarity: global by polarity; cn red-up by default. */
|
|
4
|
+
export const dt013 = rule(13, 'delta-convention', 'error', 'Directional deltas must follow the page convention and metric polarity: global by polarity; cn red-up by default.', 'apply one deltaColors convention to the whole page', function check(facts, context, emit) {
|
|
5
|
+
const tokens = getTokens(context.pack, context.mode);
|
|
6
|
+
for (const delta of facts.deltas ?? []) {
|
|
7
|
+
const swap = deltaSwapped(context.pack, facts.page?.deltaColors, delta.polarity);
|
|
8
|
+
const dir = delta.direction ?? deltaDirection(delta.text);
|
|
9
|
+
const expected = dir === 'up'
|
|
10
|
+
? swap
|
|
11
|
+
? tokens.down
|
|
12
|
+
: tokens.up
|
|
13
|
+
: dir === 'down'
|
|
14
|
+
? swap
|
|
15
|
+
? tokens.up
|
|
16
|
+
: tokens.down
|
|
17
|
+
: tokens.muted;
|
|
18
|
+
if (!conventionColor(delta.color, expected) && !sameColor(delta.color, tokens.muted))
|
|
19
|
+
emit(delta.selector);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { rule, sameColor } from './helpers.js';
|
|
2
|
+
import { rgb } from '../color.js';
|
|
3
|
+
import { getTokens } from '../packs/index.js';
|
|
4
|
+
/** Directional colour must be accompanied by an arrow or signed number. */
|
|
5
|
+
export const dt014 = rule(14, 'delta-colour-only', 'warn', 'Directional colour must be accompanied by an arrow or signed number.', 'add ▲/▼ or an explicit sign', function check(facts, context, emit) {
|
|
6
|
+
const tokens = getTokens(context.pack, context.mode);
|
|
7
|
+
for (const delta of facts.deltas ?? []) {
|
|
8
|
+
const hasDirection = /[▲▼↑↓]|[+-]\s*\d/.test(delta.text);
|
|
9
|
+
if (hasDirection || sameColor(delta.color, tokens.muted))
|
|
10
|
+
continue;
|
|
11
|
+
const channels = rgb(delta.color);
|
|
12
|
+
const hasDirectionalColor = channels && Math.max(...channels) - Math.min(...channels) > 0.08;
|
|
13
|
+
if (hasDirectionalColor)
|
|
14
|
+
emit(delta.selector);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { asArray, rule, charts, background } from './helpers.js';
|
|
2
|
+
import { contrast } from '../color.js';
|
|
3
|
+
function contrastThreshold(fontSize = 0, fontWeight = 400) {
|
|
4
|
+
const isBold = fontWeight === 'bold' || (typeof fontWeight === 'number' && fontWeight >= 700);
|
|
5
|
+
return fontSize >= 24 || (fontSize >= 18.66 && isBold) ? 3 : 4.5;
|
|
6
|
+
}
|
|
7
|
+
/** Text needs 4.5:1; at least 24px or 18.66px bold needs 3:1. Chart label defaults use 4.5:1. */
|
|
8
|
+
export const dt015 = rule(15, 'low-contrast-text', 'error', 'Text needs 4.5:1; at least 24px or 18.66px bold needs 3:1. Chart label defaults use 4.5:1.', 'increase text contrast against its background', function check(facts, context, emit) {
|
|
9
|
+
for (const text of facts.texts ?? []) {
|
|
10
|
+
const limit = contrastThreshold(text.fontSize, text.fontWeight);
|
|
11
|
+
if (contrast(text.color, text.background) < limit)
|
|
12
|
+
emit(text.selector);
|
|
13
|
+
}
|
|
14
|
+
charts(facts, (option, path) => {
|
|
15
|
+
for (const [key, entries] of [
|
|
16
|
+
['xAxis', asArray(option.xAxis)],
|
|
17
|
+
['yAxis', asArray(option.yAxis)],
|
|
18
|
+
['legend', asArray(option.legend)],
|
|
19
|
+
])
|
|
20
|
+
entries.forEach((entry, index) => {
|
|
21
|
+
const style = key === 'legend' ? entry.textStyle : entry.axisLabel;
|
|
22
|
+
if (typeof style?.color === 'string' &&
|
|
23
|
+
contrast(style.color, background(facts, context, option)) <
|
|
24
|
+
contrastThreshold(style.fontSize, style.fontWeight))
|
|
25
|
+
emit(`${path}.${key}[${index}]`);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { series, seriesColor, rule, charts, background } from './helpers.js';
|
|
2
|
+
import { contrast } from '../color.js';
|
|
3
|
+
/** Line colours need at least 3:1 against the chart background. */
|
|
4
|
+
export const dt016 = rule(16, 'thin-line-contrast', 'warn', 'Line colours need at least 3:1 against the chart background.', 'darken light-mode lines or lighten dark-mode lines', function check(facts, context, emit) {
|
|
5
|
+
charts(facts, (option, path) => series(option).forEach((item, index) => {
|
|
6
|
+
const lineColor = seriesColor(item, index, option);
|
|
7
|
+
if (item.type === 'line' &&
|
|
8
|
+
typeof lineColor === 'string' &&
|
|
9
|
+
contrast(lineColor, background(facts, context, option)) < 3)
|
|
10
|
+
emit(`${path}.series[${index}]`);
|
|
11
|
+
}));
|
|
12
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { walk, rule, charts } from './helpers.js';
|
|
2
|
+
/** Card/page gradients and linear or radial chart fills add decoration. */
|
|
3
|
+
export const dt017 = rule(17, 'gradient-decoration', 'warn', 'Card/page gradients and linear or radial chart fills add decoration.', 'use solid backgrounds and fills', function check(facts, context, emit) {
|
|
4
|
+
for (const entry of facts.gradients ?? [])
|
|
5
|
+
emit(entry.selector);
|
|
6
|
+
charts(facts, (option, path) => {
|
|
7
|
+
let found = false;
|
|
8
|
+
walk(option, (key, value) => {
|
|
9
|
+
if (key === 'type' && typeof value === 'string' && ['linear', 'radial'].includes(value))
|
|
10
|
+
found = true;
|
|
11
|
+
});
|
|
12
|
+
if (found)
|
|
13
|
+
emit(path);
|
|
14
|
+
});
|
|
15
|
+
});
|