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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -2
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +124 -0
  5. package/dist/color.d.ts +4 -0
  6. package/dist/color.js +43 -0
  7. package/dist/engine.d.ts +11 -0
  8. package/dist/engine.js +39 -0
  9. package/dist/extract-in-page.d.ts +3 -0
  10. package/dist/extract-in-page.js +149 -0
  11. package/dist/extract.d.ts +3 -0
  12. package/dist/extract.js +39 -0
  13. package/dist/format.d.ts +18 -0
  14. package/dist/format.js +65 -0
  15. package/dist/index.d.ts +5 -0
  16. package/dist/index.js +5 -0
  17. package/dist/packs/cn.d.ts +2 -0
  18. package/dist/packs/cn.js +56 -0
  19. package/dist/packs/global.d.ts +2 -0
  20. package/dist/packs/global.js +45 -0
  21. package/dist/packs/index.d.ts +53 -0
  22. package/dist/packs/index.js +13 -0
  23. package/dist/rules/dt001-default-palette.d.ts +3 -0
  24. package/dist/rules/dt001-default-palette.js +11 -0
  25. package/dist/rules/dt002-rainbow-palette.d.ts +3 -0
  26. package/dist/rules/dt002-rainbow-palette.js +11 -0
  27. package/dist/rules/dt003-smooth-line.d.ts +3 -0
  28. package/dist/rules/dt003-smooth-line.js +9 -0
  29. package/dist/rules/dt004-symbols-everywhere.d.ts +3 -0
  30. package/dist/rules/dt004-symbols-everywhere.js +21 -0
  31. package/dist/rules/dt005-pie-slices.d.ts +3 -0
  32. package/dist/rules/dt005-pie-slices.js +13 -0
  33. package/dist/rules/dt006-pie-overuse.d.ts +3 -0
  34. package/dist/rules/dt006-pie-overuse.js +14 -0
  35. package/dist/rules/dt007-three-d.d.ts +3 -0
  36. package/dist/rules/dt007-three-d.js +16 -0
  37. package/dist/rules/dt008-dual-axis.d.ts +3 -0
  38. package/dist/rules/dt008-dual-axis.js +10 -0
  39. package/dist/rules/dt009-missing-unit.d.ts +3 -0
  40. package/dist/rules/dt009-missing-unit.js +8 -0
  41. package/dist/rules/dt010-bar-baseline.d.ts +3 -0
  42. package/dist/rules/dt010-bar-baseline.js +11 -0
  43. package/dist/rules/dt011-raw-large-numbers.d.ts +3 -0
  44. package/dist/rules/dt011-raw-large-numbers.js +21 -0
  45. package/dist/rules/dt012-unit-system.d.ts +3 -0
  46. package/dist/rules/dt012-unit-system.js +12 -0
  47. package/dist/rules/dt013-delta-convention.d.ts +3 -0
  48. package/dist/rules/dt013-delta-convention.js +21 -0
  49. package/dist/rules/dt014-delta-colour-only.d.ts +3 -0
  50. package/dist/rules/dt014-delta-colour-only.js +16 -0
  51. package/dist/rules/dt015-low-contrast-text.d.ts +3 -0
  52. package/dist/rules/dt015-low-contrast-text.js +28 -0
  53. package/dist/rules/dt016-thin-line-contrast.d.ts +3 -0
  54. package/dist/rules/dt016-thin-line-contrast.js +12 -0
  55. package/dist/rules/dt017-gradient-decoration.d.ts +3 -0
  56. package/dist/rules/dt017-gradient-decoration.js +15 -0
  57. package/dist/rules/dt018-glow.d.ts +3 -0
  58. package/dist/rules/dt018-glow.js +27 -0
  59. package/dist/rules/dt019-kpi-overload.d.ts +3 -0
  60. package/dist/rules/dt019-kpi-overload.js +7 -0
  61. package/dist/rules/dt020-kpi-rainbow.d.ts +3 -0
  62. package/dist/rules/dt020-kpi-rainbow.js +27 -0
  63. package/dist/rules/dt021-emoji-icons.d.ts +3 -0
  64. package/dist/rules/dt021-emoji-icons.js +10 -0
  65. package/dist/rules/dt022-cjk-min-size.d.ts +3 -0
  66. package/dist/rules/dt022-cjk-min-size.js +9 -0
  67. package/dist/rules/dt023-legend-single-series.d.ts +3 -0
  68. package/dist/rules/dt023-legend-single-series.js +8 -0
  69. package/dist/rules/dt024-bar-width.d.ts +1 -0
  70. package/dist/rules/dt024-bar-width.js +14 -0
  71. package/dist/rules/dt025-legend-not-direct-label.d.ts +1 -0
  72. package/dist/rules/dt025-legend-not-direct-label.js +11 -0
  73. package/dist/rules/dt026-line-weight.d.ts +1 -0
  74. package/dist/rules/dt026-line-weight.js +8 -0
  75. package/dist/rules/dt027-tabular-numerals.d.ts +1 -0
  76. package/dist/rules/dt027-tabular-numerals.js +16 -0
  77. package/dist/rules/dt028-hero-metric.d.ts +1 -0
  78. package/dist/rules/dt028-hero-metric.js +14 -0
  79. package/dist/rules/helpers.d.ts +32 -0
  80. package/dist/rules/helpers.js +161 -0
  81. package/dist/rules/index.d.ts +2 -0
  82. package/dist/rules/index.js +58 -0
  83. package/dist/types.d.ts +122 -0
  84. package/dist/types.js +1 -0
  85. package/docs/rules.md +735 -0
  86. package/docs/usage.md +126 -0
  87. package/package.json +74 -4
  88. package/skills/dashtaste-cn/SKILL.md +51 -0
  89. package/skills/dashtaste-cn/references/avoid.md +135 -0
  90. package/skills/dashtaste-cn/references/charts.md +122 -0
  91. package/skills/dashtaste-cn/references/layout.md +78 -0
  92. package/skills/dashtaste-cn/references/numbers.md +93 -0
  93. package/skills/dashtaste-cn/references/tokens.md +72 -0
  94. package/skills/dashtaste-global/SKILL.md +43 -0
  95. package/skills/dashtaste-global/references/avoid.md +135 -0
  96. package/skills/dashtaste-global/references/charts.md +122 -0
  97. package/skills/dashtaste-global/references/layout.md +78 -0
  98. package/skills/dashtaste-global/references/numbers.md +83 -0
  99. 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
