27 |
28 | );
29 | }}
30 | >
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/packages/ava/src/advisor/ruler/rules/data-field-qty.ts:
--------------------------------------------------------------------------------
1 | import type { RuleModule } from '../types';
2 |
3 | export const dataFieldQty: RuleModule = {
4 | id: 'data-field-qty',
5 | type: 'HARD',
6 | docs: {
7 | lintText: 'Data must have at least the min qty of the prerequisite.',
8 | },
9 | trigger: () => {
10 | return true;
11 | },
12 | validator: (args): number => {
13 | let result = 0;
14 | const { dataProps, chartType, chartWIKI } = args;
15 | if (dataProps && chartType && chartWIKI[chartType]) {
16 | result = 1;
17 | const dataPres = chartWIKI[chartType].dataPres || [];
18 | const minFieldQty = dataPres.map((e: any) => e.minQty).reduce((acc: number, cv: number) => acc + cv);
19 |
20 | if (dataProps.length) {
21 | const fieldQty = dataProps.length;
22 | if (fieldQty >= minFieldQty) {
23 | result = 1;
24 | }
25 | }
26 | }
27 | return result;
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/site/examples/others/thumbnails/demo/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": {
3 | "zh": "中文分类",
4 | "en": "Category"
5 | },
6 | "demos": [
7 | {
8 | "filename": "usage.jsx",
9 | "title": {
10 | "en": "Thumbnails Usage",
11 | "zh": "Thumbnails 缩略图用法"
12 | },
13 | "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/ObPIQYzZJU/thumbnails-usage.png"
14 | },
15 | {
16 | "filename": "all.jsx",
17 | "title": {
18 | "en": "View All Thumbnails",
19 | "zh": "Thumbnails 缩略图一览"
20 | },
21 | "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/YQGTC0Zq%26t/thumbnails-viewall.gif"
22 | },
23 | {
24 | "filename": "select.jsx",
25 | "title": {
26 | "en": "Select Chart by Thumbnails",
27 | "zh": "通过缩略图选择图表"
28 | },
29 | "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/zwo0GDlia6/thumbnails-select.gif"
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/playground/src/DevPlayground/ChartAdvisor/Chart.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | import { render } from '@antv/g2';
4 |
5 | export const Chart = ({ id, spec }: any) => {
6 | const containerRef = useRef(null);
7 | useEffect(() => {
8 | if (containerRef.current) {
9 | const container = document.getElementById('container');
10 | const style = container ? getComputedStyle(container) : undefined;
11 | const size = style
12 | ? {
13 | width: parseInt(style.width, 10),
14 | height: parseInt(style.height, 10),
15 | }
16 | : {};
17 | const node = render({
18 | ...size,
19 | ...spec,
20 | // theme:'classic'
21 | });
22 | containerRef.current.appendChild(node);
23 | }
24 | }, []);
25 | // @ts-ignore 待 g2 确认渲染方式
26 | return ;
27 | };
28 |
--------------------------------------------------------------------------------
/site/examples/ntv/custom/demo/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": {
3 | "zh": "中文分类",
4 | "en": "Category"
5 | },
6 | "demos": [
7 | {
8 | "filename": "entity.tsx",
9 | "title": {
10 | "en": "Custom Entities",
11 | "zh": "自定义实体短语"
12 | },
13 | "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*A0AGQLEX1nwAAAAAAAAAAAAADmJ7AQ/original"
14 | },
15 | {
16 | "filename": "phrase.tsx",
17 | "title": {
18 | "en": "Custom Phrases",
19 | "zh": "自定义短语"
20 | },
21 | "screenshot": "https://mdn.alipayobjects.com/huamei_vvq19s/afts/img/A*IXhfQ41Sz44AAAAAAAAAAAAADi2DAQ/original"
22 | },
23 | {
24 | "filename": "block.tsx",
25 | "title": {
26 | "en": "Custom Block",
27 | "zh": "自定义区块"
28 | },
29 | "screenshot": "https://mdn.alipayobjects.com/huamei_vvq19s/afts/img/A*H8hzQo2NAHMAAAAAAAAAAAAADi2DAQ/original"
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/site/examples/ntv/case/demo/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": {
3 | "zh": "中文分类",
4 | "en": "Category"
5 | },
6 | "demos": [
7 | {
8 | "filename": "chart-explanation.tsx",
9 | "title": {
10 | "en": "Chart Explanation",
11 | "zh": "图表解读"
12 | },
13 | "screenshot": "https://mdn.alipayobjects.com/huamei_vvq19s/afts/img/A*K3EmTK-ywA0AAAAAAAAAAAAADi2DAQ/original"
14 | },
15 | {
16 | "filename": "report.tsx",
17 | "title": {
18 | "en": "Briefing",
19 | "zh": "业务简报"
20 | },
21 | "screenshot": "https://mdn.alipayobjects.com/huamei_vvq19s/afts/img/A*Kw1LQrr9slcAAAAAAAAAAAAADi2DAQ/original"
22 | },
23 | {
24 | "filename": "fluctuation.tsx",
25 | "title": {
26 | "en": "Fluctuation",
27 | "zh": "波动分析"
28 | },
29 | "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*VPS0Q7DGHlQAAAAAAAAAAAAADmJ7AQ/original"
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/packages/ava/src/data/statistics/bayesian.ts:
--------------------------------------------------------------------------------
1 | import BayesianChangePoint, { BreakPoint } from 'bayesian-changepoint';
2 |
3 | import { calcPValue } from './pettitt-test';
4 |
5 | import type { ChangePointItem } from './types';
6 |
7 | function breakpointVerifier(next: BreakPoint, prev: BreakPoint): boolean {
8 | if (Math.abs(next.data - prev.data) >= 1) {
9 | return true;
10 | }
11 |
12 | return false;
13 | }
14 |
15 | /**
16 | * Bayesian Online Changepoint Detection
17 | */
18 | export function bayesian(series: number[] = []): ChangePointItem[] {
19 | const detection = new BayesianChangePoint({
20 | breakpointVerifier,
21 | chunkSize: series.length,
22 | iteratee: (t: number) => t,
23 | });
24 |
25 | detection.exec(series);
26 |
27 | const result = detection.breakPoints().map((breakPoint) => ({
28 | index: breakPoint.index,
29 | significance: 1 - calcPValue(series, breakPoint.index),
30 | }));
31 |
32 | return result;
33 | }
34 |
--------------------------------------------------------------------------------
/packages/ava/src/insight/narrative/strategy/helpers.ts:
--------------------------------------------------------------------------------
1 | import { lowerCase } from 'lodash';
2 |
3 | import type { Language, InsightType } from '../../types';
4 |
5 | export const getDiffDesc = (value: number, lang: Language) => {
6 | if (value > 0) return lang === 'en-US' ? 'more than' : '超过';
7 | if (value < 0) return lang === 'en-US' ? 'less than' : '少于';
8 | return lang === 'en-US' ? 'equal' : '持平';
9 | };
10 |
11 | export const INSIGHT_TYPE_NAME: Record = {
12 | change_point: '突变点',
13 | time_series_outlier: '时序异常值',
14 | trend: '趋势',
15 | majority: '主要因素',
16 | low_variance: '分布均匀',
17 | category_outlier: '异常值',
18 | correlation: '相关性',
19 | };
20 |
21 | export const getInsightName = (insightType: InsightType, lang: Language) => {
22 | if (lang === 'en-US') return lowerCase(insightType);
23 | return INSIGHT_TYPE_NAME[insightType];
24 | };
25 |
26 | export const getDefaultSeparator = (lang: Language) => {
27 | return lang === 'zh-CN' ? ',' : ', ';
28 | };
29 |
--------------------------------------------------------------------------------
/site/examples/advice/advisor-only/demo/data-advisor.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ReactDOM from 'react-dom';
4 | import { PagList, JSONView } from 'antv-site-demo-rc';
5 | // import
6 | import { Advisor } from '@antv/ava';
7 |
8 | // contants
9 |
10 | const defaultData = [
11 | { year: '2007', sales: 28 },
12 | { year: '2008', sales: 55 },
13 | { year: '2009', sales: 43 },
14 | { year: '2010', sales: 91 },
15 | { year: '2011', sales: 81 },
16 | { year: '2012', sales: 53 },
17 | { year: '2013', sales: 19 },
18 | { year: '2014', sales: 87 },
19 | { year: '2015', sales: 52 },
20 | ];
21 |
22 | // usage
23 | const myAdvisor = new Advisor();
24 | const advices = myAdvisor.advise({ data: defaultData });
25 |
26 | const App = () => (
27 | }
30 | />
31 | );
32 |
33 | ReactDOM.render(, document.getElementById('container'));
34 |
--------------------------------------------------------------------------------
/packages/ava-react/src/InsightCard/Toolbar/types.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import type { ReactNode } from 'react';
4 | import type { InsightCardInfo } from '../types';
5 | import type { DEFAULT_TOOLS } from './constants';
6 |
7 | export type Tool = {
8 | type: string;
9 | /** display icon */
10 | icon?: ReactNode | ((type: string, data?: InsightCardInfo) => ReactNode);
11 | /** tool description, if assigned, it will show as a tooltip when icon is hovered */
12 | description?: ReactNode | ((type: string, data?: InsightCardInfo) => ReactNode);
13 | onClick?: (event: React.MouseEvent, type: string, data?: InsightCardInfo) => void;
14 | };
15 |
16 | export type ToolbarProps = {
17 | tools: Tool[];
18 | data?: InsightCardInfo;
19 | };
20 |
21 | export type DefaultToolType = (typeof DEFAULT_TOOLS)[number];
22 | export type BaseIconButtonProps = {
23 | icon: ReactNode;
24 | onClick?: React.MouseEventHandler;
25 | description?: ReactNode;
26 | };
27 |
--------------------------------------------------------------------------------
/packages/ava-react/src/InsightCard/ntvPlugins/plugins/subspaceDescriptionPlugin.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Typography } from "antd";
4 | import { createCustomPhraseFactory } from "../../../NarrativeTextVis";
5 | import { SUBSPACE_DESCRIPTION_PLUGIN_KEY } from "../../constants";
6 |
7 | export const SubspaceDescription = ({ subspaceDescription }: { subspaceDescription: string }) => {
8 | return (
9 |
18 | {subspaceDescription}
19 |
20 | );
21 | };
22 |
23 | export const subspaceDescriptionPlugin = createCustomPhraseFactory({
24 | key: SUBSPACE_DESCRIPTION_PLUGIN_KEY,
25 | overwrite: (node, value) => {
26 | return ;
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/packages/ava/src/insight/constant.ts:
--------------------------------------------------------------------------------
1 | // Should be confidence level. Is the SIGNIFICANCE_BENCHMARK naming correct? by @pddpd
2 | export const SIGNIFICANCE_BENCHMARK = 0.95;
3 |
4 | export const SIGNIFICANCE_LEVEL = 0.05;
5 |
6 | export const INSIGHT_SCORE_BENCHMARK = 0.01;
7 |
8 | export const IMPACT_SCORE_WEIGHT = 0.2;
9 |
10 | export const INSIGHT_DEFAULT_LIMIT = 20;
11 |
12 | export const IQR_K = 1.5;
13 |
14 | export const LOWESS_N_STEPS = 2;
15 |
16 | export const PATTERN_TYPES = [
17 | 'category_outlier',
18 | 'trend',
19 | 'change_point',
20 | 'time_series_outlier',
21 | 'majority',
22 | 'low_variance',
23 | 'correlation',
24 | ] as const;
25 |
26 | export const HOMOGENEOUS_PATTERN_TYPES = ['commonness', 'exception'] as const;
27 |
28 | export const VERIFICATION_FAILURE_INFO = 'The input does not meet the requirements.';
29 |
30 | export const NO_PATTERN_INFO = 'No insights were found at the specified significance threshold.';
31 |
32 | export const CHANGE_POINT_SIGNIFICANCE_BENCHMARK = 0.15;
33 |
--------------------------------------------------------------------------------
/packages/ava-react/src/NarrativeTextVis/chore/plugin/presets/createTrendDesc.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { isArray } from 'lodash';
4 |
5 | import { SingleLineChart } from '../../../line-charts';
6 | import { createEntityPhraseFactory } from '../createEntityPhraseFactory';
7 | import { getThemeColor } from '../../../theme';
8 |
9 | import type { SpecificEntityPhraseDescriptor } from '../plugin-protocol.type';
10 |
11 | const defaultTrendDescDescriptor: SpecificEntityPhraseDescriptor = {
12 | encoding: {
13 | color: (value, metadata, { theme, palette }) =>
14 | getThemeColor({ colorToken: 'colorConclusion', theme, palette, type: 'trend_desc' }),
15 | inlineChart: (value, { detail }, themeStyles) => {
16 | if (isArray(detail) && detail.length) return ;
17 | return null;
18 | },
19 | },
20 | tooltip: false,
21 | };
22 |
23 | export const createTrendDesc = createEntityPhraseFactory('trend_desc', defaultTrendDescDescriptor);
24 |
--------------------------------------------------------------------------------
/packages/ava/__tests__/unit/insight/extractors/time-series-outlier.test.ts:
--------------------------------------------------------------------------------
1 | import { insightPatternsExtractor } from '../../../../src/insight/insights';
2 |
3 | const data = [
4 | { year: '1991', value: 3 },
5 | { year: '1992', value: 4 },
6 | { year: '1993', value: 3.5 },
7 | { year: '1994', value: 5 },
8 | { year: '1995', value: 4.9 },
9 | { year: '1996', value: 6 },
10 | { year: '1997', value: 7 },
11 | { year: '1998', value: 13 },
12 | { year: '1999', value: 9 },
13 | ];
14 |
15 | describe('extract time-series-outlier insight', () => {
16 | test('check outliers result', () => {
17 | const result = insightPatternsExtractor({
18 | data,
19 | dimensions: [{ fieldName: 'year' }],
20 | measures: [{ fieldName: 'value', method: 'SUM' }],
21 | insightType: 'time_series_outlier',
22 | options: {
23 | filterInsight: true,
24 | },
25 | });
26 | const outliers = result?.map((item) => item.index);
27 | expect(outliers).toStrictEqual([7]);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/packages/ava/src/insight/chart/strategy/viewSpec.ts:
--------------------------------------------------------------------------------
1 | import type { G2Spec, Mark } from '@antv/g2';
2 | import type { InsightInfo, PatternInfo } from '../../types';
3 |
4 | export const viewSpecStrategy = (marks: Mark[], insight?: InsightInfo): G2Spec => {
5 | // majority insight pattern visualizes as 'pie', should not use y nice (G2 handle it as rescale y, the pie chart will be less than 100%)
6 | const isMajorityPattern = insight.patterns.map((pattern) => pattern.type).includes('majority');
7 | const viewConfig = isMajorityPattern
8 | ? { scale: { y: { nice: false } } }
9 | : {
10 | scale: { y: { nice: true } },
11 | };
12 |
13 | return {
14 | type: 'view',
15 | theme: 'classic',
16 | axis: {
17 | x: { labelAutoHide: true, labelAutoRotate: false, title: false },
18 | y: { title: false },
19 | },
20 | interaction: {
21 | tooltip: { groupName: false },
22 | },
23 | legend: false,
24 | children: marks,
25 | ...viewConfig,
26 | };
27 | };
28 |
--------------------------------------------------------------------------------
/packages/ava/src/data/utils/isType/constants.ts:
--------------------------------------------------------------------------------
1 | export const SPECIAL_BOOLEANS = [
2 | [true, false],
3 | [0, 1],
4 | ['true', 'false'],
5 | ['Yes', 'No'],
6 | ['True', 'False'],
7 | ['0', '1'],
8 | ['是', '否'],
9 | ];
10 |
11 | // For isDateString.ts
12 | export const DELIMITER = '([-_./\\s])';
13 | export const YEAR = '(?(18|19|20)\\d{2})';
14 | export const MONTH = '(?0?[1-9]|1[012])';
15 | export const DAY = '(?0?[1-9]|[12]\\d|3[01])';
16 | export const WEEK = '(?[0-4]\\d|5[0-2])';
17 | export const WEEKDAY = '(?[1-7])';
18 | export const BASE_HOUR = '(0?\\d|1\\d|2[0-4])';
19 | export const BASE_MINUTE = '(0?\\d|[012345]\\d)';
20 | export const HOUR = `(?${BASE_MINUTE})`;
21 | export const MINUTE = `(?${BASE_MINUTE})`;
22 | export const SECOND = `(?${BASE_MINUTE})`;
23 | export const MILLISECOND = '(?\\d{1,4})';
24 | export const YEARDAY = '(?(([0-2]\\d|3[0-5])\\d)|36[0-6])';
25 | export const OFFSET = `(?Z|[+-]${BASE_HOUR}(:${BASE_MINUTE})?)`;
26 |
--------------------------------------------------------------------------------
/packages/ava-react/src/NarrativeTextVis/styled/paragraph.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | import { getThemeColor, getFontSize, getLineHeight } from '../theme';
4 |
5 | import type { TextParagraphSpec } from '@antv/ava';
6 | import type { ThemeStylesProps } from '../types';
7 |
8 | export const P = styled.p>`
9 | white-space: pre-wrap; // 默认 pre 显示,可以显示空格和转义字符
10 | font-family: PingFangSC, sans-serif;
11 | color: ${({ theme, palette }) => getThemeColor({ colorToken: 'colorBase', theme, palette, type: 'text' })};
12 | font-size: ${({ size }) => getFontSize(size)};
13 | min-height: 24px;
14 | line-height: ${({ size }) => getLineHeight(size)};
15 | margin-bottom: 4px;
16 | text-indent: ${({ indents }) => indents?.find((item) => item.type === 'first-line')?.length};
17 | padding-left: ${({ indents }) => indents?.find((item) => item.type === 'left')?.length};
18 | padding-right: ${({ indents }) => indents?.find((item) => item.type === 'right')?.length};
19 | `;
20 |
--------------------------------------------------------------------------------
/packages/ava-react/src/NarrativeTextVis/line-charts/proportion/ProportionChart.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { getThemeColor } from '../../theme';
4 | import { useSvgWrapper } from '../hooks/useSvgWrapper';
5 |
6 | import { getArcPath } from './getArcPath';
7 |
8 | import type { ThemeStylesProps } from '../../types';
9 |
10 | export const ProportionChart: React.FC<{ data: number } & ThemeStylesProps> = ({
11 | data,
12 | size = 'normal',
13 | theme = 'light',
14 | }) => {
15 | const [Svg, fontSize] = useSvgWrapper(size);
16 | const r = fontSize / 2;
17 | return (
18 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/packages/ava/src/advisor/ruler/rules/line-field-time-ordinal.ts:
--------------------------------------------------------------------------------
1 | import { intersects } from '../../utils';
2 |
3 | import { MAX_SOFT_RULE_COEFFICIENT } from './constants';
4 |
5 | import type { RuleModule } from '../types';
6 |
7 | const applyChartTypes = ['line_chart', 'area_chart', 'stacked_area_chart', 'percent_stacked_area_chart'];
8 |
9 | export const lineFieldTimeOrdinal: RuleModule = {
10 | id: 'line-field-time-ordinal',
11 | type: 'SOFT',
12 | docs: {
13 | lintText: 'Data containing time or ordinal fields are suitable for line or area charts.',
14 | },
15 | trigger: ({ chartType }) => {
16 | return applyChartTypes.includes(chartType);
17 | },
18 | validator: (args): number => {
19 | let result = 1;
20 | const { dataProps } = args;
21 | if (dataProps) {
22 | const field4TimeOrOrdinal = dataProps.find((field) => intersects(field.levelOfMeasurements, ['Ordinal', 'Time']));
23 |
24 | if (field4TimeOrOrdinal) {
25 | result = MAX_SOFT_RULE_COEFFICIENT * 0.5;
26 | }
27 | }
28 | return result;
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/packages/ava/src/advisor/ruler/rules/no-redundant-field.ts:
--------------------------------------------------------------------------------
1 | import type { RuleModule } from '../types';
2 |
3 | export const noRedundantField: RuleModule = {
4 | id: 'no-redundant-field',
5 | type: 'HARD',
6 | docs: {
7 | lintText: 'No redundant field.',
8 | },
9 | trigger: () => {
10 | return true;
11 | },
12 | validator: (args): number => {
13 | let result = 0;
14 | const { dataProps, chartType, chartWIKI } = args;
15 |
16 | if (dataProps && chartType && chartWIKI[chartType]) {
17 | const dataPres = chartWIKI[chartType].dataPres || [];
18 | const maxFieldQty = dataPres
19 | .map((e: any) => {
20 | if (e.maxQty === '*') {
21 | return 99;
22 | }
23 | return e.maxQty;
24 | })
25 | .reduce((acc: number, cv: number) => acc + cv);
26 |
27 | if (dataProps.length) {
28 | const fieldQty = dataProps.length;
29 | if (fieldQty <= maxFieldQty) {
30 | result = 1;
31 | }
32 | }
33 | }
34 |
35 | return result;
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/packages/ava/__tests__/integration/advisor/advise-cases/charts/histogram.test.ts:
--------------------------------------------------------------------------------
1 | import { dataByChartId } from '@antv/data-samples';
2 |
3 | import { Advisor } from '../../../../../src/advisor/index';
4 |
5 | // In the following cases, the recommended result should be a histogram chart.
6 | describe('should advise histogram', () => {
7 | test('test case from @antv/data-samples', async () => {
8 | const data = await dataByChartId('histogram');
9 |
10 | const myAdvisor = new Advisor();
11 | const advices = myAdvisor.advise({ data });
12 | expect(advices[0].type).toBe('histogram');
13 | });
14 | });
15 |
16 | // In the following cases, the recommended result should NOT be a histogram chart.
17 | describe('should NOT advise histogram', () => {
18 | test('categrorical field should not be x-axis of histogram chart', async () => {
19 | const data = await dataByChartId('line_chart');
20 |
21 | const myAdvisor = new Advisor();
22 | const advices = myAdvisor.advise({ data });
23 | expect(advices[0].type === 'histogram').toBe(false);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/packages/ava/__tests__/unit/insight/extractors/majority.test.ts:
--------------------------------------------------------------------------------
1 | import { insightPatternsExtractor } from '../../../../src/insight/insights';
2 |
3 | const data = [
4 | {
5 | type: 'A',
6 | sales: 38,
7 | },
8 | {
9 | type: 'B',
10 | sales: 52,
11 | },
12 | {
13 | type: 'C',
14 | sales: 48,
15 | },
16 | {
17 | type: 'D',
18 | sales: 45,
19 | },
20 | {
21 | type: 'E',
22 | sales: 48,
23 | },
24 | {
25 | type: 'F',
26 | sales: 473,
27 | },
28 | {
29 | type: 'G',
30 | sales: 38,
31 | },
32 | {
33 | type: 'H',
34 | sales: 38,
35 | },
36 | ];
37 |
38 | describe('extract majority insight', () => {
39 | test('check majority result', () => {
40 | const result = insightPatternsExtractor({
41 | data,
42 | dimensions: [{ fieldName: 'type' }],
43 | measures: [{ fieldName: 'sales', method: 'SUM' }],
44 | insightType: 'majority',
45 | options: {
46 | filterInsight: true,
47 | },
48 | });
49 | expect(result[0]?.index).toEqual(5);
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/packages/ava/__tests__/unit/advisor/ruler/design-rule.test.ts:
--------------------------------------------------------------------------------
1 | import { Advisor } from '../../../../src/advisor';
2 | import { barWithoutAxisMin } from '../../../../src/advisor/ruler/rules/bar-without-axis-min';
3 |
4 | // design rule test
5 | test('x-axis-line-fading', () => {
6 | const myAdvisor = new Advisor();
7 | const data = [
8 | { price: 520, year: 2005 },
9 | { price: 600, year: 2006 },
10 | { price: 1500, year: 2007 },
11 | ];
12 | const advices = myAdvisor.advise({ data, fields: ['price', 'year'], options: { refine: true } });
13 | const chartSpec = advices.filter((e) => e.type === 'line_chart')[0].spec;
14 | if (chartSpec) {
15 | const layerEnc = chartSpec.layer && 'encoding' in chartSpec.layer[0] ? chartSpec.layer[0].encoding : null;
16 | if (layerEnc) {
17 | expect(layerEnc.x).toHaveProperty('axis');
18 | expect(layerEnc.y).toHaveProperty('scale');
19 | }
20 | }
21 | });
22 |
23 | test('bar-without-axis-min', () => {
24 | // @ts-ignore
25 | expect(barWithoutAxisMin.trigger({ chartType: 'bar_chart' })).toBe(true);
26 | });
27 |
--------------------------------------------------------------------------------
/.github/workflows/mirror.yml:
--------------------------------------------------------------------------------
1 | name: 🤖 Sync to Gitee Mirror
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: 🔁 Sync to Gitee
13 | uses: wearerequired/git-mirror-action@master
14 | env:
15 | # 注意在 Settings->Secrets 配置 GITEE_RSA_PRIVATE_KEY
16 | SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }}
17 | with:
18 | # 注意替换为你的 GitHub 源仓库地址
19 | source-repo: 'git@github.com:antvis/AVA.git'
20 | # 注意替换为你的 Gitee 目标仓库地址
21 | destination-repo: 'git@gitee.com:antv-ava/antv-ava.git'
22 |
23 | - name: ✅ Build Gitee Pages
24 | uses: yanglbme/gitee-pages-action@master
25 | with:
26 | # 注意替换为你的 Gitee 用户名
27 | gitee-username: neoddish
28 | # 注意在 Settings->Secrets 配置 GITEE_PASSWORD
29 | gitee-password: ${{ secrets.GITEE_PASSWORD }}
30 | # 注意替换为你的 Gitee 仓库
31 | gitee-repo: antv-ava/antv-ava
32 | # 要部署的分支
33 | branch: gh-pages
34 |
--------------------------------------------------------------------------------
/packages/ava/__tests__/integration/advisor/advise-cases/charts/donut.test.ts:
--------------------------------------------------------------------------------
1 | import { dataByChartId } from '@antv/data-samples';
2 |
3 | import { Advisor } from '../../../../../src/advisor/index';
4 |
5 | // In the following cases, the recommended result should be a donut chart.
6 | describe('should advise donut', () => {
7 | test('test case from @antv/data-samples', async () => {
8 | const data = await dataByChartId('donut_chart');
9 |
10 | const myAdvisor = new Advisor();
11 | const advices = myAdvisor.advise({ data });
12 | expect(advices.map((advice) => advice.type).includes('donut_chart')).toBe(true);
13 | });
14 |
15 | test('a categorical field + a quantitative field with significantly different values', () => {
16 | const data = [
17 | { price: 100, type: 'A' },
18 | { price: 120, type: 'B' },
19 | { price: 150, type: 'C' },
20 | ];
21 |
22 | const myAdvisor = new Advisor();
23 | const advices = myAdvisor.advise({ data });
24 | expect(advices.map((advice) => advice.type).includes('donut_chart')).toBe(true);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/packages/ava/src/insight/chart/generator/insights.ts:
--------------------------------------------------------------------------------
1 | import { G2Spec, Mark } from '@antv/g2';
2 |
3 | import { InsightInfo, PatternInfo } from '../../types';
4 | import {
5 | categoryOutlierStrategy,
6 | changePointStrategy,
7 | lowVarianceStrategy,
8 | majorityStrategy,
9 | correlationStrategy,
10 | timeSeriesOutlierStrategy,
11 | trendStrategy,
12 | viewSpecStrategy,
13 | } from '../strategy';
14 |
15 | export function generateInsightChartSpec(insight: InsightInfo): G2Spec {
16 | const { type: insightType } = insight.patterns[0];
17 |
18 | const insightType2Strategy: Record) => Mark[]> = {
19 | trend: trendStrategy,
20 | time_series_outlier: timeSeriesOutlierStrategy,
21 | category_outlier: categoryOutlierStrategy,
22 | change_point: changePointStrategy,
23 | low_variance: lowVarianceStrategy,
24 | majority: majorityStrategy,
25 | correlation: correlationStrategy,
26 | };
27 |
28 | const marks = insightType2Strategy[insightType]?.(insight);
29 | return viewSpecStrategy(marks, insight);
30 | }
31 |
--------------------------------------------------------------------------------
/packages/ava/src/insight/narrative/index.ts:
--------------------------------------------------------------------------------
1 | import InsightNarrativeStrategyFactory from './factory';
2 |
3 | import type { HomogeneousInsightInfo } from './strategy/base';
4 | import type { InsightInfo, InsightVisualizationOptions, PatternInfo, HomogeneousPatternInfo } from '../types';
5 |
6 | function isHomogeneousPattern(
7 | insightInfo: InsightInfo | HomogeneousPatternInfo
8 | ): insightInfo is HomogeneousPatternInfo {
9 | return 'childPatterns' in insightInfo;
10 | }
11 |
12 | export default function generateInsightNarrative(
13 | insightInfo: Omit, 'visualizationSpecs'> | HomogeneousInsightInfo,
14 | options: InsightVisualizationOptions
15 | ) {
16 | const insightType = isHomogeneousPattern(insightInfo) ? insightInfo?.type : insightInfo?.patterns[0]?.type;
17 | if (!insightType) throw Error('insight info has no insight type');
18 |
19 | const { lang } = options;
20 |
21 | const strategy = InsightNarrativeStrategyFactory.getStrategy(insightType);
22 | const result = strategy.generateTextSpec(insightInfo, lang);
23 | return result;
24 | }
25 |
--------------------------------------------------------------------------------
/site/docs/guide/auto-chart/intro.zh.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: AutoChart 简介
3 | order: 0
4 | ---
5 |
6 |
7 |
8 | AutoChart 是一个可以根据数据自动推荐合适的图表并渲染的 React 组件。
9 |
10 | ## ✨ 功能特性
11 |
12 | AutoChart 中透出了 `AutoChart` 组件供用户使用。
13 | 它结合了 AVA 中的图表推荐库 `ChartAdvisor` 的核心能力。
14 |
15 | AutoChart 可以到做到基于给定数据和分析需求来自动生成并渲染合适的图表,
16 | 我们推出 AutoChart 的核心目的就是为用户提供一行代码实现智能可视化的能力。
17 |
18 | ## 🔨 使用
19 |
20 |
21 | ```js
22 | import { AutoChart } from '@antv/auto-chart';
23 |
24 | const defaultData = [
25 | { price: 100, type: 'A' },
26 | { price: 120, type: 'B' },
27 | { price: 150, type: 'C' },
28 | ];
29 |
30 | ReactDOM.render(
31 | <>
32 |
38 | >,
39 | mountNode,
40 | );
41 | ```
42 |
43 |
44 | ### AutoChart 演示案例
45 |
46 |
47 |
48 | ## 📖 文档
49 |
50 | 更多用法请移步至 [官网API](https://ava.antv.antgroup.com/zh/docs/api/auto-chart/AutoChart)
51 |
--------------------------------------------------------------------------------
/packages/ava/__tests__/integration/advisor/advise-cases/charts/area.test.ts:
--------------------------------------------------------------------------------
1 | import { dataByChartId } from '@antv/data-samples';
2 |
3 | import { Advisor } from '../../../../../src/advisor/index';
4 |
5 | // In the following cases, the recommended result should be a area/line chart.
6 | describe('should advise area/line', () => {
7 | test('test case from @antv/data-samples', async () => {
8 | const data = await dataByChartId('area_chart');
9 |
10 | const myAdvisor = new Advisor();
11 | const advices = myAdvisor.advise({ data });
12 | expect(advices.map((advice) => advice.type).includes('area_chart')).toBe(true);
13 | });
14 | });
15 |
16 | // In the following cases, the recommended result should NOT be a area/line chart.
17 | describe('should NOT advise area/line', () => {
18 | test('categrorical field should not be x-axis of area chart', async () => {
19 | const data = await dataByChartId('bar_chart');
20 |
21 | const myAdvisor = new Advisor();
22 | const advices = myAdvisor.advise({ data });
23 | expect(advices[0].type === 'area_chart').toBe(false);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/packages/ava/__tests__/unit/insight/extractors/low-variance.test.ts:
--------------------------------------------------------------------------------
1 | import { insightPatternsExtractor } from '../../../../src/insight/insights';
2 |
3 | const data = [
4 | {
5 | type: 'A',
6 | sales: 38,
7 | },
8 | {
9 | type: 'B',
10 | sales: 52,
11 | },
12 | {
13 | type: 'C',
14 | sales: 48,
15 | },
16 | {
17 | type: 'D',
18 | sales: 45,
19 | },
20 | {
21 | type: 'E',
22 | sales: 48,
23 | },
24 | {
25 | type: 'F',
26 | sales: 38,
27 | },
28 | {
29 | type: 'G',
30 | sales: 38,
31 | },
32 | {
33 | type: 'H',
34 | sales: 38,
35 | },
36 | ];
37 |
38 | describe('extract low-variance insight', () => {
39 | test('check low-variance result', () => {
40 | const result = insightPatternsExtractor({
41 | data,
42 | dimensions: [{ fieldName: 'type' }],
43 | measures: [{ fieldName: 'sales', method: 'SUM' }],
44 | insightType: 'low_variance',
45 | options: {
46 | filterInsight: true,
47 | },
48 | });
49 | expect(result[0]?.significance).toBeGreaterThan(0.85);
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/packages/ava/src/utils/dataFormat.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * data format.
3 | * dataFormat(0.8849) = 0.88
4 | * dataFormat(12, 1) = 12
5 | * dataFormat(1234, 1) = 1.2k
6 | * dataFormat(123.456, 2) = 123.46
7 | * @param value value to be formatted
8 | * @param digits the number of digits to keep after the decimal point, 2 by default
9 | * @returns formatted value string
10 | */
11 | export default function dataFormat(value: number | string, digits: number = 2) {
12 | if (typeof value === 'string') return value;
13 |
14 | const formatMap = [
15 | { value: 1, symbol: '' },
16 | { value: 1e3, symbol: 'k' },
17 | { value: 1e6, symbol: 'M' },
18 | { value: 1e9, symbol: 'G' },
19 | { value: 1e12, symbol: 'T' },
20 | { value: 1e15, symbol: 'P' },
21 | { value: 1e18, symbol: 'E' },
22 | ];
23 | const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
24 | const item = formatMap
25 | .slice()
26 | .reverse()
27 | .find((item) => {
28 | return value >= item.value;
29 | });
30 | return item ? (value / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : value.toFixed(digits);
31 | }
32 |
--------------------------------------------------------------------------------
/site/examples/unused-demo/chart-advisor/relation/demo/table.jsx:
--------------------------------------------------------------------------------
1 | import { Advisor } from '@antv/ava';
2 | import { specToG6Plot } from '@antv/antv-spec';
3 |
4 | // Prepare tabular data that describe relations: each row of data represents an edge
5 | fetch('https://gw.alipayobjects.com/os/antfincdn/h7Bil5Cia/ava-eurocredit-data.json')
6 | .then((res) => res.json())
7 | .then((data) => {
8 | // specify which fields are used for source and target
9 | const extra = {
10 | sourceKey: 'Creditor',
11 | targetKey: 'Debtor',
12 | };
13 |
14 | // Initialize an advisor and pass the data to its advise function
15 | const myAdvisor = new Advisor();
16 | const advices = myAdvisor.advise({ data, options: { extra } });
17 |
18 | // The advices are returns in order from largest score to smallest score, you can choose the best advice to generate visualization
19 | const bestAdvice = advices[0];
20 | if (bestAdvice) {
21 | const { spec } = bestAdvice;
22 | const container = document.getElementById('container');
23 | specToG6Plot(spec, container);
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/packages/ava/__tests__/integration/advisor/advise-cases/charts/grouped_bar.test.ts:
--------------------------------------------------------------------------------
1 | import { dataByChartId } from '@antv/data-samples';
2 |
3 | import { Advisor } from '../../../../../src/advisor/index';
4 |
5 | // In the following cases, the recommended result should be a grouped_bar chart.
6 | describe('should advise grouped_bar', () => {
7 | test('test case from @antv/data-samples', async () => {
8 | const data = await dataByChartId('stacked_bar_chart');
9 | const myAdvisor = new Advisor();
10 | const advices = myAdvisor.advise({ data });
11 | expect(advices.map((advice) => advice.type).includes('grouped_bar_chart')).toBe(true);
12 | });
13 | });
14 |
15 | // In the following cases, the recommended result should NOT be a grouped_bar chart.
16 | describe('should NOT advise bar/bar', () => {
17 | test('categrorical field should not be x-axis of bar chart', async () => {
18 | const data = await dataByChartId('line_chart');
19 | const myAdvisor = new Advisor();
20 | const advices = myAdvisor.advise({ data });
21 | expect(advices[0].type === 'grouped_bar_chart').toBe(false);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/ava/__tests__/integration/advisor/advise-cases/charts/stacked_bar.test.ts:
--------------------------------------------------------------------------------
1 | import { dataByChartId } from '@antv/data-samples';
2 |
3 | import { Advisor } from '../../../../../src/advisor/index';
4 |
5 | // In the following cases, the recommended result should be a stacked_bar chart.
6 | describe('should advise stacked_bar', () => {
7 | test('test case from @antv/data-samples', async () => {
8 | const data = await dataByChartId('stacked_bar_chart');
9 | const myAdvisor = new Advisor();
10 | const advices = myAdvisor.advise({ data });
11 | expect(advices.map((advice) => advice.type).includes('stacked_bar_chart')).toBe(true);
12 | });
13 | });
14 |
15 | // In the following cases, the recommended result should NOT be a stacked_bar chart.
16 | describe('should NOT advise bar/bar', () => {
17 | test('categrorical field should not be x-axis of bar chart', async () => {
18 | const data = await dataByChartId('line_chart');
19 | const myAdvisor = new Advisor();
20 | const advices = myAdvisor.advise({ data });
21 | expect(advices[0].type === 'stacked_bar_chart').toBe(false);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/ava/__tests__/integration/advisor/advise-cases/charts/step_line.test.ts:
--------------------------------------------------------------------------------
1 | import { dataByChartId } from '@antv/data-samples';
2 |
3 | import { Advisor } from '../../../../../src/advisor/index';
4 |
5 | // In the following cases, the recommended result should be a step_line chart.
6 | describe('should advise step_line', () => {
7 | test('test case from @antv/data-samples', async () => {
8 | const data = await dataByChartId('step_line_chart');
9 |
10 | const myAdvisor = new Advisor();
11 | const advices = myAdvisor.advise({ data });
12 | expect(advices.map((advice) => advice.type).includes('step_line_chart')).toBe(true);
13 | });
14 | });
15 |
16 | // In the following cases, the recommended result should NOT be a step_line chart.
17 | describe('should NOT advise step_line', () => {
18 | test('categrorical field should not be x-axis of step_line chart', async () => {
19 | const data = await dataByChartId('bar_chart');
20 |
21 | const myAdvisor = new Advisor();
22 | const advices = myAdvisor.advise({ data });
23 | expect(advices[0].type === 'step_line_chart').toBe(false);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/packages/ava/src/insight/algorithms/base/compare.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * lodash sortby asc function
3 | * @param left unknown
4 | * @param right unknown
5 | * @returns number
6 | */
7 | export function ascending(left: unknown, right: unknown) {
8 | const leftIsNull = left === null || left === undefined;
9 | const rightIsNull = right === null || right === undefined;
10 | if (leftIsNull && rightIsNull) {
11 | return 0;
12 | }
13 | if (leftIsNull) {
14 | return 1;
15 | }
16 | if (rightIsNull) {
17 | return -1;
18 | }
19 | return (left as number) - (right as number);
20 | }
21 |
22 | /**
23 | * lodash sortby desc function
24 | * @param left any
25 | * @param right any
26 | * @returns number
27 | */
28 | export function descending(left: unknown, right: unknown) {
29 | const leftIsNull = left === null || left === undefined;
30 | const rightIsNull = right === null || right === undefined;
31 | if (leftIsNull && rightIsNull) {
32 | return 0;
33 | }
34 | if (leftIsNull) {
35 | return 1;
36 | }
37 | if (rightIsNull) {
38 | return -1;
39 | }
40 | return (right as number) - (left as number);
41 | }
42 |
--------------------------------------------------------------------------------
/packages/ava/src/data/statistics/cdf.ts:
--------------------------------------------------------------------------------
1 | import { isNil } from 'lodash';
2 |
3 | /**
4 | * Evaluates the cumulative distribution function (CDF) for a normal distribution at a value x
5 | * - Reference to equation 26.2.17 in https://personal.math.ubc.ca/~cbm/aands/abramowitz_and_stegun.pdf
6 | * @param mu mean
7 | * @param sigma standard deviation
8 | * */
9 | export const cdf = (x: number, mu: number = 0, sigma: number = 1): number => {
10 | if (sigma < 0 || [x, mu, sigma].some((value) => isNil(value))) return NaN;
11 | // transfer to standard normal distribution
12 | const normalX = Math.abs((x - mu) / sigma);
13 | /** probability density function of the standard normal distribution */
14 | const Zx = (1 / Math.sqrt(2 * Math.PI)) * Math.exp((-1 * normalX ** 2) / 2);
15 | /**
16 | * - use approximate elementary functional algorithm, error less than 4.5e-4
17 | * @todo add document explaining the derivation process
18 | * */
19 | const t = 1 / (1 + 0.33267 * normalX);
20 | // error less than 1e-5
21 | const Px = 1 - Zx * (0.4361836 * t - 0.1201676 * t ** 2 + 0.937298 * t ** 3);
22 | return x > mu ? Px : 1 - Px;
23 | };
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-2023 AntV team
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do { so, subject } to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/ava/__tests__/integration/advisor/advise-cases/charts/stacked_column.test.ts:
--------------------------------------------------------------------------------
1 | import { dataByChartId } from '@antv/data-samples';
2 |
3 | import { Advisor } from '../../../../../src/advisor/index';
4 |
5 | // In the following cases, the recommended result should be a stacked_column chart.
6 | describe('should advise stacked_column', () => {
7 | test('test case from @antv/data-samples', async () => {
8 | const data = await dataByChartId('stacked_column_chart');
9 | const myAdvisor = new Advisor();
10 | const advices = myAdvisor.advise({ data });
11 | expect(advices.map((advice) => advice.type).includes('stacked_column_chart')).toBe(true);
12 | });
13 | });
14 |
15 | // In the following cases, the recommended result should NOT be a stacked_column chart.
16 | describe('should NOT advise column/column', () => {
17 | test('categrorical field should not be x-axis of column chart', async () => {
18 | const data = await dataByChartId('line_chart');
19 | const myAdvisor = new Advisor();
20 | const advices = myAdvisor.advise({ data });
21 | expect(advices[0].type === 'stacked_column_chart').toBe(false);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/ava/__tests__/integration/advisor/advise-cases/charts/stacked_area.test.ts:
--------------------------------------------------------------------------------
1 | import { dataByChartId } from '@antv/data-samples';
2 |
3 | import { Advisor } from '../../../../../src/advisor/index';
4 |
5 | // In the following cases, the recommended result should be a stacked_area chart.
6 | describe('should advise stacked_area', () => {
7 | test('test case from @antv/data-samples', async () => {
8 | const data = await dataByChartId('stacked_area_chart');
9 |
10 | const myAdvisor = new Advisor();
11 | const advices = myAdvisor.advise({ data });
12 | expect(advices.map((advice) => advice.type).includes('stacked_area_chart')).toBe(true);
13 | });
14 | });
15 |
16 | // In the following cases, the recommended result should NOT be a stacked_area chart.
17 | describe('should NOT advise stacked_area', () => {
18 | test('categrorical field should not be x-axis of stacked_area chart', async () => {
19 | const data = await dataByChartId('bar_chart');
20 |
21 | const myAdvisor = new Advisor();
22 | const advices = myAdvisor.advise({ data });
23 | expect(advices[0].type === 'stacked_area_chart').toBe(false);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/playground/src/DevPlayground/CKBList/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ReactJson from 'react-json-view';
4 |
5 | import { ckb } from '../../../../packages/ava/lib';
6 |
7 | import type { ReactJsonViewProps } from 'react-json-view';
8 |
9 | export interface JSONViewProps {
10 | prefixCls?: string;
11 | className?: string;
12 | style?: React.CSSProperties;
13 | rjvConfigs?: Omit;
14 | json: any;
15 | }
16 |
17 | export const CKBJSONView: React.FC = ({ json }) => {
18 | return (
19 |
26 | {/* react types 导致抛出错误 JSX element type 'ReactElement | null' is not a constructor function for JSX elements */}
27 | {/* @ts-ignore */}
28 |
29 |