diff --git a/.github/workflows/auto-inspection.yml b/.github/workflows/auto-inspection.yml index 814433b158..797498937e 100644 --- a/.github/workflows/auto-inspection.yml +++ b/.github/workflows/auto-inspection.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v2 - name: Get branch name (pull request) run: echo "BRANCH_NAME=$(echo ${GITHUB_HEAD_REF})" >> $GITHUB_ENV - - uses: lxfu1/surge-preview@v2.0.1 + - uses: lxfu1/surge-preview@v2.0.1 id: preview_step with: project_name: 'G2Plot' diff --git a/__tests__/data/violin.ts b/__tests__/data/violin.ts new file mode 100644 index 0000000000..aaa76b8d8d --- /dev/null +++ b/__tests__/data/violin.ts @@ -0,0 +1,602 @@ +export const BASE_VIOLIN_DATA = [ + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.5 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.9 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.2 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.7 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.1 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.6 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.6 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.7 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.9 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.6 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 2.9 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.1 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.1 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.9 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.7 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.8 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.1 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.8 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.1 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.1 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.3 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.2 }, + { species: 'I. setosa', type: 'SepalWidth', value: 4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.8 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 4.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.7 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.9 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.5 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.7 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.8 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.7 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.8 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.7 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.7 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.6 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.6 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.5 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.7 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.3 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.9 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.8 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.5 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.2 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.2 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.2 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.7 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.1 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.8 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.1 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 4.1 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.2 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 4.2 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.1 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.9 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.2 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.2 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.5 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.1 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.6 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.9 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.5 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 2.3 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.2 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.6 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.5 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.9 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.8 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.8 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.8 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.2 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.6 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.7 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.3 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.3 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.7 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.2 }, + { species: 'I. versicolor', type: 'SepalLength', value: 7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.2 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.4 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.9 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.1 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.9 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.6 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.8 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.8 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.6 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.7 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.3 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.3 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.4 }, + { species: 'I. versicolor', type: 'SepalLength', value: 4.9 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.6 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.9 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.7 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.2 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.2 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.9 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.2 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.7 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.1 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.6 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.1 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.1 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.7 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.8 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.2 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.2 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.9 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.5 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.8 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.8 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.2 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.9 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.8 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.1 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.9 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.5 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.3 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.2 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.7 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.8 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.1 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.3 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.4 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.8 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.8 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.8 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.7 }, + { species: 'I. versicolor', type: 'PetalLength', value: 5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.6 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.8 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.4 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.7 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.4 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.2 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.9 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.7 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.8 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.6 }, + { species: 'I. versicolor', type: 'PetalLength', value: 5.1 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.7 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.4 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.6 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.4 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.7 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.1 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.3 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.1 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.5 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.2 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.6 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.6 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.1 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.2 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.6 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.8 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.3 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.2 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.7 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.2 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.2 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.2 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.3 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.2 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.5 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.1 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.1 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.8 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'PetalLength', value: 6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.9 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.7 }, + { species: 'I. virginica', type: 'SepalLength', value: 5.8 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.1 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.9 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.1 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.9 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.2 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.8 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.5 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.1 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.6 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.7 }, + { species: 'I. virginica', type: 'PetalLength', value: 4.5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'SepalLength', value: 4.9 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.3 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.9 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.8 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.6 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.2 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.2 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.5 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.9 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.3 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.7 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.4 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.1 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.8 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2 }, + { species: 'I. virginica', type: 'PetalLength', value: 5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'SepalLength', value: 5.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.4 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 5.8 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.3 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.2 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.4 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.5 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.2 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.7 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.9 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.6 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.5 }, + { species: 'I. virginica', type: 'PetalLength', value: 5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.2 }, + { species: 'I. virginica', type: 'SepalLength', value: 6 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.7 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.2 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.9 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2 }, + { species: 'I. virginica', type: 'PetalLength', value: 4.9 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 5.6 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.7 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 4.9 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.7 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.1 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.7 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.2 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.2 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 4.8 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.2 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 4.9 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.1 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.1 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.4 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.6 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.8 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.2 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.9 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.4 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.4 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.9 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.2 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.4 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.5 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.4 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.6 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.1 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.4 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.4 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.1 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.4 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 4.8 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.1 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.4 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.1 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.9 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.4 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.1 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.1 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.9 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.9 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.7 }, + { species: 'I. virginica', type: 'SepalLength', value: 5.8 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.9 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.2 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.8 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.7 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.2 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.9 }, + { species: 'I. virginica', type: 'PetalLength', value: 5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.2 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.5 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.4 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.4 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.2 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 5.9 }, +]; diff --git a/__tests__/unit/plots/violin/animation-spec.ts b/__tests__/unit/plots/violin/animation-spec.ts new file mode 100644 index 0000000000..1d8d520f94 --- /dev/null +++ b/__tests__/unit/plots/violin/animation-spec.ts @@ -0,0 +1,85 @@ +import { Violin } from '../../../../src'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin legend', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + animation: { + enter: { + animation: 'fade-in', + }, + leave: { + animation: 'fade-out', + }, + }, + }); + + violin.render(); + + it('default', () => { + // 新的 geometry violin 暂时未追加 + expect(violin.chart.views[0].geometries[0].animateOption).toEqual({ + enter: { + animation: 'fade-in', + }, + leave: { + animation: 'fade-out', + }, + }); + + // 追加默认的动画配置 + expect(violin.chart.views[1].geometries[0].animateOption).toMatchObject({ + appear: { + duration: 450, + easing: 'easeQuadOut', + }, + update: { + duration: 400, + easing: 'easeQuadInOut', + }, + enter: { + duration: 400, + easing: 'easeQuadInOut', + animation: 'fade-in', + }, + leave: { + duration: 350, + easing: 'easeQuadIn', + animation: 'fade-out', + }, + }); + }); + + it('update', () => { + violin.update({ + animation: { + appear: { + animation: 'fade-in', + }, + leave: { + animation: 'wave-out', + }, + }, + }); + expect(violin.chart.views[0].geometries[0].animateOption).toEqual({ + appear: { + animation: 'fade-in', + }, + enter: { + animation: 'fade-in', + }, + leave: { + animation: 'wave-out', + }, + }); + }); + + afterAll(() => { + violin.destroy(); + }); +}); diff --git a/__tests__/unit/plots/violin/annotation-spec.ts b/__tests__/unit/plots/violin/annotation-spec.ts new file mode 100644 index 0000000000..7f3ca86dce --- /dev/null +++ b/__tests__/unit/plots/violin/annotation-spec.ts @@ -0,0 +1,65 @@ +import { Violin } from '../../../../src'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin legend', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + + it('text annotation', () => { + violin.update({ + annotations: [ + { + type: 'text', + position: ['median', 'median'], + content: '辅助文本', + }, + ], + }); + + const controller = violin.chart.views[0].getController('annotation'); + expect(controller.getComponents().length).toBe(1); + expect(controller.getComponents()[0].component.get('content')).toBe('辅助文本'); + }); + + it('text annotation and line annotation', () => { + violin.update({ + ...violin.options, + annotations: [ + { + type: 'text', + position: ['min', 'median'], + content: '辅助文本', + offsetY: -4, + style: { + textBaseviolin: 'bottom', + }, + }, + { + type: 'line', + start: ['min', 'median'], + end: ['max', 'median'], + style: { + stroke: 'red', + violinDash: [2, 2], + }, + }, + ], + }); + const controller = violin.chart.views[0].getController('annotation'); + expect(controller.getComponents().length).toBe(2); + expect(controller.getComponents()[0].component.get('content')).toBe('辅助文本'); + expect(controller.getComponents()[1].component.get('type')).toBe('line'); + }); + + afterAll(() => { + violin.destroy(); + }); +}); diff --git a/__tests__/unit/plots/violin/axis-spec.ts b/__tests__/unit/plots/violin/axis-spec.ts new file mode 100644 index 0000000000..3d93a6c0bc --- /dev/null +++ b/__tests__/unit/plots/violin/axis-spec.ts @@ -0,0 +1,65 @@ +import { Violin } from '../../../../src'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin axis', () => { + it('没有 seriesField', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + + let axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents().length).toBe(4); + + violin.update({ xAxis: { grid: null } }); + axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents().length).toBe(3); + + violin.update({ xAxis: { title: { text: 'xx' } } }); + axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents()[0].component.get('title').text).toBe('xx'); + + // 关闭 xAxis + violin.update({ xAxis: false }); + axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents().length).toBe(2); + + violin.destroy(); + }); + + it('有 seriesField', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + seriesField: 'species', + }); + + violin.render(); + let axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents().length).toBe(4); + + violin.update({ xAxis: { grid: null } }); + axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents().length).toBe(3); + + violin.update({ xAxis: { title: { text: 'xx' } } }); + axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents()[0].component.get('title').text).toBe('xx'); + + // 关闭 xAxis + violin.update({ xAxis: false }); + axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents().length).toBe(2); + + violin.destroy(); + }); +}); diff --git a/__tests__/unit/plots/violin/box-spec.ts b/__tests__/unit/plots/violin/box-spec.ts new file mode 100644 index 0000000000..67d0b1df42 --- /dev/null +++ b/__tests__/unit/plots/violin/box-spec.ts @@ -0,0 +1,50 @@ +import { Violin } from '../../../../src'; +import { MIN_MAX_VIEW_ID, QUANTILE_VIEW_ID, MEDIAN_VIEW_ID } from '../../../../src/plots/violin/constant'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin', () => { + it('renders box views.', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + const minMaxView = violin.chart.views.find((view) => view.id === MIN_MAX_VIEW_ID); + const quantileView = violin.chart.views.find((view) => view.id === QUANTILE_VIEW_ID); + const medianView = violin.chart.views.find((view) => view.id === MEDIAN_VIEW_ID); + + expect(minMaxView.geometries[0].type).toBe('interval'); + expect(quantileView.geometries[0].type).toBe('interval'); + expect(medianView.geometries[0].type).toBe('point'); + + violin.destroy(); + }); + + // 暂时不开放 box 配置 + it("should not render box views when 'box' set to false.", () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + box: false, + }); + + violin.render(); + const minMaxView = violin.chart.views.find((view) => view.id === MIN_MAX_VIEW_ID); + const quantileView = violin.chart.views.find((view) => view.id === QUANTILE_VIEW_ID); + const medianView = violin.chart.views.find((view) => view.id === MEDIAN_VIEW_ID); + + expect(minMaxView).toBeUndefined(); + expect(quantileView).toBeUndefined(); + expect(medianView).toBeUndefined(); + + violin.destroy(); + }); +}); diff --git a/__tests__/unit/plots/violin/change-data-spec.ts b/__tests__/unit/plots/violin/change-data-spec.ts new file mode 100644 index 0000000000..5e150c3e17 --- /dev/null +++ b/__tests__/unit/plots/violin/change-data-spec.ts @@ -0,0 +1,30 @@ +import { group } from '@antv/util'; +import { Violin } from '../../../../src'; +import { VIOLIN_VIEW_ID } from '../../../../src/plots/violin/constant'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin change data', () => { + it('renders new violins when data changed', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + const g = violin.chart.views.find((view) => view.id === VIOLIN_VIEW_ID).geometries[0]; + expect(g.elements.length).toBe(group(BASE_VIOLIN_DATA, 'type').length); + + const newData = BASE_VIOLIN_DATA.filter((data) => data.type !== 'PetalWidth'); + + violin.changeData(newData); + const newG = violin.chart.views.find((view) => view.id === VIOLIN_VIEW_ID).geometries[0]; + expect(violin.options.data).toEqual(newData); + expect(newG.elements.length).toBe(group(newData, 'type').length); + + violin.destroy(); + }); +}); diff --git a/__tests__/unit/plots/violin/index-spec.ts b/__tests__/unit/plots/violin/index-spec.ts new file mode 100644 index 0000000000..b44abaf4c7 --- /dev/null +++ b/__tests__/unit/plots/violin/index-spec.ts @@ -0,0 +1,117 @@ +import { group } from '@antv/util'; +import { Violin } from '../../../../src'; +import { + DEFAULT_OPTIONS, + MEDIAN_FIELD, + MIN_MAX_FIELD, + QUANTILE_FIELD, + VIOLIN_VIEW_ID, + VIOLIN_Y_FIELD, + X_FIELD, +} from '../../../../src/plots/violin/constant'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin', () => { + it("renders N violins, where N equals to xField's length.", () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + expect(violin.type).toBe('violin'); + // @ts-ignore + expect(violin.getDefaultOptions()).toEqual(Violin.getDefaultOptions()); + + const g = violin.chart.views.find((view) => view.id === VIOLIN_VIEW_ID).geometries[0]; + + // 个数 + expect(g.elements.length).toBe(group(BASE_VIOLIN_DATA, 'type').length); + // 类型 + expect(g.type).toBe('violin'); + + violin.destroy(); + }); + + it("renders N violins, where N equals to (xField * sierisField)'s length.", () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + seriesField: 'species', + }); + + violin.render(); + const g = violin.chart.views.find((view) => view.id === VIOLIN_VIEW_ID).geometries[0]; + + // 个数 + expect(g.elements.length).toBe(group(BASE_VIOLIN_DATA, ['type', 'species']).length); + // 类型 + expect(g.type).toBe('violin'); + + violin.destroy(); + }); + + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + xAxis: { + tickCount: 5, + }, + yAxis: { + tickCount: 7, + // fixme 设置 min 为 10 不生效 + // min: 10, + minLimit: 10, + max: 20, + }, + }); + + violin.render(); + + it('axis & meta', () => { + const geometry = violin.chart.views[0].geometries[0]; + const geometry1 = violin.chart.views[1].geometries[0]; + const geometry2 = violin.chart.views[2].geometries[0]; + const geometry3 = violin.chart.views[3].geometries[0]; + expect(geometry.scales[X_FIELD].tickCount).toBe(5); + expect(geometry.scales[VIOLIN_Y_FIELD].min).toBe(10); + expect(geometry.scales[VIOLIN_Y_FIELD].max).toBe(20); + expect(geometry.scales[VIOLIN_Y_FIELD].tickCount).toBe(7); + + expect(geometry1.scales[MIN_MAX_FIELD].min).toBe(10); + expect(geometry1.scales[MIN_MAX_FIELD].max).toBe(20); + expect(geometry1.scales[MIN_MAX_FIELD].tickCount).toBe(7); + // @ts-ignore + expect(geometry1.scales[MIN_MAX_FIELD].sync).toBe(VIOLIN_Y_FIELD); + + expect(geometry2.scales[QUANTILE_FIELD].min).toBe(10); + expect(geometry2.scales[QUANTILE_FIELD].max).toBe(20); + expect(geometry2.scales[QUANTILE_FIELD].tickCount).toBe(7); + // @ts-ignore + expect(geometry2.scales[QUANTILE_FIELD].sync).toBe(VIOLIN_Y_FIELD); + + expect(geometry3.scales[MEDIAN_FIELD].min).toBe(10); + expect(geometry3.scales[MEDIAN_FIELD].max).toBe(20); + expect(geometry3.scales[MEDIAN_FIELD].tickCount).toBe(7); + // @ts-ignore + expect(geometry3.scales[MEDIAN_FIELD].sync).toBe(VIOLIN_Y_FIELD); + }); + + it('defaultOptions 保持从 constants 中获取', () => { + expect(Violin.getDefaultOptions()).toEqual(DEFAULT_OPTIONS); + }); + + afterAll(() => { + violin.destroy(); + }); +}); diff --git a/__tests__/unit/plots/violin/legend-spec.ts b/__tests__/unit/plots/violin/legend-spec.ts new file mode 100644 index 0000000000..3455d4ccdb --- /dev/null +++ b/__tests__/unit/plots/violin/legend-spec.ts @@ -0,0 +1,144 @@ +import { Violin } from '../../../../src'; +import { X_FIELD } from '../../../../src/plots/violin/constant'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin legend', () => { + it('没有 seriesField', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + + const legendController = violin.chart.getController('legend'); + const legendComponent = legendController.getComponents()[0].component; + let legendItems = legendComponent.get('items'); + expect(legendItems.length).toBe(4); + + violin.update({ legend: { position: 'left' } }); + expect(legendController.getComponents()[0].direction).toBe('left'); + + // 自定义 legend + violin.update({ + legend: { + custom: true, + position: 'bottom', + items: [ + { + value: '1', + name: '3', + marker: { symbol: 'square', style: { fill: 'red', r: 5 } }, + }, + { + value: '2', + name: '3', + marker: { symbol: 'square', style: { fill: '#000', r: 5 } }, + }, + { + value: '3', + name: '3', + marker: { symbol: 'circle', style: { stroke: '#eee', r: 5 } }, + }, + ], + }, + }); + expect(legendController.getComponents()[0].direction).toBe('bottom'); + legendItems = legendController.getComponents()[0].component.get('items'); + expect(legendItems.length).toBe(3); + expect(legendItems[0].marker.symbol).toBe('square'); + expect(legendItems[0].marker.style.fill).toBe('red'); + expect(legendItems[2].marker.style.stroke).toBe('#eee'); + expect(legendItems[2].marker.symbol).toBe('circle'); + + // 关闭 legend + violin.update({ legend: false }); + expect(legendComponent.get('items')).toBeUndefined(); + + violin.destroy(); + }); + + it('有 seriesField', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + seriesField: 'species', + }); + + violin.render(); + + const legendController = violin.chart.getController('legend'); + const legendComponent = legendController.getComponents()[0].component; + let legendItems = legendComponent.get('items'); + expect(legendItems.length).toBe(3); + + violin.update({ legend: { position: 'left' } }); + expect(legendController.getComponents()[0].direction).toBe('left'); + + // 自定义 legend + violin.update({ + legend: { + custom: true, + position: 'bottom', + items: [ + { + value: '1', + name: '3', + marker: { symbol: 'square', style: { fill: 'red', r: 5 } }, + }, + { + value: '3', + name: '3', + marker: { symbol: 'circle', style: { stroke: '#eee', r: 5 } }, + }, + ], + }, + }); + expect(legendController.getComponents()[0].direction).toBe('bottom'); + legendItems = legendController.getComponents()[0].component.get('items'); + expect(legendItems.length).toBe(2); + expect(legendItems[0].marker.symbol).toBe('square'); + expect(legendItems[0].marker.style.fill).toBe('red'); + expect(legendItems[1].marker.style.stroke).toBe('#eee'); + expect(legendItems[1].marker.symbol).toBe('circle'); + + // 关闭 legend + violin.update({ legend: false }); + expect(legendComponent.get('items')).toBeUndefined(); + + violin.destroy(); + }); + + it('除了 view0, 其他没有图例', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + + violin.chart.views.forEach((view, idx) => { + if (idx !== 0) { + // @ts-ignore + expect(view.options.legends).toBe(false); + } + }); + + violin.update({ legend: { position: 'left', marker: { style: { fill: 'red' } } } }); + // @ts-ignore + expect(violin.chart.options.legends[X_FIELD]).toMatchObject({ + position: 'left', + marker: { style: { fill: 'red' } }, + }); + }); +}); diff --git a/__tests__/unit/plots/violin/tooltip-spec.ts b/__tests__/unit/plots/violin/tooltip-spec.ts new file mode 100644 index 0000000000..a7b23c31ca --- /dev/null +++ b/__tests__/unit/plots/violin/tooltip-spec.ts @@ -0,0 +1,96 @@ +import { Violin } from '../../../../src'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin tooltip', () => { + const div = createDiv(); + const violin = new Violin(div, { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + + it('default', () => { + const box = violin.chart.views[0].geometries[0].elements[0].shape.getBBox(); + const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 }; + + violin.chart.showTooltip(point); + expect(div.querySelectorAll('.g2-tooltip-list-item').length).toBe(5); + violin.chart.hideTooltip(); + }); + + it('tooltip: fields', () => { + violin.update({ + seriesField: 'species', + tooltip: { + fields: ['species', 'high', 'q1', 'q3', 'low', 'median'], + }, + }); + const box = violin.chart.views[0].geometries[0].elements[0].shape.getBBox(); + const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 }; + violin.chart.showTooltip(point); + expect(div.querySelectorAll('.g2-tooltip-list-item').length).toBe(6); + violin.chart.hideTooltip(); + }); + + it('tooltip + meta', () => { + violin.update({ + meta: { + high: { + alias: '最大值', + formatter: (v) => `${v.toFixed(2)} %`, + }, + low: { + alias: '最小值', + formatter: (v) => `${v.toFixed(2)} %`, + }, + species: { + alias: '品类', + }, + }, + }); + + const box = violin.chart.views[0].geometries[0].elements[0].shape.getBBox(); + const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 }; + violin.chart.showTooltip(point); + const items = div.querySelectorAll('.g2-tooltip-name'); + expect((items[0] as HTMLElement).innerText).toBe('品类'); + expect((items[1] as HTMLElement).innerText).toBe('最大值'); + expect((items[4] as HTMLElement).innerText).toBe('最小值'); + expect((div.querySelectorAll('.g2-tooltip-value')[4] as HTMLElement).innerText).toMatch('%'); + violin.chart.hideTooltip(); + }); + + it('tooltip: customContent', () => { + violin.update({ + tooltip: { + customContent: () => '
xxx
', + }, + }); + const box = violin.chart.views[0].geometries[0].elements[0].shape.getBBox(); + const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 }; + violin.chart.showTooltip(point); + expect((div.querySelector('.custom-tooltip') as HTMLElement).innerText).toBe('xxx'); + violin.chart.hideTooltip(); + }); + + it('tooltip: false', () => { + // @ts-ignore + expect(violin.chart.options.tooltip).not.toBe(false); + // @ts-ignore + expect(violin.chart.getController('tooltip').isVisible()).toBe(true); + violin.update({ tooltip: false }); + // @ts-ignore + expect(violin.chart.options.tooltip).toBe(false); + // @ts-ignore + expect(violin.chart.getController('tooltip').isVisible()).toBe(false); + }); + + afterAll(() => { + violin.destroy(); + }); +}); diff --git a/__tests__/unit/plots/violin/utils-spec.ts b/__tests__/unit/plots/violin/utils-spec.ts new file mode 100644 index 0000000000..d4aac2eded --- /dev/null +++ b/__tests__/unit/plots/violin/utils-spec.ts @@ -0,0 +1,67 @@ +import { toBeDeepCloseTo } from 'jest-matcher-deep-close-to'; +import { group } from '@antv/util'; +import { toBoxValue, PdfOptions, toViolinValue, transformViolinData } from '../../../../src/plots/violin/utils'; +import { DEFAULT_OPTIONS } from '../../../../src/plots/violin/constant'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; + +expect.extend({ toBeDeepCloseTo }); + +describe('violin utils', () => { + it('toBoxValue() works correctly.', () => { + // 顺序数据,可口算结果。 + const ordered = [-1, 2, 3, 4, 4.4, 5, 8, 9, 100]; + // 随机打乱,不应影响计算结果。 + const shuffled = ordered.slice().sort(() => Math.random() - 0.5); + expect(toBoxValue(shuffled)).toEqual({ + high: 100, + low: -1, + q1: 3, + q3: 8, + minMax: [-1, 100], + quantile: [3, 8], + median: [4.4], + }); + }); + + it('toViolinValue() works correctly.', () => { + const options: PdfOptions = { + min: -1, + max: 10, + size: 5, + width: 2, + }; + // 顺序数据,可笔算结果。 + const ordered = [-1, 2, 3, 4, 4.4, 5, 8, 9, 10]; + // 随机打乱,不应影响计算结果。 + const shuffled = ordered.slice().sort(() => Math.random() - 0.5); + // @ts-ignore + expect(toViolinValue(shuffled, options)).toBeDeepCloseTo( + { + violinY: [-1 + (11 / 4) * 0, -1 + (11 / 4) * 1, -1 + (11 / 4) * 2, -1 + (11 / 4) * 3, -1 + (11 / 4) * 4], + violinSize: [ + 0.18518518518518517, 0.287037037037037, 0.22222222222222218, 0.17592592592592587, 0.12962962962962957, + ], + }, + 0.0001 + ); + }); + + it('transformViolinData() results in correct length.', () => { + const dataWithoutSeries = transformViolinData({ + ...DEFAULT_OPTIONS, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + const dataWithSeries = transformViolinData({ + ...DEFAULT_OPTIONS, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + seriesField: 'species', + }); + + expect(dataWithoutSeries.length).toBe(group(BASE_VIOLIN_DATA, ['type']).length); + expect(dataWithSeries.length).toBe(group(BASE_VIOLIN_DATA, ['type', 'species']).length); + }); +}); diff --git a/__tests__/unit/plots/violin/violin-shape-spec.ts b/__tests__/unit/plots/violin/violin-shape-spec.ts new file mode 100644 index 0000000000..93b19c569e --- /dev/null +++ b/__tests__/unit/plots/violin/violin-shape-spec.ts @@ -0,0 +1,40 @@ +import { Violin } from '../../../../src'; +import { VIOLIN_VIEW_ID } from '../../../../src/plots/violin/constant'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +const getViolinShapeType = (violin: Violin) => { + const g = violin.chart.views.find((view) => view.id === VIOLIN_VIEW_ID).geometries[0]; + // @ts-ignore shapeType 是私有属性 + return g.elements[0].shapeType; +}; + +describe('violin', () => { + it('renders hollow/smooth violins determined by options.', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + // Defaults to { smooth: true, hollow: false } + violin.render(); + expect(getViolinShapeType(violin)).toBe('violin'); + + violin.update({ shape: 'smooth' }); + violin.render(); + expect(getViolinShapeType(violin)).toBe('smooth'); + + violin.update({ shape: 'hollow' }); + violin.render(); + expect(getViolinShapeType(violin)).toBe('hollow'); + + violin.update({ shape: 'hollow-smooth' }); + violin.render(); + expect(getViolinShapeType(violin)).toBe('hollow-smooth'); + + violin.destroy(); + }); +}); diff --git a/__tests__/unit/utils/transform/quantile-spec.ts b/__tests__/unit/utils/transform/quantile-spec.ts new file mode 100644 index 0000000000..68f095601b --- /dev/null +++ b/__tests__/unit/utils/transform/quantile-spec.ts @@ -0,0 +1,50 @@ +import { quantile, quantileSorted, quickselect, swap } from '../../../../src/utils/transform/quantile'; + +describe('quantile', () => { + it('quantile-sorted', () => { + expect(quantileSorted([3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 0.5)).toBe(9); + }); + + it('quick-select', () => { + const arr = [65, 28, 59, 33, 21, 56, 22, 95, 50, 12, 90, 53, 28, 77, 39]; + quickselect(arr, 8); + expect(arr).toEqual([39, 28, 28, 33, 21, 12, 22, 50, 53, 56, 59, 65, 90, 77, 95]); + }); + + it('quantile', () => { + expect(quantile([3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 0.5)).toBe(9); // => 9 + + // from https://github.com/simple-statistics + const even = [3, 6, 7, 8, 8, 10, 13, 15, 16, 20]; + expect(quantile(even, 0.25)).toBe(7); + expect(quantile(even, 0.5)).toBe(9); + expect(quantile(even, 0.75)).toBe(15); + + const odd = [3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20]; + expect(quantile(odd, 0.25)).toBe(7); + expect(quantile(odd, 0.5)).toBe(9); + expect(quantile(odd, 0.75)).toBe(15); + }); + + it('quantile: throw error', () => { + const testEmptyArray = () => quantile([], 0.5); + expect(testEmptyArray).toThrowError(); + + const testBadBounds = () => quantile([1, 2, 3], 1.1); + expect(testBadBounds).toThrowError(); + }); + + it('quantile: can get an array of quantiles on a small number of elements', () => { + const input = [500, 468, 454, 469]; + expect(quantile(input, [0.25, 0.5, 0.75])).toEqual([461, 468.5, 484.5]); + expect(quantile(input, [0.05, 0.25, 0.5, 0.75, 0.95])).toEqual([454, 461, 468.5, 484.5, 500]); + }); + + it('swap', () => { + const arr = [1, 2, 3]; + swap(arr, 0, 2); + expect(arr).toEqual([3, 2, 1]); + swap(arr, 0, 2); + expect(arr).toEqual([1, 2, 3]); + }); +}); diff --git a/docs/api/plots/violin.en.md b/docs/api/plots/violin.en.md new file mode 100644 index 0000000000..c5da807e6b --- /dev/null +++ b/docs/api/plots/violin.en.md @@ -0,0 +1,102 @@ +--- +title: Violin +order: 31 +--- + +### Plot Container + +`markdown:docs/common/chart-options.en.md` + +### Data Mapping + +#### data + +**required** _array object_ + +Configure the data source. The data source is a collection of objects. For example:`[{ time: '1991',value: 20 }, { time: '1992',value: 20 }]`。 + +`markdown:docs/common/xy-field.en.md` + +#### seriesField + +**optional** _string_ + +Grouping field. It is used for grouping by default, and color is used as visual channel. +Outlier field. + +#### kde + +**optional** _object_ + +Options to generate Kernel Density Estimation. Currently only triangular kernel was supported. + +```ts +type KdeOptions = { + /** Triangular kernel */ + type: 'triangular'; + /** Min value for the kde's x range. Defaults to smallest value minus some threshold. */ + min?: number; + /** Max value for the kde's x range. Defaults to largest value plus some threshold. */ + max?: number; + /** Number of points to represent the kde. Defaults to 32. */ + sampleSize?: number; + /** Bandwith of the triangular kernel. Defaults to 3. */ + width?: number; +}; +``` + +#### meta + +`markdown:docs/common/meta.en.md` + +小提琴图内置箱线图配置。箱线图的统计数据分别为: + +- high: 数据中的最大值,作为箱线图的最高点; +- low: 数据中的最小值,作为箱线图的最低点; +- q3: 上四分位,即 25% 的数据大于该数,作为箱线图中箱子的高点; +- q1: 下四分位,即 25% 的数据小于该数,作为箱线图中箱子的低点; +- median: 数据的中位数,在箱线图中用圆点表示。 + +可以通过 `meta` 来设置字段的元信息 + + + +### Graphic Style + +#### box + +**optional** _boolean_ + +Whether to show box plot. Default show box plot, you could hide box plot by setting `box: false`. + +#### shape + +**optional** _'smooth'|'hollow'|'hollow-smooth'_ + +The shape of violin geometry. Could be 'smooth', 'hollow' or 'hollow-smooth'. Defaults to rough, solid voilins. + +#### violinStyle + +**optional** _StyleAttr | Function_ + +Violin graphic style. + +`markdown:docs/common/shape-style.en.md` + +`markdown:docs/common/color.en.md` + +### Plot Components + +`markdown:docs/common/common-component.en.md` + +### Plot Event + +`markdown:docs/common/events.en.md` + +### Plot Method + +`markdown:docs/common/chart-methods.en.md` + +### Plot Theme + +`markdown:docs/common/theme.en.md` diff --git a/docs/api/plots/violin.zh.md b/docs/api/plots/violin.zh.md new file mode 100644 index 0000000000..9a87438af6 --- /dev/null +++ b/docs/api/plots/violin.zh.md @@ -0,0 +1,106 @@ +--- +title: 小提琴图 +order: 31 +--- + +### 图表容器 + +`markdown:docs/common/chart-options.zh.md` + +### 数据映射 + +#### data + +**required** _array object_ + +设置图表数据源。数据源为对象集合,例如:`[{ time: '1991',value: 20 }, { time: '1992',value: 20 }]`。 + +`markdown:docs/common/xy-field.zh.md` + +#### seriesField + +**optional** _string_ + +分组拆分字段,默认是分组情况,颜色作为视觉通道。 + +#### kde + +**optional** _object_ + +计算小提琴轮廓线(核密度估计)的核函数算法配置。目前只支持三角核函数。 + +```ts +type KdeOptions = { + /** 三角波类型 */ + type: 'triangular'; + /** 最小值,默认为数据中的最小值减去一个固定的阈值。 */ + min?: number; + /** 最大值,默认为数据中的最大值加上一个固定的阈值。 */ + max?: number; + /** 采样数量,越大轮廓线越接近真实概率分布函数,默认32。 */ + sampleSize?: number; + /** 核函数的带宽。带宽越大产生的曲线越平滑(越模糊),带宽越小产生的曲线越陡峭。默认3。 */ + width?: number; +}; +``` + +#### meta + +`markdown:docs/common/meta.zh.md` + +小提琴图内置箱线图配置。箱线图的统计数据分别为: + +- high: 数据中的最大值,作为箱线图的最高点; +- low: 数据中的最小值,作为箱线图的最低点; +- q3: 上四分位,即 25% 的数据大于该数,作为箱线图中箱子的高点; +- q1: 下四分位,即 25% 的数据小于该数,作为箱线图中箱子的低点; +- median: 数据的中位数,在箱线图中用圆点表示。 + +可以通过 `meta` 来设置字段的元信息 + + + +`markdown:docs/common/color.zh.md` + + +### 图形样式 + +#### box + +**optional** _boolean_ + +是否展示内部箱线图。默认展示,设置为 'false' 关闭箱线图。 + +#### shape + +**optional** _'smooth'|'hollow'|'hollow-smooth'_ + +小提琴形状。 +* 默认: 非平滑、实心 +* smooth: 平滑 +* hollow: 空心 +* hollow-smooth: 平滑、空心 + +#### violinStyle + +**optional** _StyleAttr | Function_ + +小提琴轮廓样式配置。 + +`markdown:docs/common/shape-style.zh.md` + +### 图表组件 + +`markdown:docs/common/common-component.zh.md` + +### 图表事件 + +`markdown:docs/common/events.zh.md` + +### 图表方法 + +`markdown:docs/common/chart-methods.zh.md` + +### 图表主题 + +`markdown:docs/common/theme.zh.md` diff --git a/docs/common/common-component.en.md b/docs/common/common-component.en.md new file mode 100644 index 0000000000..54c047dfcf --- /dev/null +++ b/docs/common/common-component.en.md @@ -0,0 +1,25 @@ +#### axis + +xAxis、yAxis 配置相同。**注意**:由于 DualAxes(双轴图) 和 BidirectionalBar(对称条形图) 是双 y 轴, yAxis 类型是以 yField 中的字段作为 `key` 值的`object`。 + +`markdown:docs/common/axis.en.md` + +#### legend + +`markdown:docs/common/legend.en.md` + + +#### label + +> Label configuration is not supported in violin,you can use `annnotation` to replace it. + +`markdown:docs/common/label.en.md` + + +#### tooltip + +`markdown:docs/common/tooltip.en.md` + +#### annotations + +`markdown:docs/common/annotations.en.md` diff --git a/docs/common/common-component.zh.md b/docs/common/common-component.zh.md new file mode 100644 index 0000000000..c19f15a3b3 --- /dev/null +++ b/docs/common/common-component.zh.md @@ -0,0 +1,25 @@ +#### axis + +xAxis、yAxis 配置相同。**注意**:由于 DualAxes(双轴图) 和 BidirectionalBar(对称条形图) 是双 y 轴, yAxis 类型是以 yField 中的字段作为 `key` 值的`object`。 + +`markdown:docs/common/axis.zh.md` + +#### legend + +`markdown:docs/common/legend.zh.md` + + +#### label + +> 小提琴图暂时不支持 label 展示,可以使用 annnotation 进行替代 + +`markdown:docs/common/label.zh.md` + + +#### tooltip + +`markdown:docs/common/tooltip.zh.md` + +#### annotations + +`markdown:docs/common/annotations.zh.md` diff --git a/docs/common/component.en.md b/docs/common/component.en.md index ff86859164..78cd1c64b1 100644 --- a/docs/common/component.en.md +++ b/docs/common/component.en.md @@ -1,24 +1,4 @@ -#### tooltip - -`markdown:docs/common/tooltip.en.md` - -#### label - -`markdown:docs/common/label.en.md` - -#### axis - -Same for xAxis and yAxis. **Note**: Since `DualAxes` or `BidirectionalBar` has double Y-axes, the yAxis is a object which takes the field in yField as the 'key'. - -`markdown:docs/common/axis.en.md` - -#### legend - -`markdown:docs/common/legend.en.md` - -#### annotations - -`markdown:docs/common/annotations.en.md` +`markdown:docs/common/common-component.en.md` #### slider diff --git a/docs/common/component.zh.md b/docs/common/component.zh.md index b215cfb4bc..d9606b9291 100644 --- a/docs/common/component.zh.md +++ b/docs/common/component.zh.md @@ -1,24 +1,4 @@ -#### tooltip - -`markdown:docs/common/tooltip.zh.md` - -#### label - -`markdown:docs/common/label.zh.md` - -#### axis - -xAxis、yAxis 配置相同。**注意**:由于 DualAxes(双轴图) 和 BidirectionalBar(对称条形图) 是双 y 轴, yAxis 类型是以 yField 中的字段作为 `key` 值的`object`。 - -`markdown:docs/common/axis.zh.md` - -#### legend - -`markdown:docs/common/legend.zh.md` - -#### annotations - -`markdown:docs/common/annotations.zh.md` +`markdown:docs/common/common-component.zh.md` #### slider diff --git a/examples/more-plots/violin/API.en.md b/examples/more-plots/violin/API.en.md new file mode 100644 index 0000000000..c2fe954f72 --- /dev/null +++ b/examples/more-plots/violin/API.en.md @@ -0,0 +1 @@ +`markdown:docs/api/plots/violin.en.md` \ No newline at end of file diff --git a/examples/more-plots/violin/API.zh.md b/examples/more-plots/violin/API.zh.md new file mode 100644 index 0000000000..b2f7dec1f1 --- /dev/null +++ b/examples/more-plots/violin/API.zh.md @@ -0,0 +1 @@ +`markdown:docs/api/plots/violin.zh.md` \ No newline at end of file diff --git a/examples/more-plots/violin/demo/basic.ts b/examples/more-plots/violin/demo/basic.ts new file mode 100644 index 0000000000..c702f84866 --- /dev/null +++ b/examples/more-plots/violin/demo/basic.ts @@ -0,0 +1,13 @@ +import { Violin } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/bmw-prod/6b0a5f1d-5931-42ae-b3ba-3c3cb77d0861.json') + .then((response) => response.json()) + .then((data) => { + const violinPlot = new Violin('container', { + height: 500, + data: data, + xField: 'x', + yField: 'y', + }); + violinPlot.render(); + }); diff --git a/examples/more-plots/violin/demo/group.ts b/examples/more-plots/violin/demo/group.ts new file mode 100644 index 0000000000..8bdba98026 --- /dev/null +++ b/examples/more-plots/violin/demo/group.ts @@ -0,0 +1,14 @@ +import { Violin } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/bmw-prod/6b0a5f1d-5931-42ae-b3ba-3c3cb77d0861.json') + .then((response) => response.json()) + .then((data) => { + const violinPlot = new Violin('container', { + height: 500, + data: data, + xField: 'x', + yField: 'y', + seriesField: 'species', + }); + violinPlot.render(); + }); diff --git a/examples/more-plots/violin/demo/meta.json b/examples/more-plots/violin/demo/meta.json new file mode 100644 index 0000000000..63035c0d19 --- /dev/null +++ b/examples/more-plots/violin/demo/meta.json @@ -0,0 +1,44 @@ +{ + "title": { + "zh": "小提琴图", + "en": "Violin" + }, + "demos": [ + { + "filename": "basic.ts", + "title": { + "zh": "基础小提琴图", + "en": "Basic violin plot" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*mBJ1Rqh72ScAAAAAAAAAAAAAARQnAQ", + "new": true + }, + { + "filename": "group.ts", + "title": { + "zh": "分组小提琴图", + "en": "Grouped violin plot" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*eb3AQ6CiBLwAAAAAAAAAAAAAARQnAQ", + "new": true + }, + { + "filename": "shape.ts", + "title": { + "zh": "平滑/空心小提琴图", + "en": "Smooth/Hollow violin plot" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*0nOgSohp4QAAAAAAAAAAAAAAARQnAQ", + "new": true + }, + { + "filename": "tooltip.ts", + "title": { + "zh": "自定义Tooltip文案", + "en": "Customize tooltip texts" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*IsxHQYqkeEsAAAAAAAAAAAAAARQnAQ", + "new": true + } + ] +} \ No newline at end of file diff --git a/examples/more-plots/violin/demo/shape.ts b/examples/more-plots/violin/demo/shape.ts new file mode 100644 index 0000000000..d7ef6777d5 --- /dev/null +++ b/examples/more-plots/violin/demo/shape.ts @@ -0,0 +1,14 @@ +import { Violin } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/bmw-prod/6b0a5f1d-5931-42ae-b3ba-3c3cb77d0861.json') + .then((response) => response.json()) + .then((data) => { + const violinPlot = new Violin('container', { + height: 500, + data: data, + xField: 'x', + yField: 'y', + shape: 'hollow-smooth', // or 'hollow', 'smooth' + }); + violinPlot.render(); + }); diff --git a/examples/more-plots/violin/demo/tooltip.ts b/examples/more-plots/violin/demo/tooltip.ts new file mode 100644 index 0000000000..968ffab671 --- /dev/null +++ b/examples/more-plots/violin/demo/tooltip.ts @@ -0,0 +1,39 @@ +import { Violin } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/bmw-prod/6b0a5f1d-5931-42ae-b3ba-3c3cb77d0861.json') + .then((response) => response.json()) + .then((data) => { + const violinPlot = new Violin('container', { + width: 400, + height: 500, + data: data, + xField: 'x', + yField: 'y', + seriesField: 'species', + meta: { + high: { + alias: '最大值', + formatter: (v) => `${v.toFixed(2)} %`, + }, + low: { + alias: '最小值', + formatter: (v) => `${v.toFixed(2)} %`, + }, + q1: { + alias: '上四分位数', + formatter: (v) => `${v.toFixed(2)} %`, + }, + q3: { + alias: '下四分位数', + formatter: (v) => `${v.toFixed(2)} %`, + }, + species: { + alias: '品类', + }, + }, + tooltip: { + fields: ['species', 'high', 'q1', 'q3', 'low'], + }, + }); + violinPlot.render(); + }); diff --git a/examples/more-plots/violin/index.en.md b/examples/more-plots/violin/index.en.md new file mode 100644 index 0000000000..2dfefebaf0 --- /dev/null +++ b/examples/more-plots/violin/index.en.md @@ -0,0 +1,4 @@ +--- +title: Violin +order: 11 +--- diff --git a/examples/more-plots/violin/index.zh.md b/examples/more-plots/violin/index.zh.md new file mode 100644 index 0000000000..ab69780e9b --- /dev/null +++ b/examples/more-plots/violin/index.zh.md @@ -0,0 +1,4 @@ +--- +title: 小提琴图 +order: 11 +--- diff --git a/package.json b/package.json index 719e1e92ab..318ccac52d 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@antv/g2": "^4.1.0", "d3-hierarchy": "^2.0.0", "d3-regression": "^1.3.5", + "pdfast": "^0.2.0", "size-sensor": "^1.0.1", "tslib": "^2.0.3" }, @@ -88,6 +89,7 @@ "jest": "^26.0.1", "jest-electron": "^0.1.7", "jest-extended": "^0.11.2", + "jest-matcher-deep-close-to": "^2.0.1", "limit-size": "^0.1.3", "lint-md-cli": "^0.1.2", "lint-staged": "^10.0.7", @@ -136,7 +138,7 @@ "limit-size": [ { "path": "dist/g2plot.min.js", - "limit": "900 Kb" + "limit": "910 Kb" }, { "path": "dist/g2plot.min.js", diff --git a/src/adaptor/geometries/index.ts b/src/adaptor/geometries/index.ts index 35201828de..1cc9132da6 100644 --- a/src/adaptor/geometries/index.ts +++ b/src/adaptor/geometries/index.ts @@ -12,3 +12,5 @@ export { edge } from './edge'; export type { EdgeGeometryOptions } from './edge'; export { schema } from './schema'; export type { SchemaGeometryOptions } from './schema'; +export { violin } from './violin'; +export type { ViolinGeometryOptions } from './violin'; diff --git a/src/adaptor/geometries/violin.ts b/src/adaptor/geometries/violin.ts new file mode 100644 index 0000000000..5476ad7610 --- /dev/null +++ b/src/adaptor/geometries/violin.ts @@ -0,0 +1,44 @@ +import { Params } from '../../core/adaptor'; +import { getTooltipMapping } from '../../utils/tooltip'; +import { deepAssign } from '../../utils'; +import { geometry, MappingOptions, GeometryOptions } from './base'; + +export interface ViolinGeometryOptions extends GeometryOptions { + /** x 轴字段 */ + readonly xField?: string; + /** y 轴字段(指小提琴的Y轴,即概率密度) */ + readonly yField?: string; + /** 分组字段 */ + readonly seriesField?: string; + /** size 映射字段 */ + readonly sizeField?: string; + /** violin 图形映射规则 */ + readonly violin?: MappingOptions; +} + +/** + * violin 辅助点的配置处理 + * @param params + */ +export function violin(params: Params): Params { + const { options } = params; + const { violin, xField, yField, seriesField, sizeField, tooltip } = options; + + const { fields, formatter } = getTooltipMapping(tooltip, [xField, yField, seriesField, sizeField]); + + return violin + ? geometry( + deepAssign({}, params, { + options: { + type: 'violin', + colorField: seriesField, + tooltipFields: fields, + mapping: { + tooltip: formatter, + ...violin, + }, + }, + }) + ) + : params; +} diff --git a/src/index.ts b/src/index.ts index 8dda129d9d..7752203247 100644 --- a/src/index.ts +++ b/src/index.ts @@ -85,6 +85,10 @@ export type { HeatmapOptions } from './plots/heatmap'; export { Box } from './plots/box'; export type { BoxOptions } from './plots/box'; +// 小提琴图及类型定义 +export { Violin } from './plots/violin'; +export type { ViolinOptions } from './plots/violin'; + // K线图及类型定义 | author by [jhwong](https://github.com/jinhuiWong) export { Stock } from './plots/stock'; export type { StockOptions } from './plots/stock'; diff --git a/src/plots/violin/adaptor.ts b/src/plots/violin/adaptor.ts new file mode 100644 index 0000000000..1e6144c2ea --- /dev/null +++ b/src/plots/violin/adaptor.ts @@ -0,0 +1,310 @@ +import { Geometry } from '@antv/g2'; +import { get, set, omit, each } from '@antv/util'; +import { Params } from '../../core/adaptor'; +import { interaction, theme, tooltip, annotation as baseAnnotation } from '../../adaptor/common'; +import { interval, point, violin } from '../../adaptor/geometries'; +import { flow, pick, deepAssign, findViewById } from '../../utils'; +import { AXIS_META_CONFIG_KEYS } from '../../constant'; +import { ViolinOptions } from './types'; +import { transformViolinData } from './utils'; +import { + MEDIAN_FIELD, + MEDIAN_VIEW_ID, + MIN_MAX_FIELD, + MIN_MAX_VIEW_ID, + QUANTILE_FIELD, + QUANTILE_VIEW_ID, + VIOLIN_SIZE_FIELD, + VIOLIN_VIEW_ID, + VIOLIN_Y_FIELD, + X_FIELD, +} from './constant'; + +const TOOLTIP_FIELDS = ['low', 'high', 'q1', 'q3', 'median']; + +const adjustCfg = [ + { + type: 'dodge', + marginRatio: 1 / 32, + } as const, +]; + +/** 处理数据 */ +function data(params: Params): Params { + const { chart, options } = params; + chart.data(transformViolinData(options)); + return params; +} + +/** 小提琴轮廓 */ +function violinView(params: Params): Params { + const { chart, options } = params; + const { seriesField, color, shape = 'violin', violinStyle, tooltip } = options; + + const view = chart.createView({ id: VIOLIN_VIEW_ID }); + violin({ + chart: view, + options: { + xField: X_FIELD, + yField: VIOLIN_Y_FIELD, + seriesField: seriesField ? seriesField : X_FIELD, + sizeField: VIOLIN_SIZE_FIELD, + tooltip: { + fields: TOOLTIP_FIELDS, + ...tooltip, + }, + violin: { + style: violinStyle, + color, + shape, + }, + }, + }); + view.geometries[0].adjust(adjustCfg); + + return params; +} + +/** 箱线 */ +function boxView(params: Params): Params { + const { chart, options } = params; + const { seriesField, color, tooltip, box } = options; + + // 如果配置 `box` 为 false ,不渲染内部箱线图 + if (box === false) return params; + + // 边缘线 + const minMaxView = chart.createView({ id: MIN_MAX_VIEW_ID }); + interval({ + chart: minMaxView, + options: { + xField: X_FIELD, + yField: MIN_MAX_FIELD, + seriesField: seriesField ? seriesField : X_FIELD, + tooltip: { + fields: TOOLTIP_FIELDS, + ...tooltip, + }, + interval: { + color, + size: 1, + style: { + lineWidth: 0, + }, + }, + }, + }); + minMaxView.geometries[0].adjust(adjustCfg); + + // 四分点位 + const quantileView = chart.createView({ id: QUANTILE_VIEW_ID }); + interval({ + chart: quantileView, + options: { + xField: X_FIELD, + yField: QUANTILE_FIELD, + seriesField: seriesField ? seriesField : X_FIELD, + tooltip: { + fields: TOOLTIP_FIELDS, + ...tooltip, + }, + interval: { + color, + size: 8, + style: { + fillOpacity: 1, + }, + }, + }, + }); + quantileView.geometries[0].adjust(adjustCfg); + + // 中位值 + const medianView = chart.createView({ id: MEDIAN_VIEW_ID }); + point({ + chart: medianView, + options: { + xField: X_FIELD, + yField: MEDIAN_FIELD, + seriesField: seriesField ? seriesField : X_FIELD, + tooltip: { + fields: TOOLTIP_FIELDS, + ...tooltip, + }, + point: { + color, + size: 1, + style: { + fill: 'white', + lineWidth: 0, + }, + }, + }, + }); + medianView.geometries[0].adjust(adjustCfg); + + // 关闭辅助 view 的轴 + quantileView.axis(false); + minMaxView.axis(false); + medianView.axis(false); + + // 关闭辅助 view 的图例 + medianView.legend(false); + minMaxView.legend(false); + quantileView.legend(false); + + return params; +} + +/** + * meta 配置 + */ +function meta(params: Params): Params { + const { chart, options } = params; + const { meta, xAxis, yAxis } = options; + + const baseMeta = {}; + + const scales = deepAssign(baseMeta, meta, { + [X_FIELD]: { + sync: true, + ...pick(xAxis, AXIS_META_CONFIG_KEYS), + }, + [VIOLIN_Y_FIELD]: { + sync: true, + ...pick(yAxis, AXIS_META_CONFIG_KEYS), + }, + [MIN_MAX_FIELD]: { + sync: VIOLIN_Y_FIELD, + ...pick(yAxis, AXIS_META_CONFIG_KEYS), + }, + [QUANTILE_FIELD]: { + sync: VIOLIN_Y_FIELD, + ...pick(yAxis, AXIS_META_CONFIG_KEYS), + }, + [MEDIAN_FIELD]: { + sync: VIOLIN_Y_FIELD, + ...pick(yAxis, AXIS_META_CONFIG_KEYS), + }, + }); + + chart.scale(scales); + + return params; +} + +/** + * axis 配置 + */ +function axis(params: Params): Params { + const { chart, options } = params; + const { xAxis, yAxis } = options; + + const view = findViewById(chart, VIOLIN_VIEW_ID); + + // 为 false 则是不显示轴 + if (xAxis === false) { + view.axis(X_FIELD, false); + } else { + view.axis(X_FIELD, xAxis); + } + + if (yAxis === false) { + view.axis(VIOLIN_Y_FIELD, false); + } else { + view.axis(VIOLIN_Y_FIELD, yAxis); + } + + chart.axis(false); + + return params; +} + +/** + * + * @param params + * @returns + */ +function legend(params: Params): Params { + const { chart, options } = params; + const { legend, seriesField, shape } = options; + + if (legend === false) { + chart.legend(false); + } else { + const legendField = seriesField ? seriesField : X_FIELD; + // fixme 暂不明为啥有描边 + const legendOptions = omit(legend as any, ['selected']); + if (!shape || !shape.startsWith('hollow')) { + if (!get(legendOptions, ['marker', 'style', 'lineWidth'])) { + set(legendOptions, ['marker', 'style', 'lineWidth'], 0); + } + } + chart.legend(legendField, legendOptions); + // 特殊的处理 fixme G2 层得解决这个问题 + if (get(legend, 'selected')) { + each(chart.views, (view) => view.legend(legendField, legend)); + } + } + + return params; +} + +/** + * annotation, apply to violin view. + * @param params + * @returns + */ +function annotation(params: Params): Params { + const { chart } = params; + + const violinView = findViewById(chart, VIOLIN_VIEW_ID); + baseAnnotation()({ ...params, chart: violinView }); + + return params; +} + +/** + * 动画 + * @param params + */ +export function animation(params: Params): Params { + const { chart, options } = params; + const { animation } = options; + + // 所有的 Geometry 都使用同一动画(各个图形如有区别,自行覆盖) + each(chart.views, (view) => { + // 同时设置整个 view 动画选项 + if (typeof animation === 'boolean') { + view.animate(animation); + } else { + view.animate(true); + } + + each(view.geometries, (g: Geometry) => { + g.animate(animation); + }); + }); + + return params; +} + +/** + * 小提琴图适配器 + * @param params + */ +export function adaptor(params: Params) { + return flow( + theme, + data, + violinView, + boxView, + meta, + tooltip, + axis, + legend, + interaction, + annotation, + animation + )(params); +} diff --git a/src/plots/violin/constant.ts b/src/plots/violin/constant.ts new file mode 100644 index 0000000000..1308506273 --- /dev/null +++ b/src/plots/violin/constant.ts @@ -0,0 +1,62 @@ +import { Plot } from '../../core/plot'; +import { deepAssign } from '../../utils'; +import { ViolinOptions } from './types'; + +export const X_FIELD = 'x'; +export const VIOLIN_Y_FIELD = 'violinY'; +export const VIOLIN_SIZE_FIELD = 'violinSize'; +export const MIN_MAX_FIELD = 'minMax'; +export const QUANTILE_FIELD = 'quantile'; +export const MEDIAN_FIELD = 'median'; + +export const VIOLIN_VIEW_ID = 'violin_view'; +export const MIN_MAX_VIEW_ID = 'min_max_view'; +export const QUANTILE_VIEW_ID = 'quantile_view'; +export const MEDIAN_VIEW_ID = 'median_view'; + +export const DEFAULT_OPTIONS = deepAssign({}, Plot.getDefaultOptions(), { + // 多 view 组成,一定要设置 view padding 同步 + syncViewPadding: true, + // 默认核函数 + kde: { + type: 'triangular', + sampleSize: 32, + width: 3, + }, + + // 默认小提琴轮廓样式 + violinStyle: { + lineWidth: 1, + fillOpacity: 0.3, + strokeOpacity: 0.75, + }, + // 坐标轴 + xAxis: { + grid: { + line: null, + }, + tickLine: { + alignTick: false, + }, + }, + yAxis: { + grid: { + line: { + style: { + lineWidth: 0.5, + lineDash: [4, 4], + }, + }, + }, + }, + // 图例 + legend: { + position: 'top-left', + }, + // Tooltip + tooltip: { + showMarkers: false, + }, + // 默认区域交互 + // interactions: [{ type: 'active-region' }], +} as Partial); diff --git a/src/plots/violin/index.ts b/src/plots/violin/index.ts new file mode 100644 index 0000000000..53dfe1bc93 --- /dev/null +++ b/src/plots/violin/index.ts @@ -0,0 +1,42 @@ +import { Plot } from '../../core/plot'; +import { Adaptor } from '../../core/adaptor'; +import { ViolinOptions } from './types'; +import { adaptor } from './adaptor'; +import { DEFAULT_OPTIONS } from './constant'; +import { transformViolinData } from './utils'; +export type { ViolinOptions }; + +export class Violin extends Plot { + /** + * 获取 默认配置项 + * 供外部使用 + */ + static getDefaultOptions(): Partial { + return DEFAULT_OPTIONS; + } + + /** 图表类型 */ + public type: string = 'violin'; + + /** + * @override + */ + public changeData(data: ViolinOptions['data']) { + this.updateOption({ data }); + this.chart.changeData(transformViolinData(this.options)); + } + + /** + * 获取 小提琴图 默认配置项 + */ + protected getDefaultOptions(): Partial { + return Violin.getDefaultOptions(); + } + + /** + * 获取 小提琴图 的适配器 + */ + protected getSchemaAdaptor(): Adaptor { + return adaptor; + } +} diff --git a/src/plots/violin/types.ts b/src/plots/violin/types.ts new file mode 100644 index 0000000000..8665a14ab6 --- /dev/null +++ b/src/plots/violin/types.ts @@ -0,0 +1,35 @@ +import { Options, StyleAttr } from '../../types'; + +export interface ViolinOptions extends Options { + /** X 轴映射 */ + readonly xField: string; + /** Y 轴映射 */ + readonly yField: string; + /** 拆分字段映射,默认是分组情况,颜色作为视觉通道 */ + readonly seriesField?: string; + /** 内部箱线图配置,false 为不显示。 */ + readonly box?: boolean; + /** + * 小提琴的形状。 + * 默认: 非平滑、实心 + * smooth: 平滑 + * hollow: 空心 + * hollow-smooth: 平滑、空心 + */ + readonly shape?: 'smooth' | 'hollow' | 'hollow-smooth'; + /** 小提琴样式配置,可选 */ + readonly violinStyle?: StyleAttr; + /** 核函数配置,当前只支持三角核 */ + readonly kde?: { + /** 三角波类型 */ + type: 'triangular'; + /** 最小值,默认为数据中的最小值减去一个固定的阈值。 */ + min?: number; + /** 最大值,默认为数据中的最大值加上一个固定的阈值。 */ + max?: number; + /** 采样数量,越大轮廓线越接近真实概率分布函数,默认32。 */ + sampleSize?: number; + /** 核函数的带宽。带宽越大产生的曲线越平滑(越模糊),带宽越小产生的曲线越陡峭。默认3。 */ + width?: number; + } /* | { type: 'gaussian', } ⬅️ 像这样添加新的核函数支持 */; +} diff --git a/src/plots/violin/utils.ts b/src/plots/violin/utils.ts new file mode 100644 index 0000000000..75d4bab343 --- /dev/null +++ b/src/plots/violin/utils.ts @@ -0,0 +1,102 @@ +import { groupBy, min, max } from '@antv/util'; +import pdf from 'pdfast'; +import { quantile } from '../../utils/transform/quantile'; +import { ViolinOptions } from './types'; + +export type ViolinData = { + /** X轴 */ + x: string; + + /** 小提琴轮廓的 size 通道数据 */ + violinSize: number[]; + /** 小提琴轮廓的 y 通道数据 */ + violinY: number[]; + + // 箱线图基础数据 + /** 最大值 */ + high: number; + /** 最小值 */ + low: number; + /** 上四分位数 */ + q1: number; + /** 下四分位数 */ + q3: number; + /** 箱线图中的中位值 */ + median: number[]; + + /** 箱线图中的上线边缘线 */ + minMax: number[]; + /** 箱线图中的上下四分位点 */ + quantile: number[]; +}; + +export type PdfOptions = { + min: number; + max: number; + size: number; + width: number; +}; + +export const toBoxValue = (values: number[]) => { + return { + low: min(values), + high: max(values), + q1: quantile(values, 0.25), + q3: quantile(values, 0.75), + median: quantile(values, [0.5]), + minMax: [min(values), max(values)], + quantile: [quantile(values, 0.25), quantile(values, 0.75)], + }; +}; + +export const toViolinValue = (values: number[], pdfOptions: PdfOptions) => { + const pdfResults: Array<{ x: number; y: number }> = pdf.create(values, pdfOptions); + return { + violinSize: pdfResults.map((result) => result.y), + violinY: pdfResults.map((result) => result.x), + }; +}; + +export const transformViolinData = (options: ViolinOptions): ViolinData[] => { + const { xField, yField, seriesField, data, kde } = options; + + /** 生成概率密度函数的配置 */ + const pdfOptions: PdfOptions = { + min: kde.min, + max: kde.max, + size: kde.sampleSize, + width: kde.width, + }; + + // 无拆分 + if (!seriesField) { + const group = groupBy(data, xField); + return Object.keys(group).map((x) => { + const records = group[x]; + const values = records.map((record) => record[yField]); + return { + x, + ...toViolinValue(values, pdfOptions), + ...toBoxValue(values), + }; + }); + } + + // 有拆分 + const resultList: ViolinData[] = []; + const seriesGroup = groupBy(data, seriesField); + Object.keys(seriesGroup).forEach((series) => { + const group = groupBy(seriesGroup[series], xField); + return Object.keys(group).forEach((key) => { + const records = group[key]; + const values = records.map((record) => record[yField]); + resultList.push({ + x: key, + [seriesField]: series, + ...toViolinValue(values, pdfOptions), + ...toBoxValue(values), + }); + }); + }); + return resultList; +}; diff --git a/src/utils/transform/quantile.ts b/src/utils/transform/quantile.ts new file mode 100644 index 0000000000..a57547482b --- /dev/null +++ b/src/utils/transform/quantile.ts @@ -0,0 +1,216 @@ +// from https://github.com/simple-statistics + +/** + * This is the internal implementation of quantiles: when you know + * that the order is sorted, you don't need to re-sort it, and the computations + * are faster. + * + * @param {Array} x sample of one or more data points + * @param {number} p desired quantile: a number between 0 to 1, inclusive + * @returns {number} quantile value + * @throws {Error} if p ix outside of the range from 0 to 1 + * @throws {Error} if x is empty + * @example + * quantileSorted([3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 0.5); // => 9 + */ +export function quantileSorted(x: number[], p: number) { + const idx = x.length * p; + if (x.length === 0) { + throw new Error('quantile requires at least one data point.'); + } else if (p < 0 || p > 1) { + throw new Error('quantiles must be between 0 and 1'); + } else if (p === 1) { + // If p is 1, directly return the last element + return x[x.length - 1]; + } else if (p === 0) { + // If p is 0, directly return the first element + return x[0]; + } else if (idx % 1 !== 0) { + // If p is not integer, return the next element in array + return x[Math.ceil(idx) - 1]; + } else if (x.length % 2 === 0) { + // If the list has even-length, we'll take the average of this number + // and the next value, if there is one + return (x[idx - 1] + x[idx]) / 2; + } else { + // Finally, in the simple case of an integer value + // with an odd-length list, return the x value at the index. + return x[idx]; + } +} + +/** + * 交换数组位置 + * @param arr T[] + * @param i number + * @param j number + */ +export function swap(arr: T[], i: number, j: number): void { + const tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; +} + +/** + * Rearrange items in `arr` so that all items in `[left, k]` range are the smallest. + * The `k`-th element will have the `(k - left + 1)`-th smallest value in `[left, right]`. + * + * Implements Floyd-Rivest selection algorithm https://en.wikipedia.org/wiki/Floyd-Rivest_algorithm + * + * @param {Array} arr input array + * @param {number} k pivot index + * @param {number} [left] left index + * @param {number} [right] right index + * @returns {void} mutates input array + * @example + * var arr = [65, 28, 59, 33, 21, 56, 22, 95, 50, 12, 90, 53, 28, 77, 39]; + * quickselect(arr, 8); + * // = [39, 28, 28, 33, 21, 12, 22, 50, 53, 56, 59, 65, 90, 77, 95] + */ +export function quickselect(arr: number[], k, left?: number, right?: number): void { + left = left || 0; + right = right || arr.length - 1; + + while (right > left) { + // 600 and 0.5 are arbitrary constants chosen in the original paper to minimize execution time + if (right - left > 600) { + const n = right - left + 1; + const m = k - left + 1; + const z = Math.log(n); + const s = 0.5 * Math.exp((2 * z) / 3); + let sd = 0.5 * Math.sqrt((z * s * (n - s)) / n); + if (m - n / 2 < 0) sd *= -1; + const newLeft = Math.max(left, Math.floor(k - (m * s) / n + sd)); + const newRight = Math.min(right, Math.floor(k + ((n - m) * s) / n + sd)); + quickselect(arr, k, newLeft, newRight); + } + + const t = arr[k]; + let i = left; + let j = right; + + swap(arr, left, k); + if (arr[right] > t) swap(arr, left, right); + + while (i < j) { + swap(arr, i, j); + i++; + j--; + while (arr[i] < t) i++; + while (arr[j] > t) j--; + } + + if (arr[left] === t) swap(arr, left, j); + else { + j++; + swap(arr, j, right); + } + + if (j <= k) left = j + 1; + if (k <= j) right = j - 1; + } +} + +/** + * The [quantile](https://en.wikipedia.org/wiki/Quantile): + * this is a population quantile, since we assume to know the entire + * dataset in this library. This is an implementation of the + * [Quantiles of a Population](http://en.wikipedia.org/wiki/Quantile#Quantiles_of_a_population) + * algorithm from wikipedia. + * + * Sample is a one-dimensional array of numbers, + * and p is either a decimal number from 0 to 1 or an array of decimal + * numbers from 0 to 1. + * In terms of a k/q quantile, p = k/q - it's just dealing with fractions or dealing + * with decimal values. + * When p is an array, the result of the function is also an array containing the appropriate + * quantiles in input order + * + * @param {Array} x sample of one or more numbers + * @param {Array | number} p the desired quantile, as a number between 0 and 1 + * @returns {number} quantile + * @example + * quantile([3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 0.5); // => 9 + */ +function quantile(x: number[], p: number): number; +function quantile(x: number[], p: number[]): number[]; +function quantile(x: any, p: any): any { + const copy = x.slice(); + + if (Array.isArray(p)) { + // rearrange elements so that each element corresponding to a requested + // quantile is on a place it would be if the array was fully sorted + multiQuantileSelect(copy, p); + // Initialize the result array + const results: number[] = []; + // For each requested quantile + for (let i = 0; i < p.length; i++) { + results[i] = quantileSorted(copy, p[i]); + } + return results; + } else { + const idx = quantileIndex(copy.length, p); + quantileSelect(copy, idx, 0, copy.length - 1); + return quantileSorted(copy, p); + } +} + +function quantileSelect(arr, k, left, right) { + if (k % 1 === 0) { + quickselect(arr, k, left, right); + } else { + k = Math.floor(k); + quickselect(arr, k, left, right); + quickselect(arr, k + 1, k + 1, right); + } +} + +function multiQuantileSelect(arr, p) { + const indices = [0]; + for (let i = 0; i < p.length; i++) { + indices.push(quantileIndex(arr.length, p[i])); + } + indices.push(arr.length - 1); + indices.sort(compare); + + const stack = [0, indices.length - 1]; + + while (stack.length) { + const r = Math.ceil(stack.pop()); + const l = Math.floor(stack.pop()); + if (r - l <= 1) continue; + + const m = Math.floor((l + r) / 2); + quantileSelect(arr, indices[m], Math.floor(indices[l]), Math.ceil(indices[r])); + + stack.push(l, m, m, r); + } +} + +function compare(a, b) { + return a - b; +} + +function quantileIndex(len, p) { + const idx = len * p; + if (p === 1) { + // If p is 1, directly return the last index + return len - 1; + } else if (p === 0) { + // If p is 0, directly return the first index + return 0; + } else if (idx % 1 !== 0) { + // If index is not integer, return the next index in array + return Math.ceil(idx) - 1; + } else if (len % 2 === 0) { + // If the list has even-length, we'll return the middle of two indices + // around quantile to indicate that we need an average value of the two + return idx - 0.5; + } else { + // Finally, in the simple case of an integer index + // with an odd-length list, return the index + return idx; + } +} + +export { quantile };