+ }
@@ -0,0 +1,5 @@
1
+ export * from './engine.js';
2
+ export * from './types.js';
3
+ export * from './packs/index.js';
4
+ export { rules } from './rules/index.js';
5
+ export { extractPage } from './extract.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from './engine.js';
2
+ export * from './types.js';
3
+ export * from './packs/index.js';
4
+ export { rules } from './rules/index.js';
5
+ export { extractPage } from './extract.js';
@@ -0,0 +1,2 @@
1
+ import type { Pack } from './index.js';
2
+ export declare const cn: Pack;
@@ -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,2 @@
1
+ import type { Pack } from './index.js';
2
+ export declare const global: Pack;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Multiple series need explicit colours. Any default palette colour is flagged. */
3
+ export declare const dt001: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** More than 6 distinct categorical colours or more than 5 saturated 60-degree hue buckets. */
3
+ export declare const dt002: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Line smoothing greater than zero draws values that do not exist. */
3
+ export declare const dt003: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Use small explicit dots only on short series; avoid default symbols everywhere. */
3
+ export declare const dt004: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Maximum 5 slices; cn rose charts allow 8. */
3
+ export declare const dt005: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** More than one pie per page or radius ratio above 0.75 warns. A global pie is informational. */
3
+ export declare const dt006: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** 3D series, axes, coordinates, and pseudo-3D pie decoration distort comparisons. */
3
+ export declare const dt007: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Multiple value axes obscure comparisons, including mixed bar and line charts. */
3
+ export declare const dt008: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Value axes need a name, formatter, unit metadata, or units in rendered ticks. */
3
+ export declare const dt009: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Bar value axes must start at zero; positive min and scale: true distort lengths. */
3
+ export declare const dt010: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** At data maximum magnitude 10000 or above, axis, visible data label, and tooltip channels need formatters. */
3
+ export declare const dt011: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** String formatter templates must use the selected compact unit system. */
3
+ export declare const dt012: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Directional deltas must follow the page convention and metric polarity: global by polarity; cn red-up by default. */
3
+ export declare const dt013: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Directional colour must be accompanied by an arrow or signed number. */
3
+ export declare const dt014: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Text needs 4.5:1; at least 24px or 18.66px bold needs 3:1. Chart label defaults use 4.5:1. */
3
+ export declare const dt015: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Line colours need at least 3:1 against the chart background. */
3
+ export declare const dt016: Rule;
@@ -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,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Card/page gradients and linear or radial chart fills add decoration. */
3
+ export declare const dt017: Rule;
@@ -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
+ });
@@ -0,0 +1,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Coloured CSS shadows with blur at least 12px, or chart shadowBlur at least 10, create glow. */
3
+ export declare const dt018: Rule;